Improve scim querying

This commit is contained in:
William Brown 2025-02-22 10:56:32 +10:00
parent 892c013613
commit 2369b1a755
12 changed files with 471 additions and 107 deletions

1
Cargo.lock generated
View file

@ -3015,7 +3015,6 @@ dependencies = [
"sshkey-attest",
"sshkeys",
"time",
"tracing",
"url",
"urlencoding",
"utoipa",

View file

@ -38,7 +38,6 @@ uuid = { workspace = true, features = ["serde"] }
webauthn-rs-proto = { workspace = true }
sshkey-attest = { workspace = true }
sshkeys = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
enum-iterator = { workspace = true }

View file

@ -631,6 +631,71 @@ impl From<Attribute> for String {
}
}
/// Sub attributes are a component of SCIM, allowing tagged sub properties of a complex
/// attribute to be accessed.
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "lowercase", try_from = "&str", into = "AttrString")]
pub enum SubAttribute {
/// Denotes a primary value.
Primary,
#[cfg(not(test))]
Custom(AttrString),
}
impl From<SubAttribute> for AttrString {
fn from(val: SubAttribute) -> Self {
AttrString::from(val.as_str())
}
}
impl From<&str> for SubAttribute {
fn from(value: &str) -> Self {
Self::inner_from_str(value)
}
}
impl FromStr for SubAttribute {
type Err = Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self::inner_from_str(value))
}
}
impl SubAttribute {
pub fn as_str(&self) -> &str {
match self {
SubAttribute::Primary => SUB_ATTR_PRIMARY,
#[cfg(not(test))]
SubAttribute::Custom(s) => s,
}
}
// We allow this because the standard lib from_str is fallible, and we want an infallible version.
#[allow(clippy::should_implement_trait)]
fn inner_from_str(value: &str) -> Self {
// Could this be something like heapless to save allocations? Also gives a way
// to limit length of str?
match value.to_lowercase().as_str() {
SUB_ATTR_PRIMARY => SubAttribute::Primary,
#[cfg(not(test))]
_ => SubAttribute::Custom(AttrString::from(value)),
// Allowed only in tests
#[allow(clippy::unreachable)]
#[cfg(test)]
_ => {
unreachable!(
"Check that you've implemented the SubAttribute conversion for {:?}",
value
);
}
}
}
}
#[cfg(test)]
mod test {
use super::Attribute;

View file

@ -220,6 +220,8 @@ pub const ATTR_VERSION: &str = "version";
pub const ATTR_WEBAUTHN_ATTESTATION_CA_LIST: &str = "webauthn_attestation_ca_list";
pub const ATTR_ALLOW_PRIMARY_CRED_FALLBACK: &str = "allow_primary_cred_fallback";
pub const SUB_ATTR_PRIMARY: &str = "primary";
pub const OAUTH2_SCOPE_EMAIL: &str = ATTR_EMAIL;
pub const OAUTH2_SCOPE_GROUPS: &str = "groups";
pub const OAUTH2_SCOPE_SSH_PUBLICKEYS: &str = "ssh_publickeys";

View file

@ -1,7 +1,7 @@
//! These are types that a client will send to the server.
use super::ScimEntryGetQuery;
use super::ScimOauth2ClaimMapJoinChar;
use crate::attribute::Attribute;
use crate::attribute::{Attribute, SubAttribute};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use serde_with::formats::PreferMany;
@ -134,3 +134,59 @@ impl TryFrom<ScimEntryPutKanidm> for ScimEntryPutGeneric {
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct AttrPath {
pub a: Attribute,
pub s: Option<SubAttribute>,
}
impl From<Attribute> for AttrPath {
fn from(a: Attribute) -> Self {
Self { a, s: None }
}
}
impl From<(Attribute, SubAttribute)> for AttrPath {
fn from((a, s): (Attribute, SubAttribute)) -> Self {
Self { a, s: Some(s) }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub enum ScimFilter {
Or(Box<ScimFilter>, Box<ScimFilter>),
And(Box<ScimFilter>, Box<ScimFilter>),
Not(Box<ScimFilter>),
Present(AttrPath),
Equal(AttrPath, JsonValue),
NotEqual(AttrPath, JsonValue),
Contains(AttrPath, JsonValue),
StartsWith(AttrPath, JsonValue),
EndsWith(AttrPath, JsonValue),
Greater(AttrPath, JsonValue),
Less(AttrPath, JsonValue),
GreaterOrEqual(AttrPath, JsonValue),
LessOrEqual(AttrPath, JsonValue),
Complex(Attribute, Box<ScimComplexFilter>),
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub enum ScimComplexFilter {
Or(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
And(Box<ScimComplexFilter>, Box<ScimComplexFilter>),
Not(Box<ScimComplexFilter>),
Present(SubAttribute),
Equal(SubAttribute, JsonValue),
NotEqual(SubAttribute, JsonValue),
Contains(SubAttribute, JsonValue),
StartsWith(SubAttribute, JsonValue),
EndsWith(SubAttribute, JsonValue),
Greater(SubAttribute, JsonValue),
Less(SubAttribute, JsonValue),
GreaterOrEqual(SubAttribute, JsonValue),
LessOrEqual(SubAttribute, JsonValue),
}

View file

@ -9,7 +9,6 @@ use serde_with::{base64, formats, hex::Hex, serde_as, skip_serializing_none};
use std::collections::{BTreeMap, BTreeSet};
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use tracing::debug;
use url::Url;
use utoipa::ToSchema;
use uuid::Uuid;
@ -263,7 +262,7 @@ pub enum ScimValueKanidm {
pub struct ScimPerson {
pub uuid: Uuid,
pub name: String,
pub displayname: Option<String>,
pub displayname: String,
pub spn: String,
pub description: Option<String>,
pub mails: Vec<ScimMail>,
@ -275,61 +274,67 @@ impl TryFrom<ScimEntryKanidm> for ScimPerson {
type Error = ();
fn try_from(scim_entry: ScimEntryKanidm) -> Result<Self, Self::Error> {
let attr_str = |attr: &Attribute| -> Option<&str> {
match scim_entry.attrs.get(attr) {
Some(ScimValueKanidm::String(inner_string)) => Some(inner_string.as_str()),
Some(sv) => {
debug!("SCIM entry had the {} attribute but it was not a ScimValueKanidm::String type, actual: {:?}", attr, sv);
None
}
None => None,
}
};
let attr_mails = || -> Option<&Vec<ScimMail>> {
match scim_entry.attrs.get(&Attribute::Mail) {
Some(ScimValueKanidm::Mail(inner_string)) => Some(inner_string),
Some(sv) => {
debug!("SCIM entry had the {} attribute but it was not a ScimValueKanidm::Mail type, actual: {:?}", Attribute::Mail, sv);
None
}
None => None,
}
};
let attr_reference = |attr: &Attribute| -> Option<&ScimReference> {
match scim_entry.attrs.get(attr) {
Some(ScimValueKanidm::EntryReference(refer)) => Some(refer),
Some(sv) => {
debug!("SCIM entry had the {} attribute but it was not a ScimValueKanidm::ScimReference type, actual: {:?}", attr, sv);
None
}
None => None,
}
};
let attr_references = |attr: &Attribute| -> Option<&Vec<ScimReference>> {
match scim_entry.attrs.get(attr) {
Some(ScimValueKanidm::EntryReferences(refs)) => Some(refs),
Some(sv) => {
debug!("SCIM entry had the {} attribute but it was not a ScimValueKanidm::EntryReferences type, actual: {:?}", attr, sv);
None
}
None => None,
}
};
let uuid = scim_entry.header.id;
let name = attr_str(&Attribute::Name).ok_or(())?.to_string();
let displayname = attr_str(&Attribute::DisplayName).map(|s| s.to_string());
let spn = attr_str(&Attribute::Spn).ok_or(())?.to_string();
let description = attr_str(&Attribute::Description).map(|t| t.to_string());
let mails = attr_mails().cloned().unwrap_or_default();
let groups = attr_references(&Attribute::DirectMemberOf)
.cloned()
.unwrap_or(vec![]);
let name = scim_entry
.attrs
.get(&Attribute::Name)
.and_then(|v| match v {
ScimValueKanidm::String(s) => Some(s.clone()),
_ => None,
})
.ok_or(())?;
let managed_by = attr_reference(&Attribute::EntryManagedBy).cloned();
let displayname = scim_entry
.attrs
.get(&Attribute::DisplayName)
.and_then(|v| match v {
ScimValueKanidm::String(s) => Some(s.clone()),
_ => None,
})
.ok_or(())?;
let spn = scim_entry
.attrs
.get(&Attribute::Spn)
.and_then(|v| match v {
ScimValueKanidm::String(s) => Some(s.clone()),
_ => None,
})
.ok_or(())?;
let description = scim_entry
.attrs
.get(&Attribute::Description)
.and_then(|v| match v {
ScimValueKanidm::String(s) => Some(s.clone()),
_ => None,
});
let mails = scim_entry
.attrs
.get(&Attribute::Mail)
.and_then(|v| match v {
ScimValueKanidm::Mail(m) => Some(m.clone()),
_ => None,
})
.unwrap_or_default();
let groups = scim_entry
.attrs
.get(&Attribute::DirectMemberOf)
.and_then(|v| match v {
ScimValueKanidm::EntryReferences(v) => Some(v.clone()),
_ => None,
})
.unwrap_or_default();
let managed_by = scim_entry
.attrs
.get(&Attribute::EntryManagedBy)
.and_then(|v| match v {
ScimValueKanidm::EntryReference(v) => Some(v.clone()),
_ => None,
});
Ok(ScimPerson {
uuid,

View file

@ -1,13 +1,12 @@
use super::{QueryServerReadV1, QueryServerWriteV1};
use kanidm_proto::scim_v1::{
server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
client::ScimFilter, server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
};
use kanidmd_lib::idm::scim::{
GenerateScimSyncTokenEvent, ScimSyncFinaliseEvent, ScimSyncTerminateEvent, ScimSyncUpdateEvent,
};
use kanidmd_lib::idm::server::IdmServerTransaction;
use kanidmd_lib::prelude::*;
use std::collections::BTreeSet;
impl QueryServerWriteV1 {
#[instrument(
@ -239,10 +238,9 @@ impl QueryServerReadV1 {
pub async fn scim_entry_search(
&self,
client_auth_info: ClientAuthInfo,
filter_intent: Filter<FilterInvalid>,
eventid: Uuid,
attrs: Option<BTreeSet<Attribute>>,
acp: bool,
filter: ScimFilter,
query: ScimEntryGetQuery,
) -> Result<Vec<ScimEntryKanidm>, OperationError> {
let ct = duration_from_epoch_now();
let mut idms_prox_read = self.idms.proxy_read().await?;
@ -252,13 +250,6 @@ impl QueryServerReadV1 {
error!(?err, "Invalid identity");
})?;
let filter = filter_all!(f_and!([f_eq(Attribute::Class, EntryClass::Account.into())]));
idms_prox_read
.qs_read
.impersonate_search_ext(filter_intent, filter, &ident, attrs, acp)?
.into_iter()
.map(|entry| entry.to_scim_kanidm(&mut idms_prox_read.qs_read))
.collect()
idms_prox_read.qs_read.scim_search_ext(ident, filter, query)
}
}

View file

@ -13,13 +13,12 @@ use axum_htmx::{HxPushUrl, HxRequest};
use futures_util::TryFutureExt;
use kanidm_proto::attribute::Attribute;
use kanidm_proto::internal::OperationError;
use kanidm_proto::scim_v1::client::ScimFilter;
use kanidm_proto::scim_v1::server::{ScimEffectiveAccess, ScimEntryKanidm, ScimPerson};
use kanidm_proto::scim_v1::ScimEntryGetQuery;
use kanidmd_lib::constants::EntryClass;
use kanidmd_lib::filter::{f_and, f_eq, Filter, FC};
use kanidmd_lib::idm::server::DomainInfoRead;
use kanidmd_lib::idm::ClientAuthInfo;
use std::collections::BTreeSet;
use std::str::FromStr;
use uuid::Uuid;
@ -155,11 +154,19 @@ async fn get_persons_info(
client_auth_info: ClientAuthInfo,
domain_info: DomainInfoRead,
) -> Result<Vec<(ScimPerson, ScimEffectiveAccess)>, ErrorResponse> {
let filter = filter_all!(f_and!([f_eq(Attribute::Class, EntryClass::Person.into())]));
let attrs = Some(BTreeSet::from(PERSON_ATTRIBUTES));
let filter = ScimFilter::Equal(Attribute::Class.into(), EntryClass::Person.into());
let base: Vec<ScimEntryKanidm> = state
.qe_r_ref
.scim_entry_search(client_auth_info.clone(), filter, kopid.eventid, attrs, true)
.scim_entry_search(
client_auth_info.clone(),
kopid.eventid,
filter,
ScimEntryGetQuery {
attributes: Some(Vec::from(PERSON_ATTRIBUTES)),
ext_access_check: true,
},
)
.map_err(|op_err| HtmxError::new(kopid, op_err, domain_info.clone()))
.await?;
@ -172,6 +179,7 @@ async fn get_persons_info(
persons.sort_by_key(|(sp, _)| sp.uuid);
persons.reverse();
Ok(persons)
}

View file

@ -13,12 +13,7 @@
(% call string_attr("UUID", "uuid", person.uuid, false, Attribute::Uuid) %)
(% call string_attr("SPN", "spn", person.spn, false, Attribute::Spn) %)
(% call string_attr("Name", "name", person.name, true, Attribute::Name) %)
(% if let Some(displayname) = person.displayname %)
(% call string_attr("Displayname", "displayname", displayname, true, Attribute::DisplayName) %)
(% else %)
(% call string_attr("Displayname", "displayname", "none", true, Attribute::DisplayName) %)
(% endif %)
(% call string_attr("Displayname", "displayname", person.displayname, true, Attribute::DisplayName) %)
(% if let Some(description) = person.description %)
(% call string_attr("Description", "description", description, true, Attribute::Description) %)
@ -31,4 +26,4 @@
(% else %)
(% call string_attr("Managed By", "managed_by", "none", true, Attribute::EntryManagedBy) %)
(% endif %)
</form>
</form>

View file

@ -11,6 +11,7 @@ use crate::valueset::{ValueSet, ValueSetIutf8};
pub use kanidm_proto::attribute::Attribute;
use kanidm_proto::constants::*;
use kanidm_proto::internal::OperationError;
use kanidm_proto::scim_v1::JsonValue;
use kanidm_proto::v1::AccountType;
use uuid::Uuid;
@ -129,6 +130,12 @@ impl From<EntryClass> for &'static str {
}
}
impl From<EntryClass> for JsonValue {
fn from(value: EntryClass) -> Self {
Self::String(value.as_ref().to_string())
}
}
impl AsRef<str> for EntryClass {
fn as_ref(&self) -> &str {
self.into()

View file

@ -23,6 +23,7 @@ use hashbrown::HashMap;
use hashbrown::HashSet;
use kanidm_proto::constants::ATTR_UUID;
use kanidm_proto::internal::{Filter as ProtoFilter, OperationError, SchemaError};
use kanidm_proto::scim_v1::client::{AttrPath as ScimAttrPath, ScimFilter};
use ldap3_proto::proto::{LdapFilter, LdapSubstringFilter};
use serde::Deserialize;
use uuid::Uuid;
@ -764,6 +765,21 @@ impl Filter<FilterInvalid> {
},
})
}
#[instrument(name = "filter::from_scim_ro", level = "trace", skip_all)]
pub fn from_scim_ro(
ev: &Identity,
f: &ScimFilter,
qs: &mut QueryServerReadTransaction,
) -> Result<Self, OperationError> {
let depth = DEFAULT_LIMIT_FILTER_DEPTH_MAX as usize;
let mut elems = ev.limits().filter_max_elements;
Ok(Filter {
state: FilterInvalid {
inner: FilterComp::from_scim_ro(f, qs, depth, &mut elems)?,
},
})
}
}
impl FromStr for Filter<FilterInvalid> {
@ -1087,32 +1103,21 @@ impl FilterComp {
elems: &mut usize,
) -> Result<Self, OperationError> {
let ndepth = depth.checked_sub(1).ok_or(OperationError::ResourceLimit)?;
*elems = (*elems)
.checked_sub(1)
.ok_or(OperationError::ResourceLimit)?;
Ok(match f {
LdapFilter::And(l) => {
*elems = (*elems)
.checked_sub(l.len())
.ok_or(OperationError::ResourceLimit)?;
FilterComp::And(
l.iter()
.map(|f| Self::from_ldap_ro(f, qs, ndepth, elems))
.collect::<Result<Vec<_>, _>>()?,
)
}
LdapFilter::Or(l) => {
*elems = (*elems)
.checked_sub(l.len())
.ok_or(OperationError::ResourceLimit)?;
FilterComp::Or(
l.iter()
.map(|f| Self::from_ldap_ro(f, qs, ndepth, elems))
.collect::<Result<Vec<_>, _>>()?,
)
}
LdapFilter::And(l) => FilterComp::And(
l.iter()
.map(|f| Self::from_ldap_ro(f, qs, ndepth, elems))
.collect::<Result<Vec<_>, _>>()?,
),
LdapFilter::Or(l) => FilterComp::Or(
l.iter()
.map(|f| Self::from_ldap_ro(f, qs, ndepth, elems))
.collect::<Result<Vec<_>, _>>()?,
),
LdapFilter::Not(l) => {
*elems = (*elems)
.checked_sub(1)
.ok_or(OperationError::ResourceLimit)?;
FilterComp::AndNot(Box::new(Self::from_ldap_ro(l, qs, ndepth, elems)?))
}
LdapFilter::Equality(a, v) => {
@ -1172,6 +1177,103 @@ impl FilterComp {
}
})
}
fn from_scim_ro(
f: &ScimFilter,
qs: &mut QueryServerReadTransaction,
depth: usize,
elems: &mut usize,
) -> Result<Self, OperationError> {
let ndepth = depth.checked_sub(1).ok_or(OperationError::ResourceLimit)?;
*elems = (*elems)
.checked_sub(1)
.ok_or(OperationError::ResourceLimit)?;
Ok(match f {
ScimFilter::Present(ScimAttrPath { a, s: None }) => FilterComp::Pres(a.clone()),
ScimFilter::Equal(ScimAttrPath { a, s: None }, json_value) => {
let pv = qs.resolve_scim_json_get(a, json_value)?;
FilterComp::Eq(a.clone(), pv)
}
ScimFilter::Contains(ScimAttrPath { a, s: None }, json_value) => {
let pv = qs.resolve_scim_json_get(a, json_value)?;
FilterComp::Cnt(a.clone(), pv)
}
ScimFilter::StartsWith(ScimAttrPath { a, s: None }, json_value) => {
let pv = qs.resolve_scim_json_get(a, json_value)?;
FilterComp::Stw(a.clone(), pv)
}
ScimFilter::EndsWith(ScimAttrPath { a, s: None }, json_value) => {
let pv = qs.resolve_scim_json_get(a, json_value)?;
FilterComp::Enw(a.clone(), pv)
}
ScimFilter::Greater(ScimAttrPath { a, s: None }, json_value) => {
let pv = qs.resolve_scim_json_get(a, json_value)?;
// Greater is equivalent to "not equal or less than".
FilterComp::And(vec![
FilterComp::Pres(a.clone()),
FilterComp::AndNot(Box::new(FilterComp::Or(vec![
FilterComp::LessThan(a.clone(), pv.clone()),
FilterComp::Eq(a.clone(), pv),
]))),
])
}
ScimFilter::Less(ScimAttrPath { a, s: None }, json_value) => {
let pv = qs.resolve_scim_json_get(a, json_value)?;
FilterComp::LessThan(a.clone(), pv)
}
ScimFilter::GreaterOrEqual(ScimAttrPath { a, s: None }, json_value) => {
let pv = qs.resolve_scim_json_get(a, json_value)?;
// Greater or equal is equivalent to "not less than".
FilterComp::And(vec![
FilterComp::Pres(a.clone()),
FilterComp::AndNot(Box::new(FilterComp::LessThan(a.clone(), pv.clone()))),
])
}
ScimFilter::LessOrEqual(ScimAttrPath { a, s: None }, json_value) => {
let pv = qs.resolve_scim_json_get(a, json_value)?;
FilterComp::Or(vec![
FilterComp::LessThan(a.clone(), pv.clone()),
FilterComp::Eq(a.clone(), pv),
])
}
ScimFilter::Not(f) => {
let f = Self::from_scim_ro(f, qs, ndepth, elems)?;
FilterComp::AndNot(Box::new(f))
}
ScimFilter::Or(left, right) => {
let left = Self::from_scim_ro(left, qs, ndepth, elems)?;
let right = Self::from_scim_ro(right, qs, ndepth, elems)?;
FilterComp::Or(vec![left, right])
}
ScimFilter::And(left, right) => {
let left = Self::from_scim_ro(left, qs, ndepth, elems)?;
let right = Self::from_scim_ro(right, qs, ndepth, elems)?;
FilterComp::And(vec![left, right])
}
ScimFilter::NotEqual(ScimAttrPath { s: None, .. }, _) => {
error!("Unsupported filter operation - not-equal");
return Err(OperationError::FilterGeneration);
}
ScimFilter::Present(ScimAttrPath { s: Some(_), .. })
| ScimFilter::Equal(ScimAttrPath { s: Some(_), .. }, _)
| ScimFilter::NotEqual(ScimAttrPath { s: Some(_), .. }, _)
| ScimFilter::Contains(ScimAttrPath { s: Some(_), .. }, _)
| ScimFilter::StartsWith(ScimAttrPath { s: Some(_), .. }, _)
| ScimFilter::EndsWith(ScimAttrPath { s: Some(_), .. }, _)
| ScimFilter::Greater(ScimAttrPath { s: Some(_), .. }, _)
| ScimFilter::Less(ScimAttrPath { s: Some(_), .. }, _)
| ScimFilter::GreaterOrEqual(ScimAttrPath { s: Some(_), .. }, _)
| ScimFilter::LessOrEqual(ScimAttrPath { s: Some(_), .. }, _) => {
error!("Unsupported filter operation - sub-attribute");
return Err(OperationError::FilterGeneration);
}
ScimFilter::Complex(..) => {
error!("Unsupported filter operation - complex");
return Err(OperationError::FilterGeneration);
}
})
}
}
/* We only configure partial eq if cfg test on the invalid/valid types */

View file

@ -35,6 +35,7 @@ use concread::arcache::{ARCacheBuilder, ARCacheReadTxn};
use concread::cowcell::*;
use hashbrown::{HashMap, HashSet};
use kanidm_proto::internal::{DomainInfo as ProtoDomainInfo, ImageValue, UiHint};
use kanidm_proto::scim_v1::client::ScimFilter;
use kanidm_proto::scim_v1::server::ScimOAuth2ClaimMap;
use kanidm_proto::scim_v1::server::ScimOAuth2ScopeMap;
use kanidm_proto::scim_v1::server::ScimReference;
@ -938,6 +939,64 @@ pub trait QueryServerTransaction<'a> {
}
}
fn resolve_scim_json_get(
&mut self,
attr: &Attribute,
value: &JsonValue,
) -> Result<PartialValue, 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()));
};
match schema_a.syntax {
SyntaxType::Utf8String => {
let JsonValue::String(value) = value else {
return Err(OperationError::InvalidAttribute(attr.to_string()));
};
Ok(PartialValue::Utf8(value.to_string()))
}
SyntaxType::Utf8StringInsensitive => {
let JsonValue::String(value) = value else {
return Err(OperationError::InvalidAttribute(attr.to_string()));
};
Ok(PartialValue::new_iutf8(value))
}
SyntaxType::Utf8StringIname => {
let JsonValue::String(value) = value else {
return Err(OperationError::InvalidAttribute(attr.to_string()));
};
Ok(PartialValue::new_iname(value))
}
SyntaxType::Uuid => {
let JsonValue::String(value) = value else {
return Err(OperationError::InvalidAttribute(attr.to_string()));
};
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
Ok(PartialValue::Uuid(un))
}
SyntaxType::ReferenceUuid
| SyntaxType::OauthScopeMap
| SyntaxType::Session
| SyntaxType::ApiToken
| SyntaxType::Oauth2Session
| SyntaxType::ApplicationPassword => {
let JsonValue::String(value) = value else {
return Err(OperationError::InvalidAttribute(attr.to_string()));
};
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
Ok(PartialValue::Refer(un))
}
_ => return Err(OperationError::InvalidAttribute(attr.to_string())),
}
}
fn resolve_scim_json_put(
&mut self,
attr: &Attribute,
@ -1559,6 +1618,40 @@ impl QueryServerReadTransaction<'_> {
}
}
}
#[instrument(level = "debug", skip_all)]
pub fn scim_search_ext(
&mut self,
ident: Identity,
filter: ScimFilter,
query: ScimEntryGetQuery,
) -> Result<Vec<ScimEntryKanidm>, OperationError> {
let filter_intent = Filter::from_scim_ro(&ident, &filter, self)?;
let f_intent_valid = filter_intent
.validate(self.get_schema())
.map_err(OperationError::SchemaViolation)?;
let f_valid = f_intent_valid.clone().into_ignore_hidden();
let r_attrs = query
.attributes
.map(|attr_set| attr_set.into_iter().collect());
let se = SearchEvent {
ident,
filter: f_valid,
filter_orig: f_intent_valid,
attrs: r_attrs,
effective_access_check: query.ext_access_check,
};
let vs = self.search_ext(&se)?;
vs.into_iter()
.map(|entry| entry.to_scim_kanidm(self))
.collect()
}
}
impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
@ -2629,7 +2722,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
#[cfg(test)]
mod tests {
use crate::prelude::*;
use kanidm_proto::scim_v1::client::ScimFilter;
use kanidm_proto::scim_v1::server::ScimReference;
use kanidm_proto::scim_v1::JsonValue;
use kanidm_proto::scim_v1::ScimEntryGetQuery;
#[qs_test]
@ -3081,4 +3176,44 @@ mod tests {
assert!(ext_access_check.modify_present.check(&Attribute::Name));
assert!(ext_access_check.modify_remove.check(&Attribute::Name));
}
#[qs_test]
async fn test_scim_basic_search_ext_query(server: &QueryServer) {
let mut server_txn = server.write(duration_from_epoch_now()).await.unwrap();
let group_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Uuid, Value::Uuid(group_uuid))
);
assert!(server_txn.internal_create(vec![e1]).is_ok());
assert!(server_txn.commit().is_ok());
// Now read that entry.
let mut server_txn = server.read().await.unwrap();
let idm_admin_entry = server_txn.internal_search_uuid(UUID_IDM_ADMIN).unwrap();
let idm_admin_ident = Identity::from_impersonate_entry_readwrite(idm_admin_entry);
let filter = ScimFilter::And(
Box::new(ScimFilter::Equal(
Attribute::Class.into(),
EntryClass::Group.into(),
)),
Box::new(ScimFilter::Equal(
Attribute::Uuid.into(),
JsonValue::String(group_uuid.to_string()),
)),
);
let base: Vec<ScimEntryKanidm> = server_txn
.scim_search_ext(idm_admin_ident, filter, ScimEntryGetQuery::default())
.unwrap();
assert_eq!(base.len(), 1);
assert_eq!(base[0].header.id, group_uuid);
}
}