mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-22 00:43:54 +02:00
Improve scim querying
This commit is contained in:
parent
892c013613
commit
2369b1a755
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3015,7 +3015,6 @@ dependencies = [
|
|||
"sshkey-attest",
|
||||
"sshkeys",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue