Further SCIM sync testing, minor fixes (#3305)

This adds further testing of SCIM sync, especially around
conversion of the SCIM Sync Person and Group types into
SCIM Entry. This test would have prevented #3298 and
 #3299 from occuring.

During testing two more fixes were found. external_id should have
been required (not optional) and a group with no members would
cause a serialisation issue.
This commit is contained in:
Firstyear 2024-12-20 17:16:07 +10:00 committed by GitHub
parent c6432cad83
commit 9f499f3913
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 146 additions and 64 deletions

View file

@ -120,12 +120,15 @@ mod tests {
// Group // Group
let group_uuid = uuid::uuid!("2d0a9e7c-cc08-4ca2-8d7f-114f9abcfc8a"); let group_uuid = uuid::uuid!("2d0a9e7c-cc08-4ca2-8d7f-114f9abcfc8a");
let group = ScimSyncGroup::builder("testgroup".to_string(), group_uuid) let group = ScimSyncGroup::builder(
.set_description(Some("test desc".to_string())) group_uuid,
.set_gidnumber(Some(12345)) "cn=testgroup".to_string(),
.set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter()) "testgroup".to_string(),
.set_external_id(Some("cn=testgroup".to_string())) )
.build(); .set_description(Some("test desc".to_string()))
.set_gidnumber(Some(12345))
.set_members(vec!["member_a".to_string(), "member_a".to_string()].into_iter())
.build();
let entry: Result<ScimEntry, _> = group.try_into(); let entry: Result<ScimEntry, _> = group.try_into();
@ -136,32 +139,35 @@ mod tests {
let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey"; let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
let person = let person = ScimSyncPerson::builder(
ScimSyncPerson::builder(user_uuid, "testuser".to_string(), "Test User".to_string()) user_uuid,
.set_password_import(Some("new_password".to_string())) "cn=testuser".to_string(),
.set_unix_password_import(Some("new_password".to_string())) "testuser".to_string(),
.set_totp_import(vec![ScimTotp { "Test User".to_string(),
external_id: "Totp".to_string(), )
secret: "abcd".to_string(), .set_password_import(Some("new_password".to_string()))
algo: "SHA3".to_string(), .set_unix_password_import(Some("new_password".to_string()))
step: 60, .set_totp_import(vec![ScimTotp {
digits: 8, external_id: "Totp".to_string(),
}]) secret: "abcd".to_string(),
.set_mail(vec![MultiValueAttr { algo: "SHA3".to_string(),
primary: Some(true), step: 60,
value: "testuser@example.com".to_string(), digits: 8,
..Default::default() }])
}]) .set_mail(vec![MultiValueAttr {
.set_ssh_publickey(vec![ScimSshPubKey { primary: Some(true),
label: "Key McKeyface".to_string(), value: "testuser@example.com".to_string(),
value: user_sshkey.to_string(), ..Default::default()
}]) }])
.set_login_shell(Some("/bin/false".to_string())) .set_ssh_publickey(vec![ScimSshPubKey {
.set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string())) label: "Key McKeyface".to_string(),
.set_account_expire(Some("2023-11-28T04:57:55Z".to_string())) value: user_sshkey.to_string(),
.set_gidnumber(Some(54321)) }])
.set_external_id(Some("cn=testuser".to_string())) .set_login_shell(Some("/bin/false".to_string()))
.build(); .set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
.set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
.set_gidnumber(Some(54321))
.build();
let entry: Result<ScimEntry, _> = person.try_into(); let entry: Result<ScimEntry, _> = person.try_into();

View file

@ -119,7 +119,12 @@ pub struct ScimSyncPersonBuilder {
} }
impl ScimSyncPerson { impl ScimSyncPerson {
pub fn builder(id: Uuid, name: String, displayname: String) -> ScimSyncPersonBuilder { pub fn builder(
id: Uuid,
external_id: String,
name: String,
displayname: String,
) -> ScimSyncPersonBuilder {
ScimSyncPersonBuilder { ScimSyncPersonBuilder {
inner: ScimSyncPerson { inner: ScimSyncPerson {
entry: ScimEntryHeader { entry: ScimEntryHeader {
@ -128,7 +133,7 @@ impl ScimSyncPerson {
SCIM_SCHEMA_SYNC_PERSON.to_string(), SCIM_SCHEMA_SYNC_PERSON.to_string(),
], ],
id, id,
external_id: None, external_id: Some(external_id),
meta: None, meta: None,
}, },
name, name,
@ -205,11 +210,6 @@ impl ScimSyncPersonBuilder {
self self
} }
pub fn set_external_id(mut self, external_id: Option<String>) -> Self {
self.inner.entry.external_id = external_id;
self
}
pub fn build(self) -> ScimSyncPerson { pub fn build(self) -> ScimSyncPerson {
self.inner self.inner
} }
@ -230,6 +230,7 @@ pub struct ScimSyncGroup {
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub gidnumber: Option<u32>, pub gidnumber: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub member: Vec<ScimExternalMember>, pub member: Vec<ScimExternalMember>,
} }
@ -248,13 +249,13 @@ pub struct ScimSyncGroupBuilder {
} }
impl ScimSyncGroup { impl ScimSyncGroup {
pub fn builder(name: String, id: Uuid) -> ScimSyncGroupBuilder { pub fn builder(id: Uuid, external_id: String, name: String) -> ScimSyncGroupBuilder {
ScimSyncGroupBuilder { ScimSyncGroupBuilder {
inner: ScimSyncGroup { inner: ScimSyncGroup {
entry: ScimEntryHeader { entry: ScimEntryHeader {
schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()], schemas: vec![SCIM_SCHEMA_SYNC_GROUP.to_string()],
id, id,
external_id: None, external_id: Some(external_id),
meta: None, meta: None,
}, },
name, name,
@ -295,11 +296,6 @@ impl ScimSyncGroupBuilder {
self self
} }
pub fn set_external_id(mut self, external_id: Option<String>) -> Self {
self.inner.entry.external_id = external_id;
self
}
pub fn build(self) -> ScimSyncGroup { pub fn build(self) -> ScimSyncGroup {
self.inner self.inner
} }

View file

@ -1584,10 +1584,6 @@ mod tests {
assert!(matches!(sync_state, ScimSyncState::Refresh)); assert!(matches!(sync_state, ScimSyncState::Refresh));
drop(idms_prox_read); drop(idms_prox_read);
// Use the current state and update.
// TODO!!!
} }
#[idm_test] #[idm_test]
@ -3167,6 +3163,100 @@ mod tests {
assert!(idms_prox_write.commit().is_ok()); assert!(idms_prox_write.commit().is_ok());
} }
#[idm_test]
/// Assert that a SCIM JSON proto entry correctly serialises and deserialises
/// and can be applied as a changeset. This serialisation is performed during
/// the ScimEntry::try_from step.
async fn test_idm_scim_sync_json_proto(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
let (_sync_uuid, ident) = test_scim_sync_apply_setup_ident(&mut idms_prox_write, ct);
let sse = ScimSyncUpdateEvent { ident };
// Minimum Viable Person
let person_1 = ScimSyncPerson::builder(
Uuid::new_v4(),
"cn=testperson_1".to_string(),
"testperson_1".to_string(),
"Test Person One".to_string(),
)
.build()
.try_into()
.unwrap();
// Minimum Viable Group
let group_1 = ScimSyncGroup::builder(
Uuid::new_v4(),
"cn=testgroup_1".to_string(),
"testgroup_1".to_string(),
)
.build()
.try_into()
.unwrap();
let user_sshkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== testuser@fidokey";
// All Attribute Person
let person_2 = ScimSyncPerson::builder(
Uuid::new_v4(),
"cn=testperson_2".to_string(),
"testperson_2".to_string(),
"Test Person Two".to_string(),
)
.set_password_import(Some("ipaNTHash: iEb36u6PsRetBr3YMLdYbA".to_string()))
.set_unix_password_import(Some("ipaNTHash: iEb36u6PsRetBr3YMLdYbA".to_string()))
.set_totp_import(vec![ScimTotp {
external_id: "Totp".to_string(),
secret: "QICWZTON72IBS5MXWNURKAONC3JNOOOFMLKNRTIPXBYQ4BLRSEBM7KF5".to_string(),
algo: "sha256".to_string(),
step: 60,
digits: 8,
}])
.set_mail(vec![MultiValueAttr {
primary: Some(true),
value: "testuser@example.com".to_string(),
..Default::default()
}])
.set_ssh_publickey(vec![ScimSshPubKey {
label: "Key McKeyface".to_string(),
value: user_sshkey.to_string(),
}])
.set_login_shell(Some("/bin/zsh".to_string()))
.set_account_valid_from(Some("2023-11-28T04:57:55Z".to_string()))
.set_account_expire(Some("2023-11-28T04:57:55Z".to_string()))
.set_gidnumber(Some(12346))
.build()
.try_into()
.unwrap();
// All Attribute Group
let group_2 = ScimSyncGroup::builder(
Uuid::new_v4(),
"cn=testgroup_2".to_string(),
"testgroup_2".to_string(),
)
.set_description(Some("description".to_string()))
.set_gidnumber(Some(12345))
.set_members(vec!["cn=testperson_1".to_string(), "cn=testperson_2".to_string()].into_iter())
.build()
.try_into()
.unwrap();
let entries = vec![person_1, group_1, person_2, group_2];
let changes = ScimSyncRequest {
from_state: ScimSyncState::Refresh,
to_state: ScimSyncState::Active {
cookie: vec![1, 2, 3, 4],
},
entries,
retain: ScimSyncRetentionMode::Ignore,
};
assert!(idms_prox_write.scim_sync_apply(&sse, &changes, ct).is_ok());
assert!(idms_prox_write.commit().is_ok());
}
const TEST_SYNC_SCIM_IPA_1: &str = r#" const TEST_SYNC_SCIM_IPA_1: &str = r#"
{ {
"from_state": "Refresh", "from_state": "Refresh",

View file

@ -926,9 +926,8 @@ fn ipa_to_scim_entry(
let account_valid_from = None; let account_valid_from = None;
let login_shell = entry.remove_ava_single(Attribute::LoginShell.as_ref()); let login_shell = entry.remove_ava_single(Attribute::LoginShell.as_ref());
let external_id = Some(entry.dn);
let scim_sync_person = ScimSyncPerson::builder(id, user_name, display_name) let scim_sync_person = ScimSyncPerson::builder(id, entry.dn, user_name, display_name)
.set_gidnumber(gidnumber) .set_gidnumber(gidnumber)
.set_password_import(password_import) .set_password_import(password_import)
.set_unix_password_import(unix_password_import) .set_unix_password_import(unix_password_import)
@ -938,7 +937,6 @@ fn ipa_to_scim_entry(
.set_ssh_publickey(ssh_publickey) .set_ssh_publickey(ssh_publickey)
.set_account_expire(account_expire) .set_account_expire(account_expire)
.set_account_valid_from(account_valid_from) .set_account_valid_from(account_valid_from)
.set_external_id(external_id)
.build(); .build();
let scim_entry_generic: ScimEntry = scim_sync_person.try_into().map_err(|json_err| { let scim_entry_generic: ScimEntry = scim_sync_person.try_into().map_err(|json_err| {
@ -983,13 +981,10 @@ fn ipa_to_scim_entry(
.map(|set| set.into_iter().collect()) .map(|set| set.into_iter().collect())
.unwrap_or_default(); .unwrap_or_default();
let external_id = Some(entry.dn); let scim_sync_group = ScimSyncGroup::builder(id, entry.dn, name)
let scim_sync_group = ScimSyncGroup::builder(name, id)
.set_description(description) .set_description(description)
.set_gidnumber(gidnumber) .set_gidnumber(gidnumber)
.set_members(members.into_iter()) .set_members(members.into_iter())
.set_external_id(external_id)
.build(); .build();
let scim_entry_generic: ScimEntry = scim_sync_group.try_into().map_err(|json_err| { let scim_entry_generic: ScimEntry = scim_sync_group.try_into().map_err(|json_err| {

View file

@ -617,9 +617,8 @@ fn ldap_to_scim_entry(
let login_shell = entry let login_shell = entry
.get_ava_single(&sync_config.person_attr_login_shell) .get_ava_single(&sync_config.person_attr_login_shell)
.map(str::to_string); .map(str::to_string);
let external_id = Some(entry.dn);
let scim_sync_person = ScimSyncPerson::builder(id, user_name, display_name) let scim_sync_person = ScimSyncPerson::builder(id, entry.dn, user_name, display_name)
.set_gidnumber(gidnumber) .set_gidnumber(gidnumber)
.set_password_import(password_import) .set_password_import(password_import)
.set_unix_password_import(unix_password_import) .set_unix_password_import(unix_password_import)
@ -629,7 +628,6 @@ fn ldap_to_scim_entry(
.set_ssh_publickey(ssh_publickey) .set_ssh_publickey(ssh_publickey)
.set_account_expire(account_expire) .set_account_expire(account_expire)
.set_account_valid_from(account_valid_from) .set_account_valid_from(account_valid_from)
.set_external_id(external_id)
.build(); .build();
let scim_entry_generic: ScimEntry = scim_sync_person.try_into().map_err(|json_err| { let scim_entry_generic: ScimEntry = scim_sync_person.try_into().map_err(|json_err| {
@ -678,13 +676,10 @@ fn ldap_to_scim_entry(
.map(|set| set.into_iter().collect()) .map(|set| set.into_iter().collect())
.unwrap_or_default(); .unwrap_or_default();
let external_id = Some(entry.dn); let scim_sync_group = ScimSyncGroup::builder(id, entry.dn, name)
let scim_sync_group = ScimSyncGroup::builder(name, id)
.set_description(description) .set_description(description)
.set_gidnumber(gidnumber) .set_gidnumber(gidnumber)
.set_members(members.into_iter()) .set_members(members.into_iter())
.set_external_id(external_id)
.build(); .build();
let scim_entry_generic: ScimEntry = scim_sync_group.try_into().map_err(|json_err| { let scim_entry_generic: ScimEntry = scim_sync_group.try_into().map_err(|json_err| {