diff --git a/proto/src/internal/error.rs b/proto/src/internal/error.rs index 0bdd890b1..27d7e40f7 100644 --- a/proto/src/internal/error.rs +++ b/proto/src/internal/error.rs @@ -214,6 +214,7 @@ pub enum OperationError { SC0025UiHintSyntaxInvalid, SC0026Utf8SyntaxInvalid, SC0027ClassSetInvalid, + SC0028CreatedUuidsInvalid, // Migration MG0001InvalidReMigrationLevel, MG0002RaiseDomainLevelExceedsMaximum, @@ -494,6 +495,7 @@ impl OperationError { Self::SC0025UiHintSyntaxInvalid => Some("A SCIM UiHint contained invalid syntax".into()), Self::SC0026Utf8SyntaxInvalid => Some("A SCIM Utf8 String Scope Map contained invalid syntax".into()), Self::SC0027ClassSetInvalid => Some("The internal set of class templates used in this create operation was invalid. THIS IS A BUG.".into()), + Self::SC0028CreatedUuidsInvalid => Some("The internal create query did not return the set of created UUIDs. THIS IS A BUG".into()), Self::UI0001ChallengeSerialisation => Some("The WebAuthn challenge was unable to be serialised.".into()), Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition.".into()), diff --git a/proto/src/scim_v1/client.rs b/proto/src/scim_v1/client.rs index fb32246ff..4866840e6 100644 --- a/proto/src/scim_v1/client.rs +++ b/proto/src/scim_v1/client.rs @@ -32,6 +32,18 @@ pub struct ScimReference { pub value: Option<String>, } +impl<T> From<T> for ScimReference +where + T: AsRef<str>, +{ + fn from(value: T) -> Self { + ScimReference { + uuid: None, + value: Some(value.as_ref().to_string()), + } + } +} + pub type ScimReferences = Vec<ScimReference>; #[serde_as] @@ -82,21 +94,27 @@ pub struct ScimOAuth2ScopeMap { #[serde_as] #[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "snake_case")] pub struct ScimEntryApplicationPost { pub name: String, - pub display_name: String, + pub displayname: String, + pub linked_group: ScimReference, } #[serde_as] #[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "snake_case")] pub struct ScimEntryApplication { #[serde(flatten)] pub header: ScimEntryHeader, pub name: String, - pub display_name: String, + pub displayname: String, + + pub linked_group: Vec<super::ScimReference>, + + #[serde(flatten)] + pub attrs: BTreeMap<Attribute, JsonValue>, } #[derive(Serialize, Debug, Clone)] diff --git a/proto/src/scim_v1/mod.rs b/proto/src/scim_v1/mod.rs index db9c7a8aa..05cab6b67 100644 --- a/proto/src/scim_v1/mod.rs +++ b/proto/src/scim_v1/mod.rs @@ -18,13 +18,13 @@ use crate::attribute::Attribute; use serde::{Deserialize, Serialize}; +use serde_with::formats::CommaSeparator; +use serde_with::{serde_as, skip_serializing_none, StringWithSeparator}; use sshkey_attest::proto::PublicKey as SshPublicKey; use std::collections::BTreeMap; use std::ops::Not; use utoipa::ToSchema; - -use serde_with::formats::CommaSeparator; -use serde_with::{serde_as, skip_serializing_none, StringWithSeparator}; +use uuid::Uuid; pub use self::synch::*; pub use scim_proto::prelude::*; @@ -86,6 +86,13 @@ pub struct ScimSshPublicKey { pub value: SshPublicKey, } +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ScimReference { + pub uuid: Uuid, + pub value: String, +} + #[derive(Deserialize, Serialize, Debug, Clone, ToSchema)] pub enum ScimOauth2ClaimMapJoinChar { #[serde(rename = ",", alias = "csv")] diff --git a/server/core/src/actors/v1_scim.rs b/server/core/src/actors/v1_scim.rs index 6761ba1f2..90a77741a 100644 --- a/server/core/src/actors/v1_scim.rs +++ b/server/core/src/actors/v1_scim.rs @@ -202,7 +202,8 @@ impl QueryServerWriteV1 { e })?; - let scim_create_event = ScimCreateEvent::try_from(ident, classes, entry, &mut idms_prox_write.qs_write)?; + let scim_create_event = + ScimCreateEvent::try_from(ident, classes, entry, &mut idms_prox_write.qs_write)?; idms_prox_write .qs_write diff --git a/server/core/src/https/apidocs/mod.rs b/server/core/src/https/apidocs/mod.rs index 31eca9e55..6e5d40cb9 100644 --- a/server/core/src/https/apidocs/mod.rs +++ b/server/core/src/https/apidocs/mod.rs @@ -78,6 +78,8 @@ impl Modify for SecurityAddon { super::v1_scim::scim_sync_get, super::v1_scim::scim_entry_id_get, super::v1_scim::scim_person_id_get, + super::v1_scim::scim_application_post, + super::v1_scim::scim_application_id_delete, super::v1::schema_get, super::v1::whoami, diff --git a/server/core/src/https/apidocs/tests.rs b/server/core/src/https/apidocs/tests.rs index 5f9aac8eb..5125fc9ef 100644 --- a/server/core/src/https/apidocs/tests.rs +++ b/server/core/src/https/apidocs/tests.rs @@ -43,7 +43,7 @@ fn figure_out_if_we_have_all_the_routes() { .unwrap(); // work our way through the source files in this package looking for routedefs let mut found_routes: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new(); - let walker = walkdir::WalkDir::new(format!("{}/src", env!("CARGO_MANIFEST_DIR"))) + let walker = walkdir::WalkDir::new(format!("{}/src/https", env!("CARGO_MANIFEST_DIR"))) .follow_links(false) .into_iter(); diff --git a/server/core/src/https/v1_scim.rs b/server/core/src/https/v1_scim.rs index c6e7c3d4a..80be42a0b 100644 --- a/server/core/src/https/v1_scim.rs +++ b/server/core/src/https/v1_scim.rs @@ -393,7 +393,7 @@ async fn scim_person_id_get( ), security(("token_jwt" = [])), tag = "scim", - operation_id = "" + operation_id = "scim_application_post" )] async fn scim_application_post( State(state): State<ServerState>, @@ -427,18 +427,19 @@ async fn scim_application_post( ), security(("token_jwt" = [])), tag = "scim", - operation_id = "scim_person_id_get" + operation_id = "scim_application_id_delete" )] 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> { +) -> Result<Json<()>, WebError> { state .qe_w_ref .scim_entry_id_delete(client_auth_info, kopid.eventid, id, EntryClass::Application) .await + .map(Json::from) .map_err(WebError::from) } diff --git a/server/lib/src/event.rs b/server/lib/src/event.rs index e9019693e..692b540d9 100644 --- a/server/lib/src/event.rs +++ b/server/lib/src/event.rs @@ -346,6 +346,8 @@ pub struct CreateEvent { pub entries: Vec<Entry<EntryInit, EntryNew>>, // Is the CreateEvent from an internal or external source? // This may affect which plugins are run ... + /// If true, the list of created entry UUID's will be returned. + pub return_created_uuids: bool, } impl CreateEvent { @@ -363,7 +365,11 @@ impl CreateEvent { // What is the correct consuming iterator here? Can we // even do that? match rentries { - Ok(entries) => Ok(CreateEvent { ident, entries }), + Ok(entries) => Ok(CreateEvent { + ident, + entries, + return_created_uuids: false, + }), Err(e) => Err(e), } } @@ -373,13 +379,18 @@ impl CreateEvent { ident: Identity, entries: Vec<Entry<EntryInit, EntryNew>>, ) -> Self { - CreateEvent { ident, entries } + CreateEvent { + ident, + entries, + return_created_uuids: false, + } } pub fn new_internal(entries: Vec<Entry<EntryInit, EntryNew>>) -> Self { CreateEvent { ident: Identity::from_internal(), entries, + return_created_uuids: false, } } } diff --git a/server/lib/src/plugins/base.rs b/server/lib/src/plugins/base.rs index a143c984a..d14382e0b 100644 --- a/server/lib/src/plugins/base.rs +++ b/server/lib/src/plugins/base.rs @@ -365,7 +365,7 @@ mod tests { let create = vec![e]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -468,7 +468,7 @@ mod tests { let create = vec![e]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, diff --git a/server/lib/src/plugins/cred_import.rs b/server/lib/src/plugins/cred_import.rs index 11de5164c..30ce467ee 100644 --- a/server/lib/src/plugins/cred_import.rs +++ b/server/lib/src/plugins/cred_import.rs @@ -205,7 +205,7 @@ mod tests { let create = vec![e]; - run_create_test!(Ok(()), preload, create, None, |_| {}); + run_create_test!(Ok(None), preload, create, None, |_| {}); } #[test] diff --git a/server/lib/src/plugins/dyngroup.rs b/server/lib/src/plugins/dyngroup.rs index c2d2075fd..c53f3f0c4 100644 --- a/server/lib/src/plugins/dyngroup.rs +++ b/server/lib/src/plugins/dyngroup.rs @@ -464,7 +464,7 @@ mod tests { let create = vec![e_dyn]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -513,7 +513,7 @@ mod tests { let create = vec![e_group]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -562,7 +562,7 @@ mod tests { let create = vec![e_group]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -607,7 +607,7 @@ mod tests { let create = vec![e_dyn, e_group]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, diff --git a/server/lib/src/plugins/eckeygen.rs b/server/lib/src/plugins/eckeygen.rs index b79672eec..6610ea8fb 100644 --- a/server/lib/src/plugins/eckeygen.rs +++ b/server/lib/src/plugins/eckeygen.rs @@ -108,7 +108,7 @@ mod tests { let create = vec![ea]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, diff --git a/server/lib/src/plugins/jwskeygen.rs b/server/lib/src/plugins/jwskeygen.rs index 2221eef35..38ec0d67a 100644 --- a/server/lib/src/plugins/jwskeygen.rs +++ b/server/lib/src/plugins/jwskeygen.rs @@ -136,7 +136,7 @@ mod tests { let create = vec![e]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, diff --git a/server/lib/src/plugins/memberof.rs b/server/lib/src/plugins/memberof.rs index a8b980c9e..845c110bf 100644 --- a/server/lib/src/plugins/memberof.rs +++ b/server/lib/src/plugins/memberof.rs @@ -858,7 +858,7 @@ mod tests { let preload = Vec::with_capacity(0); let create = vec![ea, eb]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -889,7 +889,7 @@ mod tests { let preload = Vec::with_capacity(0); let create = vec![ea, eb, ec]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -941,7 +941,7 @@ mod tests { let preload = Vec::with_capacity(0); let create = vec![ea, eb, ec]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -999,7 +999,7 @@ mod tests { let preload = Vec::with_capacity(0); let create = vec![ea, eb, ec, ed]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, diff --git a/server/lib/src/plugins/namehistory.rs b/server/lib/src/plugins/namehistory.rs index 2645e2413..095942e19 100644 --- a/server/lib/src/plugins/namehistory.rs +++ b/server/lib/src/plugins/namehistory.rs @@ -181,7 +181,7 @@ mod tests { let preload = Vec::with_capacity(0); let create = vec![ea]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, diff --git a/server/lib/src/plugins/refint.rs b/server/lib/src/plugins/refint.rs index bc5f6602f..94e30f9c7 100644 --- a/server/lib/src/plugins/refint.rs +++ b/server/lib/src/plugins/refint.rs @@ -501,7 +501,7 @@ mod tests { let create = vec![eb]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -534,7 +534,7 @@ mod tests { let create = vec![e_group]; run_create_test!( - Ok(()), + Ok(None), preload, create, None, diff --git a/server/lib/src/plugins/spn.rs b/server/lib/src/plugins/spn.rs index 1ba210991..0b5122444 100644 --- a/server/lib/src/plugins/spn.rs +++ b/server/lib/src/plugins/spn.rs @@ -233,7 +233,7 @@ mod tests { let preload = Vec::with_capacity(0); run_create_test!( - Ok(()), + Ok(None), preload, create, None, @@ -286,7 +286,7 @@ mod tests { let preload = Vec::with_capacity(0); run_create_test!( - Ok(()), + Ok(None), preload, create, None, diff --git a/server/lib/src/server/create.rs b/server/lib/src/server/create.rs index c70f2dab5..3465bb361 100644 --- a/server/lib/src/server/create.rs +++ b/server/lib/src/server/create.rs @@ -7,7 +7,7 @@ impl QueryServerWriteTransaction<'_> { /// The create event is a raw, read only representation of the request /// that was made to us, including information about the identity /// performing the request. - pub fn create(&mut self, ce: &CreateEvent) -> Result<(), OperationError> { + pub fn create(&mut self, ce: &CreateEvent) -> Result<Option<Vec<Uuid>>, OperationError> { if !ce.ident.is_internal() { security_info!(name = %ce.ident, "create initiator"); } @@ -174,7 +174,12 @@ impl QueryServerWriteTransaction<'_> { } else { admin_info!("Create operation success"); } - Ok(()) + + if ce.return_created_uuids { + Ok(Some(commit_cand.iter().map(|e| e.get_uuid()).collect())) + } else { + Ok(None) + } } pub fn internal_create( @@ -182,7 +187,7 @@ impl QueryServerWriteTransaction<'_> { entries: Vec<Entry<EntryInit, EntryNew>>, ) -> Result<(), OperationError> { let ce = CreateEvent::new_internal(entries); - self.create(&ce) + self.create(&ce).map(|_| ()) } } diff --git a/server/lib/src/server/scim.rs b/server/lib/src/server/scim.rs index b48c84e3b..eb389c9d6 100644 --- a/server/lib/src/server/scim.rs +++ b/server/lib/src/server/scim.rs @@ -74,12 +74,7 @@ impl ScimCreateEvent { }) .collect::<Result<EntryInitNew, _>>()?; - - let classes = - ValueSetIutf8::from_iter( - classes.iter() - .map(|cls| cls.as_ref()) - ) + let classes = ValueSetIutf8::from_iter(classes.iter().map(|cls| cls.as_ref())) .ok_or(OperationError::SC0027ClassSetInvalid)?; entry.set_ava_set(&Attribute::Class, classes); @@ -179,17 +174,21 @@ impl QueryServerWriteTransaction<'_> { } } - pub fn scim_create(&mut self, scim_create: ScimCreateEvent) -> Result<(), OperationError> { - let ScimCreateEvent { - ident, entry - } = scim_create; + pub fn scim_create( + &mut self, + scim_create: ScimCreateEvent, + ) -> Result<ScimEntryKanidm, OperationError> { + let ScimCreateEvent { ident, entry } = scim_create; let create_event = CreateEvent { ident, entries: vec![entry], + return_created_uuids: true, }; - let mut changed_uuids = self.create(&create_event)?; + let changed_uuids = self.create(&create_event)?; + + let mut changed_uuids = changed_uuids.ok_or(OperationError::SC0028CreatedUuidsInvalid)?; let target = if let Some(target) = changed_uuids.pop() { if !changed_uuids.is_empty() { @@ -214,7 +213,7 @@ impl QueryServerWriteTransaction<'_> { let f_valid = f_intent_valid.clone().into_ignore_hidden(); let se = SearchEvent { - ident, + ident: create_event.ident, filter: f_valid, filter_orig: f_intent_valid, // Return all attributes diff --git a/server/lib/src/valueset/uuid.rs b/server/lib/src/valueset/uuid.rs index 1c3e109e6..761f77c10 100644 --- a/server/lib/src/valueset/uuid.rs +++ b/server/lib/src/valueset/uuid.rs @@ -238,6 +238,13 @@ impl ValueSetScimPut for ValueSetRefer { fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> { use kanidm_proto::scim_v1::client::{ScimReference, ScimReferences}; + // May be a single reference, lets wrap it in an array to proceed. + let value = if !value.is_array() && value.is_object() { + JsonValue::Array(vec![value]) + } else { + value + }; + let scim_refs: ScimReferences = serde_json::from_value(value).map_err(|err| { warn!(?err, "Invalid SCIM reference set syntax"); OperationError::SC0002ReferenceSyntaxInvalid diff --git a/server/testkit/tests/testkit/ldap_basic.rs b/server/testkit/tests/testkit/ldap_basic.rs index 635bb2517..68e126296 100644 --- a/server/testkit/tests/testkit/ldap_basic.rs +++ b/server/testkit/tests/testkit/ldap_basic.rs @@ -1,9 +1,10 @@ +use kanidm_proto::scim_v1::client::{ScimEntryApplicationPost, ScimReference}; use kanidmd_testkit::{AsyncTestEnvironment, IDM_ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_USER}; use ldap3_client::LdapClientBuilder; - -use kanidm_proto::scim_v1::client::ScimEntryApplicationPost; +use tracing::debug; const TEST_PERSON: &str = "user_mcuserton"; +const TEST_GROUP: &str = "group_mcgroupington"; #[kanidmd_testkit::test(ldap = true)] async fn test_ldap_basic_unix_bind(test_env: &AsyncTestEnvironment) { @@ -41,18 +42,26 @@ async fn test_ldap_application_password_basic(test_env: &AsyncTestEnvironment) { .await .expect("Failed to create the user"); + idm_admin_rsclient + .idm_group_create(TEST_GROUP, None) + .await + .expect("Failed to create test group"); + // Create two applications let application_1 = ScimEntryApplicationPost { name: APPLICATION_1_NAME.to_string(), - display_name: APPLICATION_1_NAME.to_string(), + displayname: APPLICATION_1_NAME.to_string(), + linked_group: ScimReference::from(TEST_GROUP), }; - let _application_entry = idm_admin_rsclient + let application_entry = idm_admin_rsclient .idm_application_create(&application_1) .await .expect("Failed to create the user"); + debug!(?application_entry); + // List, get them. // Login as the person @@ -69,11 +78,13 @@ async fn test_ldap_application_password_basic(test_env: &AsyncTestEnvironment) { // let mut ldap_client = LdapClientBuilder::new(ldap_url).build().await.unwrap(); - idm_admin_rsclient + let result = idm_admin_rsclient .idm_application_delete(APPLICATION_1_NAME) .await .expect("Failed to create the user"); + debug!(?result); + // Delete the applications // Check that you can no longer bind.