20240927 SCIM put (#3151)

This commit is contained in:
Firstyear 2024-11-30 16:56:17 +10:00 committed by GitHub
parent 8bbdf6bd6a
commit ea0e63cc2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 2112 additions and 375 deletions

View file

@ -140,6 +140,9 @@ pub enum Attribute {
Refers,
Replicated,
Rs256PrivateKeyDer,
/// A set of scim schemas. This is similar to a kanidm class.
#[serde(rename = "schemas")]
ScimSchemas,
Scope,
SourceUuid,
Spn,
@ -368,6 +371,7 @@ impl Attribute {
Attribute::Replicated => ATTR_REPLICATED,
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
Attribute::Scope => ATTR_SCOPE,
Attribute::ScimSchemas => ATTR_SCIM_SCHEMAS,
Attribute::SourceUuid => ATTR_SOURCE_UUID,
Attribute::Spn => ATTR_SPN,
Attribute::SshPublicKey => ATTR_SSH_PUBLICKEY,
@ -548,6 +552,7 @@ impl Attribute {
ATTR_REFERS => Attribute::Refers,
ATTR_REPLICATED => Attribute::Replicated,
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
ATTR_SCIM_SCHEMAS => Attribute::ScimSchemas,
ATTR_SCOPE => Attribute::Scope,
ATTR_SOURCE_UUID => Attribute::SourceUuid,
ATTR_SPN => Attribute::Spn,

View file

@ -179,6 +179,7 @@ pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
pub const ATTR_REFERS: &str = "refers";
pub const ATTR_REPLICATED: &str = "replicated";
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
pub const ATTR_SCIM_SCHEMAS: &str = "schemas";
pub const ATTR_SCOPE: &str = "scope";
pub const ATTR_SELF: &str = "self";
pub const ATTR_SOURCE_UUID: &str = "source_uuid";

View file

@ -148,7 +148,6 @@ pub enum OperationError {
KG002TaskCommFailure,
KG003CacheClearFailed,
// What about something like this for unique errors?
// Credential Update Errors
CU0001WebauthnAttestationNotTrusted,
CU0002WebauthnRegistrationError,
@ -173,6 +172,31 @@ pub enum OperationError {
// SCIM
SC0001IncomingSshPublicKey,
SC0002ReferenceSyntaxInvalid,
SC0003MailSyntaxInvalid,
SC0004UuidSyntaxInvalid,
SC0005BoolSyntaxInvalid,
SC0006Uint32SyntaxInvalid,
SC0007UrlSyntaxInvalid,
SC0008SyntaxTypeSyntaxInvalid,
SC0009IndexTypeSyntaxInvalid,
SC0010DateTimeSyntaxInvalid,
SC0011AddressSyntaxInvalid,
SC0012CertificateSyntaxInvalid,
SC0013CertificateInvalidDer,
SC0014CertificateInvalidDigest,
SC0015CredentialTypeSyntaxInvalid,
SC0016InameSyntaxInvalid,
SC0017Iutf8SyntaxInvalid,
SC0018NsUniqueIdSyntaxInvalid,
SC0019Oauth2ScopeSyntaxInvalid,
SC0020Oauth2ScopeMapSyntaxInvalid,
SC0021Oauth2ScopeMapMissingGroupIdentifier,
SC0022Oauth2ClaimMapSyntaxInvalid,
SC0023Oauth2ClaimMapMissingGroupIdentifier,
SC0024SshPublicKeySyntaxInvalid,
SC0025UiHintSyntaxInvalid,
SC0026Utf8SyntaxInvalid,
// Migration
MG0001InvalidReMigrationLevel,
MG0002RaiseDomainLevelExceedsMaximum,
@ -409,6 +433,33 @@ impl OperationError {
Self::MG0008SkipUpgradeAttempted => Some("Skip Upgrade Attempted.".into()),
Self::PL0001GidOverlapsSystemRange => None,
Self::SC0001IncomingSshPublicKey => None,
Self::SC0002ReferenceSyntaxInvalid => Some("A SCIM Reference Set contained invalid syntax and can not be processed.".into()),
Self::SC0003MailSyntaxInvalid => Some("A SCIM Mail Address contained invalid syntax".into()),
Self::SC0004UuidSyntaxInvalid => Some("A SCIM Uuid contained invalid syntax".into()),
Self::SC0005BoolSyntaxInvalid => Some("A SCIM boolean contained invalid syntax".into()),
Self::SC0006Uint32SyntaxInvalid => Some("A SCIM Uint32 contained invalid syntax".into()),
Self::SC0007UrlSyntaxInvalid => Some("A SCIM Url contained invalid syntax".into()),
Self::SC0008SyntaxTypeSyntaxInvalid => Some("A SCIM SyntaxType contained invalid syntax".into()),
Self::SC0009IndexTypeSyntaxInvalid => Some("A SCIM IndexType contained invalid syntax".into()),
Self::SC0010DateTimeSyntaxInvalid => Some("A SCIM DateTime contained invalid syntax".into()),
Self::SC0011AddressSyntaxInvalid => Some("A SCIM Address contained invalid syntax".into()),
Self::SC0012CertificateSyntaxInvalid => Some("A SCIM Certificate contained invalid binary data".into()),
Self::SC0013CertificateInvalidDer => Some("A SCIM Certificate did not contain valid DER".into()),
Self::SC0014CertificateInvalidDigest => Some("A SCIM Certificate was unable to be digested".into()),
Self::SC0015CredentialTypeSyntaxInvalid => Some("A SCIM CredentialType contained invalid syntax".into()),
Self::SC0016InameSyntaxInvalid => Some("A SCIM Iname string contained invalid syntax".into()),
Self::SC0017Iutf8SyntaxInvalid => Some("A SCIM Iutf8 string contained invalid syntax".into()),
Self::SC0018NsUniqueIdSyntaxInvalid => Some("A SCIM NsUniqueID contained invalid syntax".into()),
Self::SC0019Oauth2ScopeSyntaxInvalid => Some("A SCIM Oauth2 Scope contained invalid syntax".into()),
Self::SC0020Oauth2ScopeMapSyntaxInvalid => Some("A SCIM Oauth2 Scope Map contained invalid syntax".into()),
Self::SC0021Oauth2ScopeMapMissingGroupIdentifier => Some("A SCIM Oauth2 Scope Map was missing a group name or uuid".into()),
Self::SC0022Oauth2ClaimMapSyntaxInvalid => Some("A SCIM Oauth2 Claim Map contained invalid syntax".into()),
Self::SC0023Oauth2ClaimMapMissingGroupIdentifier => Some("A SCIM Claim Map was missing a group name or uuid".into()),
Self::SC0024SshPublicKeySyntaxInvalid => Some("A SCIM Ssh Public Key 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::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::VL0001ValueSshPublicKeyString => None,

View file

@ -33,6 +33,7 @@ pub struct PkceRequest {
/// An OAuth2 client redirects to the authorisation server with Authorisation Request
/// parameters.
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthorisationRequest {
@ -43,7 +44,8 @@ pub struct AuthorisationRequest {
#[serde(flatten)]
pub pkce_request: Option<PkceRequest>,
pub redirect_uri: Url,
pub scope: String,
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
pub scope: BTreeSet<String>,
// OIDC adds a nonce parameter that is optional.
pub nonce: Option<String>,
// OIDC also allows other optional params
@ -185,6 +187,7 @@ pub struct OAuth2RFC9068TokenExtensions {
}
/// The response for an access token
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessTokenResponse {
@ -195,7 +198,8 @@ pub struct AccessTokenResponse {
pub refresh_token: Option<String>,
/// Space separated list of scopes that were approved, if this differs from the
/// original request.
pub scope: Option<String>,
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
pub scope: BTreeSet<String>,
/// If the `openid` scope was requested, an `id_token` may be present in the response.
pub id_token: Option<String>,
}
@ -248,11 +252,13 @@ pub struct AccessTokenIntrospectRequest {
/// Response to an introspection request. If the token is inactive or revoked, only
/// `active` will be set to the value of `false`.
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessTokenIntrospectResponse {
pub active: bool,
pub scope: Option<String>,
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
pub scope: BTreeSet<String>,
pub client_id: Option<String>,
pub username: Option<String>,
pub token_type: Option<AccessTokenType>,
@ -269,7 +275,7 @@ impl AccessTokenIntrospectResponse {
pub fn inactive() -> Self {
AccessTokenIntrospectResponse {
active: false,
scope: None,
scope: BTreeSet::default(),
client_id: None,
username: None,
token_type: None,

View file

@ -1,11 +1,124 @@
//! These are types that a client will send to the server.
use super::ScimOauth2ClaimMapJoinChar;
use crate::attribute::Attribute;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use serde_with::formats::PreferMany;
use serde_with::OneOrMany;
use serde_with::{base64, formats, serde_as, skip_serializing_none};
use sshkey_attest::proto::PublicKey as SshPublicKey;
use std::collections::{BTreeMap, BTreeSet};
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use uuid::Uuid;
pub type ScimSshPublicKeys = Vec<ScimSshPublicKey>;
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ScimSshPublicKey {
pub label: String,
pub value: SshPublicKey,
}
#[serde_as]
#[skip_serializing_none]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ScimReference {
pub uuid: Option<Uuid>,
pub value: Option<String>,
}
pub type ScimReferences = Vec<ScimReference>;
#[serde_as]
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(transparent)]
pub struct ScimDateTime {
#[serde_as(as = "Rfc3339")]
pub date_time: OffsetDateTime,
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ScimCertificate {
#[serde_as(as = "base64::Base64<base64::UrlSafe, formats::Unpadded>")]
pub der: Vec<u8>,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ScimAddress {
pub street_address: String,
pub locality: String,
pub region: String,
pub postal_code: String,
pub country: String,
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2ClaimMap {
pub group: Option<String>,
pub group_uuid: Option<Uuid>,
pub claim: String,
pub join_char: ScimOauth2ClaimMapJoinChar,
pub values: BTreeSet<String>,
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2ScopeMap {
pub group: Option<String>,
pub group_uuid: Option<Uuid>,
pub scopes: BTreeSet<String>,
}
#[derive(Serialize, Debug, Clone)]
pub struct ScimEntryPutKanidm {
pub id: Uuid,
#[serde(flatten)]
pub attrs: BTreeMap<Attribute, Option<super::server::ScimValueKanidm>>,
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ScimStrings(#[serde_as(as = "OneOrMany<_, PreferMany>")] pub Vec<String>);
#[derive(Debug, Clone, Deserialize)]
pub struct ScimEntryPutGeneric {
// id is only used to target the entry in question
pub id: Uuid,
// external_id can't be set by put
// meta is skipped on put
// Schemas are decoded as part of "attrs".
/// Update an attribute to contain the following value state.
/// If the attribute is None, it is removed.
#[serde(flatten)]
pub attrs: BTreeMap<Attribute, Option<JsonValue>>,
}
impl TryFrom<ScimEntryPutKanidm> for ScimEntryPutGeneric {
type Error = serde_json::Error;
fn try_from(value: ScimEntryPutKanidm) -> Result<Self, Self::Error> {
let ScimEntryPutKanidm { id, attrs } = value;
let attrs = attrs
.into_iter()
.map(|(attr, value)| {
if let Some(v) = value {
serde_json::to_value(v).map(|json_value| (attr, Some(json_value)))
} else {
Ok((attr, None))
}
})
.collect::<Result<_, _>>()?;
Ok(ScimEntryPutGeneric { id, attrs })
}
}

View file

@ -18,7 +18,7 @@
use crate::attribute::Attribute;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sshkey_attest::proto::PublicKey as SshPublicKey;
use std::collections::BTreeMap;
use utoipa::ToSchema;
@ -27,6 +27,7 @@ use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
pub use self::synch::*;
pub use scim_proto::prelude::*;
pub use serde_json::Value as JsonValue;
pub mod client;
pub mod server;
@ -52,6 +53,46 @@ pub struct ScimEntryGetQuery {
pub attributes: Option<Vec<Attribute>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
pub enum ScimSchema {
#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:account")]
SyncAccountV1,
#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:group")]
SyncV1GroupV1,
#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:person")]
SyncV1PersonV1,
#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixaccount")]
SyncV1PosixAccountV1,
#[serde(rename = "urn:ietf:params:scim:schemas:kanidm:sync:1:posixgroup")]
SyncV1PosixGroupV1,
}
#[serde_as]
#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ScimMail {
#[serde(default)]
pub primary: bool,
pub value: String,
}
#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimSshPublicKey {
pub label: String,
pub value: SshPublicKey,
}
#[derive(Deserialize, Serialize, Debug, Clone, ToSchema)]
pub enum ScimOauth2ClaimMapJoinChar {
#[serde(rename = ",", alias = "csv")]
CommaSeparatedValue,
#[serde(rename = " ", alias = "ssv")]
SpaceSeparatedValue,
#[serde(rename = ";", alias = "json_array")]
JsonArray,
}
#[cfg(test)]
mod tests {
// use super::*;

View file

@ -1,8 +1,11 @@
use super::ScimMail;
use super::ScimOauth2ClaimMapJoinChar;
use super::ScimSshPublicKey;
use crate::attribute::Attribute;
use crate::internal::UiHint;
use scim_proto::ScimEntryHeader;
use serde::Serialize;
use serde_with::{base64, formats, hex::Hex, serde_as, skip_serializing_none, StringWithSeparator};
use sshkey_attest::proto::PublicKey as SshPublicKey;
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;
@ -32,13 +35,6 @@ pub struct ScimAddress {
pub country: String,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimMail {
pub primary: bool,
pub value: String,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimApplicationPassword {
@ -75,13 +71,6 @@ pub struct ScimAuditString {
pub value: String,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimSshPublicKey {
pub label: String,
pub value: SshPublicKey,
}
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub enum ScimIntentTokenState {
@ -164,8 +153,8 @@ pub struct ScimApiToken {
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2ScopeMap {
pub uuid: Uuid,
#[serde_as(as = "StringWithSeparator::<formats::SpaceSeparator, String>")]
pub group: String,
pub group_uuid: Uuid,
pub scopes: BTreeSet<String>,
}
@ -173,14 +162,13 @@ pub struct ScimOAuth2ScopeMap {
#[derive(Serialize, Debug, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimOAuth2ClaimMap {
pub group: Uuid,
pub group: String,
pub group_uuid: Uuid,
pub claim: String,
pub join_char: String,
#[serde_as(as = "StringWithSeparator::<formats::SpaceSeparator, String>")]
pub join_char: ScimOauth2ClaimMapJoinChar,
pub values: BTreeSet<String>,
}
#[serde_as]
#[derive(Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ScimReference {
@ -226,6 +214,7 @@ pub enum ScimValueKanidm {
OAuth2ScopeMap(Vec<ScimOAuth2ScopeMap>),
OAuth2ClaimMap(Vec<ScimOAuth2ClaimMap>),
KeyInternal(Vec<ScimKeyInternal>),
UiHints(Vec<UiHint>),
}
impl From<bool> for ScimValueKanidm {
@ -240,6 +229,12 @@ impl From<OffsetDateTime> for ScimValueKanidm {
}
}
impl From<Vec<UiHint>> for ScimValueKanidm {
fn from(set: Vec<UiHint>) -> Self {
Self::UiHints(set)
}
}
impl From<Vec<OffsetDateTime>> for ScimValueKanidm {
fn from(set: Vec<OffsetDateTime>) -> Self {
Self::ArrayDateTime(set)
@ -252,6 +247,12 @@ impl From<String> for ScimValueKanidm {
}
}
impl From<&str> for ScimValueKanidm {
fn from(s: &str) -> Self {
Self::String(s.to_string())
}
}
impl From<Vec<String>> for ScimValueKanidm {
fn from(set: Vec<String>) -> Self {
Self::ArrayString(set)

View file

@ -7,6 +7,7 @@ use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew};
use crate::idm::account::Account;
use crate::value::PartialValue;
use crate::value::Value;
use crate::valueset::{ValueSet, ValueSetIutf8};
pub use kanidm_proto::attribute::Attribute;
use kanidm_proto::constants::*;
use kanidm_proto::internal::OperationError;
@ -178,6 +179,11 @@ impl EntryClass {
Value::new_iutf8(s)
}
pub fn to_valueset(self) -> ValueSet {
let s: &'static str = self.into();
ValueSetIutf8::new(s)
}
pub fn to_partialvalue(self) -> PartialValue {
let s: &'static str = self.into();
PartialValue::new_iutf8(s)

View file

@ -2253,10 +2253,13 @@ impl Entry<EntryReduced, EntryCommitted> {
Ok(ProtoEntry { attrs: attrs? })
}
pub fn to_scim_kanidm(
pub fn to_scim_kanidm<'a, TXN>(
&self,
read_txn: &mut QueryServerReadTransaction,
) -> Result<ScimEntryKanidm, OperationError> {
read_txn: &mut TXN,
) -> Result<ScimEntryKanidm, OperationError>
where
TXN: QueryServerTransaction<'a>,
{
let result: Result<BTreeMap<Attribute, ScimValueKanidm>, OperationError> = self
.attrs
.iter()
@ -3199,6 +3202,7 @@ where
error!("Modification assertion was not met. {} {:?}", attr, value);
})?;
}
Modify::Set(attr, valueset) => self.set_ava_set(attr, valueset.clone()),
}
}
Ok(())

View file

@ -53,7 +53,6 @@ use crate::idm::server::{
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
};
use crate::prelude::*;
use crate::utils::str_join;
use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
#[derive(Serialize, Deserialize, Debug, PartialEq)]
@ -1475,11 +1474,7 @@ impl IdmServerProxyWriteTransaction<'_> {
let session_id = Uuid::new_v4();
let scope = if granted_scopes.is_empty() {
None
} else {
Some(str_join(&granted_scopes))
};
let scope = granted_scopes.clone();
let uuid = o2rs.uuid;
@ -1564,11 +1559,7 @@ impl IdmServerProxyWriteTransaction<'_> {
let refresh_expiry = iat + OAUTH_REFRESH_TOKEN_EXPIRY as i64;
let odt_refresh_expiry = odt_ct + Duration::from_secs(OAUTH_REFRESH_TOKEN_EXPIRY);
let scope = if scopes.is_empty() {
None
} else {
Some(str_join(&scopes))
};
let scope = scopes.clone();
let iss = o2rs.iss.clone();
@ -1937,11 +1928,7 @@ impl IdmServerProxyReadTransaction<'_> {
}
// scopes - you need to have every requested scope or this auth_req is denied.
let req_scopes: BTreeSet<String> = auth_req
.scope
.split_ascii_whitespace()
.map(str::to_string)
.collect();
let req_scopes: BTreeSet<String> = auth_req.scope.clone();
if req_scopes.is_empty() {
admin_error!("Invalid OAuth2 request - must contain at least one requested scope");
@ -2273,11 +2260,7 @@ impl IdmServerProxyReadTransaction<'_> {
// ==== good to generate response ====
let scope = if scopes.is_empty() {
None
} else {
Some(str_join(&scopes))
};
let scope = scopes.clone();
let preferred_username = if prefer_short_username {
Some(account.name.clone())
@ -2343,11 +2326,7 @@ impl IdmServerProxyReadTransaction<'_> {
return Ok(AccessTokenIntrospectResponse::inactive());
};
let scope = if scopes.is_empty() {
None
} else {
Some(str_join(&scopes))
};
let scope = scopes.clone();
let token_type = Some(AccessTokenType::Bearer);
@ -2904,6 +2883,7 @@ fn check_is_loopback(redirect_uri: &Url) -> bool {
#[cfg(test)]
mod tests {
use base64::{engine::general_purpose, Engine as _};
use std::collections::BTreeSet;
use std::convert::TryFrom;
use std::str::FromStr;
use std::time::Duration;
@ -2953,6 +2933,8 @@ mod tests {
$code_challenge:expr,
$scope:expr
) => {{
let scope: BTreeSet<String> = $scope.split(" ").map(|s| s.to_string()).collect();
let auth_req = AuthorisationRequest {
response_type: "code".to_string(),
client_id: "test_resource_server".to_string(),
@ -2962,7 +2944,7 @@ mod tests {
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: $scope,
scope,
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
@ -3454,7 +3436,7 @@ mod tests {
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3475,7 +3457,7 @@ mod tests {
state: "123".to_string(),
pkce_request: None,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3496,7 +3478,7 @@ mod tests {
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3517,7 +3499,7 @@ mod tests {
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3538,7 +3520,7 @@ mod tests {
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/wrong_place").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3559,7 +3541,7 @@ mod tests {
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3582,7 +3564,7 @@ mod tests {
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "invalid_scope read".to_string(),
scope: btreeset!["invalid_scope".to_string(), "read".to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3603,7 +3585,7 @@ mod tests {
state: "123".to_string(),
pkce_request: pkce_request.clone(),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "read openid".to_string(),
scope: btreeset!["openid".to_string(), "read".to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3624,7 +3606,7 @@ mod tests {
state: "123".to_string(),
pkce_request,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "read openid".to_string(),
scope: btreeset!["openid".to_string(), "read".to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -3918,7 +3900,7 @@ mod tests {
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("https://portal.example.com").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
@ -3988,7 +3970,7 @@ mod tests {
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("app://cheese").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
@ -4092,7 +4074,10 @@ mod tests {
eprintln!("👉 {intr_response:?}");
assert!(intr_response.active);
assert_eq!(intr_response.scope.as_deref(), Some("openid supplement"));
assert_eq!(
intr_response.scope,
btreeset!["openid".to_string(), "supplement".to_string()]
);
assert_eq!(
intr_response.client_id.as_deref(),
Some("test_resource_server")
@ -5191,7 +5176,7 @@ mod tests {
state: "123".to_string(),
pkce_request: None,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_GROUPS.to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
@ -5405,7 +5390,7 @@ mod tests {
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: "openid email".to_string(),
scope: btreeset!["openid".to_string(), "email".to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
@ -5464,7 +5449,7 @@ mod tests {
}),
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
// Note the scope isn't requested here!
scope: "openid email".to_string(),
scope: btreeset!["openid".to_string(), "email".to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
@ -5602,7 +5587,7 @@ mod tests {
state: "123".to_string(),
pkce_request: None,
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -5680,7 +5665,7 @@ mod tests {
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: Url::parse("http://demo.example.com/oauth2/result").unwrap(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: None,
oidc_ext: Default::default(),
max_age: None,
@ -6581,7 +6566,10 @@ mod tests {
eprintln!("👉 {intr_response:?}");
assert!(intr_response.active);
assert_eq!(intr_response.scope.as_deref(), Some("openid supplement"));
assert_eq!(
intr_response.scope,
btreeset!["openid".to_string(), "supplement".to_string()]
);
assert_eq!(
intr_response.client_id.as_deref(),
Some("test_resource_server")
@ -6640,8 +6628,8 @@ mod tests {
code_challenge,
code_challenge_method: CodeChallengeMethod::S256,
}),
redirect_uri: redirect_uri.clone(),
scope: OAUTH2_SCOPE_OPENID.to_string(),
redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(),
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
nonce: Some("abcdef".to_string()),
oidc_ext: Default::default(),
max_age: None,
@ -6731,7 +6719,7 @@ mod tests {
eprintln!("👉 {intr_response:?}");
assert!(intr_response.active);
assert_eq!(intr_response.scope.as_deref(), Some("supplement"));
assert_eq!(intr_response.scope, btreeset!["supplement".to_string()]);
assert_eq!(
intr_response.client_id.as_deref(),
Some("test_resource_server")

View file

@ -10,6 +10,7 @@ use kanidm_proto::internal::{
use kanidm_proto::v1::Entry as ProtoEntry;
// Should this be std?
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::prelude::*;
use crate::schema::SchemaTransaction;
@ -23,16 +24,17 @@ pub struct ModifyInvalid;
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Modify {
// This value *should* exist.
// Clippy doesn't like value here, as value > pv. It could be an improvement to
// box here, but not sure. ... TODO and thought needed.
/// This value *should* exist for this attribute.
Present(Attribute, Value),
// This value *should not* exist.
/// This value *should not* exist for this attribute.
Removed(Attribute, PartialValue),
// This attr *should not* exist.
/// This attr should not exist, and if it does exist, will have all content removed.
Purged(Attribute),
// This attr and value must exist *in this state* for this change to proceed.
/// This attr and value must exist *in this state* for this change to proceed.
Assert(Attribute, PartialValue),
/// Set and replace the entire content of an attribute. This requires both presence
/// and removal access to the attribute to proceed.
Set(Attribute, ValueSet),
}
pub fn m_pres(attr: Attribute, v: &Value) -> Modify {
@ -201,6 +203,10 @@ impl ModifyList<ModifyInvalid> {
Some(_attr_name) => Ok(Modify::Purged(attr.clone())),
None => Err(SchemaError::InvalidAttribute(attr.to_string())),
},
Modify::Set(attr, valueset) => match schema_attributes.get(attr) {
Some(_attr_name) => Ok(Modify::Set(attr.clone(), valueset.clone())),
None => Err(SchemaError::InvalidAttribute(attr.to_string())),
},
})
.collect();
@ -227,6 +233,26 @@ impl ModifyList<ModifyInvalid> {
}
}
impl From<BTreeMap<Attribute, Option<ValueSet>>> for ModifyList<ModifyInvalid> {
fn from(attrs: BTreeMap<Attribute, Option<ValueSet>>) -> Self {
let mods = attrs
.into_iter()
.map(|(attr, maybe_valueset)| {
if let Some(valueset) = maybe_valueset {
Modify::Set(attr, valueset)
} else {
Modify::Purged(attr)
}
})
.collect();
ModifyList {
valid: ModifyInvalid,
mods,
}
}
}
impl ModifyList<ModifyValid> {
/// ⚠️ - Create a new modlist that is considered valid, bypassing schema.
/// This is a TEST ONLY method and will never be exposed in production.

View file

@ -164,9 +164,10 @@ impl Plugin for Base {
) -> Result<(), OperationError> {
me.modlist.iter().try_for_each(|modify| {
let attr = match &modify {
Modify::Present(a, _) => Some(a),
Modify::Removed(a, _) => Some(a),
Modify::Purged(a) => Some(a),
Modify::Present(a, _)
| Modify::Removed(a, _)
| Modify::Purged(a)
| Modify::Set(a, _) => Some(a),
Modify::Assert(_, _) => None,
};
if attr == Some(&Attribute::Uuid) {
@ -191,9 +192,10 @@ impl Plugin for Base {
.flat_map(|ml| ml.iter())
.try_for_each(|modify| {
let attr = match &modify {
Modify::Present(a, _) => Some(a),
Modify::Removed(a, _) => Some(a),
Modify::Purged(a) => Some(a),
Modify::Present(a, _)
| Modify::Removed(a, _)
| Modify::Set(a, _)
| Modify::Purged(a) => Some(a),
Modify::Assert(_, _) => None,
};
if attr == Some(&Attribute::Uuid) {

View file

@ -146,7 +146,10 @@ impl Plugin for Protected {
me.modlist.into_iter().try_fold((), |(), m| {
// Already hit an error, move on.
let a = match m {
Modify::Present(a, _) | Modify::Removed(a, _) | Modify::Purged(a) => Some(a),
Modify::Present(a, _)
| Modify::Removed(a, _)
| Modify::Set(a, _)
| Modify::Purged(a) => Some(a),
Modify::Assert(_, _) => None,
};
if let Some(attr) = a {
@ -225,7 +228,7 @@ impl Plugin for Protected {
.try_fold((), |(), m| {
// Already hit an error, move on.
let a = match m {
Modify::Present(a, _) | Modify::Removed(a, _) | Modify::Purged(a) => Some(a),
Modify::Present(a, _) | Modify::Removed(a, _) | Modify::Set(a, _) | Modify::Purged(a) => Some(a),
Modify::Assert(_, _) => None,
};
if let Some(attr) = a {

View file

@ -345,9 +345,13 @@ pub trait AccessControlsTransaction<'a> {
.into_iter()
.filter_map(|e| {
match apply_search_access(&se.ident, related_acp.as_slice(), &e) {
SearchResult::Denied | SearchResult::Grant => {
SearchResult::Denied => {
None
}
SearchResult::Grant => {
// No properly written access module should allow
// unbounded attribute read!
error!("An access module allowed full read, this is a BUG! Denying read to prevent data leaks.");
None
}
SearchResult::Allow(allowed_attrs) => {
@ -450,8 +454,8 @@ pub trait AccessControlsTransaction<'a> {
.modlist
.iter()
.filter_map(|m| match m {
Modify::Present(a, _) => Some(a.clone()),
_ => None,
Modify::Present(a, _) | Modify::Set(a, _) => Some(a.clone()),
Modify::Removed(..) | Modify::Assert(..) | Modify::Purged(_) => None,
})
.collect();
@ -459,19 +463,18 @@ pub trait AccessControlsTransaction<'a> {
.modlist
.iter()
.filter_map(|m| match m {
Modify::Removed(a, _) => Some(a.clone()),
Modify::Purged(a) => Some(a.clone()),
_ => None,
Modify::Set(a, _) | Modify::Removed(a, _) | Modify::Purged(a) => Some(a.clone()),
Modify::Present(..) | Modify::Assert(..) => None,
})
.collect();
// Build the set of classes that we to work on, only in terms of "addition". To remove
// I think we have no limit, but ... william of the future may find a problem with this
// policy.
let requested_classes: BTreeSet<&str> = me
.modlist
.iter()
.filter_map(|m| match m {
let mut requested_classes: BTreeSet<&str> = Default::default();
for modify in me.modlist.iter() {
match modify {
Modify::Present(a, v) => {
if a == Attribute::Class.as_ref() {
// Here we have an option<&str> which could mean there is a risk of
@ -480,21 +483,23 @@ pub trait AccessControlsTransaction<'a> {
// existence, and second, we would have failed the mod at schema checking
// earlier in the process as these were not correctly type. As a result
// we can trust these to be correct here and not to be "None".
v.to_str()
} else {
None
requested_classes.extend(v.to_str())
}
}
Modify::Removed(a, v) => {
if a == Attribute::Class.as_ref() {
v.to_str()
} else {
None
requested_classes.extend(v.to_str())
}
}
_ => None,
})
.collect();
Modify::Set(a, v) => {
if a == Attribute::Class.as_ref() {
// flatten to remove the option down to an iterator
requested_classes.extend(v.as_iutf8_iter().into_iter().flatten())
}
}
_ => {}
}
}
debug!(?requested_pres, "Requested present set");
debug!(?requested_rem, "Requested remove set");
@ -1081,6 +1086,7 @@ mod tests {
Access, AccessClass, AccessControls, AccessControlsTransaction, AccessEffectivePermission,
};
use crate::prelude::*;
use crate::valueset::ValueSetIname;
const UUID_TEST_ACCOUNT_1: Uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
const UUID_TEST_ACCOUNT_2: Uuid = uuid::uuid!("cec0852a-abdf-4ea6-9dae-d3157cb33d3a");
@ -1948,7 +1954,7 @@ mod tests {
debug!("result --> {:?}", res);
debug!("expect --> {:?}", $expect);
// should be ok, and same as expect.
assert_eq!(res, $expect);
assert_eq!($expect, res);
}};
(
$me:expr,
@ -1975,12 +1981,14 @@ mod tests {
debug!("result --> {:?}", res);
debug!("expect --> {:?}", $expect);
// should be ok, and same as expect.
assert_eq!(res, $expect);
assert_eq!($expect, res);
}};
}
#[test]
fn test_access_enforce_modify() {
sketching::test_init();
let ev1 = E_TESTPERSON_1.clone().into_sealed_committed();
let r_set = vec![Arc::new(ev1)];
@ -2012,6 +2020,16 @@ mod tests {
modlist!([m_purge(Attribute::Name)]),
);
// Name Set
let me_set = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
modlist!([Modify::Set(Attribute::Name, ValueSetIname::new("value"))]),
);
// Class account pres
let me_pres_class = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
@ -2043,6 +2061,19 @@ mod tests {
modlist!([m_purge(Attribute::Class)]),
);
// Set Class
let me_set_class = ModifyEvent::new_impersonate_entry(
E_TEST_ACCOUNT_1.clone(),
filter_all!(f_eq(
Attribute::Name,
PartialValue::new_iname("testperson1")
)),
modlist!([Modify::Set(
Attribute::Class,
EntryClass::Account.to_valueset()
)]),
);
// Allow name and class, class is account
let acp_allow = AccessControlModify::from_raw(
"test_modify_allow",
@ -2104,6 +2135,8 @@ mod tests {
test_acp_modify!(&me_rem, vec![acp_allow.clone()], &r_set, true);
// test allowed purge
test_acp_modify!(&me_purge, vec![acp_allow.clone()], &r_set, true);
// test allowed set
test_acp_modify!(&me_set, vec![acp_allow.clone()], &r_set, true);
// Test rejected pres
test_acp_modify!(&me_pres, vec![acp_deny.clone()], &r_set, false);
@ -2111,22 +2144,31 @@ mod tests {
test_acp_modify!(&me_rem, vec![acp_deny.clone()], &r_set, false);
// Test rejected purge
test_acp_modify!(&me_purge, vec![acp_deny.clone()], &r_set, false);
// Test rejected set
test_acp_modify!(&me_set, vec![acp_deny.clone()], &r_set, false);
// test allowed pres class
test_acp_modify!(&me_pres_class, vec![acp_allow.clone()], &r_set, true);
// test allowed rem class
test_acp_modify!(&me_rem_class, vec![acp_allow.clone()], &r_set, true);
// test reject purge-class even if class present in allowed remattrs
test_acp_modify!(&me_purge_class, vec![acp_allow], &r_set, false);
test_acp_modify!(&me_purge_class, vec![acp_allow.clone()], &r_set, false);
// test allowed set class
test_acp_modify!(&me_set_class, vec![acp_allow], &r_set, true);
// Test reject pres class, but class not in classes
test_acp_modify!(&me_pres_class, vec![acp_no_class.clone()], &r_set, false);
// Test reject pres class, class in classes but not in pres attrs
test_acp_modify!(&me_pres_class, vec![acp_deny.clone()], &r_set, false);
// test reject rem class, but class not in classes
test_acp_modify!(&me_rem_class, vec![acp_no_class], &r_set, false);
test_acp_modify!(&me_rem_class, vec![acp_no_class.clone()], &r_set, false);
// test reject rem class, class in classes but not in pres attrs
test_acp_modify!(&me_rem_class, vec![acp_deny], &r_set, false);
test_acp_modify!(&me_rem_class, vec![acp_deny.clone()], &r_set, false);
// Test reject set class, but class not in classes
test_acp_modify!(&me_set_class, vec![acp_no_class], &r_set, false);
// Test reject set class, class in classes but not in pres attrs
test_acp_modify!(&me_set_class, vec![acp_deny], &r_set, false);
}
#[test]

View file

@ -1,9 +1,9 @@
use super::{ChangeFlag, QueryServerWriteTransaction};
use crate::prelude::*;
use crate::server::Plugins;
use hashbrown::HashMap;
use std::collections::BTreeMap;
pub type ModSetValid = HashMap<Uuid, ModifyList<ModifyValid>>;
pub type ModSetValid = BTreeMap<Uuid, ModifyList<ModifyValid>>;
pub struct BatchModifyEvent {
pub ident: Identity,

View file

@ -1,19 +1,6 @@
//! `server` contains the query server, which is the main high level construction
//! to coordinate queries and operations in the server.
use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction};
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::server::ScimReference;
use kanidm_proto::scim_v1::ScimEntryGetQuery;
use std::collections::BTreeSet;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::{Semaphore, SemaphorePermit};
use tracing::trace;
// We use so many, we just import them all ...
use self::access::{
profiles::{
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
@ -25,6 +12,7 @@ use self::keys::{
KeyObject, KeyProvider, KeyProviders, KeyProvidersReadTransaction, KeyProvidersTransaction,
KeyProvidersWriteTransaction,
};
use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction};
use crate::filter::{
Filter, FilterInvalid, FilterValid, FilterValidResolved, ResolveFilterCache,
ResolveFilterCacheReadTxn,
@ -42,6 +30,21 @@ use crate::schema::{
use crate::value::{CredentialType, EXTRACT_VAL_DN};
use crate::valueset::uuid_to_proto_string;
use crate::valueset::ScimValueIntermediate;
use crate::valueset::*;
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::server::ScimOAuth2ClaimMap;
use kanidm_proto::scim_v1::server::ScimOAuth2ScopeMap;
use kanidm_proto::scim_v1::server::ScimReference;
use kanidm_proto::scim_v1::JsonValue;
use kanidm_proto::scim_v1::ScimEntryGetQuery;
use std::collections::BTreeSet;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::{Semaphore, SemaphorePermit};
use tracing::trace;
pub(crate) mod access;
pub mod batch_modify;
@ -52,6 +55,7 @@ pub(crate) mod keys;
pub(crate) mod migrations;
pub mod modify;
pub(crate) mod recycle;
pub mod scim;
const RESOLVE_FILTER_CACHE_MAX: usize = 256;
const RESOLVE_FILTER_CACHE_LOCAL: usize = 8;
@ -845,29 +849,283 @@ pub trait QueryServerTransaction<'a> {
scim_value_intermediate: ScimValueIntermediate,
) -> Result<Option<ScimValueKanidm>, OperationError> {
match scim_value_intermediate {
ScimValueIntermediate::Refer(uuid) => {
if let Some(option) = self.uuid_to_spn(uuid)? {
Ok(Some(ScimValueKanidm::EntryReference(ScimReference {
uuid,
value: option.to_proto_string_clone(),
})))
} else {
// TODO: didn't have spn, fallback to uuid.to_string ?
Ok(None)
}
}
ScimValueIntermediate::ReferMany(uuids) => {
let mut scim_references = vec![];
for uuid in uuids {
if let Some(option) = self.uuid_to_spn(uuid)? {
scim_references.push(ScimReference {
uuid,
value: option.to_proto_string_clone(),
})
}
}
ScimValueIntermediate::References(uuids) => {
let scim_references = uuids
.into_iter()
.map(|uuid| {
self.uuid_to_spn(uuid)
.and_then(|maybe_value| {
maybe_value.ok_or(OperationError::InvalidValueState)
})
.map(|value| ScimReference {
uuid,
value: value.to_proto_string_clone(),
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(ScimValueKanidm::EntryReferences(scim_references)))
}
ScimValueIntermediate::Oauth2ClaimMap(unresolved_maps) => {
let scim_claim_maps = unresolved_maps
.into_iter()
.map(
|UnresolvedScimValueOauth2ClaimMap {
group_uuid,
claim,
join_char,
values,
}| {
self.uuid_to_spn(group_uuid)
.and_then(|maybe_value| {
maybe_value.ok_or(OperationError::InvalidValueState)
})
.map(|value| ScimOAuth2ClaimMap {
group: value.to_proto_string_clone(),
group_uuid,
claim,
join_char,
values,
})
},
)
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(ScimValueKanidm::OAuth2ClaimMap(scim_claim_maps)))
}
ScimValueIntermediate::Oauth2ScopeMap(unresolved_maps) => {
let scim_claim_maps = unresolved_maps
.into_iter()
.map(|UnresolvedScimValueOauth2ScopeMap { group_uuid, scopes }| {
self.uuid_to_spn(group_uuid)
.and_then(|maybe_value| {
maybe_value.ok_or(OperationError::InvalidValueState)
})
.map(|value| ScimOAuth2ScopeMap {
group: value.to_proto_string_clone(),
group_uuid,
scopes,
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(ScimValueKanidm::OAuth2ScopeMap(scim_claim_maps)))
}
}
}
fn resolve_scim_json_put(
&mut self,
attr: &Attribute,
value: Option<JsonValue>,
) -> Result<Option<ValueSet>, 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()));
};
let Some(value) = value else {
// It's a none so the value needs to be unset, and the attr DOES exist in
// schema.
return Ok(None);
};
let resolve_status = match schema_a.syntax {
SyntaxType::Utf8String => ValueSetUtf8::from_scim_json_put(value),
SyntaxType::Utf8StringInsensitive => ValueSetIutf8::from_scim_json_put(value),
SyntaxType::Uuid => ValueSetUuid::from_scim_json_put(value),
SyntaxType::Boolean => ValueSetBool::from_scim_json_put(value),
SyntaxType::SyntaxId => ValueSetSyntax::from_scim_json_put(value),
SyntaxType::IndexId => ValueSetIndex::from_scim_json_put(value),
SyntaxType::ReferenceUuid => ValueSetRefer::from_scim_json_put(value),
SyntaxType::Utf8StringIname => ValueSetIname::from_scim_json_put(value),
SyntaxType::NsUniqueId => ValueSetNsUniqueId::from_scim_json_put(value),
SyntaxType::DateTime => ValueSetDateTime::from_scim_json_put(value),
SyntaxType::EmailAddress => ValueSetEmailAddress::from_scim_json_put(value),
SyntaxType::Url => ValueSetUrl::from_scim_json_put(value),
SyntaxType::OauthScope => ValueSetOauthScope::from_scim_json_put(value),
SyntaxType::OauthScopeMap => ValueSetOauthScopeMap::from_scim_json_put(value),
SyntaxType::OauthClaimMap => ValueSetOauthClaimMap::from_scim_json_put(value),
SyntaxType::UiHint => ValueSetUiHint::from_scim_json_put(value),
SyntaxType::CredentialType => ValueSetCredentialType::from_scim_json_put(value),
SyntaxType::Certificate => ValueSetCertificate::from_scim_json_put(value),
SyntaxType::SshKey => ValueSetSshKey::from_scim_json_put(value),
SyntaxType::Uint32 => ValueSetUint32::from_scim_json_put(value),
// Not Yet ... if ever
// SyntaxType::JsonFilter => ValueSetJsonFilter::from_scim_json_put(value),
SyntaxType::JsonFilter => Err(OperationError::InvalidAttribute(
"Json Filters are not able to be set.".to_string(),
)),
// Can't be set currently as these are only internally generated for key-id's
// SyntaxType::HexString => ValueSetHexString::from_scim_json_put(value),
SyntaxType::HexString => Err(OperationError::InvalidAttribute(
"Hex strings are not able to be set.".to_string(),
)),
// Can't be set until we have better error handling in the set paths
// SyntaxType::Image => ValueSetImage::from_scim_json_put(value),
SyntaxType::Image => Err(OperationError::InvalidAttribute(
"Images are not able to be set.".to_string(),
)),
// Can't be set yet, mostly as I'm lazy
// SyntaxType::WebauthnAttestationCaList => {
// ValueSetWebauthnAttestationCaList::from_scim_json_put(value)
// }
SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
"Webauthn Attestation Ca Lists are not able to be set.".to_string(),
)),
// Syntax types that can not be submitted
SyntaxType::Credential => Err(OperationError::InvalidAttribute(
"Credentials are not able to be set.".to_string(),
)),
SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute(
"Secrets are not able to be set.".to_string(),
)),
SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute(
"SPNs are not able to be set.".to_string(),
)),
SyntaxType::Cid => Err(OperationError::InvalidAttribute(
"CIDs are not able to be set.".to_string(),
)),
SyntaxType::PrivateBinary => Err(OperationError::InvalidAttribute(
"Private Binaries are not able to be set.".to_string(),
)),
SyntaxType::IntentToken => Err(OperationError::InvalidAttribute(
"Intent Tokens are not able to be set.".to_string(),
)),
SyntaxType::Passkey => Err(OperationError::InvalidAttribute(
"Passkeys are not able to be set.".to_string(),
)),
SyntaxType::AttestedPasskey => Err(OperationError::InvalidAttribute(
"Attested Passkeys are not able to be set.".to_string(),
)),
SyntaxType::Session => Err(OperationError::InvalidAttribute(
"Sessions are not able to be set.".to_string(),
)),
SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute(
"Jws ES256 Private Keys are not able to be set.".to_string(),
)),
SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute(
"Jws RS256 Private Keys are not able to be set.".to_string(),
)),
SyntaxType::Oauth2Session => Err(OperationError::InvalidAttribute(
"Sessions are not able to be set.".to_string(),
)),
SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute(
"TOTP Secrets are not able to be set.".to_string(),
)),
SyntaxType::ApiToken => Err(OperationError::InvalidAttribute(
"API Tokens are not able to be set.".to_string(),
)),
SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute(
"Audit Strings are not able to be set.".to_string(),
)),
SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute(
"EC Private Keys are not able to be set.".to_string(),
)),
SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute(
"Key Internal Structures are not able to be set.".to_string(),
)),
SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute(
"Application Passwords are not able to be set.".to_string(),
)),
}?;
match resolve_status {
ValueSetResolveStatus::Resolved(vs) => Ok(vs),
ValueSetResolveStatus::NeedsResolution(vs_inter) => {
self.resolve_valueset_intermediate(vs_inter)
}
}
.map(Some)
}
fn resolve_valueset_intermediate(
&mut self,
vs_inter: ValueSetIntermediate,
) -> Result<ValueSet, OperationError> {
match vs_inter {
ValueSetIntermediate::References {
mut resolved,
unresolved,
} => {
for value in unresolved {
let un = self.name_to_uuid(value.as_str()).unwrap_or_else(|_| {
warn!(
?value,
"Value can not be resolved to a uuid - assuming it does not exist."
);
UUID_DOES_NOT_EXIST
});
resolved.insert(un);
}
let vs = ValueSetRefer::from_set(resolved);
Ok(vs)
}
ValueSetIntermediate::Oauth2ClaimMap {
mut resolved,
unresolved,
} => {
resolved.extend(unresolved.into_iter().map(
|UnresolvedValueSetOauth2ClaimMap {
group_name,
claim,
join_char,
claim_values,
}| {
let group_uuid =
self.name_to_uuid(group_name.as_str()).unwrap_or_else(|_| {
warn!(
?group_name,
"Value can not be resolved to a uuid - assuming it does not exist."
);
UUID_DOES_NOT_EXIST
});
ResolvedValueSetOauth2ClaimMap {
group_uuid,
claim,
join_char,
claim_values,
}
},
));
let vs = ValueSetOauthClaimMap::from_set(resolved);
Ok(vs)
}
ValueSetIntermediate::Oauth2ScopeMap {
mut resolved,
unresolved,
} => {
resolved.extend(unresolved.into_iter().map(
|UnresolvedValueSetOauth2ScopeMap { group_name, scopes }| {
let group_uuid =
self.name_to_uuid(group_name.as_str()).unwrap_or_else(|_| {
warn!(
?group_name,
"Value can not be resolved to a uuid - assuming it does not exist."
);
UUID_DOES_NOT_EXIST
});
ResolvedValueSetOauth2ScopeMap { group_uuid, scopes }
},
));
let vs = ValueSetOauthScopeMap::from_set(resolved);
Ok(vs)
}
}
}

View file

@ -0,0 +1,344 @@
use crate::prelude::*;
use crate::server::batch_modify::{BatchModifyEvent, ModSetValid};
use kanidm_proto::scim_v1::client::ScimEntryPutGeneric;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct ScimEntryPutEvent {
/// The identity performing the change.
pub ident: Identity,
// future - etags to detect version changes.
/// The target entry that will be changed
pub target: Uuid,
/// Update an attribute to contain the following value state.
/// If the attribute is None, it is removed.
pub attrs: BTreeMap<Attribute, Option<ValueSet>>,
}
impl ScimEntryPutEvent {
pub fn try_from(
ident: Identity,
entry: ScimEntryPutGeneric,
qs: &mut QueryServerWriteTransaction,
) -> Result<Self, OperationError> {
let target = entry.id;
let attrs = entry
.attrs
.into_iter()
.map(|(attr, json_value)| {
qs.resolve_scim_json_put(&attr, json_value)
.map(|kani_value| (attr, kani_value))
})
.collect::<Result<_, _>>()?;
Ok(ScimEntryPutEvent {
ident,
target,
attrs,
})
}
}
impl<'a> QueryServerWriteTransaction<'a> {
/// SCIM PUT is the handler where a single entry is updated. In a SCIM PUT request
/// the request defines the state of an attribute in entirety for the update. This
/// means if the caller wants to add one email address, they must PUT all existing
/// addresses in addition to the new one.
pub fn scim_put(
&mut self,
scim_entry_put: ScimEntryPutEvent,
) -> Result<ScimEntryKanidm, OperationError> {
let ScimEntryPutEvent {
ident,
target,
attrs,
} = scim_entry_put;
// This function transforms the put event into a modify event.
let mods_invalid: ModifyList<ModifyInvalid> = attrs.into();
let mods_valid = mods_invalid
.validate(self.get_schema())
.map_err(OperationError::SchemaViolation)?;
let mut modset = ModSetValid::default();
modset.insert(target, mods_valid);
let modify_event = BatchModifyEvent {
ident: ident.clone(),
modset,
};
// dispatch to batch modify
self.batch_modify(&modify_event)?;
// Now get the entry. We handle a lot of the errors here nicely,
// but if we got to this point, they really can't happen.
let filter_intent = filter!(f_and!([f_eq(Attribute::Uuid, PartialValue::Uuid(target))]));
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 se = SearchEvent {
ident,
filter: f_valid,
filter_orig: f_intent_valid,
// Return all attributes, even ones we didn't affect
attrs: None,
};
let mut vs = self.search_ext(&se)?;
match vs.pop() {
Some(entry) if vs.is_empty() => entry.to_scim_kanidm(self),
_ => {
if vs.is_empty() {
Err(OperationError::NoMatchingEntries)
} else {
// Multiple entries matched, should not be possible!
Err(OperationError::UniqueConstraintViolation)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::ScimEntryPutEvent;
use crate::prelude::*;
use kanidm_proto::scim_v1::client::ScimEntryPutKanidm;
use kanidm_proto::scim_v1::server::ScimReference;
#[qs_test]
async fn scim_put_basic(server: &QueryServer) {
let mut server_txn = server.write(duration_from_epoch_now()).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);
// Make an entry.
let group_uuid = Uuid::new_v4();
// Add members to our groups to test reference handling in scim
let extra1_uuid = Uuid::new_v4();
let extra2_uuid = Uuid::new_v4();
let extra3_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))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("extra_1")),
(Attribute::Uuid, Value::Uuid(extra1_uuid))
);
let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("extra_2")),
(Attribute::Uuid, Value::Uuid(extra2_uuid))
);
let e4 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("extra_3")),
(Attribute::Uuid, Value::Uuid(extra3_uuid))
);
assert!(server_txn.internal_create(vec![e1, e2, e3, e4]).is_ok());
// Set an attr
let put = ScimEntryPutKanidm {
id: group_uuid,
attrs: [(Attribute::Description, Some("Group Description".into()))].into(),
};
let put_generic = put.try_into().unwrap();
let put_event =
ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
.expect("Failed to resolve data type");
let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
let desc = updated_entry.attrs.get(&Attribute::Description).unwrap();
match desc {
ScimValueKanidm::String(gdesc) if gdesc == "Group Description" => {}
_ => assert!(false),
};
// null removes attr
let put = ScimEntryPutKanidm {
id: group_uuid,
attrs: [(Attribute::Description, None)].into(),
};
let put_generic = put.try_into().unwrap();
let put_event =
ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
.expect("Failed to resolve data type");
let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
assert!(updated_entry.attrs.get(&Attribute::Description).is_none());
// set one
let put = ScimEntryPutKanidm {
id: group_uuid,
attrs: [(
Attribute::Member,
Some(ScimValueKanidm::EntryReferences(vec![ScimReference {
uuid: extra1_uuid,
// Doesn't matter what this is, because there is a UUID, it's ignored
value: String::default(),
}])),
)]
.into(),
};
let put_generic = put.try_into().unwrap();
let put_event =
ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
.expect("Failed to resolve data type");
let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
trace!(?members);
match members {
ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 1 => {
assert!(member_set.contains(&ScimReference {
uuid: extra1_uuid,
value: "extra_1@example.com".to_string(),
}));
}
_ => assert!(false),
};
// set many
let put = ScimEntryPutKanidm {
id: group_uuid,
attrs: [(
Attribute::Member,
Some(ScimValueKanidm::EntryReferences(vec![
ScimReference {
uuid: extra1_uuid,
value: String::default(),
},
ScimReference {
uuid: extra2_uuid,
value: String::default(),
},
ScimReference {
uuid: extra3_uuid,
value: String::default(),
},
])),
)]
.into(),
};
let put_generic = put.try_into().unwrap();
let put_event =
ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
.expect("Failed to resolve data type");
let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
trace!(?members);
match members {
ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 3 => {
assert!(member_set.contains(&ScimReference {
uuid: extra1_uuid,
value: "extra_1@example.com".to_string(),
}));
assert!(member_set.contains(&ScimReference {
uuid: extra2_uuid,
value: "extra_2@example.com".to_string(),
}));
assert!(member_set.contains(&ScimReference {
uuid: extra3_uuid,
value: "extra_3@example.com".to_string(),
}));
}
_ => assert!(false),
};
// set many with a removal
let put = ScimEntryPutKanidm {
id: group_uuid,
attrs: [(
Attribute::Member,
Some(ScimValueKanidm::EntryReferences(vec![
ScimReference {
uuid: extra1_uuid,
value: String::default(),
},
ScimReference {
uuid: extra3_uuid,
value: String::default(),
},
])),
)]
.into(),
};
let put_generic = put.try_into().unwrap();
let put_event =
ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
.expect("Failed to resolve data type");
let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
let members = updated_entry.attrs.get(&Attribute::Member).unwrap();
trace!(?members);
match members {
ScimValueKanidm::EntryReferences(member_set) if member_set.len() == 2 => {
assert!(member_set.contains(&ScimReference {
uuid: extra1_uuid,
value: "extra_1@example.com".to_string(),
}));
assert!(member_set.contains(&ScimReference {
uuid: extra3_uuid,
value: "extra_3@example.com".to_string(),
}));
// Member 2 is gone
assert!(!member_set.contains(&ScimReference {
uuid: extra2_uuid,
value: "extra_2@example.com".to_string(),
}));
}
_ => assert!(false),
};
// empty set removes attr
let put = ScimEntryPutKanidm {
id: group_uuid,
attrs: [(Attribute::Member, None)].into(),
};
let put_generic = put.try_into().unwrap();
let put_event =
ScimEntryPutEvent::try_from(idm_admin_ident.clone(), put_generic, &mut server_txn)
.expect("Failed to resolve data type");
let updated_entry = server_txn.scim_put(put_event).expect("Failed to put");
assert!(updated_entry.attrs.get(&Attribute::Member).is_none());
}
}

View file

@ -4,7 +4,6 @@ use crate::prelude::*;
use hashbrown::HashSet;
use rand::distributions::{Distribution, Uniform};
use rand::{thread_rng, Rng};
use std::collections::BTreeSet;
use std::ops::Range;
#[derive(Debug)]
@ -75,20 +74,6 @@ pub fn readable_password_from_random() -> String {
)
}
pub fn str_join(set: &BTreeSet<String>) -> String {
let alloc_len = set.iter().fold(0, |acc, s| acc + s.len() + 1);
let mut buf = String::with_capacity(alloc_len);
set.iter().for_each(|s| {
buf.push_str(s);
buf.push(' ');
});
// Remove the excess trailing space.
let _ = buf.pop();
buf
}
impl Distribution<char> for DistinctAlpha {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> char {
const RANGE: u32 = 55;

View file

@ -43,6 +43,7 @@ use crate::valueset::image::ImageValueThings;
use crate::valueset::uuid_to_proto_string;
use kanidm_proto::internal::{ApiTokenPurpose, Filter as ProtoFilter, UiHint};
use kanidm_proto::scim_v1::ScimOauth2ClaimMapJoinChar;
use kanidm_proto::v1::UatPurposeStatus;
use std::hash::Hash;
@ -1113,6 +1114,34 @@ pub enum OauthClaimMapJoin {
JsonArray,
}
impl From<OauthClaimMapJoin> for ScimOauth2ClaimMapJoinChar {
fn from(value: OauthClaimMapJoin) -> Self {
match value {
OauthClaimMapJoin::CommaSeparatedValue => {
ScimOauth2ClaimMapJoinChar::CommaSeparatedValue
}
OauthClaimMapJoin::SpaceSeparatedValue => {
ScimOauth2ClaimMapJoinChar::SpaceSeparatedValue
}
OauthClaimMapJoin::JsonArray => ScimOauth2ClaimMapJoinChar::JsonArray,
}
}
}
impl From<ScimOauth2ClaimMapJoinChar> for OauthClaimMapJoin {
fn from(value: ScimOauth2ClaimMapJoinChar) -> Self {
match value {
ScimOauth2ClaimMapJoinChar::CommaSeparatedValue => {
OauthClaimMapJoin::CommaSeparatedValue
}
ScimOauth2ClaimMapJoinChar::SpaceSeparatedValue => {
OauthClaimMapJoin::SpaceSeparatedValue
}
ScimOauth2ClaimMapJoinChar::JsonArray => OauthClaimMapJoin::JsonArray,
}
}
}
impl OauthClaimMapJoin {
pub(crate) fn to_str(self) -> &'static str {
match self {

View file

@ -1,15 +1,16 @@
use std::collections::BTreeSet;
use smolset::SmolSet;
use crate::be::dbvalue::DbValueAddressV1;
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::utils::trigraph_iter;
use crate::value::{Address, VALIDATE_EMAIL_RE};
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
use kanidm_proto::scim_v1::server::{ScimAddress, ScimMail};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::client::ScimAddress as ScimAddressClient;
use kanidm_proto::scim_v1::JsonValue;
use kanidm_proto::scim_v1::{server::ScimAddress, ScimMail};
use smolset::SmolSet;
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
pub struct ValueSetAddress {
@ -54,6 +55,43 @@ impl ValueSetAddress {
}
}
impl ValueSetScimPut for ValueSetAddress {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let addresses: Vec<ScimAddressClient> = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Address syntax invalid");
OperationError::SC0011AddressSyntaxInvalid
})?;
let set = addresses
.into_iter()
.map(
|ScimAddressClient {
street_address,
locality,
region,
postal_code,
country,
}| {
let formatted =
format!("{street_address}, {locality}, {region}, {postal_code}, {country}");
Address {
formatted,
street_address,
locality,
region,
postal_code,
country,
}
},
)
.collect();
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetAddress {
set,
})))
}
}
impl FromIterator<Address> for Option<Box<ValueSetAddress>> {
fn from_iter<T>(iter: T) -> Option<Box<ValueSetAddress>>
where
@ -286,6 +324,44 @@ impl ValueSetEmailAddress {
}
}
impl ValueSetScimPut for ValueSetEmailAddress {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let scim_mails: Vec<ScimMail> = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Mail Attribute Syntax Invalid");
OperationError::SC0003MailSyntaxInvalid
})?;
let mut primary = None;
let set: BTreeSet<_> = scim_mails
.into_iter()
.map(
|ScimMail {
value,
primary: is_primary,
}| {
if is_primary {
primary = Some(value.clone());
}
value
},
)
.collect();
let primary = primary
.or_else(|| set.iter().next().cloned())
.ok_or_else(|| {
error!(
"Mail attribute has no values that can be used as the primary mail address."
);
OperationError::SC0003MailSyntaxInvalid
})?;
Ok(ValueSetResolveStatus::Resolved(Box::new(
ValueSetEmailAddress { primary, set },
)))
}
}
impl ValueSetT for ValueSetEmailAddress {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -607,7 +683,10 @@ mod tests {
"value": "claire@example.com"
}
]"#;
crate::valueset::scim_json_reflexive(vs, data);
crate::valueset::scim_json_reflexive(vs.clone(), data);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetEmailAddress>(vs, &[])
}
#[test]
@ -632,6 +711,9 @@ mod tests {
}
]"#;
crate::valueset::scim_json_reflexive(vs, data);
crate::valueset::scim_json_reflexive(vs.clone(), data);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetAddress>(vs, &[])
}
}

View file

@ -1,15 +1,13 @@
use crate::valueset::ScimResolveStatus;
use base64urlsafedata::Base64UrlSafeData;
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use smolset::SmolSet;
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::utils::trigraph_iter;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use base64urlsafedata::Base64UrlSafeData;
use kanidm_proto::scim_v1::server::ScimBinary;
use smolset::SmolSet;
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct ValueSetPrivateBinary {

View file

@ -1,7 +1,8 @@
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use crate::valueset::{DbValueSetV2, ValueSet, ValueSetResolveStatus, ValueSetScimPut};
use kanidm_proto::scim_v1::JsonValue;
use smolset::SmolSet;
#[derive(Debug, Clone)]
@ -37,6 +38,22 @@ impl ValueSetBool {
}
}
impl ValueSetScimPut for ValueSetBool {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value: bool = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM boolean syntax invalid");
OperationError::SC0005BoolSyntaxInvalid
})?;
let mut set = SmolSet::new();
set.insert(value);
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetBool {
set,
})))
}
}
impl ValueSetT for ValueSetBool {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -168,6 +185,9 @@ mod tests {
#[test]
fn test_scim_boolean() {
let vs: ValueSet = ValueSetBool::new(true);
crate::valueset::scim_json_reflexive(vs, "true");
crate::valueset::scim_json_reflexive(vs.clone(), "true");
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetBool>(vs, &[])
}
}

View file

@ -2,8 +2,10 @@ use crate::be::dbvalue::DbValueCertificate;
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use crate::valueset::{DbValueSetV2, ValueSet, ValueSetResolveStatus, ValueSetScimPut};
use kanidm_proto::scim_v1::client::ScimCertificate as ClientScimCertificate;
use kanidm_proto::scim_v1::server::ScimCertificate;
use kanidm_proto::scim_v1::JsonValue;
use std::collections::BTreeMap;
use kanidm_lib_crypto::{
@ -101,6 +103,41 @@ impl ValueSetCertificate {
}
}
impl ValueSetScimPut for ValueSetCertificate {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let der_values: Vec<ClientScimCertificate> =
serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Certificate syntax invalid");
OperationError::SC0012CertificateSyntaxInvalid
})?;
// For each one, check it's a real der certificate.
let mut map = BTreeMap::new();
for ClientScimCertificate { der } in der_values {
// Parse the DER
let certificate = Certificate::from_der(&der)
.map(Box::new)
.map_err(|x509_err| {
error!(?x509_err, "Unable to restore certificate from DER");
OperationError::SC0013CertificateInvalidDer
})?;
// sha256 the public key
let pk_s256 = x509_public_key_s256(&certificate).ok_or_else(|| {
error!("Unable to digest public key");
OperationError::SC0014CertificateInvalidDigest
})?;
map.insert(pk_s256, certificate);
}
Ok(ValueSetResolveStatus::Resolved(Box::new(
ValueSetCertificate { map },
)))
}
}
impl ValueSetT for ValueSetCertificate {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -313,5 +350,8 @@ raBy6edj7W0EIH+yQxkDEwIhAI0nVKaI6duHLAvtKW6CfEQFG6jKg7dyk37YYiRD
.unwrap();
assert_eq!(cert.s256, expect_s256);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetCertificate>(vs, &[])
}
}

View file

@ -1,13 +1,3 @@
use crate::valueset::ScimResolveStatus;
use smolset::SmolSet;
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use time::OffsetDateTime;
use webauthn_rs::prelude::{
AttestationCaList, AttestedPasskey as AttestedPasskeyV4, Passkey as PasskeyV4,
};
use crate::be::dbvalue::{
DbValueAttestedPasskeyV1, DbValueCredV1, DbValueIntentTokenStateV1, DbValuePasskeyV1,
};
@ -16,9 +6,18 @@ use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::utils::trigraph_iter;
use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState};
use crate::valueset::{DbValueSetV2, ValueSet};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::server::{ScimIntentToken, ScimIntentTokenState};
use kanidm_proto::scim_v1::JsonValue;
use smolset::SmolSet;
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use time::OffsetDateTime;
use webauthn_rs::prelude::{
AttestationCaList, AttestedPasskey as AttestedPasskeyV4, Passkey as PasskeyV4,
};
#[derive(Debug, Clone)]
pub struct ValueSetCredential {
@ -880,6 +879,29 @@ impl ValueSetCredentialType {
}
}
impl ValueSetScimPut for ValueSetCredentialType {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value = serde_json::from_value::<String>(value)
.map_err(|err| {
error!(?err, "SCIM CredentialType syntax invalid");
OperationError::SC0015CredentialTypeSyntaxInvalid
})
.and_then(|value| {
CredentialType::try_from(value.as_str()).map_err(|()| {
error!("SCIM CredentialType syntax invalid - value");
OperationError::SC0015CredentialTypeSyntaxInvalid
})
})?;
let mut set = SmolSet::new();
set.insert(value);
Ok(ValueSetResolveStatus::Resolved(Box::new(
ValueSetCredentialType { set },
)))
}
}
impl ValueSetT for ValueSetCredentialType {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -949,9 +971,10 @@ impl ValueSetT for ValueSetCredentialType {
}
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
self.set.iter().map(|ct| ct.to_string()).collect::<Vec<_>>(),
)))
self.set
.iter()
.next()
.map(|ct| ScimResolveStatus::Resolved(ScimValueKanidm::from(ct.to_string())))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
@ -1166,6 +1189,9 @@ mod tests {
#[test]
fn test_scim_credential_type() {
let vs: ValueSet = ValueSetCredentialType::new(CredentialType::Mfa);
crate::valueset::scim_json_reflexive(vs, r#"["mfa"]"#);
crate::valueset::scim_json_reflexive(vs.clone(), r#""mfa""#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetCredentialType>(vs, &[])
}
}

View file

@ -1,8 +1,9 @@
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::{client::ScimDateTime, JsonValue};
use smolset::SmolSet;
use time::OffsetDateTime;
@ -46,6 +47,22 @@ impl ValueSetDateTime {
}
}
impl ValueSetScimPut for ValueSetDateTime {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let ScimDateTime { date_time } = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM DateTime syntax invalid");
OperationError::SC0010DateTimeSyntaxInvalid
})?;
let mut set = SmolSet::new();
set.insert(date_time);
Ok(ValueSetResolveStatus::Resolved(Box::new(
ValueSetDateTime { set },
)))
}
}
impl ValueSetT for ValueSetDateTime {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -125,14 +142,7 @@ impl ValueSetT for ValueSetDateTime {
}
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
let mut iter = self.set.iter().copied();
if self.len() == 1 {
let v = iter.next().unwrap_or(OffsetDateTime::UNIX_EPOCH);
Some(v.into())
} else {
let arr = iter.collect::<Vec<_>>();
Some(arr.into())
}
self.set.iter().next().copied().map(|v| v.into())
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
@ -200,6 +210,9 @@ mod tests {
let odt = OffsetDateTime::UNIX_EPOCH + Duration::from_secs(69_420);
let vs: ValueSet = ValueSetDateTime::new(odt);
crate::valueset::scim_json_reflexive(vs, r#""1970-01-01T19:17:00Z""#);
crate::valueset::scim_json_reflexive(vs.clone(), r#""1970-01-01T19:17:00Z""#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetDateTime>(vs, &[])
}
}

View file

@ -1,13 +1,11 @@
use crate::valueset::ScimResolveStatus;
use std::iter::{self};
use super::ValueSet;
use crate::be::dbvalue::DbValueSetV2;
use crate::prelude::*;
use crate::value::{PartialValue, SyntaxType, Value};
use crate::valueset::ScimResolveStatus;
use openssl::ec::EcKey;
use openssl::pkey::{Private, Public};
use std::iter::{self};
#[derive(Debug, Clone)]
struct EcKeyPrivate {

View file

@ -183,6 +183,9 @@ mod tests {
fn test_scim_hexstring() {
let vs: ValueSet =
ValueSetHexString::new("D68475C760A7A0F6A924C28F095573A967F600D6".to_string());
crate::valueset::scim_json_reflexive(vs, r#""D68475C760A7A0F6A924C28F095573A967F600D6""#);
crate::valueset::scim_json_reflexive(
vs.clone(),
r#""D68475C760A7A0F6A924C28F095573A967F600D6""#,
);
}
}

View file

@ -458,6 +458,7 @@ impl ValueSetT for ValueSetImage {
#[cfg(test)]
mod tests {
// use super::ValueSetImage;
use super::{ImageType, ImageValue, ImageValueThings};
#[test]
@ -511,8 +512,8 @@ mod tests {
assert!(!image.hash_imagevalue().is_empty());
}
/*
// This test is broken on github as it appears to be changing the binary image hash.
/*
#[test]
fn test_scim_imagevalue() {
let filename = format!(
@ -531,7 +532,7 @@ mod tests {
"142dc7984dd548dd5dacfe2ad30f8473e3217e39b3b6c8d17a0cf6e4e24b02e0"
]"#;
crate::valueset::scim_json_reflexive(vs, data);
crate::valueset::scim_json_reflexive(vs.clone(), data);
}
*/
}

View file

@ -2,7 +2,8 @@ use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::utils::trigraph_iter;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use crate::valueset::{DbValueSetV2, ValueSet, ValueSetResolveStatus, ValueSetScimPut};
use kanidm_proto::scim_v1::JsonValue;
use std::collections::BTreeSet;
@ -39,6 +40,22 @@ impl ValueSetIname {
}
}
impl ValueSetScimPut for ValueSetIname {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value = serde_json::from_value::<String>(value).map_err(|err| {
error!(?err, "SCIM Iname Syntax Invalid");
OperationError::SC0016InameSyntaxInvalid
})?;
let mut set = BTreeSet::new();
set.insert(value.to_lowercase());
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetIname {
set,
})))
}
}
impl ValueSetT for ValueSetIname {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -209,6 +226,9 @@ mod tests {
#[test]
fn test_scim_iname() {
let vs: ValueSet = ValueSetIname::new("stevo");
crate::valueset::scim_json_reflexive(vs, r#""stevo""#);
crate::valueset::scim_json_reflexive(vs.clone(), r#""stevo""#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetIname>(vs, &[])
}
}

View file

@ -1,7 +1,8 @@
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use crate::valueset::{DbValueSetV2, ValueSet, ValueSetResolveStatus, ValueSetScimPut};
use kanidm_proto::scim_v1::JsonValue;
use smolset::SmolSet;
@ -38,6 +39,29 @@ impl ValueSetIndex {
}
}
impl ValueSetScimPut for ValueSetIndex {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value = serde_json::from_value::<Vec<String>>(value).map_err(|err| {
error!(?err, "SCIM IndexType syntax invalid");
OperationError::SC0009IndexTypeSyntaxInvalid
})?;
let set = value
.into_iter()
.map(|s| {
IndexType::try_from(s.as_str()).map_err(|_| {
error!("SCIM IndexType syntax invalid value");
OperationError::SC0009IndexTypeSyntaxInvalid
})
})
.collect::<Result<_, _>>()?;
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetIndex {
set,
})))
}
}
impl ValueSetT for ValueSetIndex {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -159,6 +183,9 @@ mod tests {
#[test]
fn test_scim_index() {
let vs: ValueSet = ValueSetIndex::new(IndexType::Equality);
crate::valueset::scim_json_reflexive(vs, r#"["EQUALITY"]"#);
crate::valueset::scim_json_reflexive(vs.clone(), r#"["EQUALITY"]"#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetIndex>(vs, &[])
}
}

View file

@ -3,8 +3,9 @@ use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::utils::trigraph_iter;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use crate::valueset::{DbValueSetV2, ValueSet, ValueSetResolveStatus, ValueSetScimPut};
use kanidm_proto::scim_v1::client::ScimStrings;
use kanidm_proto::scim_v1::JsonValue;
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
@ -40,6 +41,21 @@ impl ValueSetIutf8 {
}
}
impl ValueSetScimPut for ValueSetIutf8 {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let ScimStrings(values) = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Iutf8 Syntax Invalid");
OperationError::SC0017Iutf8SyntaxInvalid
})?;
let set = values.iter().map(|s| s.to_lowercase()).collect();
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetIutf8 {
set,
})))
}
}
impl ValueSetT for ValueSetIutf8 {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -128,9 +144,11 @@ impl ValueSetT for ValueSetIutf8 {
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
self.set
.iter()
.all(|s| Value::validate_str_escapes(s) && Value::validate_singleline(s))
self.set.iter().all(|s| {
Value::validate_str_escapes(s) && Value::validate_singleline(s) &&
// I'm sure there is a better way ...
s.to_lowercase().as_str() == s.as_str()
})
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
@ -209,6 +227,9 @@ mod tests {
#[test]
fn test_scim_iutf8() {
let vs: ValueSet = ValueSetIutf8::new("lowercase string");
crate::valueset::scim_json_reflexive(vs, r#""lowercase string""#);
crate::valueset::scim_json_reflexive(vs.clone(), r#""lowercase string""#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetIutf8>(vs, &[])
}
}

View file

@ -3,7 +3,6 @@ use crate::schema::SchemaAttribute;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use kanidm_proto::internal::Filter as ProtoFilter;
use smolset::SmolSet;
#[derive(Debug, Clone)]
@ -206,6 +205,9 @@ mod tests {
"{\"pres\":\"class\"}"
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
crate::valueset::scim_json_reflexive(vs.clone(), data);
// Test that we can parse json values into a valueset.
// crate::valueset::scim_json_put_reflexive::<ValueSetJsonFilter>(vs, &[])
}
}

View file

@ -1,18 +1,14 @@
use crate::be::dbvalue::{DbValueKeyInternal, DbValueKeyStatus, DbValueKeyUsage};
use crate::prelude::*;
use crate::valueset::ScimResolveStatus;
use crate::server::keys::KeyId;
use crate::value::{KeyStatus, KeyUsage};
use crate::be::dbvalue::{DbValueKeyInternal, DbValueKeyStatus, DbValueKeyUsage};
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::ScimKeyInternal;
use std::collections::BTreeMap;
use std::fmt;
use time::OffsetDateTime;
use kanidm_proto::scim_v1::server::ScimKeyInternal;
#[derive(Clone, PartialEq, Eq)]
pub struct KeyInternalData {
pub usage: KeyUsage,

View file

@ -1,30 +1,30 @@
use std::collections::{BTreeMap, BTreeSet};
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
use dyn_clone::DynClone;
use hashbrown::HashSet;
use kanidm_lib_crypto::{x509_cert::Certificate, Sha256Digest};
use kanidm_proto::internal::ImageValue;
use openssl::ec::EcKey;
use openssl::pkey::Private;
use openssl::pkey::Public;
use serde::Serialize;
use serde_with::serde_as;
use smolset::SmolSet;
use sshkey_attest::proto::PublicKey as SshPublicKey;
use time::OffsetDateTime;
use webauthn_rs::prelude::AttestationCaList;
use webauthn_rs::prelude::AttestedPasskey as AttestedPasskeyV4;
use webauthn_rs::prelude::Passkey as PasskeyV4;
use crate::be::dbvalue::DbValueSetV2;
use crate::credential::{apppwd::ApplicationPassword, totp::Totp, Credential};
use crate::prelude::*;
use crate::repl::cid::Cid;
use crate::schema::SchemaAttribute;
use crate::server::keys::KeyId;
use crate::value::{Address, ApiToken, CredentialType, IntentTokenState, Oauth2Session, Session};
use crate::value::{
Address, ApiToken, CredentialType, IntentTokenState, Oauth2Session, OauthClaimMapJoin, Session,
};
use compact_jwt::{crypto::JwsRs256Signer, JwsEs256Signer};
use dyn_clone::DynClone;
use hashbrown::HashSet;
use kanidm_lib_crypto::{x509_cert::Certificate, Sha256Digest};
use kanidm_proto::internal::ImageValue;
use kanidm_proto::internal::{Filter as ProtoFilter, UiHint};
use kanidm_proto::scim_v1::JsonValue;
use kanidm_proto::scim_v1::ScimOauth2ClaimMapJoinChar;
use openssl::ec::EcKey;
use openssl::pkey::Private;
use openssl::pkey::Public;
use smolset::SmolSet;
use sshkey_attest::proto::PublicKey as SshPublicKey;
use std::collections::{BTreeMap, BTreeSet};
use time::OffsetDateTime;
use webauthn_rs::prelude::AttestationCaList;
use webauthn_rs::prelude::AttestedPasskey as AttestedPasskeyV4;
use webauthn_rs::prelude::Passkey as PasskeyV4;
pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
use self::apppwd::ValueSetApplicationPassword;
@ -661,17 +661,32 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
}
}
pub trait ValueSetScimPut {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError>;
}
impl PartialEq for ValueSet {
fn eq(&self, other: &ValueSet) -> bool {
self.equal(other)
}
}
#[serde_as]
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct UnresolvedScimValueOauth2ClaimMap {
pub group_uuid: Uuid,
pub claim: String,
pub join_char: ScimOauth2ClaimMapJoinChar,
pub values: BTreeSet<String>,
}
pub struct UnresolvedScimValueOauth2ScopeMap {
pub group_uuid: Uuid,
pub scopes: BTreeSet<String>,
}
pub enum ScimValueIntermediate {
Refer(Uuid),
ReferMany(Vec<Uuid>),
References(Vec<Uuid>),
Oauth2ClaimMap(Vec<UnresolvedScimValueOauth2ClaimMap>),
Oauth2ScopeMap(Vec<UnresolvedScimValueOauth2ScopeMap>),
}
pub enum ScimResolveStatus {
@ -707,6 +722,69 @@ impl ScimResolveStatus {
}
}
pub enum ValueSetResolveStatus {
Resolved(ValueSet),
NeedsResolution(ValueSetIntermediate),
}
#[cfg(test)]
impl ValueSetResolveStatus {
pub fn assume_resolved(self) -> ValueSet {
match self {
ValueSetResolveStatus::Resolved(v) => v,
ValueSetResolveStatus::NeedsResolution(_) => {
panic!("assume_resolved called on NeedsResolution")
}
}
}
pub fn assume_unresolved(self) -> ValueSetIntermediate {
match self {
ValueSetResolveStatus::Resolved(_) => panic!("assume_unresolved called on Resolved"),
ValueSetResolveStatus::NeedsResolution(svi) => svi,
}
}
}
pub enum ValueSetIntermediate {
References {
resolved: BTreeSet<Uuid>,
unresolved: Vec<String>,
},
Oauth2ClaimMap {
resolved: Vec<ResolvedValueSetOauth2ClaimMap>,
unresolved: Vec<UnresolvedValueSetOauth2ClaimMap>,
},
Oauth2ScopeMap {
resolved: Vec<ResolvedValueSetOauth2ScopeMap>,
unresolved: Vec<UnresolvedValueSetOauth2ScopeMap>,
},
}
pub struct UnresolvedValueSetOauth2ClaimMap {
pub group_name: String,
pub claim: String,
pub join_char: OauthClaimMapJoin,
pub claim_values: BTreeSet<String>,
}
pub struct ResolvedValueSetOauth2ClaimMap {
pub group_uuid: Uuid,
pub claim: String,
pub join_char: OauthClaimMapJoin,
pub claim_values: BTreeSet<String>,
}
pub struct UnresolvedValueSetOauth2ScopeMap {
pub group_name: String,
pub scopes: BTreeSet<String>,
}
pub struct ResolvedValueSetOauth2ScopeMap {
pub group_uuid: Uuid,
pub scopes: BTreeSet<String>,
}
pub fn uuid_to_proto_string(u: Uuid) -> String {
u.as_hyphenated().to_string()
}
@ -925,14 +1003,20 @@ pub(crate) fn scim_json_reflexive(vs: ValueSet, data: &str) {
let json_value: serde_json::Value = serde_json::to_value(&scim_value).unwrap();
eprintln!("{}", data);
let expect: serde_json::Value = serde_json::from_str(data).unwrap();
assert_eq!(json_value, expect);
}
#[cfg(test)]
pub(crate) fn scim_json_reflexive_unresolved(vs: ValueSet, data: &str) {
let scim_value = vs.to_scim_value().unwrap().assume_unresolved();
pub(crate) fn scim_json_reflexive_unresolved(
write_txn: &mut QueryServerWriteTransaction,
vs: ValueSet,
data: &str,
) {
let scim_int_value = vs.to_scim_value().unwrap().assume_unresolved();
let scim_value = write_txn.resolve_scim_interim(scim_int_value).unwrap();
let strout = serde_json::to_string_pretty(&scim_value).unwrap();
eprintln!("{}", strout);
@ -943,3 +1027,50 @@ pub(crate) fn scim_json_reflexive_unresolved(vs: ValueSet, data: &str) {
assert_eq!(json_value, expect);
}
#[cfg(test)]
pub(crate) fn scim_json_put_reflexive<T: ValueSetScimPut>(
expect_vs: ValueSet,
additional_tests: &[(JsonValue, ValueSet)],
) {
let scim_value = expect_vs.to_scim_value().unwrap().assume_resolved();
let strout = serde_json::to_string_pretty(&scim_value).unwrap();
eprintln!("{}", strout);
let generic = serde_json::to_value(scim_value).unwrap();
// Check that we can turn back into a vs from the generic version.
let vs = T::from_scim_json_put(generic).unwrap().assume_resolved();
assert_eq!(&vs, &expect_vs);
// For each additional check, assert they work as expected.
for (jv, expect_vs) in additional_tests {
let vs = T::from_scim_json_put(jv.clone()).unwrap().assume_resolved();
assert_eq!(&vs, expect_vs);
}
}
#[cfg(test)]
pub(crate) fn scim_json_put_reflexive_unresolved<T: ValueSetScimPut>(
write_txn: &mut QueryServerWriteTransaction,
expect_vs: ValueSet,
additional_tests: &[(JsonValue, ValueSet)],
) {
let scim_int_value = expect_vs.to_scim_value().unwrap().assume_unresolved();
let scim_value = write_txn.resolve_scim_interim(scim_int_value).unwrap();
let generic = serde_json::to_value(scim_value).unwrap();
// Check that we can turn back into a vs from the generic version.
let vs_inter = T::from_scim_json_put(generic).unwrap().assume_unresolved();
let vs = write_txn.resolve_valueset_intermediate(vs_inter).unwrap();
assert_eq!(&vs, &expect_vs);
// For each additional check, assert they work as expected.
for (jv, expect_vs) in additional_tests {
let vs_inter = T::from_scim_json_put(jv.clone())
.unwrap()
.assume_unresolved();
let vs = write_txn.resolve_valueset_intermediate(vs_inter).unwrap();
assert_eq!(&vs, expect_vs);
}
}

View file

@ -2,7 +2,8 @@ use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::value::NSUNIQUEID_RE;
use crate::valueset::ScimResolveStatus;
use crate::valueset::{DbValueSetV2, ValueSet};
use crate::valueset::{DbValueSetV2, ValueSet, ValueSetResolveStatus, ValueSetScimPut};
use kanidm_proto::scim_v1::JsonValue;
use smolset::SmolSet;
@ -39,6 +40,22 @@ impl ValueSetNsUniqueId {
}
}
impl ValueSetScimPut for ValueSetNsUniqueId {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value = serde_json::from_value::<String>(value).map_err(|err| {
error!(?err, "SCIM NsUniqueId Syntax Invalid");
OperationError::SC0018NsUniqueIdSyntaxInvalid
})?;
let mut set = SmolSet::new();
set.insert(value.to_lowercase());
Ok(ValueSetResolveStatus::Resolved(Box::new(
ValueSetNsUniqueId { set },
)))
}
}
impl ValueSetT for ValueSetNsUniqueId {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -172,6 +189,12 @@ mod tests {
fn test_scim_nsuniqueid() {
let vs: ValueSet =
ValueSetNsUniqueId::new("3a163ca0-47624620-a18806b7-50c84c86".to_string());
crate::valueset::scim_json_reflexive(vs, r#""3a163ca0-47624620-a18806b7-50c84c86""#);
crate::valueset::scim_json_reflexive(
vs.clone(),
r#""3a163ca0-47624620-a18806b7-50c84c86""#,
);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetNsUniqueId>(vs, &[])
}
}

View file

@ -5,12 +5,17 @@ use std::collections::{BTreeMap, BTreeSet};
use crate::be::dbvalue::{DbValueOauthClaimMap, DbValueOauthScopeMapV1};
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::utils::str_join;
use crate::value::{OauthClaimMapJoin, OAUTHSCOPE_RE};
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
use kanidm_proto::scim_v1::server::ScimOAuth2ClaimMap;
use kanidm_proto::scim_v1::server::ScimOAuth2ScopeMap;
use crate::valueset::{
uuid_to_proto_string, DbValueSetV2, ResolvedValueSetOauth2ClaimMap,
ResolvedValueSetOauth2ScopeMap, ScimValueIntermediate, UnresolvedScimValueOauth2ClaimMap,
UnresolvedScimValueOauth2ScopeMap, UnresolvedValueSetOauth2ClaimMap,
UnresolvedValueSetOauth2ScopeMap, ValueSet, ValueSetIntermediate, ValueSetResolveStatus,
ValueSetScimPut,
};
use kanidm_proto::scim_v1::client::ScimOAuth2ClaimMap as ClientScimOAuth2ClaimMap;
use kanidm_proto::scim_v1::client::ScimOAuth2ScopeMap as ClientScimOAuth2ScopeMap;
use kanidm_proto::scim_v1::JsonValue;
#[derive(Debug, Clone)]
pub struct ValueSetOauthScope {
@ -45,6 +50,19 @@ impl ValueSetOauthScope {
}
}
impl ValueSetScimPut for ValueSetOauthScope {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let set: BTreeSet<String> = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Oauth2Scope syntax invalid");
OperationError::SC0019Oauth2ScopeSyntaxInvalid
})?;
Ok(ValueSetResolveStatus::Resolved(Box::new(
ValueSetOauthScope { set },
)))
}
}
impl ValueSetT for ValueSetOauthScope {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -114,7 +132,9 @@ impl ValueSetT for ValueSetOauthScope {
}
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
Some(ScimResolveStatus::Resolved(str_join(&self.set).into()))
Some(ScimResolveStatus::Resolved(ScimValueKanidm::ArrayString(
self.set.iter().cloned().collect(),
)))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
@ -200,6 +220,58 @@ impl ValueSetOauthScopeMap {
let map = iter.into_iter().collect();
Some(Box::new(ValueSetOauthScopeMap { map }))
}
pub(crate) fn from_set(resolved: Vec<ResolvedValueSetOauth2ScopeMap>) -> ValueSet {
let map = resolved
.into_iter()
.map(|ResolvedValueSetOauth2ScopeMap { group_uuid, scopes }| (group_uuid, scopes))
.collect();
Box::new(ValueSetOauthScopeMap { map })
}
}
impl ValueSetScimPut for ValueSetOauthScopeMap {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let scope_maps: Vec<ClientScimOAuth2ScopeMap> =
serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Oauth2ScopeMap syntax invalid");
OperationError::SC0020Oauth2ScopeMapSyntaxInvalid
})?;
// We make these both the same len as claim maps as during the resolve
// process we move everything from unresolved to resolved, and worst
// case is everything is unresolved.
let mut resolved = Vec::with_capacity(scope_maps.len());
let mut unresolved = Vec::with_capacity(scope_maps.len());
for ClientScimOAuth2ScopeMap {
group,
group_uuid,
scopes,
} in scope_maps.into_iter()
{
match (group_uuid, group) {
(None, None) => {
error!("SCIM Oauth2ScopeMap a group name or uuid must be present");
return Err(OperationError::SC0021Oauth2ScopeMapMissingGroupIdentifier);
}
(Some(group_uuid), _) => {
resolved.push(ResolvedValueSetOauth2ScopeMap { group_uuid, scopes })
}
(None, Some(group_name)) => {
unresolved.push(UnresolvedValueSetOauth2ScopeMap { group_name, scopes })
}
}
}
Ok(ValueSetResolveStatus::NeedsResolution(
ValueSetIntermediate::Oauth2ScopeMap {
resolved,
unresolved,
},
))
}
}
impl ValueSetT for ValueSetOauthScopeMap {
@ -291,18 +363,18 @@ impl ValueSetT for ValueSetOauthScopeMap {
}
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
self.map
.iter()
.map(|(uuid, scopes)| {
ScimOAuth2ScopeMap {
uuid: *uuid,
// Flattened to a space separated list.
scopes: scopes.clone(),
}
})
.collect::<Vec<_>>(),
)))
let unresolved_maps = self
.map
.iter()
.map(|(group_uuid, scopes)| UnresolvedScimValueOauth2ScopeMap {
group_uuid: *group_uuid,
scopes: scopes.clone(),
})
.collect::<Vec<_>>();
Some(ScimResolveStatus::NeedsResolution(
ScimValueIntermediate::Oauth2ScopeMap(unresolved_maps),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
@ -420,6 +492,45 @@ impl ValueSetOauthClaimMap {
Ok(Box::new(ValueSetOauthClaimMap { map }))
}
pub(crate) fn from_set(resolved: Vec<ResolvedValueSetOauth2ClaimMap>) -> ValueSet {
let mut map = BTreeMap::new();
for ResolvedValueSetOauth2ClaimMap {
group_uuid,
claim,
join_char,
claim_values,
} in resolved.into_iter()
{
match map.entry(claim) {
BTreeEntry::Vacant(e) => {
let mut values = BTreeMap::default();
values.insert(group_uuid, claim_values);
let claim_map = OauthClaimMapping {
join: join_char,
values,
};
e.insert(claim_map);
}
BTreeEntry::Occupied(mut e) => {
// Just add the uuid/value, this claim name already exists.
let mapping_mut = e.get_mut();
match mapping_mut.values.entry(group_uuid) {
BTreeEntry::Vacant(e) => {
e.insert(claim_values);
}
BTreeEntry::Occupied(mut e) => {
e.insert(claim_values);
}
}
}
}
}
Box::new(ValueSetOauthClaimMap { map })
}
fn trim(&mut self) {
self.map
.values_mut()
@ -429,6 +540,59 @@ impl ValueSetOauthClaimMap {
}
}
impl ValueSetScimPut for ValueSetOauthClaimMap {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let claim_maps: Vec<ClientScimOAuth2ClaimMap> =
serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Oauth2ClaimMap syntax invalid");
OperationError::SC0022Oauth2ClaimMapSyntaxInvalid
})?;
// We make these both the same len as claim maps as during the resolve
// process we move everything from unresolved to resolved, and worst
// case is everything is unresolved.
let mut resolved = Vec::with_capacity(claim_maps.len());
let mut unresolved = Vec::with_capacity(claim_maps.len());
for ClientScimOAuth2ClaimMap {
group,
group_uuid,
claim,
join_char,
values: claim_values,
} in claim_maps.into_iter()
{
let join_char = OauthClaimMapJoin::from(join_char);
match (group_uuid, group) {
(None, None) => {
error!("SCIM Oauth2ClaimMap a group name or uuid must be present");
return Err(OperationError::SC0023Oauth2ClaimMapMissingGroupIdentifier);
}
(Some(group_uuid), _) => resolved.push(ResolvedValueSetOauth2ClaimMap {
group_uuid,
claim,
join_char,
claim_values,
}),
(None, Some(group_name)) => unresolved.push(UnresolvedValueSetOauth2ClaimMap {
group_name,
claim,
join_char,
claim_values,
}),
}
}
Ok(ValueSetResolveStatus::NeedsResolution(
ValueSetIntermediate::Oauth2ClaimMap {
resolved,
unresolved,
},
))
}
}
impl ValueSetT for ValueSetOauthClaimMap {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -622,22 +786,24 @@ impl ValueSetT for ValueSetOauthClaimMap {
}
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
self.map
.iter()
.flat_map(|(claim_name, mappings)| {
mappings
.values
.iter()
.map(|(group_uuid, claim_values)| ScimOAuth2ClaimMap {
group: *group_uuid,
claim: claim_name.to_string(),
join_char: mappings.join.to_str().to_string(),
values: claim_values.clone(),
})
let unresolved_maps = self
.map
.iter()
.flat_map(|(claim_name, mappings)| {
mappings.values.iter().map(|(group_uuid, claim_values)| {
UnresolvedScimValueOauth2ClaimMap {
group_uuid: *group_uuid,
claim: claim_name.to_string(),
join_char: mappings.join.into(),
values: claim_values.clone(),
}
})
.collect::<Vec<_>>(),
)))
})
.collect::<Vec<_>>();
Some(ScimResolveStatus::NeedsResolution(
ScimValueIntermediate::Oauth2ClaimMap(unresolved_maps),
))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
@ -704,8 +870,7 @@ impl ValueSetT for ValueSetOauthClaimMap {
#[cfg(test)]
mod tests {
use super::{ValueSetOauthClaimMap, ValueSetOauthScope, ValueSetOauthScopeMap};
use crate::prelude::ValueSet;
use crate::valueset::ValueSetT;
use crate::prelude::*;
use std::collections::BTreeSet;
#[test]
@ -726,43 +891,88 @@ mod tests {
#[test]
fn test_scim_oauth2_scope() {
let vs: ValueSet = ValueSetOauthScope::new("fully_sick_scope_m8".to_string());
let data = r#""fully_sick_scope_m8""#;
crate::valueset::scim_json_reflexive(vs, data);
let data = r#"["fully_sick_scope_m8"]"#;
crate::valueset::scim_json_reflexive(vs.clone(), data);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetOauthScope>(vs, &[])
}
#[test]
fn test_scim_oauth2_scope_map() {
let u = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
#[qs_test]
async fn test_scim_oauth2_scope_map(server: &QueryServer) {
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
let g_uuid = uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f");
assert!(write_txn
.internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Uuid, Value::Uuid(g_uuid))
),])
.is_ok());
let set = ["read".to_string(), "write".to_string()].into();
let vs: ValueSet = ValueSetOauthScopeMap::new(u, set);
let vs: ValueSet = ValueSetOauthScopeMap::new(g_uuid, set);
let data = r#"
[
{
"scopes": "read write",
"uuid": "3a163ca0-4762-4620-a188-06b750c84c86"
"scopes": ["read", "write"],
"group": "testgroup@example.com",
"groupUuid": "4d21d04a-dc0e-42eb-b850-34dd180b107f"
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, vs.clone(), data);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthScopeMap>(
&mut write_txn,
vs,
&[],
);
assert!(write_txn.commit().is_ok());
}
#[test]
fn test_scim_oauth2_claim_map() {
let u = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
#[qs_test]
async fn test_scim_oauth2_claim_map(server: &QueryServer) {
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
let g_uuid = uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f");
assert!(write_txn
.internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup")),
(Attribute::Uuid, Value::Uuid(g_uuid))
),])
.is_ok());
let set = ["read".to_string(), "write".to_string()].into();
let vs: ValueSet = ValueSetOauthClaimMap::new_value("claim".to_string(), u, set);
let vs: ValueSet = ValueSetOauthClaimMap::new_value("claim".to_string(), g_uuid, set);
let data = r#"
[
{
"claim": "claim",
"group": "3a163ca0-4762-4620-a188-06b750c84c86",
"group": "testgroup@example.com",
"groupUuid": "4d21d04a-dc0e-42eb-b850-34dd180b107f",
"joinChar": ";",
"values": "read write"
"values": ["read", "write"]
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, vs.clone(), data);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetOauthClaimMap>(
&mut write_txn,
vs,
&[],
);
assert!(write_txn.commit().is_ok());
}
}

View file

@ -1,8 +1,3 @@
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use time::OffsetDateTime;
use crate::be::dbvalue::{
DbCidV1, DbValueAccessScopeV1, DbValueApiToken, DbValueApiTokenScopeV1, DbValueAuthTypeV1,
DbValueIdentityId, DbValueOauth2Session, DbValueSession, DbValueSessionStateV1,
@ -14,10 +9,12 @@ use crate::value::{
ApiToken, ApiTokenScope, AuthType, Oauth2Session, Session, SessionScope, SessionState,
};
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ScimResolveStatus, ValueSet};
use kanidm_proto::scim_v1::server::ScimApiToken;
use kanidm_proto::scim_v1::server::ScimAuthSession;
use kanidm_proto::scim_v1::server::ScimOAuth2Session;
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use time::OffsetDateTime;
#[derive(Debug, Clone)]
pub struct ValueSetSession {

View file

@ -1,15 +1,15 @@
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use crate::be::dbvalue::DbValueTaggedStringV1;
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::utils::trigraph_iter;
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::JsonValue;
use kanidm_proto::scim_v1::ScimSshPublicKey;
use sshkey_attest::proto::PublicKey as SshPublicKey;
use kanidm_proto::scim_v1::server::ScimSshPublicKey;
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct ValueSetSshKey {
@ -54,6 +54,24 @@ impl ValueSetSshKey {
}
}
impl ValueSetScimPut for ValueSetSshKey {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value: Vec<ScimSshPublicKey> = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Ssh Public Key syntax invalid");
OperationError::SC0024SshPublicKeySyntaxInvalid
})?;
let map = value
.into_iter()
.map(|ScimSshPublicKey { label, value }| (label, value))
.collect();
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetSshKey {
map,
})))
}
}
impl ValueSetT for ValueSetSshKey {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -229,6 +247,9 @@ mod tests {
}
]
"#;
crate::valueset::scim_json_reflexive(vs, data);
crate::valueset::scim_json_reflexive(vs.clone(), data);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetSshKey>(vs, &[])
}
}

View file

@ -1,7 +1,9 @@
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::JsonValue;
use smolset::SmolSet;
#[derive(Debug, Clone)]
@ -27,6 +29,29 @@ impl ValueSetSyntax {
}
}
impl ValueSetScimPut for ValueSetSyntax {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value = serde_json::from_value::<String>(value)
.map_err(|err| {
error!(?err, "SCIM SyntaxType syntax invalid");
OperationError::SC0008SyntaxTypeSyntaxInvalid
})
.and_then(|value| {
SyntaxType::try_from(value.as_str()).map_err(|()| {
error!("SCIM SyntaxType syntax invalid - value");
OperationError::SC0008SyntaxTypeSyntaxInvalid
})
})?;
let mut set = SmolSet::new();
set.insert(value);
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetSyntax {
set,
})))
}
}
impl FromIterator<SyntaxType> for Option<Box<ValueSetSyntax>> {
fn from_iter<T>(iter: T) -> Option<Box<ValueSetSyntax>>
where
@ -106,9 +131,10 @@ impl ValueSetT for ValueSetSyntax {
}
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
self.set.iter().map(|u| u.to_string()).collect::<Vec<_>>(),
)))
self.set
.iter()
.next()
.map(|u| ScimResolveStatus::Resolved(ScimValueKanidm::from(u.to_string())))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
@ -162,6 +188,9 @@ mod tests {
#[test]
fn test_scim_syntax() {
let vs: ValueSet = ValueSetSyntax::new(SyntaxType::Uuid);
crate::valueset::scim_json_reflexive(vs, r#"["UUID"]"#);
crate::valueset::scim_json_reflexive(vs.clone(), r#""UUID""#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetSyntax>(vs, &[])
}
}

View file

@ -1,12 +1,10 @@
use crate::be::dbvalue::DbTotpV1;
use crate::credential::totp::Totp;
use crate::prelude::*;
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
use crate::be::dbvalue::DbTotpV1;
use crate::schema::SchemaAttribute;
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
use std::collections::btree_map::Entry as BTreeEntry;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct ValueSetTotpSecret {

View file

@ -1,10 +1,11 @@
use std::collections::BTreeSet;
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::internal::UiHint;
use kanidm_proto::scim_v1::JsonValue;
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
pub struct ValueSetUiHint {
@ -29,6 +30,21 @@ impl ValueSetUiHint {
}
}
impl ValueSetScimPut for ValueSetUiHint {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value = serde_json::from_value::<Vec<UiHint>>(value).map_err(|err| {
error!(?err, "SCIM UiHint syntax invalid");
OperationError::SC0025UiHintSyntaxInvalid
})?;
let set = value.into_iter().collect();
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetUiHint {
set,
})))
}
}
impl ValueSetT for ValueSetUiHint {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -96,7 +112,7 @@ impl ValueSetT for ValueSetUiHint {
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
self.set.iter().map(|u| u.to_string()).collect::<Vec<_>>(),
self.set.iter().copied().collect::<Vec<_>>(),
)))
}
@ -147,6 +163,9 @@ mod tests {
#[test]
fn test_scim_uihint() {
let vs: ValueSet = ValueSetUiHint::new(UiHint::PosixAccount);
crate::valueset::scim_json_reflexive(vs, r#"["PosixAccount"]"#);
crate::valueset::scim_json_reflexive(vs.clone(), r#"["posixaccount"]"#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUiHint>(vs, &[])
}
}

View file

@ -1,7 +1,9 @@
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::JsonValue;
use smolset::SmolSet;
#[derive(Debug, Clone)]
@ -37,6 +39,22 @@ impl ValueSetUint32 {
}
}
impl ValueSetScimPut for ValueSetUint32 {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value: u32 = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM uint32 syntax invalid");
OperationError::SC0006Uint32SyntaxInvalid
})?;
let mut set = SmolSet::new();
set.insert(value);
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetUint32 {
set,
})))
}
}
impl ValueSetT for ValueSetUint32 {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -178,6 +196,9 @@ mod tests {
#[test]
fn test_scim_uint32() {
let vs: ValueSet = ValueSetUint32::new(69);
crate::valueset::scim_json_reflexive(vs, "69");
crate::valueset::scim_json_reflexive(vs.clone(), "69");
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUint32>(vs, &[])
}
}

View file

@ -1,7 +1,9 @@
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::JsonValue;
use smolset::SmolSet;
#[derive(Debug, Clone)]
@ -37,6 +39,22 @@ impl ValueSetUrl {
}
}
impl ValueSetScimPut for ValueSetUrl {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let value: Url = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM URL syntax invalid");
OperationError::SC0007UrlSyntaxInvalid
})?;
let mut set = SmolSet::new();
set.insert(value);
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetUrl {
set,
})))
}
}
impl ValueSetT for ValueSetUrl {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -165,6 +183,9 @@ mod tests {
fn test_scim_url() {
let u = Url::parse("https://idm.example.com").unwrap();
let vs: ValueSet = ValueSetUrl::new(u);
crate::valueset::scim_json_reflexive(vs, r#""https://idm.example.com/""#);
crate::valueset::scim_json_reflexive(vs.clone(), r#""https://idm.example.com/""#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUrl>(vs, &[])
}
}

View file

@ -1,8 +1,11 @@
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::utils::trigraph_iter;
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
use crate::valueset::{
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::client::ScimStrings;
use kanidm_proto::scim_v1::JsonValue;
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
@ -27,6 +30,21 @@ impl ValueSetUtf8 {
}
}
impl ValueSetScimPut for ValueSetUtf8 {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let ScimStrings(values) = serde_json::from_value(value).map_err(|err| {
error!(?err, "SCIM Utf8 Syntax Invalid");
OperationError::SC0026Utf8SyntaxInvalid
})?;
let set = values.into_iter().collect();
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetUtf8 {
set,
})))
}
}
impl ValueSetT for ValueSetUtf8 {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -227,6 +245,10 @@ mod tests {
#[test]
fn test_scim_utf8() {
let vs: ValueSet = ValueSetUtf8::new("Test".to_string());
crate::valueset::scim_json_reflexive(vs, r#""Test""#);
// Test that the output json matches some known str
crate::valueset::scim_json_reflexive(vs.clone(), r#""Test""#);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUtf8>(vs, &[])
}
}

View file

@ -1,11 +1,12 @@
use std::collections::BTreeSet;
use crate::prelude::*;
use crate::schema::SchemaAttribute;
use crate::valueset::{
uuid_to_proto_string, DbValueSetV2, ScimResolveStatus, ScimValueIntermediate, ValueSet,
ValueSetIntermediate, ValueSetResolveStatus, ValueSetScimPut,
};
use kanidm_proto::scim_v1::JsonValue;
use smolset::SmolSet;
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
pub struct ValueSetUuid {
@ -40,6 +41,21 @@ impl ValueSetUuid {
}
}
impl ValueSetScimPut for ValueSetUuid {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
let uuid: Uuid = serde_json::from_value(value).map_err(|err| {
warn!(?err, "Invalid SCIM Uuid syntax");
OperationError::SC0004UuidSyntaxInvalid
})?;
let mut set = SmolSet::new();
set.insert(uuid);
Ok(ValueSetResolveStatus::Resolved(Box::new(ValueSetUuid {
set,
})))
}
}
impl ValueSetT for ValueSetUuid {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
@ -119,7 +135,8 @@ impl ValueSetT for ValueSetUuid {
.iter()
.next()
.copied()
.map(|uuid| ScimResolveStatus::NeedsResolution(ScimValueIntermediate::Refer(uuid)))
.map(ScimValueKanidm::Uuid)
.map(ScimResolveStatus::Resolved)
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
@ -211,6 +228,55 @@ impl ValueSetRefer {
Some(Box::new(ValueSetRefer { set }))
}
}
pub(crate) fn from_set(set: BTreeSet<Uuid>) -> ValueSet {
Box::new(ValueSetRefer { set })
}
}
impl ValueSetScimPut for ValueSetRefer {
fn from_scim_json_put(value: JsonValue) -> Result<ValueSetResolveStatus, OperationError> {
use kanidm_proto::scim_v1::client::{ScimReference, ScimReferences};
let scim_refs: ScimReferences = serde_json::from_value(value).map_err(|err| {
warn!(?err, "Invalid SCIM reference set syntax");
OperationError::SC0002ReferenceSyntaxInvalid
})?;
let mut resolved = BTreeSet::default();
let mut unresolved = Vec::with_capacity(scim_refs.len());
for scim_ref in scim_refs.into_iter() {
match scim_ref {
ScimReference {
uuid: None,
value: None,
} => {
warn!("Invalid SCIM reference set syntax, uuid and value are both unset.");
return Err(OperationError::SC0002ReferenceSyntaxInvalid);
}
ScimReference {
uuid: Some(uuid), ..
} => {
resolved.insert(uuid);
}
ScimReference {
value: Some(val), ..
} => {
unresolved.push(val);
}
}
}
// We may not actually need to resolve anything, but to make tests easier we
// always return that we need resolution.
Ok(ValueSetResolveStatus::NeedsResolution(
ValueSetIntermediate::References {
resolved,
unresolved,
},
))
}
}
impl ValueSetT for ValueSetRefer {
@ -290,7 +356,7 @@ impl ValueSetT for ValueSetRefer {
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
let uuids = self.set.iter().copied().collect::<Vec<_>>();
Some(ScimResolveStatus::NeedsResolution(
ScimValueIntermediate::ReferMany(uuids),
ScimValueIntermediate::References(uuids),
))
}
@ -348,23 +414,50 @@ impl ValueSetT for ValueSetRefer {
#[cfg(test)]
mod tests {
use super::{ValueSetRefer, ValueSetUuid};
use crate::prelude::ValueSet;
use crate::prelude::*;
#[test]
fn test_scim_uuid() {
let vs: ValueSet = ValueSetUuid::new(uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f"));
let data = r#"{"Refer": "4d21d04a-dc0e-42eb-b850-34dd180b107f"}"#;
let data = r#""4d21d04a-dc0e-42eb-b850-34dd180b107f""#;
crate::valueset::scim_json_reflexive_unresolved(vs, data);
crate::valueset::scim_json_reflexive(vs.clone(), data);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive::<ValueSetUuid>(vs, &[])
}
#[test]
fn test_scim_refer() {
let vs: ValueSet = ValueSetRefer::new(uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f"));
#[qs_test]
async fn test_scim_refer(server: &QueryServer) {
let mut write_txn = server.write(duration_from_epoch_now()).await.unwrap();
let data = r#"{"ReferMany": ["4d21d04a-dc0e-42eb-b850-34dd180b107f"]}"#;
let t_uuid = uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f");
assert!(write_txn
.internal_create(vec![entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(t_uuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
),])
.is_ok());
crate::valueset::scim_json_reflexive_unresolved(vs, data);
let vs: ValueSet = ValueSetRefer::new(t_uuid);
let data = r#"[{"uuid": "4d21d04a-dc0e-42eb-b850-34dd180b107f", "value": "testperson1@example.com"}]"#;
crate::valueset::scim_json_reflexive_unresolved(&mut write_txn, vs.clone(), data);
// Test that we can parse json values into a valueset.
crate::valueset::scim_json_put_reflexive_unresolved::<ValueSetRefer>(
&mut write_txn,
vs,
&[],
);
assert!(write_txn.commit().is_ok());
}
}

View file

@ -365,7 +365,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
.expect("Unable to decode AccessTokenIntrospectResponse");
assert!(tir.active);
assert!(tir.scope.is_some());
assert!(!tir.scope.is_empty());
assert_eq!(tir.client_id.as_deref(), Some(TEST_INTEGRATION_RS_ID));
assert_eq!(
tir.username.as_deref(),
@ -469,7 +469,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
.expect("Unable to decode AccessTokenIntrospectResponse");
assert!(tir.active);
assert!(tir.scope.is_some());
assert!(!tir.scope.is_empty());
assert_eq!(tir.client_id.as_deref(), Some(TEST_INTEGRATION_RS_ID));
assert_eq!(tir.username.as_deref(), Some("test_integration@localhost"));
assert_eq!(tir.token_type, Some(AccessTokenType::Bearer));