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();
+}