This commit is contained in:
William Brown 2025-03-29 13:59:46 +10:00
parent 298ce0c9ce
commit 0fe42f62bd
21 changed files with 116 additions and 52 deletions

View file

@ -214,6 +214,7 @@ pub enum OperationError {
SC0025UiHintSyntaxInvalid, SC0025UiHintSyntaxInvalid,
SC0026Utf8SyntaxInvalid, SC0026Utf8SyntaxInvalid,
SC0027ClassSetInvalid, SC0027ClassSetInvalid,
SC0028CreatedUuidsInvalid,
// Migration // Migration
MG0001InvalidReMigrationLevel, MG0001InvalidReMigrationLevel,
MG0002RaiseDomainLevelExceedsMaximum, MG0002RaiseDomainLevelExceedsMaximum,
@ -494,6 +495,7 @@ impl OperationError {
Self::SC0025UiHintSyntaxInvalid => Some("A SCIM UiHint contained invalid syntax".into()), Self::SC0025UiHintSyntaxInvalid => Some("A SCIM UiHint contained invalid syntax".into()),
Self::SC0026Utf8SyntaxInvalid => Some("A SCIM Utf8 String Scope Map 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::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::UI0001ChallengeSerialisation => Some("The WebAuthn challenge was unable to be serialised.".into()),
Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition.".into()), Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition.".into()),

View file

@ -32,6 +32,18 @@ pub struct ScimReference {
pub value: Option<String>, 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>; pub type ScimReferences = Vec<ScimReference>;
#[serde_as] #[serde_as]
@ -82,21 +94,27 @@ pub struct ScimOAuth2ScopeMap {
#[serde_as] #[serde_as]
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "snake_case")]
pub struct ScimEntryApplicationPost { pub struct ScimEntryApplicationPost {
pub name: String, pub name: String,
pub display_name: String, pub displayname: String,
pub linked_group: ScimReference,
} }
#[serde_as] #[serde_as]
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "snake_case")]
pub struct ScimEntryApplication { pub struct ScimEntryApplication {
#[serde(flatten)] #[serde(flatten)]
pub header: ScimEntryHeader, pub header: ScimEntryHeader,
pub name: String, 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)] #[derive(Serialize, Debug, Clone)]

View file

@ -18,13 +18,13 @@
use crate::attribute::Attribute; use crate::attribute::Attribute;
use serde::{Deserialize, Serialize}; 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 sshkey_attest::proto::PublicKey as SshPublicKey;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Not; use std::ops::Not;
use utoipa::ToSchema; use utoipa::ToSchema;
use uuid::Uuid;
use serde_with::formats::CommaSeparator;
use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
pub use self::synch::*; pub use self::synch::*;
pub use scim_proto::prelude::*; pub use scim_proto::prelude::*;
@ -86,6 +86,13 @@ pub struct ScimSshPublicKey {
pub value: SshPublicKey, 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)] #[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
pub enum ScimOauth2ClaimMapJoinChar { pub enum ScimOauth2ClaimMapJoinChar {
#[serde(rename = ",", alias = "csv")] #[serde(rename = ",", alias = "csv")]

View file

@ -202,7 +202,8 @@ impl QueryServerWriteV1 {
e 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 idms_prox_write
.qs_write .qs_write

View file

@ -78,6 +78,8 @@ impl Modify for SecurityAddon {
super::v1_scim::scim_sync_get, super::v1_scim::scim_sync_get,
super::v1_scim::scim_entry_id_get, super::v1_scim::scim_entry_id_get,
super::v1_scim::scim_person_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::schema_get,
super::v1::whoami, super::v1::whoami,

View file

@ -43,7 +43,7 @@ fn figure_out_if_we_have_all_the_routes() {
.unwrap(); .unwrap();
// work our way through the source files in this package looking for routedefs // 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 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) .follow_links(false)
.into_iter(); .into_iter();

View file

@ -393,7 +393,7 @@ async fn scim_person_id_get(
), ),
security(("token_jwt" = [])), security(("token_jwt" = [])),
tag = "scim", tag = "scim",
operation_id = "" operation_id = "scim_application_post"
)] )]
async fn scim_application_post( async fn scim_application_post(
State(state): State<ServerState>, State(state): State<ServerState>,
@ -427,18 +427,19 @@ async fn scim_application_post(
), ),
security(("token_jwt" = [])), security(("token_jwt" = [])),
tag = "scim", tag = "scim",
operation_id = "scim_person_id_get" operation_id = "scim_application_id_delete"
)] )]
async fn scim_application_id_delete( async fn scim_application_id_delete(
State(state): State<ServerState>, State(state): State<ServerState>,
Path(id): Path<String>, Path(id): Path<String>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation, VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
) -> Result<(), WebError> { ) -> Result<Json<()>, WebError> {
state state
.qe_w_ref .qe_w_ref
.scim_entry_id_delete(client_auth_info, kopid.eventid, id, EntryClass::Application) .scim_entry_id_delete(client_auth_info, kopid.eventid, id, EntryClass::Application)
.await .await
.map(Json::from)
.map_err(WebError::from) .map_err(WebError::from)
} }

View file

@ -346,6 +346,8 @@ pub struct CreateEvent {
pub entries: Vec<Entry<EntryInit, EntryNew>>, pub entries: Vec<Entry<EntryInit, EntryNew>>,
// Is the CreateEvent from an internal or external source? // Is the CreateEvent from an internal or external source?
// This may affect which plugins are run ... // 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 { impl CreateEvent {
@ -363,7 +365,11 @@ impl CreateEvent {
// What is the correct consuming iterator here? Can we // What is the correct consuming iterator here? Can we
// even do that? // even do that?
match rentries { match rentries {
Ok(entries) => Ok(CreateEvent { ident, entries }), Ok(entries) => Ok(CreateEvent {
ident,
entries,
return_created_uuids: false,
}),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
@ -373,13 +379,18 @@ impl CreateEvent {
ident: Identity, ident: Identity,
entries: Vec<Entry<EntryInit, EntryNew>>, entries: Vec<Entry<EntryInit, EntryNew>>,
) -> Self { ) -> Self {
CreateEvent { ident, entries } CreateEvent {
ident,
entries,
return_created_uuids: false,
}
} }
pub fn new_internal(entries: Vec<Entry<EntryInit, EntryNew>>) -> Self { pub fn new_internal(entries: Vec<Entry<EntryInit, EntryNew>>) -> Self {
CreateEvent { CreateEvent {
ident: Identity::from_internal(), ident: Identity::from_internal(),
entries, entries,
return_created_uuids: false,
} }
} }
} }

View file

@ -365,7 +365,7 @@ mod tests {
let create = vec![e]; let create = vec![e];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -468,7 +468,7 @@ mod tests {
let create = vec![e]; let create = vec![e];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,

View file

@ -205,7 +205,7 @@ mod tests {
let create = vec![e]; let create = vec![e];
run_create_test!(Ok(()), preload, create, None, |_| {}); run_create_test!(Ok(None), preload, create, None, |_| {});
} }
#[test] #[test]

View file

@ -464,7 +464,7 @@ mod tests {
let create = vec![e_dyn]; let create = vec![e_dyn];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -513,7 +513,7 @@ mod tests {
let create = vec![e_group]; let create = vec![e_group];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -562,7 +562,7 @@ mod tests {
let create = vec![e_group]; let create = vec![e_group];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -607,7 +607,7 @@ mod tests {
let create = vec![e_dyn, e_group]; let create = vec![e_dyn, e_group];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,

View file

@ -108,7 +108,7 @@ mod tests {
let create = vec![ea]; let create = vec![ea];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,

View file

@ -136,7 +136,7 @@ mod tests {
let create = vec![e]; let create = vec![e];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,

View file

@ -858,7 +858,7 @@ mod tests {
let preload = Vec::with_capacity(0); let preload = Vec::with_capacity(0);
let create = vec![ea, eb]; let create = vec![ea, eb];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -889,7 +889,7 @@ mod tests {
let preload = Vec::with_capacity(0); let preload = Vec::with_capacity(0);
let create = vec![ea, eb, ec]; let create = vec![ea, eb, ec];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -941,7 +941,7 @@ mod tests {
let preload = Vec::with_capacity(0); let preload = Vec::with_capacity(0);
let create = vec![ea, eb, ec]; let create = vec![ea, eb, ec];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -999,7 +999,7 @@ mod tests {
let preload = Vec::with_capacity(0); let preload = Vec::with_capacity(0);
let create = vec![ea, eb, ec, ed]; let create = vec![ea, eb, ec, ed];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,

View file

@ -181,7 +181,7 @@ mod tests {
let preload = Vec::with_capacity(0); let preload = Vec::with_capacity(0);
let create = vec![ea]; let create = vec![ea];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,

View file

@ -501,7 +501,7 @@ mod tests {
let create = vec![eb]; let create = vec![eb];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -534,7 +534,7 @@ mod tests {
let create = vec![e_group]; let create = vec![e_group];
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,

View file

@ -233,7 +233,7 @@ mod tests {
let preload = Vec::with_capacity(0); let preload = Vec::with_capacity(0);
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,
@ -286,7 +286,7 @@ mod tests {
let preload = Vec::with_capacity(0); let preload = Vec::with_capacity(0);
run_create_test!( run_create_test!(
Ok(()), Ok(None),
preload, preload,
create, create,
None, None,

View file

@ -7,7 +7,7 @@ impl QueryServerWriteTransaction<'_> {
/// The create event is a raw, read only representation of the request /// The create event is a raw, read only representation of the request
/// that was made to us, including information about the identity /// that was made to us, including information about the identity
/// performing the request. /// 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() { if !ce.ident.is_internal() {
security_info!(name = %ce.ident, "create initiator"); security_info!(name = %ce.ident, "create initiator");
} }
@ -174,7 +174,12 @@ impl QueryServerWriteTransaction<'_> {
} else { } else {
admin_info!("Create operation success"); 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( pub fn internal_create(
@ -182,7 +187,7 @@ impl QueryServerWriteTransaction<'_> {
entries: Vec<Entry<EntryInit, EntryNew>>, entries: Vec<Entry<EntryInit, EntryNew>>,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
let ce = CreateEvent::new_internal(entries); let ce = CreateEvent::new_internal(entries);
self.create(&ce) self.create(&ce).map(|_| ())
} }
} }

View file

@ -74,12 +74,7 @@ impl ScimCreateEvent {
}) })
.collect::<Result<EntryInitNew, _>>()?; .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)?; .ok_or(OperationError::SC0027ClassSetInvalid)?;
entry.set_ava_set(&Attribute::Class, classes); entry.set_ava_set(&Attribute::Class, classes);
@ -179,17 +174,21 @@ impl QueryServerWriteTransaction<'_> {
} }
} }
pub fn scim_create(&mut self, scim_create: ScimCreateEvent) -> Result<(), OperationError> { pub fn scim_create(
let ScimCreateEvent { &mut self,
ident, entry scim_create: ScimCreateEvent,
} = scim_create; ) -> Result<ScimEntryKanidm, OperationError> {
let ScimCreateEvent { ident, entry } = scim_create;
let create_event = CreateEvent { let create_event = CreateEvent {
ident, ident,
entries: vec![entry], 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() { let target = if let Some(target) = changed_uuids.pop() {
if !changed_uuids.is_empty() { if !changed_uuids.is_empty() {
@ -214,7 +213,7 @@ impl QueryServerWriteTransaction<'_> {
let f_valid = f_intent_valid.clone().into_ignore_hidden(); let f_valid = f_intent_valid.clone().into_ignore_hidden();
let se = SearchEvent { let se = SearchEvent {
ident, ident: create_event.ident,
filter: f_valid, filter: f_valid,
filter_orig: f_intent_valid, filter_orig: f_intent_valid,
// Return all attributes // Return all attributes

View file

@ -238,6 +238,13 @@ impl ValueSetScimPut for ValueSetRefer {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> { fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
use kanidm_proto::scim_v1::client::{ScimReference, ScimReferences}; 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| { let scim_refs: ScimReferences = serde_json::from_value(value).map_err(|err| {
warn!(?err, "Invalid SCIM reference set syntax"); warn!(?err, "Invalid SCIM reference set syntax");
OperationError::SC0002ReferenceSyntaxInvalid OperationError::SC0002ReferenceSyntaxInvalid

View file

@ -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 kanidmd_testkit::{AsyncTestEnvironment, IDM_ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_USER};
use ldap3_client::LdapClientBuilder; use ldap3_client::LdapClientBuilder;
use tracing::debug;
use kanidm_proto::scim_v1::client::ScimEntryApplicationPost;
const TEST_PERSON: &str = "user_mcuserton"; const TEST_PERSON: &str = "user_mcuserton";
const TEST_GROUP: &str = "group_mcgroupington";
#[kanidmd_testkit::test(ldap = true)] #[kanidmd_testkit::test(ldap = true)]
async fn test_ldap_basic_unix_bind(test_env: &AsyncTestEnvironment) { 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 .await
.expect("Failed to create the user"); .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 // Create two applications
let application_1 = ScimEntryApplicationPost { let application_1 = ScimEntryApplicationPost {
name: APPLICATION_1_NAME.to_string(), 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) .idm_application_create(&application_1)
.await .await
.expect("Failed to create the user"); .expect("Failed to create the user");
debug!(?application_entry);
// List, get them. // List, get them.
// Login as the person // 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(); // 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) .idm_application_delete(APPLICATION_1_NAME)
.await .await
.expect("Failed to create the user"); .expect("Failed to create the user");
debug!(?result);
// Delete the applications // Delete the applications
// Check that you can no longer bind. // Check that you can no longer bind.