Further SCIM sync testing, minor fixes ()

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  and
  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
proto/src/scim_v1
server/lib/src/idm
tools/iam_migrations
freeipa/src
ldap/src

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| {