From 96f8bdcea328606b3891647d3bf014ab2f12bb80 Mon Sep 17 00:00:00 2001
From: William Brown <william@blackhats.net.au>
Date: Fri, 28 Mar 2025 17:38:49 +1000
Subject: [PATCH] What was ldap turning into scim

---
 libs/client/src/lib.rs                     |   1 +
 libs/client/src/scim.rs                    |   2 -
 proto/src/scim_v1/client.rs                |  27 +++
 server/core/src/actors/v1_scim.rs          |  72 +++++-
 server/core/src/https/v1_scim.rs           |  74 +++++-
 server/lib/src/entry.rs                    |  35 +--
 server/lib/src/server/mod.rs               | 134 -----------
 server/lib/src/server/scim.rs              | 253 ++++++++++++++++++++-
 server/testkit/tests/testkit/ldap_basic.rs |  32 ++-
 9 files changed, 468 insertions(+), 162 deletions(-)

diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs
index f389a1110..74bb73063 100644
--- a/libs/client/src/lib.rs
+++ b/libs/client/src/lib.rs
@@ -50,6 +50,7 @@ use webauthn_rs_proto::{
     PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
 };
 
+mod application;
 mod domain;
 mod group;
 mod oauth;
diff --git a/libs/client/src/scim.rs b/libs/client/src/scim.rs
index 922ad05d8..b3eb67f08 100644
--- a/libs/client/src/scim.rs
+++ b/libs/client/src/scim.rs
@@ -2,12 +2,10 @@ use crate::{ClientError, KanidmClient};
 use kanidm_proto::scim_v1::{ScimEntryGeneric, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState};
 
 impl KanidmClient {
-    // TODO: testing for this
     pub async fn scim_v1_sync_status(&self) -> Result<ScimSyncState, ClientError> {
         self.perform_get_request("/scim/v1/Sync").await
     }
 
-    // TODO: testing for this
     pub async fn scim_v1_sync_update(
         &self,
         scim_sync_request: &ScimSyncRequest,
diff --git a/proto/src/scim_v1/client.rs b/proto/src/scim_v1/client.rs
index ff2dd07fc..fb32246ff 100644
--- a/proto/src/scim_v1/client.rs
+++ b/proto/src/scim_v1/client.rs
@@ -2,6 +2,7 @@
 use super::ScimEntryGetQuery;
 use super::ScimOauth2ClaimMapJoinChar;
 use crate::attribute::{Attribute, SubAttribute};
+use scim_proto::ScimEntryHeader;
 use serde::{Deserialize, Serialize};
 use serde_json::Value as JsonValue;
 use serde_with::formats::PreferMany;
@@ -79,6 +80,25 @@ pub struct ScimOAuth2ScopeMap {
     pub scopes: BTreeSet<String>,
 }
 
+#[serde_as]
+#[derive(Serialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct ScimEntryApplicationPost {
+    pub name: String,
+    pub display_name: String,
+}
+
+#[serde_as]
+#[derive(Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct ScimEntryApplication {
+    #[serde(flatten)]
+    pub header: ScimEntryHeader,
+
+    pub name: String,
+    pub display_name: String,
+}
+
 #[derive(Serialize, Debug, Clone)]
 pub struct ScimEntryPutKanidm {
     pub id: Uuid,
@@ -90,6 +110,13 @@ pub struct ScimEntryPutKanidm {
 #[derive(Deserialize, Serialize, Debug, Clone)]
 pub struct ScimStrings(#[serde_as(as = "OneOrMany<_, PreferMany>")] pub Vec<String>);
 
+#[derive(Debug, Clone, Deserialize, Default)]
+pub struct ScimEntryPostGeneric {
+    /// Create an attribute to contain the following value state.
+    #[serde(flatten)]
+    pub attrs: BTreeMap<Attribute, JsonValue>,
+}
+
 #[derive(Debug, Clone, Deserialize, Default)]
 pub struct ScimEntryPutGeneric {
     // id is only used to target the entry in question
diff --git a/server/core/src/actors/v1_scim.rs b/server/core/src/actors/v1_scim.rs
index e6cffa91c..e8326f317 100644
--- a/server/core/src/actors/v1_scim.rs
+++ b/server/core/src/actors/v1_scim.rs
@@ -1,10 +1,14 @@
 use super::{QueryServerReadV1, QueryServerWriteV1};
 use kanidm_proto::scim_v1::{
-    client::ScimFilter, server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
+    client::ScimEntryPostGeneric, client::ScimFilter, server::ScimEntryKanidm, ScimEntryGetQuery,
+    ScimSyncRequest, ScimSyncState,
 };
 use kanidmd_lib::idm::scim::{
     GenerateScimSyncTokenEvent, ScimSyncFinaliseEvent, ScimSyncTerminateEvent, ScimSyncUpdateEvent,
 };
+
+use kanidmd_lib::server::scim::{ScimCreateEvent, ScimDeleteEvent};
+
 use kanidmd_lib::idm::server::IdmServerTransaction;
 use kanidmd_lib::prelude::*;
 
@@ -176,6 +180,72 @@ impl QueryServerWriteV1 {
             .scim_sync_apply(&sse, &changes, ct)
             .and_then(|r| idms_prox_write.commit().map(|_| r))
     }
+
+    #[instrument(
+        level = "info",
+        skip_all,
+        fields(uuid = ?eventid)
+    )]
+    pub async fn scim_entry_create(
+        &self,
+        client_auth_info: ClientAuthInfo,
+        eventid: Uuid,
+        classes: &[EntryClass],
+        entry: ScimEntryPostGeneric,
+    ) -> Result<ScimEntryKanidm, OperationError> {
+        let ct = duration_from_epoch_now();
+        let mut idms_prox_write = self.idms.proxy_write(ct).await?;
+        let ident = idms_prox_write
+            .validate_client_auth_info_to_ident(client_auth_info, ct)
+            .map_err(|e| {
+                admin_error!(err = ?e, "Invalid identity");
+                e
+            })?;
+
+        let scim_create_event = ScimCreateEvent::try_from(ident, classes, entry, idms_prox_write)?;
+
+        idms_prox_write
+            .qs_write
+            .scim_create(scim_create_event)
+            .and_then(|r| idms_prox_write.commit().map(|_| r))
+    }
+
+    #[instrument(
+        level = "info",
+        skip_all,
+        fields(uuid = ?eventid)
+    )]
+    pub async fn scim_entry_id_delete(
+        &self,
+        client_auth_info: ClientAuthInfo,
+        eventid: Uuid,
+        uuid_or_name: String,
+        class: EntryClass,
+    ) -> Result<(), OperationError> {
+        let ct = duration_from_epoch_now();
+        let mut idms_prox_write = self.idms.proxy_write(ct).await?;
+        let ident = idms_prox_write
+            .validate_client_auth_info_to_ident(client_auth_info, ct)
+            .map_err(|e| {
+                admin_error!(err = ?e, "Invalid identity");
+                e
+            })?;
+
+        let target = idms_prox_write
+            .qs_write
+            .name_to_uuid(uuid_or_name.as_str())
+            .map_err(|e| {
+                admin_error!(err = ?e, "Error resolving id to target");
+                e
+            })?;
+
+        let scim_delete_event = ScimDeleteEvent::new(ident, target, class);
+
+        idms_prox_write
+            .qs_write
+            .scim_delete(scim_delete_event)
+            .and_then(|r| idms_prox_write.commit().map(|_| r))
+    }
 }
 
 impl QueryServerReadV1 {
diff --git a/server/core/src/https/v1_scim.rs b/server/core/src/https/v1_scim.rs
index 35ba8da92..c6e7c3d4a 100644
--- a/server/core/src/https/v1_scim.rs
+++ b/server/core/src/https/v1_scim.rs
@@ -9,10 +9,11 @@ use super::ServerState;
 use crate::https::extractors::VerifiedClientInformation;
 use axum::extract::{rejection::JsonRejection, DefaultBodyLimit, Path, Query, State};
 use axum::response::{Html, IntoResponse, Response};
-use axum::routing::{get, post};
+use axum::routing::{delete, get, post};
 use axum::{Extension, Json, Router};
 use kanidm_proto::scim_v1::{
-    server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
+    client::ScimEntryPostGeneric, server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest,
+    ScimSyncState,
 };
 use kanidm_proto::v1::Entry as ProtoEntry;
 use kanidmd_lib::prelude::*;
@@ -383,6 +384,64 @@ async fn scim_person_id_get(
         .map_err(WebError::from)
 }
 
+#[utoipa::path(
+    post,
+    path = "/scim/v1/Application",
+    responses(
+        (status = 200, content_type="application/json", body=ScimEntry),
+        ApiResponseWithout200,
+    ),
+    security(("token_jwt" = [])),
+    tag = "scim",
+    operation_id = ""
+)]
+async fn scim_application_post(
+    State(state): State<ServerState>,
+    Extension(kopid): Extension<KOpId>,
+    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
+    Json(entry_post): Json<ScimEntryPostGeneric>,
+) -> Result<Json<ScimEntryKanidm>, WebError> {
+    state
+        .qe_w_ref
+        .scim_entry_create(
+            client_auth_info,
+            kopid.eventid,
+            &[
+                EntryClass::Account,
+                EntryClass::ServiceAccount,
+                EntryClass::Application,
+            ],
+            entry_post,
+        )
+        .await
+        .map(Json::from)
+        .map_err(WebError::from)
+}
+
+#[utoipa::path(
+    delete,
+    path = "/scim/v1/Application/{id}",
+    responses(
+        (status = 200, content_type="application/json"),
+        ApiResponseWithout200,
+    ),
+    security(("token_jwt" = [])),
+    tag = "scim",
+    operation_id = "scim_person_id_get"
+)]
+async fn scim_application_id_delete(
+    State(state): State<ServerState>,
+    Path(id): Path<String>,
+    Extension(kopid): Extension<KOpId>,
+    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
+) -> Result<(), WebError> {
+    state
+        .qe_w_ref
+        .scim_entry_id_delete(client_auth_info, kopid.eventid, id, EntryClass::Application)
+        .await
+        .map_err(WebError::from)
+}
+
 pub fn route_setup() -> Router<ServerState> {
     Router::new()
         .route(
@@ -486,6 +545,17 @@ pub fn route_setup() -> Router<ServerState> {
         //
         //                            POST                   Send a sync update
         //
+        //
+        //  Application   /Application     Post              Create a new application
+        //
+        .route("/scim/v1/Application", post(scim_application_post))
+        //  Application   /Application/{id}     Delete      Delete the application identified by id
+        //
+        .route(
+            "/scim/v1/Application/:id",
+            delete(scim_application_id_delete),
+        )
+        // Synchronisation routes.
         .route(
             "/scim/v1/Sync",
             post(scim_sync_post)
diff --git a/server/lib/src/entry.rs b/server/lib/src/entry.rs
index aac8a519a..6ac88d3e0 100644
--- a/server/lib/src/entry.rs
+++ b/server/lib/src/entry.rs
@@ -24,11 +24,6 @@
 //! [`filter`]: ../filter/index.html
 //! [`schema`]: ../schema/index.html
 
-use std::cmp::Ordering;
-pub use std::collections::BTreeSet as Set;
-use std::collections::{BTreeMap as Map, BTreeMap, BTreeSet};
-use std::sync::Arc;
-
 use crate::be::dbentry::{DbEntry, DbEntryVers};
 use crate::be::dbvalue::DbValueSetV2;
 use crate::be::{IdxKey, IdxSlope};
@@ -41,7 +36,13 @@ use crate::prelude::*;
 use crate::repl::cid::Cid;
 use crate::repl::entry::EntryChangeState;
 use crate::repl::proto::{ReplEntryV1, ReplIncrementalEntryV1};
+use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
 use crate::server::access::AccessEffectivePermission;
+use crate::value::{
+    ApiToken, CredentialType, IndexType, IntentTokenState, Oauth2Session, PartialValue, Session,
+    SyntaxType, Value,
+};
+use crate::valueset::{self, ScimResolveStatus, ValueSet};
 use compact_jwt::JwsEs256Signer;
 use hashbrown::{HashMap, HashSet};
 use kanidm_proto::internal::ImageValue;
@@ -53,6 +54,10 @@ use kanidm_proto::v1::Entry as ProtoEntry;
 use ldap3_proto::simple::{LdapPartialAttribute, LdapSearchResultEntry};
 use openssl::ec::EcKey;
 use openssl::pkey::{Private, Public};
+use std::cmp::Ordering;
+pub use std::collections::BTreeSet as Set;
+use std::collections::{BTreeMap as Map, BTreeMap, BTreeSet};
+use std::sync::Arc;
 use time::OffsetDateTime;
 use tracing::trace;
 use uuid::Uuid;
@@ -60,13 +65,6 @@ use webauthn_rs::prelude::{
     AttestationCaList, AttestedPasskey as AttestedPasskeyV4, Passkey as PasskeyV4,
 };
 
-use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
-use crate::value::{
-    ApiToken, CredentialType, IndexType, IntentTokenState, Oauth2Session, PartialValue, Session,
-    SyntaxType, Value,
-};
-use crate::valueset::{self, ScimResolveStatus, ValueSet};
-
 pub type EntryInitNew = Entry<EntryInit, EntryNew>;
 pub type EntryInvalidNew = Entry<EntryInvalid, EntryNew>;
 pub type EntryRefreshNew = Entry<EntryRefresh, EntryNew>;
@@ -285,6 +283,18 @@ impl Default for Entry<EntryInit, EntryNew> {
     }
 }
 
+impl FromIterator<(Attribute, ValueSet)> for EntryInitNew {
+    fn from_iter<I: IntoIterator<Item = (Attribute, ValueSet)>>(iter: I) -> Self {
+        let attrs = Eattrs::from_iter(iter);
+
+        Entry {
+            valid: EntryInit,
+            state: EntryNew,
+            attrs,
+        }
+    }
+}
+
 impl Entry<EntryInit, EntryNew> {
     pub fn new() -> Self {
         Entry {
@@ -292,7 +302,6 @@ impl Entry<EntryInit, EntryNew> {
             valid: EntryInit,
             state: EntryNew,
             attrs: Map::new(),
-            // attrs: Map::with_capacity(32),
         }
     }
 
diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs
index 29a962958..ea82ed0c0 100644
--- a/server/lib/src/server/mod.rs
+++ b/server/lib/src/server/mod.rs
@@ -28,8 +28,6 @@ use crate::schema::{
     SchemaWriteTransaction,
 };
 use crate::value::{CredentialType, EXTRACT_VAL_DN};
-use crate::valueset::uuid_to_proto_string;
-use crate::valueset::ScimValueIntermediate;
 use crate::valueset::*;
 use concread::arcache::{ARCacheBuilder, ARCacheReadTxn, ARCacheWriteTxn};
 use concread::cowcell::*;
@@ -1004,138 +1002,6 @@ pub trait QueryServerTransaction<'a> {
         }
     }
 
-    fn resolve_scim_json_put(
-        &mut self,
-        attr: &Attribute,
-        value: Option<JsonValue>,
-    ) -> Result<Option<ValueSet>, OperationError> {
-        let schema = self.get_schema();
-        // Lookup the attr
-        let Some(schema_a) = schema.get_attributes().get(attr) else {
-            // No attribute of this name exists - fail fast, there is no point to
-            // proceed, as nothing can be satisfied.
-            return Err(OperationError::InvalidAttributeName(attr.to_string()));
-        };
-
-        let Some(value) = value else {
-            // It's a none so the value needs to be unset, and the attr DOES exist in
-            // schema.
-            return Ok(None);
-        };
-
-        let resolve_status = match schema_a.syntax {
-            SyntaxType::Utf8String => ValueSetUtf8::from_scim_json_put(value),
-            SyntaxType::Utf8StringInsensitive => ValueSetIutf8::from_scim_json_put(value),
-            SyntaxType::Uuid => ValueSetUuid::from_scim_json_put(value),
-            SyntaxType::Boolean => ValueSetBool::from_scim_json_put(value),
-            SyntaxType::SyntaxId => ValueSetSyntax::from_scim_json_put(value),
-            SyntaxType::IndexId => ValueSetIndex::from_scim_json_put(value),
-            SyntaxType::ReferenceUuid => ValueSetRefer::from_scim_json_put(value),
-            SyntaxType::Utf8StringIname => ValueSetIname::from_scim_json_put(value),
-            SyntaxType::NsUniqueId => ValueSetNsUniqueId::from_scim_json_put(value),
-            SyntaxType::DateTime => ValueSetDateTime::from_scim_json_put(value),
-            SyntaxType::EmailAddress => ValueSetEmailAddress::from_scim_json_put(value),
-            SyntaxType::Url => ValueSetUrl::from_scim_json_put(value),
-            SyntaxType::OauthScope => ValueSetOauthScope::from_scim_json_put(value),
-            SyntaxType::OauthScopeMap => ValueSetOauthScopeMap::from_scim_json_put(value),
-            SyntaxType::OauthClaimMap => ValueSetOauthClaimMap::from_scim_json_put(value),
-            SyntaxType::UiHint => ValueSetUiHint::from_scim_json_put(value),
-            SyntaxType::CredentialType => ValueSetCredentialType::from_scim_json_put(value),
-            SyntaxType::Certificate => ValueSetCertificate::from_scim_json_put(value),
-            SyntaxType::SshKey => ValueSetSshKey::from_scim_json_put(value),
-            SyntaxType::Uint32 => ValueSetUint32::from_scim_json_put(value),
-
-            // Not Yet ... if ever
-            // SyntaxType::JsonFilter => ValueSetJsonFilter::from_scim_json_put(value),
-            SyntaxType::JsonFilter => Err(OperationError::InvalidAttribute(
-                "Json Filters are not able to be set.".to_string(),
-            )),
-            // Can't be set currently as these are only internally generated for key-id's
-            // SyntaxType::HexString => ValueSetHexString::from_scim_json_put(value),
-            SyntaxType::HexString => Err(OperationError::InvalidAttribute(
-                "Hex strings are not able to be set.".to_string(),
-            )),
-
-            // Can't be set until we have better error handling in the set paths
-            // SyntaxType::Image => ValueSetImage::from_scim_json_put(value),
-            SyntaxType::Image => Err(OperationError::InvalidAttribute(
-                "Images are not able to be set.".to_string(),
-            )),
-
-            // Can't be set yet, mostly as I'm lazy
-            // SyntaxType::WebauthnAttestationCaList => {
-            //    ValueSetWebauthnAttestationCaList::from_scim_json_put(value)
-            // }
-            SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
-                "Webauthn Attestation Ca Lists are not able to be set.".to_string(),
-            )),
-
-            // Syntax types that can not be submitted
-            SyntaxType::Credential => Err(OperationError::InvalidAttribute(
-                "Credentials are not able to be set.".to_string(),
-            )),
-            SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute(
-                "Secrets are not able to be set.".to_string(),
-            )),
-            SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute(
-                "SPNs are not able to be set.".to_string(),
-            )),
-            SyntaxType::Cid => Err(OperationError::InvalidAttribute(
-                "CIDs are not able to be set.".to_string(),
-            )),
-            SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute(
-                "Private Binaries are not able to be set.".to_string(),
-            )),
-            SyntaxType::IntentToken => Err(OperationError::InvalidAttribute(
-                "Intent Tokens are not able to be set.".to_string(),
-            )),
-            SyntaxType::Passkey => Err(OperationError::InvalidAttribute(
-                "Passkeys are not able to be set.".to_string(),
-            )),
-            SyntaxType::AttestedPasskey => Err(OperationError::InvalidAttribute(
-                "Attested Passkeys are not able to be set.".to_string(),
-            )),
-            SyntaxType::Session => Err(OperationError::InvalidAttribute(
-                "Sessions are not able to be set.".to_string(),
-            )),
-            SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute(
-                "Jws ES256 Private Keys are not able to be set.".to_string(),
-            )),
-            SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute(
-                "Jws RS256 Private Keys are not able to be set.".to_string(),
-            )),
-            SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute(
-                "Sessions are not able to be set.".to_string(),
-            )),
-            SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute(
-                "TOTP Secrets are not able to be set.".to_string(),
-            )),
-            SyntaxType::ApiToken => Err(OperationError::InvalidAttribute(
-                "API Tokens are not able to be set.".to_string(),
-            )),
-            SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute(
-                "Audit Strings are not able to be set.".to_string(),
-            )),
-            SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute(
-                "EC Private Keys are not able to be set.".to_string(),
-            )),
-            SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute(
-                "Key Internal Structures are not able to be set.".to_string(),
-            )),
-            SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute(
-                "Application Passwords are not able to be set.".to_string(),
-            )),
-        }?;
-
-        match resolve_status {
-            ValueSetResolveStatus::Resolved(vs) => Ok(vs),
-            ValueSetResolveStatus::NeedsResolution(vs_inter) => {
-                self.resolve_valueset_intermediate(vs_inter)
-            }
-        }
-        .map(Some)
-    }
-
     fn resolve_valueset_intermediate(
         &mut self,
         vs_inter: ValueSetIntermediate,
diff --git a/server/lib/src/server/scim.rs b/server/lib/src/server/scim.rs
index be529e8be..4d3e48122 100644
--- a/server/lib/src/server/scim.rs
+++ b/server/lib/src/server/scim.rs
@@ -1,23 +1,27 @@
 use crate::prelude::*;
+use crate::schema::{SchemaAttribute, SchemaTransaction};
 use crate::server::batch_modify::{BatchModifyEvent, ModSetValid};
-use kanidm_proto::scim_v1::client::ScimEntryPutGeneric;
+use crate::server::ValueSetResolveStatus;
+use crate::valueset::*;
+use kanidm_proto::scim_v1::client::{ScimEntryPostGeneric, ScimEntryPutGeneric};
+use kanidm_proto::scim_v1::JsonValue;
 use std::collections::BTreeMap;
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 pub struct ScimEntryPutEvent {
     /// The identity performing the change.
-    pub ident: Identity,
+    pub(crate) ident: Identity,
 
     // future - etags to detect version changes.
     /// The target entry that will be changed
-    pub target: Uuid,
+    pub(crate) target: Uuid,
     /// Update an attribute to contain the following value state.
     /// If the attribute is None, it is removed.
-    pub attrs: BTreeMap<Attribute, Option<ValueSet>>,
+    pub(crate) attrs: BTreeMap<Attribute, Option<ValueSet>>,
 
     /// If an effective access check should be carried out post modification
     /// of the entries
-    pub effective_access_check: bool,
+    pub(crate) effective_access_check: bool,
 }
 
 impl ScimEntryPutEvent {
@@ -48,6 +52,55 @@ impl ScimEntryPutEvent {
     }
 }
 
+#[derive(Debug)]
+pub struct ScimCreateEvent {
+    pub(crate) ident: Identity,
+    pub(crate) entry: EntryInitNew,
+}
+
+impl ScimCreateEvent {
+    pub fn try_from(
+        ident: Identity,
+        classes: &[EntryClass],
+        entry: ScimEntryPostGeneric,
+        qs: &mut QueryServerWriteTransaction,
+    ) -> Result<Self, OperationError> {
+        let entry = entry
+            .attrs
+            .into_iter()
+            .map(|(attr, json_value)| {
+                qs.resolve_scim_json_post(&attr, json_value)
+                    .map(|kani_value| (attr, kani_value))
+            })
+            .collect::<Result<_, _>>()?;
+
+        Ok(ScimCreateEvent { ident, entry })
+    }
+}
+
+#[derive(Debug)]
+pub struct ScimDeleteEvent {
+    /// The identity performing the change.
+    pub(crate) ident: Identity,
+
+    // future - etags to detect version changes.
+    /// The target entry that will be changed
+    pub(crate) target: Uuid,
+
+    /// The class of the target entry.
+    pub(crate) class: EntryClass,
+}
+
+impl ScimDeleteEvent {
+    pub fn new(ident: Identity, target: Uuid, class: EntryClass) -> Self {
+        ScimDeleteEvent {
+            ident,
+            target,
+            class,
+        }
+    }
+}
+
 impl QueryServerWriteTransaction<'_> {
     /// SCIM PUT is the handler where a single entry is updated. In a SCIM PUT request
     /// the request defines the state of an attribute in entirety for the update. This
@@ -115,6 +168,194 @@ impl QueryServerWriteTransaction<'_> {
             }
         }
     }
+
+    pub fn scim_create(&mut self, _scim_create: ScimCreateEvent) -> Result<(), OperationError> {
+        todo!();
+    }
+
+    pub fn scim_delete(&mut self, scim_delete: ScimDeleteEvent) -> Result<(), OperationError> {
+        let ScimDeleteEvent {
+            ident,
+            target,
+            class,
+        } = scim_delete;
+
+        let filter_intent = filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(target)));
+        let f_intent_valid = filter_intent
+            .validate(self.get_schema())
+            .map_err(OperationError::SchemaViolation)?;
+
+        let filter = filter!(f_and!([
+            f_eq(Attribute::Uuid, PartialValue::Uuid(target)),
+            f_eq(Attribute::Class, class.into())
+        ]));
+        let f_valid = filter
+            .validate(self.get_schema())
+            .map_err(OperationError::SchemaViolation)?;
+
+        let de = DeleteEvent {
+            ident,
+            filter: f_valid,
+            filter_orig: f_intent_valid,
+        };
+
+        self.delete(&de)
+    }
+
+    pub(crate) fn resolve_scim_json_put(
+        &mut self,
+        attr: &Attribute,
+        value: Option<JsonValue>,
+    ) -> Result<Option<ValueSet>, OperationError> {
+        let schema = self.get_schema();
+        // Lookup the attr
+        let Some(schema_a) = schema.get_attributes().get(attr) else {
+            // No attribute of this name exists - fail fast, there is no point to
+            // proceed, as nothing can be satisfied.
+            return Err(OperationError::InvalidAttributeName(attr.to_string()));
+        };
+
+        let Some(value) = value else {
+            // It's a none so the value needs to be unset, and the attr DOES exist in
+            // schema.
+            return Ok(None);
+        };
+
+        self.resolve_scim_json(schema_a, value).map(Some)
+    }
+
+    pub(crate) fn resolve_scim_json_post(
+        &mut self,
+        attr: &Attribute,
+        value: JsonValue,
+    ) -> Result<ValueSet, OperationError> {
+        let schema = self.get_schema();
+        // Lookup the attr
+        let Some(schema_a) = schema.get_attributes().get(attr) else {
+            // No attribute of this name exists - fail fast, there is no point to
+            // proceed, as nothing can be satisfied.
+            return Err(OperationError::InvalidAttributeName(attr.to_string()));
+        };
+
+        self.resolve_scim_json(schema_a, value)
+    }
+
+    fn resolve_scim_json(
+        &mut self,
+        schema_a: &SchemaAttribute,
+        value: JsonValue,
+    ) -> Result<ValueSet, OperationError> {
+        let resolve_status = match schema_a.syntax {
+            SyntaxType::Utf8String => ValueSetUtf8::from_scim_json_put(value),
+            SyntaxType::Utf8StringInsensitive => ValueSetIutf8::from_scim_json_put(value),
+            SyntaxType::Uuid => ValueSetUuid::from_scim_json_put(value),
+            SyntaxType::Boolean => ValueSetBool::from_scim_json_put(value),
+            SyntaxType::SyntaxId => ValueSetSyntax::from_scim_json_put(value),
+            SyntaxType::IndexId => ValueSetIndex::from_scim_json_put(value),
+            SyntaxType::ReferenceUuid => ValueSetRefer::from_scim_json_put(value),
+            SyntaxType::Utf8StringIname => ValueSetIname::from_scim_json_put(value),
+            SyntaxType::NsUniqueId => ValueSetNsUniqueId::from_scim_json_put(value),
+            SyntaxType::DateTime => ValueSetDateTime::from_scim_json_put(value),
+            SyntaxType::EmailAddress => ValueSetEmailAddress::from_scim_json_put(value),
+            SyntaxType::Url => ValueSetUrl::from_scim_json_put(value),
+            SyntaxType::OauthScope => ValueSetOauthScope::from_scim_json_put(value),
+            SyntaxType::OauthScopeMap => ValueSetOauthScopeMap::from_scim_json_put(value),
+            SyntaxType::OauthClaimMap => ValueSetOauthClaimMap::from_scim_json_put(value),
+            SyntaxType::UiHint => ValueSetUiHint::from_scim_json_put(value),
+            SyntaxType::CredentialType => ValueSetCredentialType::from_scim_json_put(value),
+            SyntaxType::Certificate => ValueSetCertificate::from_scim_json_put(value),
+            SyntaxType::SshKey => ValueSetSshKey::from_scim_json_put(value),
+            SyntaxType::Uint32 => ValueSetUint32::from_scim_json_put(value),
+
+            // Not Yet ... if ever
+            // SyntaxType::JsonFilter => ValueSetJsonFilter::from_scim_json_put(value),
+            SyntaxType::JsonFilter => Err(OperationError::InvalidAttribute(
+                "Json Filters are not able to be set.".to_string(),
+            )),
+            // Can't be set currently as these are only internally generated for key-id's
+            // SyntaxType::HexString => ValueSetHexString::from_scim_json_put(value),
+            SyntaxType::HexString => Err(OperationError::InvalidAttribute(
+                "Hex strings are not able to be set.".to_string(),
+            )),
+
+            // Can't be set until we have better error handling in the set paths
+            // SyntaxType::Image => ValueSetImage::from_scim_json_put(value),
+            SyntaxType::Image => Err(OperationError::InvalidAttribute(
+                "Images are not able to be set.".to_string(),
+            )),
+
+            // Can't be set yet, mostly as I'm lazy
+            // SyntaxType::WebauthnAttestationCaList => {
+            //    ValueSetWebauthnAttestationCaList::from_scim_json_put(value)
+            // }
+            SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
+                "Webauthn Attestation Ca Lists are not able to be set.".to_string(),
+            )),
+
+            // Syntax types that can not be submitted
+            SyntaxType::Credential => Err(OperationError::InvalidAttribute(
+                "Credentials are not able to be set.".to_string(),
+            )),
+            SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute(
+                "Secrets are not able to be set.".to_string(),
+            )),
+            SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute(
+                "SPNs are not able to be set.".to_string(),
+            )),
+            SyntaxType::Cid => Err(OperationError::InvalidAttribute(
+                "CIDs are not able to be set.".to_string(),
+            )),
+            SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute(
+                "Private Binaries are not able to be set.".to_string(),
+            )),
+            SyntaxType::IntentToken => Err(OperationError::InvalidAttribute(
+                "Intent Tokens are not able to be set.".to_string(),
+            )),
+            SyntaxType::Passkey => Err(OperationError::InvalidAttribute(
+                "Passkeys are not able to be set.".to_string(),
+            )),
+            SyntaxType::AttestedPasskey => Err(OperationError::InvalidAttribute(
+                "Attested Passkeys are not able to be set.".to_string(),
+            )),
+            SyntaxType::Session => Err(OperationError::InvalidAttribute(
+                "Sessions are not able to be set.".to_string(),
+            )),
+            SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute(
+                "Jws ES256 Private Keys are not able to be set.".to_string(),
+            )),
+            SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute(
+                "Jws RS256 Private Keys are not able to be set.".to_string(),
+            )),
+            SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute(
+                "Sessions are not able to be set.".to_string(),
+            )),
+            SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute(
+                "TOTP Secrets are not able to be set.".to_string(),
+            )),
+            SyntaxType::ApiToken => Err(OperationError::InvalidAttribute(
+                "API Tokens are not able to be set.".to_string(),
+            )),
+            SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute(
+                "Audit Strings are not able to be set.".to_string(),
+            )),
+            SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute(
+                "EC Private Keys are not able to be set.".to_string(),
+            )),
+            SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute(
+                "Key Internal Structures are not able to be set.".to_string(),
+            )),
+            SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute(
+                "Application Passwords are not able to be set.".to_string(),
+            )),
+        }?;
+
+        match resolve_status {
+            ValueSetResolveStatus::Resolved(vs) => Ok(vs),
+            ValueSetResolveStatus::NeedsResolution(vs_inter) => {
+                self.resolve_valueset_intermediate(vs_inter)
+            }
+        }
+    }
 }
 
 #[cfg(test)]
diff --git a/server/testkit/tests/testkit/ldap_basic.rs b/server/testkit/tests/testkit/ldap_basic.rs
index 8ef66bcbc..635bb2517 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, IDM_ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_USER};
 use ldap3_client::LdapClientBuilder;
 
+use kanidm_proto::scim_v1::client::ScimEntryApplicationPost;
+
 const TEST_PERSON: &str = "user_mcuserton";
 
 #[kanidmd_testkit::test(ldap = true)]
@@ -19,27 +21,38 @@ async fn test_ldap_basic_unix_bind(test_env: &AsyncTestEnvironment) {
 
 #[kanidmd_testkit::test(ldap = true)]
 async fn test_ldap_application_password_basic(test_env: &AsyncTestEnvironment) {
+    const APPLICATION_1_NAME: &str = "test_application_1";
+
     // 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();
+    let idm_admin_rsclient = test_env.rsclient.new_session().unwrap();
 
     // Create a person
 
-    rsclient
+    idm_admin_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_admin_rsclient
         .idm_person_account_create(TEST_PERSON, TEST_PERSON)
         .await
         .expect("Failed to create the user");
 
     // Create two applications
 
+    let application_1 = ScimEntryApplicationPost {
+        name: APPLICATION_1_NAME.to_string(),
+        display_name: APPLICATION_1_NAME.to_string(),
+    };
+
+    let _application_entry = idm_admin_rsclient
+        .idm_application_create(&application_1)
+        .await
+        .expect("Failed to create the user");
+
     // List, get them.
 
     // Login as the person
@@ -55,4 +68,15 @@ async fn test_ldap_application_password_basic(test_env: &AsyncTestEnvironment) {
     // let ldap_url = test_env.ldap_url.as_ref().unwrap();
 
     // let mut ldap_client = LdapClientBuilder::new(ldap_url).build().await.unwrap();
+
+    idm_admin_rsclient
+        .idm_application_delete(APPLICATION_1_NAME)
+        .await
+        .expect("Failed to create the user");
+
+    // Delete the applications
+
+    // Check that you can no longer bind.
+
+    // They no longer list
 }