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 }