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,
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()),

View file

@ -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)]

View file

@ -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")]

View file

@ -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

View file

@ -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,

View file

@ -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();

View file

@ -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)
}

View file

@ -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,
}
}
}

View file

@ -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,

View file

@ -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]

View file

@ -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,

View file

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

View file

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

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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(|_| ())
}
}

View file

@ -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

View file

@ -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

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 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.