From 40cc9932a5e80f38d9d75f51d93095ce194b1ddf Mon Sep 17 00:00:00 2001 From: William Brown <william@blackhats.net.au> Date: Fri, 28 Mar 2025 15:50:07 +1000 Subject: [PATCH] Tidy up schema --- proto/src/attribute.rs | 3 + proto/src/constants.rs | 1 + server/lib/src/constants/uuids.rs | 1 + server/lib/src/idm/application.rs | 138 +------------------ server/lib/src/idm/ldap.rs | 12 ++ server/lib/src/migration_data/dl10/mod.rs | 3 +- server/lib/src/migration_data/dl10/schema.rs | 23 +++- server/testkit/tests/testkit/ldap_basic.rs | 44 +++++- 8 files changed, 86 insertions(+), 139 deletions(-) diff --git a/proto/src/attribute.rs b/proto/src/attribute.rs index 493509944..1c42fa336 100644 --- a/proto/src/attribute.rs +++ b/proto/src/attribute.rs @@ -32,6 +32,7 @@ pub enum Attribute { AcpTargetScope, ApiTokenSession, ApplicationPassword, + ApplicationUrl, AttestedPasskeys, #[default] Attr, @@ -267,6 +268,7 @@ impl Attribute { Attribute::AcpTargetScope => ATTR_ACP_TARGET_SCOPE, Attribute::ApiTokenSession => ATTR_API_TOKEN_SESSION, Attribute::ApplicationPassword => ATTR_APPLICATION_PASSWORD, + Attribute::ApplicationUrl => ATTR_APPLICATION_URL, Attribute::AttestedPasskeys => ATTR_ATTESTED_PASSKEYS, Attribute::Attr => ATTR_ATTR, Attribute::AttributeName => ATTR_ATTRIBUTENAME, @@ -454,6 +456,7 @@ impl Attribute { ATTR_ACP_TARGET_SCOPE => Attribute::AcpTargetScope, ATTR_API_TOKEN_SESSION => Attribute::ApiTokenSession, ATTR_APPLICATION_PASSWORD => Attribute::ApplicationPassword, + ATTR_APPLICATION_URL => Attribute::ApplicationUrl, ATTR_ATTESTED_PASSKEYS => Attribute::AttestedPasskeys, ATTR_ATTR => Attribute::Attr, ATTR_ATTRIBUTENAME => Attribute::AttributeName, diff --git a/proto/src/constants.rs b/proto/src/constants.rs index 414c51791..c3983c4e0 100644 --- a/proto/src/constants.rs +++ b/proto/src/constants.rs @@ -72,6 +72,7 @@ pub const ATTR_ACP_SEARCH_ATTR: &str = "acp_search_attr"; pub const ATTR_ACP_TARGET_SCOPE: &str = "acp_targetscope"; pub const ATTR_API_TOKEN_SESSION: &str = "api_token_session"; pub const ATTR_APPLICATION_PASSWORD: &str = "application_password"; +pub const ATTR_APPLICATION_URL: &str = "application_url"; pub const ATTR_ATTESTED_PASSKEYS: &str = "attested_passkeys"; pub const ATTR_ATTR: &str = "attr"; pub const ATTR_ATTRIBUTENAME: &str = "attributename"; diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs index fc6c2f286..1ea5f1851 100644 --- a/server/lib/src/constants/uuids.rs +++ b/server/lib/src/constants/uuids.rs @@ -334,6 +334,7 @@ pub const UUID_SCHEMA_ATTR_ACP_MODIFY_PRESENT_CLASS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000189"); pub const UUID_SCHEMA_ATTR_ACP_MODIFY_REMOVE_CLASS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000190"); +pub const UUID_SCHEMA_ATTR_APPLICATION_URL: Uuid = uuid!("00000000-0000-0000-0000-ffff00000191"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. diff --git a/server/lib/src/idm/application.rs b/server/lib/src/idm/application.rs index 42fb98485..ff4e92645 100644 --- a/server/lib/src/idm/application.rs +++ b/server/lib/src/idm/application.rs @@ -255,139 +255,6 @@ mod tests { const TEST_CURRENT_TIME: u64 = 6000; - // Tests that only the correct combinations of [Account, Person, Application and - // ServiceAccount] classes are allowed. - #[idm_test] - async fn test_idm_application_excludes(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(); - - // ServiceAccount, Application and Person not allowed together - let test_grp_name = "testgroup1"; - let test_grp_uuid = Uuid::new_v4(); - let e1 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Group.to_value()), - (Attribute::Name, Value::new_iname(test_grp_name)), - (Attribute::Uuid, Value::Uuid(test_grp_uuid)) - ); - let test_entry_uuid = Uuid::new_v4(); - let e2 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Account.to_value()), - (Attribute::Class, EntryClass::ServiceAccount.to_value()), - (Attribute::Class, EntryClass::Application.to_value()), - (Attribute::Class, EntryClass::Person.to_value()), - (Attribute::Name, Value::new_iname("test_app_name")), - (Attribute::Uuid, Value::Uuid(test_entry_uuid)), - (Attribute::Description, Value::new_utf8s("test_app_desc")), - ( - Attribute::DisplayName, - Value::new_utf8s("test_app_dispname") - ), - (Attribute::LinkedGroup, Value::Refer(test_grp_uuid)) - ); - let ce = CreateEvent::new_internal(vec![e1, e2]); - let cr = idms_prox_write.qs_write.create(&ce); - assert!(cr.is_err()); - - // Application and Person not allowed together - let test_grp_name = "testgroup1"; - let test_grp_uuid = Uuid::new_v4(); - let e1 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Group.to_value()), - (Attribute::Name, Value::new_iname(test_grp_name)), - (Attribute::Uuid, Value::Uuid(test_grp_uuid)) - ); - let test_entry_uuid = Uuid::new_v4(); - let e2 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Account.to_value()), - (Attribute::Class, EntryClass::Application.to_value()), - (Attribute::Class, EntryClass::Person.to_value()), - (Attribute::Name, Value::new_iname("test_app_name")), - (Attribute::Uuid, Value::Uuid(test_entry_uuid)), - (Attribute::Description, Value::new_utf8s("test_app_desc")), - ( - Attribute::DisplayName, - Value::new_utf8s("test_app_dispname") - ), - (Attribute::LinkedGroup, Value::Refer(test_grp_uuid)) - ); - let ce = CreateEvent::new_internal(vec![e1, e2]); - let cr = idms_prox_write.qs_write.create(&ce); - assert!(cr.is_err()); - - // Supplements not satisfied, Application supplements ServiceAccount - let test_grp_name = "testgroup1"; - let test_grp_uuid = Uuid::new_v4(); - let e1 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Group.to_value()), - (Attribute::Name, Value::new_iname(test_grp_name)), - (Attribute::Uuid, Value::Uuid(test_grp_uuid)) - ); - let test_entry_uuid = Uuid::new_v4(); - let e2 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Account.to_value()), - (Attribute::Class, EntryClass::Application.to_value()), - (Attribute::Name, Value::new_iname("test_app_name")), - (Attribute::Uuid, Value::Uuid(test_entry_uuid)), - (Attribute::Description, Value::new_utf8s("test_app_desc")), - (Attribute::LinkedGroup, Value::Refer(test_grp_uuid)) - ); - let ce = CreateEvent::new_internal(vec![e1, e2]); - let cr = idms_prox_write.qs_write.create(&ce); - assert!(cr.is_err()); - - // Supplements not satisfied, Application supplements ServiceAccount - let test_grp_name = "testgroup1"; - let test_grp_uuid = Uuid::new_v4(); - let e1 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Group.to_value()), - (Attribute::Name, Value::new_iname(test_grp_name)), - (Attribute::Uuid, Value::Uuid(test_grp_uuid)) - ); - let test_entry_uuid = Uuid::new_v4(); - let e2 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Application.to_value()), - (Attribute::Name, Value::new_iname("test_app_name")), - (Attribute::Uuid, Value::Uuid(test_entry_uuid)), - (Attribute::Description, Value::new_utf8s("test_app_desc")), - (Attribute::LinkedGroup, Value::Refer(test_grp_uuid)) - ); - let ce = CreateEvent::new_internal(vec![e1, e2]); - let cr = idms_prox_write.qs_write.create(&ce); - assert!(cr.is_err()); - - // Supplements satisfied, Application supplements ServiceAccount - let test_grp_name = "testgroup1"; - let test_grp_uuid = Uuid::new_v4(); - let e1 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Group.to_value()), - (Attribute::Name, Value::new_iname(test_grp_name)), - (Attribute::Uuid, Value::Uuid(test_grp_uuid)) - ); - let test_entry_uuid = Uuid::new_v4(); - let e2 = entry_init!( - (Attribute::Class, EntryClass::Object.to_value()), - (Attribute::Class, EntryClass::Application.to_value()), - (Attribute::Class, EntryClass::ServiceAccount.to_value()), - (Attribute::Name, Value::new_iname("test_app_name")), - (Attribute::Uuid, Value::Uuid(test_entry_uuid)), - (Attribute::Description, Value::new_utf8s("test_app_desc")), - (Attribute::LinkedGroup, Value::Refer(test_grp_uuid)) - ); - let ce = CreateEvent::new_internal(vec![e1, e2]); - let cr = idms_prox_write.qs_write.create(&ce); - assert!(cr.is_ok()); - } - // Tests it is not possible to create an application without the linked group attribute #[idm_test] async fn test_idm_application_no_linked_group( @@ -404,6 +271,7 @@ mod tests { (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname("test_app_name")), (Attribute::Uuid, Value::Uuid(test_entry_uuid)), (Attribute::Description, Value::new_utf8s("test_app_desc")), @@ -547,8 +415,10 @@ mod tests { let e3 = entry_init!( (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname(test_app_name)), (Attribute::Uuid, Value::Uuid(test_app_uuid)), (Attribute::LinkedGroup, Value::Refer(test_grp_uuid)) @@ -647,7 +517,9 @@ mod tests { let e2 = entry_init!( (Attribute::Class, EntryClass::Object.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname("test_app_name")), (Attribute::Uuid, Value::Uuid(test_entry_uuid)), (Attribute::Description, Value::new_utf8s("test_app_desc")), diff --git a/server/lib/src/idm/ldap.rs b/server/lib/src/idm/ldap.rs index 4b7a4e37e..2fe619f22 100644 --- a/server/lib/src/idm/ldap.rs +++ b/server/lib/src/idm/ldap.rs @@ -1119,8 +1119,10 @@ mod tests { let e3 = entry_init!( (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname(app_name)), (Attribute::Uuid, Value::Uuid(app_uuid)), (Attribute::LinkedGroup, Value::Refer(grp_uuid)) @@ -1283,8 +1285,10 @@ mod tests { let e3 = entry_init!( (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname("testapp1")), (Attribute::Uuid, Value::Uuid(app_uuid)), (Attribute::LinkedGroup, Value::Refer(grp_uuid)) @@ -1456,8 +1460,10 @@ mod tests { let e4 = entry_init!( (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname(app1_name)), (Attribute::Uuid, Value::Uuid(app1_uuid)), (Attribute::LinkedGroup, Value::Refer(grp1_uuid)) @@ -1465,8 +1471,10 @@ mod tests { let e5 = entry_init!( (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname(app2_name)), (Attribute::Uuid, Value::Uuid(app2_uuid)), (Attribute::LinkedGroup, Value::Refer(grp2_uuid)) @@ -1651,8 +1659,10 @@ mod tests { let e3 = entry_init!( (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname(app1_name)), (Attribute::Uuid, Value::Uuid(app1_uuid)), (Attribute::LinkedGroup, Value::Refer(grp1_uuid)) @@ -2693,8 +2703,10 @@ mod tests { let e3 = entry_init!( (Attribute::Class, EntryClass::Object.to_value()), + (Attribute::Class, EntryClass::Account.to_value()), (Attribute::Class, EntryClass::ServiceAccount.to_value()), (Attribute::Class, EntryClass::Application.to_value()), + (Attribute::DisplayName, Value::new_utf8s("Application")), (Attribute::Name, Value::new_iname(app_name)), (Attribute::Uuid, Value::Uuid(app_uuid)), (Attribute::LinkedGroup, Value::Refer(grp_uuid)) diff --git a/server/lib/src/migration_data/dl10/mod.rs b/server/lib/src/migration_data/dl10/mod.rs index 8eac91720..885967b1d 100644 --- a/server/lib/src/migration_data/dl10/mod.rs +++ b/server/lib/src/migration_data/dl10/mod.rs @@ -105,6 +105,7 @@ pub fn phase_1_schema_attrs() -> Vec<EntryInitNew> { // DL10 SCHEMA_ATTR_DENIED_NAME_DL10.clone().into(), SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES.clone().into(), + SCHEMA_ATTR_APPLICATION_URL.clone().into(), ] } @@ -134,7 +135,7 @@ pub fn phase_2_schema_classes() -> Vec<EntryInitNew> { SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7.clone().into(), // DL8 SCHEMA_CLASS_ACCOUNT_POLICY_DL8.clone().into(), - SCHEMA_CLASS_APPLICATION_DL8.clone().into(), + SCHEMA_CLASS_APPLICATION.clone().into(), SCHEMA_CLASS_PERSON_DL8.clone().into(), // DL9 SCHEMA_CLASS_OAUTH2_RS_DL9.clone().into(), diff --git a/server/lib/src/migration_data/dl10/schema.rs b/server/lib/src/migration_data/dl10/schema.rs index 5117f7121..27123b381 100644 --- a/server/lib/src/migration_data/dl10/schema.rs +++ b/server/lib/src/migration_data/dl10/schema.rs @@ -729,6 +729,14 @@ pub static ref SCHEMA_ATTR_APPLICATION_PASSWORD_DL8: SchemaAttribute = SchemaAtt ..Default::default() }; +pub static ref SCHEMA_ATTR_APPLICATION_URL: SchemaAttribute = SchemaAttribute { + uuid: UUID_SCHEMA_ATTR_APPLICATION_URL, + name: Attribute::ApplicationUrl, + description: "The URL of an external application".to_string(), + syntax: SyntaxType::Url, + ..Default::default() +}; + // === classes === pub static ref SCHEMA_CLASS_PERSON_DL8: SchemaClass = SchemaClass { uuid: UUID_SCHEMA_CLASS_PERSON, @@ -838,9 +846,9 @@ pub static ref SCHEMA_CLASS_ACCOUNT_DL5: SchemaClass = SchemaClass { Attribute::Spn ], systemsupplements: vec![ + EntryClass::OAuth2ResourceServer.into(), EntryClass::Person.into(), EntryClass::ServiceAccount.into(), - EntryClass::OAuth2ResourceServer.into(), ], ..Default::default() }; @@ -1082,13 +1090,20 @@ pub static ref SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7: SchemaClass = SchemaClass { ..Default::default() }; -pub static ref SCHEMA_CLASS_APPLICATION_DL8: SchemaClass = SchemaClass { +pub static ref SCHEMA_CLASS_APPLICATION: SchemaClass = SchemaClass { uuid: UUID_SCHEMA_CLASS_APPLICATION, name: EntryClass::Application.into(), description: "The class representing an application".to_string(), - systemmust: vec![Attribute::Name, Attribute::LinkedGroup], - systemmay: vec![Attribute::Description], + systemmust: vec![Attribute::LinkedGroup], + systemmay: vec![ + Attribute::ApplicationUrl, + ], + // I think this could change before release - I can see a world + // whe we may want an oauth2 application to have application passwords, + // or for this to be it's own thing. But service accounts also don't + // quite do enough, they have api tokens, but that's all we kind + // of want from them? systemsupplements: vec![EntryClass::ServiceAccount.into()], ..Default::default() }; diff --git a/server/testkit/tests/testkit/ldap_basic.rs b/server/testkit/tests/testkit/ldap_basic.rs index 928390e35..8ef66bcbc 100644 --- a/server/testkit/tests/testkit/ldap_basic.rs +++ b/server/testkit/tests/testkit/ldap_basic.rs @@ -1,6 +1,8 @@ -use kanidmd_testkit::AsyncTestEnvironment; +use kanidmd_testkit::{AsyncTestEnvironment, IDM_ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_USER}; use ldap3_client::LdapClientBuilder; +const TEST_PERSON: &str = "user_mcuserton"; + #[kanidmd_testkit::test(ldap = true)] async fn test_ldap_basic_unix_bind(test_env: &AsyncTestEnvironment) { let ldap_url = test_env.ldap_url.as_ref().unwrap(); @@ -14,3 +16,43 @@ async fn test_ldap_basic_unix_bind(test_env: &AsyncTestEnvironment) { assert_eq!(whoami, Some("u: anonymous@localhost".to_string())); } + +#[kanidmd_testkit::test(ldap = true)] +async fn test_ldap_application_password_basic(test_env: &AsyncTestEnvironment) { + // Remember, this isn't the exhaustive test for application password behaviours, + // those are in the main server. This is just a basic smoke test that the interfaces + // are exposed and work in a basic manner. + + let rsclient = test_env.rsclient.new_session().unwrap(); + + // Create a person + + rsclient + .auth_simple_password(IDM_ADMIN_TEST_USER, IDM_ADMIN_TEST_PASSWORD) + .await + .expect("Failed to login as admin"); + + #[allow(clippy::expect_used)] + rsclient + .idm_person_account_create(TEST_PERSON, TEST_PERSON) + .await + .expect("Failed to create the user"); + + // Create two applications + + // List, get them. + + // Login as the person + + // Create application passwords + + // Check the work. + + // Check they can't cross talk. + + // Done! + + // let ldap_url = test_env.ldap_url.as_ref().unwrap(); + + // let mut ldap_client = LdapClientBuilder::new(ldap_url).build().await.unwrap(); +}