mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
20240927 SCIM put (#3151)
This commit is contained in:
parent
8bbdf6bd6a
commit
ea0e63cc2a
|
@ -140,6 +140,9 @@ pub enum Attribute {
|
||||||
Refers,
|
Refers,
|
||||||
Replicated,
|
Replicated,
|
||||||
Rs256PrivateKeyDer,
|
Rs256PrivateKeyDer,
|
||||||
|
/// A set of scim schemas. This is similar to a kanidm class.
|
||||||
|
#[serde(rename = "schemas")]
|
||||||
|
ScimSchemas,
|
||||||
Scope,
|
Scope,
|
||||||
SourceUuid,
|
SourceUuid,
|
||||||
Spn,
|
Spn,
|
||||||
|
@ -368,6 +371,7 @@ impl Attribute {
|
||||||
Attribute::Replicated => ATTR_REPLICATED,
|
Attribute::Replicated => ATTR_REPLICATED,
|
||||||
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
|
Attribute::Rs256PrivateKeyDer => ATTR_RS256_PRIVATE_KEY_DER,
|
||||||
Attribute::Scope => ATTR_SCOPE,
|
Attribute::Scope => ATTR_SCOPE,
|
||||||
|
Attribute::ScimSchemas => ATTR_SCIM_SCHEMAS,
|
||||||
Attribute::SourceUuid => ATTR_SOURCE_UUID,
|
Attribute::SourceUuid => ATTR_SOURCE_UUID,
|
||||||
Attribute::Spn => ATTR_SPN,
|
Attribute::Spn => ATTR_SPN,
|
||||||
Attribute::SshPublicKey => ATTR_SSH_PUBLICKEY,
|
Attribute::SshPublicKey => ATTR_SSH_PUBLICKEY,
|
||||||
|
@ -548,6 +552,7 @@ impl Attribute {
|
||||||
ATTR_REFERS => Attribute::Refers,
|
ATTR_REFERS => Attribute::Refers,
|
||||||
ATTR_REPLICATED => Attribute::Replicated,
|
ATTR_REPLICATED => Attribute::Replicated,
|
||||||
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
|
ATTR_RS256_PRIVATE_KEY_DER => Attribute::Rs256PrivateKeyDer,
|
||||||
|
ATTR_SCIM_SCHEMAS => Attribute::ScimSchemas,
|
||||||
ATTR_SCOPE => Attribute::Scope,
|
ATTR_SCOPE => Attribute::Scope,
|
||||||
ATTR_SOURCE_UUID => Attribute::SourceUuid,
|
ATTR_SOURCE_UUID => Attribute::SourceUuid,
|
||||||
ATTR_SPN => Attribute::Spn,
|
ATTR_SPN => Attribute::Spn,
|
||||||
|
|
|
@ -179,6 +179,7 @@ pub const ATTR_RECYCLEDDIRECTMEMBEROF: &str = "recycled_directmemberof";
|
||||||
pub const ATTR_REFERS: &str = "refers";
|
pub const ATTR_REFERS: &str = "refers";
|
||||||
pub const ATTR_REPLICATED: &str = "replicated";
|
pub const ATTR_REPLICATED: &str = "replicated";
|
||||||
pub const ATTR_RS256_PRIVATE_KEY_DER: &str = "rs256_private_key_der";
|
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_SCOPE: &str = "scope";
|
||||||
pub const ATTR_SELF: &str = "self";
|
pub const ATTR_SELF: &str = "self";
|
||||||
pub const ATTR_SOURCE_UUID: &str = "source_uuid";
|
pub const ATTR_SOURCE_UUID: &str = "source_uuid";
|
||||||
|
|
|
@ -148,7 +148,6 @@ pub enum OperationError {
|
||||||
KG002TaskCommFailure,
|
KG002TaskCommFailure,
|
||||||
KG003CacheClearFailed,
|
KG003CacheClearFailed,
|
||||||
|
|
||||||
// What about something like this for unique errors?
|
|
||||||
// Credential Update Errors
|
// Credential Update Errors
|
||||||
CU0001WebauthnAttestationNotTrusted,
|
CU0001WebauthnAttestationNotTrusted,
|
||||||
CU0002WebauthnRegistrationError,
|
CU0002WebauthnRegistrationError,
|
||||||
|
@ -173,6 +172,31 @@ pub enum OperationError {
|
||||||
|
|
||||||
// SCIM
|
// SCIM
|
||||||
SC0001IncomingSshPublicKey,
|
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
|
// Migration
|
||||||
MG0001InvalidReMigrationLevel,
|
MG0001InvalidReMigrationLevel,
|
||||||
MG0002RaiseDomainLevelExceedsMaximum,
|
MG0002RaiseDomainLevelExceedsMaximum,
|
||||||
|
@ -409,6 +433,33 @@ impl OperationError {
|
||||||
Self::MG0008SkipUpgradeAttempted => Some("Skip Upgrade Attempted.".into()),
|
Self::MG0008SkipUpgradeAttempted => Some("Skip Upgrade Attempted.".into()),
|
||||||
Self::PL0001GidOverlapsSystemRange => None,
|
Self::PL0001GidOverlapsSystemRange => None,
|
||||||
Self::SC0001IncomingSshPublicKey => 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::UI0001ChallengeSerialisation => Some("The WebAuthn challenge was unable to be serialised.".into()),
|
||||||
Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition.".into()),
|
Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition.".into()),
|
||||||
Self::VL0001ValueSshPublicKeyString => None,
|
Self::VL0001ValueSshPublicKeyString => None,
|
||||||
|
|
|
@ -33,6 +33,7 @@ pub struct PkceRequest {
|
||||||
|
|
||||||
/// An OAuth2 client redirects to the authorisation server with Authorisation Request
|
/// An OAuth2 client redirects to the authorisation server with Authorisation Request
|
||||||
/// parameters.
|
/// parameters.
|
||||||
|
#[serde_as]
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct AuthorisationRequest {
|
pub struct AuthorisationRequest {
|
||||||
|
@ -43,7 +44,8 @@ pub struct AuthorisationRequest {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub pkce_request: Option<PkceRequest>,
|
pub pkce_request: Option<PkceRequest>,
|
||||||
pub redirect_uri: Url,
|
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.
|
// OIDC adds a nonce parameter that is optional.
|
||||||
pub nonce: Option<String>,
|
pub nonce: Option<String>,
|
||||||
// OIDC also allows other optional params
|
// OIDC also allows other optional params
|
||||||
|
@ -185,6 +187,7 @@ pub struct OAuth2RFC9068TokenExtensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The response for an access token
|
/// The response for an access token
|
||||||
|
#[serde_as]
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct AccessTokenResponse {
|
pub struct AccessTokenResponse {
|
||||||
|
@ -195,7 +198,8 @@ pub struct AccessTokenResponse {
|
||||||
pub refresh_token: Option<String>,
|
pub refresh_token: Option<String>,
|
||||||
/// Space separated list of scopes that were approved, if this differs from the
|
/// Space separated list of scopes that were approved, if this differs from the
|
||||||
/// original request.
|
/// 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.
|
/// If the `openid` scope was requested, an `id_token` may be present in the response.
|
||||||
pub id_token: Option<String>,
|
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
|
/// Response to an introspection request. If the token is inactive or revoked, only
|
||||||
/// `active` will be set to the value of `false`.
|
/// `active` will be set to the value of `false`.
|
||||||
|
#[serde_as]
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct AccessTokenIntrospectResponse {
|
pub struct AccessTokenIntrospectResponse {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub scope: Option<String>,
|
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
|
||||||
|
pub scope: BTreeSet<String>,
|
||||||
pub client_id: Option<String>,
|
pub client_id: Option<String>,
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
pub token_type: Option<AccessTokenType>,
|
pub token_type: Option<AccessTokenType>,
|
||||||
|
@ -269,7 +275,7 @@ impl AccessTokenIntrospectResponse {
|
||||||
pub fn inactive() -> Self {
|
pub fn inactive() -> Self {
|
||||||
AccessTokenIntrospectResponse {
|
AccessTokenIntrospectResponse {
|
||||||
active: false,
|
active: false,
|
||||||
scope: None,
|
scope: BTreeSet::default(),
|
||||||
client_id: None,
|
client_id: None,
|
||||||
username: None,
|
username: None,
|
||||||
token_type: None,
|
token_type: None,
|
||||||
|
|
|
@ -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::{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 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>;
|
pub type ScimSshPublicKeys = Vec<ScimSshPublicKey>;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
pub struct ScimSshPublicKey {
|
pub struct ScimSshPublicKey {
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub value: SshPublicKey,
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
use crate::attribute::Attribute;
|
use crate::attribute::Attribute;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value as JsonValue;
|
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
|
||||||
|
|
||||||
pub use self::synch::*;
|
pub use self::synch::*;
|
||||||
pub use scim_proto::prelude::*;
|
pub use scim_proto::prelude::*;
|
||||||
|
pub use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
@ -52,6 +53,46 @@ pub struct ScimEntryGetQuery {
|
||||||
pub attributes: Option<Vec<Attribute>>,
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
// use super::*;
|
// use super::*;
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
use super::ScimMail;
|
||||||
|
use super::ScimOauth2ClaimMapJoinChar;
|
||||||
|
use super::ScimSshPublicKey;
|
||||||
use crate::attribute::Attribute;
|
use crate::attribute::Attribute;
|
||||||
|
use crate::internal::UiHint;
|
||||||
use scim_proto::ScimEntryHeader;
|
use scim_proto::ScimEntryHeader;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_with::{base64, formats, hex::Hex, serde_as, skip_serializing_none, StringWithSeparator};
|
use serde_with::{base64, formats, hex::Hex, serde_as, skip_serializing_none};
|
||||||
use sshkey_attest::proto::PublicKey as SshPublicKey;
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::format_description::well_known::Rfc3339;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
@ -32,13 +35,6 @@ pub struct ScimAddress {
|
||||||
pub country: String,
|
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)]
|
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ScimApplicationPassword {
|
pub struct ScimApplicationPassword {
|
||||||
|
@ -75,13 +71,6 @@ pub struct ScimAuditString {
|
||||||
pub value: String,
|
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)]
|
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum ScimIntentTokenState {
|
pub enum ScimIntentTokenState {
|
||||||
|
@ -164,8 +153,8 @@ pub struct ScimApiToken {
|
||||||
#[derive(Serialize, Debug, Clone, ToSchema)]
|
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ScimOAuth2ScopeMap {
|
pub struct ScimOAuth2ScopeMap {
|
||||||
pub uuid: Uuid,
|
pub group: String,
|
||||||
#[serde_as(as = "StringWithSeparator::<formats::SpaceSeparator, String>")]
|
pub group_uuid: Uuid,
|
||||||
pub scopes: BTreeSet<String>,
|
pub scopes: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,14 +162,13 @@ pub struct ScimOAuth2ScopeMap {
|
||||||
#[derive(Serialize, Debug, Clone, ToSchema)]
|
#[derive(Serialize, Debug, Clone, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ScimOAuth2ClaimMap {
|
pub struct ScimOAuth2ClaimMap {
|
||||||
pub group: Uuid,
|
pub group: String,
|
||||||
|
pub group_uuid: Uuid,
|
||||||
pub claim: String,
|
pub claim: String,
|
||||||
pub join_char: String,
|
pub join_char: ScimOauth2ClaimMapJoinChar,
|
||||||
#[serde_as(as = "StringWithSeparator::<formats::SpaceSeparator, String>")]
|
|
||||||
pub values: BTreeSet<String>,
|
pub values: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
|
#[derive(Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ScimReference {
|
pub struct ScimReference {
|
||||||
|
@ -226,6 +214,7 @@ pub enum ScimValueKanidm {
|
||||||
OAuth2ScopeMap(Vec<ScimOAuth2ScopeMap>),
|
OAuth2ScopeMap(Vec<ScimOAuth2ScopeMap>),
|
||||||
OAuth2ClaimMap(Vec<ScimOAuth2ClaimMap>),
|
OAuth2ClaimMap(Vec<ScimOAuth2ClaimMap>),
|
||||||
KeyInternal(Vec<ScimKeyInternal>),
|
KeyInternal(Vec<ScimKeyInternal>),
|
||||||
|
UiHints(Vec<UiHint>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<bool> for ScimValueKanidm {
|
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 {
|
impl From<Vec<OffsetDateTime>> for ScimValueKanidm {
|
||||||
fn from(set: Vec<OffsetDateTime>) -> Self {
|
fn from(set: Vec<OffsetDateTime>) -> Self {
|
||||||
Self::ArrayDateTime(set)
|
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 {
|
impl From<Vec<String>> for ScimValueKanidm {
|
||||||
fn from(set: Vec<String>) -> Self {
|
fn from(set: Vec<String>) -> Self {
|
||||||
Self::ArrayString(set)
|
Self::ArrayString(set)
|
||||||
|
|
|
@ -7,6 +7,7 @@ use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew};
|
||||||
use crate::idm::account::Account;
|
use crate::idm::account::Account;
|
||||||
use crate::value::PartialValue;
|
use crate::value::PartialValue;
|
||||||
use crate::value::Value;
|
use crate::value::Value;
|
||||||
|
use crate::valueset::{ValueSet, ValueSetIutf8};
|
||||||
pub use kanidm_proto::attribute::Attribute;
|
pub use kanidm_proto::attribute::Attribute;
|
||||||
use kanidm_proto::constants::*;
|
use kanidm_proto::constants::*;
|
||||||
use kanidm_proto::internal::OperationError;
|
use kanidm_proto::internal::OperationError;
|
||||||
|
@ -178,6 +179,11 @@ impl EntryClass {
|
||||||
Value::new_iutf8(s)
|
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 {
|
pub fn to_partialvalue(self) -> PartialValue {
|
||||||
let s: &'static str = self.into();
|
let s: &'static str = self.into();
|
||||||
PartialValue::new_iutf8(s)
|
PartialValue::new_iutf8(s)
|
||||||
|
|
|
@ -2253,10 +2253,13 @@ impl Entry<EntryReduced, EntryCommitted> {
|
||||||
Ok(ProtoEntry { attrs: attrs? })
|
Ok(ProtoEntry { attrs: attrs? })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_scim_kanidm(
|
pub fn to_scim_kanidm<'a, TXN>(
|
||||||
&self,
|
&self,
|
||||||
read_txn: &mut QueryServerReadTransaction,
|
read_txn: &mut TXN,
|
||||||
) -> Result<ScimEntryKanidm, OperationError> {
|
) -> Result<ScimEntryKanidm, OperationError>
|
||||||
|
where
|
||||||
|
TXN: QueryServerTransaction<'a>,
|
||||||
|
{
|
||||||
let result: Result<BTreeMap<Attribute, ScimValueKanidm>, OperationError> = self
|
let result: Result<BTreeMap<Attribute, ScimValueKanidm>, OperationError> = self
|
||||||
.attrs
|
.attrs
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -3199,6 +3202,7 @@ where
|
||||||
error!("Modification assertion was not met. {} {:?}", attr, value);
|
error!("Modification assertion was not met. {} {:?}", attr, value);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
Modify::Set(attr, valueset) => self.set_ava_set(attr, valueset.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -53,7 +53,6 @@ use crate::idm::server::{
|
||||||
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
|
IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction, IdmServerTransaction,
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::utils::str_join;
|
|
||||||
use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
|
use crate::value::{Oauth2Session, OauthClaimMapJoin, SessionState, OAUTHSCOPE_RE};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||||
|
@ -1475,11 +1474,7 @@ impl IdmServerProxyWriteTransaction<'_> {
|
||||||
|
|
||||||
let session_id = Uuid::new_v4();
|
let session_id = Uuid::new_v4();
|
||||||
|
|
||||||
let scope = if granted_scopes.is_empty() {
|
let scope = granted_scopes.clone();
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(str_join(&granted_scopes))
|
|
||||||
};
|
|
||||||
|
|
||||||
let uuid = o2rs.uuid;
|
let uuid = o2rs.uuid;
|
||||||
|
|
||||||
|
@ -1564,11 +1559,7 @@ impl IdmServerProxyWriteTransaction<'_> {
|
||||||
let refresh_expiry = iat + OAUTH_REFRESH_TOKEN_EXPIRY as i64;
|
let refresh_expiry = iat + OAUTH_REFRESH_TOKEN_EXPIRY as i64;
|
||||||
let odt_refresh_expiry = odt_ct + Duration::from_secs(OAUTH_REFRESH_TOKEN_EXPIRY);
|
let odt_refresh_expiry = odt_ct + Duration::from_secs(OAUTH_REFRESH_TOKEN_EXPIRY);
|
||||||
|
|
||||||
let scope = if scopes.is_empty() {
|
let scope = scopes.clone();
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(str_join(&scopes))
|
|
||||||
};
|
|
||||||
|
|
||||||
let iss = o2rs.iss.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.
|
// scopes - you need to have every requested scope or this auth_req is denied.
|
||||||
let req_scopes: BTreeSet<String> = auth_req
|
let req_scopes: BTreeSet<String> = auth_req.scope.clone();
|
||||||
.scope
|
|
||||||
.split_ascii_whitespace()
|
|
||||||
.map(str::to_string)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if req_scopes.is_empty() {
|
if req_scopes.is_empty() {
|
||||||
admin_error!("Invalid OAuth2 request - must contain at least one requested scope");
|
admin_error!("Invalid OAuth2 request - must contain at least one requested scope");
|
||||||
|
@ -2273,11 +2260,7 @@ impl IdmServerProxyReadTransaction<'_> {
|
||||||
|
|
||||||
// ==== good to generate response ====
|
// ==== good to generate response ====
|
||||||
|
|
||||||
let scope = if scopes.is_empty() {
|
let scope = scopes.clone();
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(str_join(&scopes))
|
|
||||||
};
|
|
||||||
|
|
||||||
let preferred_username = if prefer_short_username {
|
let preferred_username = if prefer_short_username {
|
||||||
Some(account.name.clone())
|
Some(account.name.clone())
|
||||||
|
@ -2343,11 +2326,7 @@ impl IdmServerProxyReadTransaction<'_> {
|
||||||
return Ok(AccessTokenIntrospectResponse::inactive());
|
return Ok(AccessTokenIntrospectResponse::inactive());
|
||||||
};
|
};
|
||||||
|
|
||||||
let scope = if scopes.is_empty() {
|
let scope = scopes.clone();
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(str_join(&scopes))
|
|
||||||
};
|
|
||||||
|
|
||||||
let token_type = Some(AccessTokenType::Bearer);
|
let token_type = Some(AccessTokenType::Bearer);
|
||||||
|
|
||||||
|
@ -2904,6 +2883,7 @@ fn check_is_loopback(redirect_uri: &Url) -> bool {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -2953,6 +2933,8 @@ mod tests {
|
||||||
$code_challenge:expr,
|
$code_challenge:expr,
|
||||||
$scope:expr
|
$scope:expr
|
||||||
) => {{
|
) => {{
|
||||||
|
let scope: BTreeSet<String> = $scope.split(" ").map(|s| s.to_string()).collect();
|
||||||
|
|
||||||
let auth_req = AuthorisationRequest {
|
let auth_req = AuthorisationRequest {
|
||||||
response_type: "code".to_string(),
|
response_type: "code".to_string(),
|
||||||
client_id: "test_resource_server".to_string(),
|
client_id: "test_resource_server".to_string(),
|
||||||
|
@ -2962,7 +2944,7 @@ mod tests {
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
}),
|
}),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
scope: $scope,
|
scope,
|
||||||
nonce: Some("abcdef".to_string()),
|
nonce: Some("abcdef".to_string()),
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3454,7 +3436,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: pkce_request.clone(),
|
pkce_request: pkce_request.clone(),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3475,7 +3457,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: None,
|
pkce_request: None,
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3496,7 +3478,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: pkce_request.clone(),
|
pkce_request: pkce_request.clone(),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3517,7 +3499,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: pkce_request.clone(),
|
pkce_request: pkce_request.clone(),
|
||||||
redirect_uri: Url::parse("https://totes.not.sus.org/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3538,7 +3520,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: pkce_request.clone(),
|
pkce_request: pkce_request.clone(),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/wrong_place").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3559,7 +3541,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: pkce_request.clone(),
|
pkce_request: pkce_request.clone(),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3582,7 +3564,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: pkce_request.clone(),
|
pkce_request: pkce_request.clone(),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3603,7 +3585,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: pkce_request.clone(),
|
pkce_request: pkce_request.clone(),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3624,7 +3606,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request,
|
pkce_request,
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3918,7 +3900,7 @@ mod tests {
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
}),
|
}),
|
||||||
redirect_uri: Url::parse("https://portal.example.com").unwrap(),
|
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()),
|
nonce: Some("abcdef".to_string()),
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -3988,7 +3970,7 @@ mod tests {
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
}),
|
}),
|
||||||
redirect_uri: Url::parse("app://cheese").unwrap(),
|
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()),
|
nonce: Some("abcdef".to_string()),
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -4092,7 +4074,10 @@ mod tests {
|
||||||
|
|
||||||
eprintln!("👉 {intr_response:?}");
|
eprintln!("👉 {intr_response:?}");
|
||||||
assert!(intr_response.active);
|
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!(
|
assert_eq!(
|
||||||
intr_response.client_id.as_deref(),
|
intr_response.client_id.as_deref(),
|
||||||
Some("test_resource_server")
|
Some("test_resource_server")
|
||||||
|
@ -5191,7 +5176,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: None,
|
pkce_request: None,
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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()),
|
nonce: Some("abcdef".to_string()),
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -5405,7 +5390,7 @@ mod tests {
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
}),
|
}),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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()),
|
nonce: Some("abcdef".to_string()),
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -5464,7 +5449,7 @@ mod tests {
|
||||||
}),
|
}),
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
||||||
// Note the scope isn't requested here!
|
// 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()),
|
nonce: Some("abcdef".to_string()),
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -5602,7 +5587,7 @@ mod tests {
|
||||||
state: "123".to_string(),
|
state: "123".to_string(),
|
||||||
pkce_request: None,
|
pkce_request: None,
|
||||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -5680,7 +5665,7 @@ mod tests {
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
}),
|
}),
|
||||||
redirect_uri: Url::parse("http://demo.example.com/oauth2/result").unwrap(),
|
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,
|
nonce: None,
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -6581,7 +6566,10 @@ mod tests {
|
||||||
|
|
||||||
eprintln!("👉 {intr_response:?}");
|
eprintln!("👉 {intr_response:?}");
|
||||||
assert!(intr_response.active);
|
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!(
|
assert_eq!(
|
||||||
intr_response.client_id.as_deref(),
|
intr_response.client_id.as_deref(),
|
||||||
Some("test_resource_server")
|
Some("test_resource_server")
|
||||||
|
@ -6640,8 +6628,8 @@ mod tests {
|
||||||
code_challenge,
|
code_challenge,
|
||||||
code_challenge_method: CodeChallengeMethod::S256,
|
code_challenge_method: CodeChallengeMethod::S256,
|
||||||
}),
|
}),
|
||||||
redirect_uri: redirect_uri.clone(),
|
redirect_uri: Url::parse("http://localhost:8765/oauth2/result").unwrap(),
|
||||||
scope: OAUTH2_SCOPE_OPENID.to_string(),
|
scope: btreeset![OAUTH2_SCOPE_OPENID.to_string()],
|
||||||
nonce: Some("abcdef".to_string()),
|
nonce: Some("abcdef".to_string()),
|
||||||
oidc_ext: Default::default(),
|
oidc_ext: Default::default(),
|
||||||
max_age: None,
|
max_age: None,
|
||||||
|
@ -6731,7 +6719,7 @@ mod tests {
|
||||||
|
|
||||||
eprintln!("👉 {intr_response:?}");
|
eprintln!("👉 {intr_response:?}");
|
||||||
assert!(intr_response.active);
|
assert!(intr_response.active);
|
||||||
assert_eq!(intr_response.scope.as_deref(), Some("supplement"));
|
assert_eq!(intr_response.scope, btreeset!["supplement".to_string()]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intr_response.client_id.as_deref(),
|
intr_response.client_id.as_deref(),
|
||||||
Some("test_resource_server")
|
Some("test_resource_server")
|
||||||
|
|
|
@ -10,6 +10,7 @@ use kanidm_proto::internal::{
|
||||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||||
// Should this be std?
|
// Should this be std?
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaTransaction;
|
use crate::schema::SchemaTransaction;
|
||||||
|
@ -23,16 +24,17 @@ pub struct ModifyInvalid;
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum Modify {
|
pub enum Modify {
|
||||||
// This value *should* exist.
|
/// This value *should* exist for this attribute.
|
||||||
// Clippy doesn't like value here, as value > pv. It could be an improvement to
|
|
||||||
// box here, but not sure. ... TODO and thought needed.
|
|
||||||
Present(Attribute, Value),
|
Present(Attribute, Value),
|
||||||
// This value *should not* exist.
|
/// This value *should not* exist for this attribute.
|
||||||
Removed(Attribute, PartialValue),
|
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),
|
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),
|
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 {
|
pub fn m_pres(attr: Attribute, v: &Value) -> Modify {
|
||||||
|
@ -201,6 +203,10 @@ impl ModifyList<ModifyInvalid> {
|
||||||
Some(_attr_name) => Ok(Modify::Purged(attr.clone())),
|
Some(_attr_name) => Ok(Modify::Purged(attr.clone())),
|
||||||
None => Err(SchemaError::InvalidAttribute(attr.to_string())),
|
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();
|
.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> {
|
impl ModifyList<ModifyValid> {
|
||||||
/// ⚠️ - Create a new modlist that is considered valid, bypassing schema.
|
/// ⚠️ - Create a new modlist that is considered valid, bypassing schema.
|
||||||
/// This is a TEST ONLY method and will never be exposed in production.
|
/// This is a TEST ONLY method and will never be exposed in production.
|
||||||
|
|
|
@ -164,9 +164,10 @@ impl Plugin for Base {
|
||||||
) -> Result<(), OperationError> {
|
) -> Result<(), OperationError> {
|
||||||
me.modlist.iter().try_for_each(|modify| {
|
me.modlist.iter().try_for_each(|modify| {
|
||||||
let attr = match &modify {
|
let attr = match &modify {
|
||||||
Modify::Present(a, _) => Some(a),
|
Modify::Present(a, _)
|
||||||
Modify::Removed(a, _) => Some(a),
|
| Modify::Removed(a, _)
|
||||||
Modify::Purged(a) => Some(a),
|
| Modify::Purged(a)
|
||||||
|
| Modify::Set(a, _) => Some(a),
|
||||||
Modify::Assert(_, _) => None,
|
Modify::Assert(_, _) => None,
|
||||||
};
|
};
|
||||||
if attr == Some(&Attribute::Uuid) {
|
if attr == Some(&Attribute::Uuid) {
|
||||||
|
@ -191,9 +192,10 @@ impl Plugin for Base {
|
||||||
.flat_map(|ml| ml.iter())
|
.flat_map(|ml| ml.iter())
|
||||||
.try_for_each(|modify| {
|
.try_for_each(|modify| {
|
||||||
let attr = match &modify {
|
let attr = match &modify {
|
||||||
Modify::Present(a, _) => Some(a),
|
Modify::Present(a, _)
|
||||||
Modify::Removed(a, _) => Some(a),
|
| Modify::Removed(a, _)
|
||||||
Modify::Purged(a) => Some(a),
|
| Modify::Set(a, _)
|
||||||
|
| Modify::Purged(a) => Some(a),
|
||||||
Modify::Assert(_, _) => None,
|
Modify::Assert(_, _) => None,
|
||||||
};
|
};
|
||||||
if attr == Some(&Attribute::Uuid) {
|
if attr == Some(&Attribute::Uuid) {
|
||||||
|
|
|
@ -146,7 +146,10 @@ impl Plugin for Protected {
|
||||||
me.modlist.into_iter().try_fold((), |(), m| {
|
me.modlist.into_iter().try_fold((), |(), m| {
|
||||||
// Already hit an error, move on.
|
// Already hit an error, move on.
|
||||||
let a = match m {
|
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,
|
Modify::Assert(_, _) => None,
|
||||||
};
|
};
|
||||||
if let Some(attr) = a {
|
if let Some(attr) = a {
|
||||||
|
@ -225,7 +228,7 @@ impl Plugin for Protected {
|
||||||
.try_fold((), |(), m| {
|
.try_fold((), |(), m| {
|
||||||
// Already hit an error, move on.
|
// Already hit an error, move on.
|
||||||
let a = match m {
|
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,
|
Modify::Assert(_, _) => None,
|
||||||
};
|
};
|
||||||
if let Some(attr) = a {
|
if let Some(attr) = a {
|
||||||
|
|
|
@ -345,9 +345,13 @@ pub trait AccessControlsTransaction<'a> {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|e| {
|
.filter_map(|e| {
|
||||||
match apply_search_access(&se.ident, related_acp.as_slice(), &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
|
// No properly written access module should allow
|
||||||
// unbounded attribute read!
|
// unbounded attribute read!
|
||||||
|
error!("An access module allowed full read, this is a BUG! Denying read to prevent data leaks.");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
SearchResult::Allow(allowed_attrs) => {
|
SearchResult::Allow(allowed_attrs) => {
|
||||||
|
@ -450,8 +454,8 @@ pub trait AccessControlsTransaction<'a> {
|
||||||
.modlist
|
.modlist
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|m| match m {
|
.filter_map(|m| match m {
|
||||||
Modify::Present(a, _) => Some(a.clone()),
|
Modify::Present(a, _) | Modify::Set(a, _) => Some(a.clone()),
|
||||||
_ => None,
|
Modify::Removed(..) | Modify::Assert(..) | Modify::Purged(_) => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -459,19 +463,18 @@ pub trait AccessControlsTransaction<'a> {
|
||||||
.modlist
|
.modlist
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|m| match m {
|
.filter_map(|m| match m {
|
||||||
Modify::Removed(a, _) => Some(a.clone()),
|
Modify::Set(a, _) | Modify::Removed(a, _) | Modify::Purged(a) => Some(a.clone()),
|
||||||
Modify::Purged(a) => Some(a.clone()),
|
Modify::Present(..) | Modify::Assert(..) => None,
|
||||||
_ => None,
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Build the set of classes that we to work on, only in terms of "addition". To remove
|
// 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
|
// I think we have no limit, but ... william of the future may find a problem with this
|
||||||
// policy.
|
// policy.
|
||||||
let requested_classes: BTreeSet<&str> = me
|
let mut requested_classes: BTreeSet<&str> = Default::default();
|
||||||
.modlist
|
|
||||||
.iter()
|
for modify in me.modlist.iter() {
|
||||||
.filter_map(|m| match m {
|
match modify {
|
||||||
Modify::Present(a, v) => {
|
Modify::Present(a, v) => {
|
||||||
if a == Attribute::Class.as_ref() {
|
if a == Attribute::Class.as_ref() {
|
||||||
// Here we have an option<&str> which could mean there is a risk of
|
// 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
|
// 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
|
// 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".
|
// we can trust these to be correct here and not to be "None".
|
||||||
v.to_str()
|
requested_classes.extend(v.to_str())
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Modify::Removed(a, v) => {
|
Modify::Removed(a, v) => {
|
||||||
if a == Attribute::Class.as_ref() {
|
if a == Attribute::Class.as_ref() {
|
||||||
v.to_str()
|
requested_classes.extend(v.to_str())
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => None,
|
Modify::Set(a, v) => {
|
||||||
})
|
if a == Attribute::Class.as_ref() {
|
||||||
.collect();
|
// 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_pres, "Requested present set");
|
||||||
debug!(?requested_rem, "Requested remove set");
|
debug!(?requested_rem, "Requested remove set");
|
||||||
|
@ -1081,6 +1086,7 @@ mod tests {
|
||||||
Access, AccessClass, AccessControls, AccessControlsTransaction, AccessEffectivePermission,
|
Access, AccessClass, AccessControls, AccessControlsTransaction, AccessEffectivePermission,
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::valueset::ValueSetIname;
|
||||||
|
|
||||||
const UUID_TEST_ACCOUNT_1: Uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
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");
|
const UUID_TEST_ACCOUNT_2: Uuid = uuid::uuid!("cec0852a-abdf-4ea6-9dae-d3157cb33d3a");
|
||||||
|
@ -1948,7 +1954,7 @@ mod tests {
|
||||||
debug!("result --> {:?}", res);
|
debug!("result --> {:?}", res);
|
||||||
debug!("expect --> {:?}", $expect);
|
debug!("expect --> {:?}", $expect);
|
||||||
// should be ok, and same as expect.
|
// should be ok, and same as expect.
|
||||||
assert_eq!(res, $expect);
|
assert_eq!($expect, res);
|
||||||
}};
|
}};
|
||||||
(
|
(
|
||||||
$me:expr,
|
$me:expr,
|
||||||
|
@ -1975,12 +1981,14 @@ mod tests {
|
||||||
debug!("result --> {:?}", res);
|
debug!("result --> {:?}", res);
|
||||||
debug!("expect --> {:?}", $expect);
|
debug!("expect --> {:?}", $expect);
|
||||||
// should be ok, and same as expect.
|
// should be ok, and same as expect.
|
||||||
assert_eq!(res, $expect);
|
assert_eq!($expect, res);
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_access_enforce_modify() {
|
fn test_access_enforce_modify() {
|
||||||
|
sketching::test_init();
|
||||||
|
|
||||||
let ev1 = E_TESTPERSON_1.clone().into_sealed_committed();
|
let ev1 = E_TESTPERSON_1.clone().into_sealed_committed();
|
||||||
let r_set = vec![Arc::new(ev1)];
|
let r_set = vec![Arc::new(ev1)];
|
||||||
|
|
||||||
|
@ -2012,6 +2020,16 @@ mod tests {
|
||||||
modlist!([m_purge(Attribute::Name)]),
|
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
|
// Class account pres
|
||||||
let me_pres_class = ModifyEvent::new_impersonate_entry(
|
let me_pres_class = ModifyEvent::new_impersonate_entry(
|
||||||
E_TEST_ACCOUNT_1.clone(),
|
E_TEST_ACCOUNT_1.clone(),
|
||||||
|
@ -2043,6 +2061,19 @@ mod tests {
|
||||||
modlist!([m_purge(Attribute::Class)]),
|
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
|
// Allow name and class, class is account
|
||||||
let acp_allow = AccessControlModify::from_raw(
|
let acp_allow = AccessControlModify::from_raw(
|
||||||
"test_modify_allow",
|
"test_modify_allow",
|
||||||
|
@ -2104,6 +2135,8 @@ mod tests {
|
||||||
test_acp_modify!(&me_rem, vec![acp_allow.clone()], &r_set, true);
|
test_acp_modify!(&me_rem, vec![acp_allow.clone()], &r_set, true);
|
||||||
// test allowed purge
|
// test allowed purge
|
||||||
test_acp_modify!(&me_purge, vec![acp_allow.clone()], &r_set, true);
|
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 rejected pres
|
||||||
test_acp_modify!(&me_pres, vec![acp_deny.clone()], &r_set, false);
|
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_acp_modify!(&me_rem, vec![acp_deny.clone()], &r_set, false);
|
||||||
// Test rejected purge
|
// Test rejected purge
|
||||||
test_acp_modify!(&me_purge, vec![acp_deny.clone()], &r_set, false);
|
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 allowed pres class
|
||||||
test_acp_modify!(&me_pres_class, vec![acp_allow.clone()], &r_set, true);
|
test_acp_modify!(&me_pres_class, vec![acp_allow.clone()], &r_set, true);
|
||||||
// test allowed rem class
|
// test allowed rem class
|
||||||
test_acp_modify!(&me_rem_class, vec![acp_allow.clone()], &r_set, true);
|
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 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 reject pres class, but class not in classes
|
||||||
test_acp_modify!(&me_pres_class, vec![acp_no_class.clone()], &r_set, false);
|
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 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_acp_modify!(&me_pres_class, vec![acp_deny.clone()], &r_set, false);
|
||||||
// test reject rem class, but class not in classes
|
// 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 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]
|
#[test]
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use super::{ChangeFlag, QueryServerWriteTransaction};
|
use super::{ChangeFlag, QueryServerWriteTransaction};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::server::Plugins;
|
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 struct BatchModifyEvent {
|
||||||
pub ident: Identity,
|
pub ident: Identity,
|
||||||
|
|
|
@ -1,19 +1,6 @@
|
||||||
//! `server` contains the query server, which is the main high level construction
|
//! `server` contains the query server, which is the main high level construction
|
||||||
//! to coordinate queries and operations in the server.
|
//! 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::{
|
use self::access::{
|
||||||
profiles::{
|
profiles::{
|
||||||
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
|
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
|
||||||
|
@ -25,6 +12,7 @@ use self::keys::{
|
||||||
KeyObject, KeyProvider, KeyProviders, KeyProvidersReadTransaction, KeyProvidersTransaction,
|
KeyObject, KeyProvider, KeyProviders, KeyProvidersReadTransaction, KeyProvidersTransaction,
|
||||||
KeyProvidersWriteTransaction,
|
KeyProvidersWriteTransaction,
|
||||||
};
|
};
|
||||||
|
use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction};
|
||||||
use crate::filter::{
|
use crate::filter::{
|
||||||
Filter, FilterInvalid, FilterValid, FilterValidResolved, ResolveFilterCache,
|
Filter, FilterInvalid, FilterValid, FilterValidResolved, ResolveFilterCache,
|
||||||
ResolveFilterCacheReadTxn,
|
ResolveFilterCacheReadTxn,
|
||||||
|
@ -42,6 +30,21 @@ use crate::schema::{
|
||||||
use crate::value::{CredentialType, EXTRACT_VAL_DN};
|
use crate::value::{CredentialType, EXTRACT_VAL_DN};
|
||||||
use crate::valueset::uuid_to_proto_string;
|
use crate::valueset::uuid_to_proto_string;
|
||||||
use crate::valueset::ScimValueIntermediate;
|
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(crate) mod access;
|
||||||
pub mod batch_modify;
|
pub mod batch_modify;
|
||||||
|
@ -52,6 +55,7 @@ pub(crate) mod keys;
|
||||||
pub(crate) mod migrations;
|
pub(crate) mod migrations;
|
||||||
pub mod modify;
|
pub mod modify;
|
||||||
pub(crate) mod recycle;
|
pub(crate) mod recycle;
|
||||||
|
pub mod scim;
|
||||||
|
|
||||||
const RESOLVE_FILTER_CACHE_MAX: usize = 256;
|
const RESOLVE_FILTER_CACHE_MAX: usize = 256;
|
||||||
const RESOLVE_FILTER_CACHE_LOCAL: usize = 8;
|
const RESOLVE_FILTER_CACHE_LOCAL: usize = 8;
|
||||||
|
@ -845,29 +849,283 @@ pub trait QueryServerTransaction<'a> {
|
||||||
scim_value_intermediate: ScimValueIntermediate,
|
scim_value_intermediate: ScimValueIntermediate,
|
||||||
) -> Result<Option<ScimValueKanidm>, OperationError> {
|
) -> Result<Option<ScimValueKanidm>, OperationError> {
|
||||||
match scim_value_intermediate {
|
match scim_value_intermediate {
|
||||||
ScimValueIntermediate::Refer(uuid) => {
|
ScimValueIntermediate::References(uuids) => {
|
||||||
if let Some(option) = self.uuid_to_spn(uuid)? {
|
let scim_references = uuids
|
||||||
Ok(Some(ScimValueKanidm::EntryReference(ScimReference {
|
.into_iter()
|
||||||
uuid,
|
.map(|uuid| {
|
||||||
value: option.to_proto_string_clone(),
|
self.uuid_to_spn(uuid)
|
||||||
})))
|
.and_then(|maybe_value| {
|
||||||
} else {
|
maybe_value.ok_or(OperationError::InvalidValueState)
|
||||||
// TODO: didn't have spn, fallback to uuid.to_string ?
|
})
|
||||||
Ok(None)
|
.map(|value| ScimReference {
|
||||||
}
|
uuid,
|
||||||
}
|
value: value.to_proto_string_clone(),
|
||||||
ScimValueIntermediate::ReferMany(uuids) => {
|
})
|
||||||
let mut scim_references = vec![];
|
})
|
||||||
for uuid in uuids {
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
if let Some(option) = self.uuid_to_spn(uuid)? {
|
|
||||||
scim_references.push(ScimReference {
|
|
||||||
uuid,
|
|
||||||
value: option.to_proto_string_clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Some(ScimValueKanidm::EntryReferences(scim_references)))
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
344
server/lib/src/server/scim.rs
Normal file
344
server/lib/src/server/scim.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ use crate::prelude::*;
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
use rand::distributions::{Distribution, Uniform};
|
use rand::distributions::{Distribution, Uniform};
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use std::collections::BTreeSet;
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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 {
|
impl Distribution<char> for DistinctAlpha {
|
||||||
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> char {
|
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> char {
|
||||||
const RANGE: u32 = 55;
|
const RANGE: u32 = 55;
|
||||||
|
|
|
@ -43,6 +43,7 @@ use crate::valueset::image::ImageValueThings;
|
||||||
use crate::valueset::uuid_to_proto_string;
|
use crate::valueset::uuid_to_proto_string;
|
||||||
|
|
||||||
use kanidm_proto::internal::{ApiTokenPurpose, Filter as ProtoFilter, UiHint};
|
use kanidm_proto::internal::{ApiTokenPurpose, Filter as ProtoFilter, UiHint};
|
||||||
|
use kanidm_proto::scim_v1::ScimOauth2ClaimMapJoinChar;
|
||||||
use kanidm_proto::v1::UatPurposeStatus;
|
use kanidm_proto::v1::UatPurposeStatus;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
@ -1113,6 +1114,34 @@ pub enum OauthClaimMapJoin {
|
||||||
JsonArray,
|
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 {
|
impl OauthClaimMapJoin {
|
||||||
pub(crate) fn to_str(self) -> &'static str {
|
pub(crate) fn to_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use smolset::SmolSet;
|
|
||||||
|
|
||||||
use crate::be::dbvalue::DbValueAddressV1;
|
use crate::be::dbvalue::DbValueAddressV1;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::utils::trigraph_iter;
|
use crate::utils::trigraph_iter;
|
||||||
use crate::value::{Address, VALIDATE_EMAIL_RE};
|
use crate::value::{Address, VALIDATE_EMAIL_RE};
|
||||||
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
|
use crate::valueset::{
|
||||||
|
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
|
||||||
use kanidm_proto::scim_v1::server::{ScimAddress, ScimMail};
|
};
|
||||||
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetAddress {
|
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>> {
|
impl FromIterator<Address> for Option<Box<ValueSetAddress>> {
|
||||||
fn from_iter<T>(iter: T) -> Option<Box<ValueSetAddress>>
|
fn from_iter<T>(iter: T) -> Option<Box<ValueSetAddress>>
|
||||||
where
|
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 {
|
impl ValueSetT for ValueSetEmailAddress {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -607,7 +683,10 @@ mod tests {
|
||||||
"value": "claire@example.com"
|
"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]
|
#[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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::utils::trigraph_iter;
|
use crate::utils::trigraph_iter;
|
||||||
|
use crate::valueset::ScimResolveStatus;
|
||||||
use crate::valueset::{DbValueSetV2, ValueSet};
|
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||||
|
use base64urlsafedata::Base64UrlSafeData;
|
||||||
use kanidm_proto::scim_v1::server::ScimBinary;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetPrivateBinary {
|
pub struct ValueSetPrivateBinary {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::valueset::ScimResolveStatus;
|
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;
|
use smolset::SmolSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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 {
|
impl ValueSetT for ValueSetBool {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -168,6 +185,9 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_boolean() {
|
fn test_scim_boolean() {
|
||||||
let vs: ValueSet = ValueSetBool::new(true);
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ use crate::be::dbvalue::DbValueCertificate;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::valueset::ScimResolveStatus;
|
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::server::ScimCertificate;
|
||||||
|
use kanidm_proto::scim_v1::JsonValue;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use kanidm_lib_crypto::{
|
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 {
|
impl ValueSetT for ValueSetCertificate {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -313,5 +350,8 @@ raBy6edj7W0EIH+yQxkDEwIhAI0nVKaI6duHLAvtKW6CfEQFG6jKg7dyk37YYiRD
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(cert.s256, expect_s256);
|
assert_eq!(cert.s256, expect_s256);
|
||||||
|
|
||||||
|
// Test that we can parse json values into a valueset.
|
||||||
|
crate::valueset::scim_json_put_reflexive::<ValueSetCertificate>(vs, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::{
|
use crate::be::dbvalue::{
|
||||||
DbValueAttestedPasskeyV1, DbValueCredV1, DbValueIntentTokenStateV1, DbValuePasskeyV1,
|
DbValueAttestedPasskeyV1, DbValueCredV1, DbValueIntentTokenStateV1, DbValuePasskeyV1,
|
||||||
};
|
};
|
||||||
|
@ -16,9 +6,18 @@ use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::utils::trigraph_iter;
|
use crate::utils::trigraph_iter;
|
||||||
use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState};
|
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::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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetCredential {
|
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 {
|
impl ValueSetT for ValueSetCredentialType {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -949,9 +971,10 @@ impl ValueSetT for ValueSetCredentialType {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
||||||
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
|
self.set
|
||||||
self.set.iter().map(|ct| ct.to_string()).collect::<Vec<_>>(),
|
.iter()
|
||||||
)))
|
.next()
|
||||||
|
.map(|ct| ScimResolveStatus::Resolved(ScimValueKanidm::from(ct.to_string())))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
@ -1166,6 +1189,9 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_credential_type() {
|
fn test_scim_credential_type() {
|
||||||
let vs: ValueSet = ValueSetCredentialType::new(CredentialType::Mfa);
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::valueset::ScimResolveStatus;
|
use crate::valueset::{
|
||||||
use crate::valueset::{DbValueSetV2, ValueSet};
|
DbValueSetV2, ScimResolveStatus, ValueSet, ValueSetResolveStatus, ValueSetScimPut,
|
||||||
|
};
|
||||||
|
use kanidm_proto::scim_v1::{client::ScimDateTime, JsonValue};
|
||||||
use smolset::SmolSet;
|
use smolset::SmolSet;
|
||||||
use time::OffsetDateTime;
|
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 {
|
impl ValueSetT for ValueSetDateTime {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -125,14 +142,7 @@ impl ValueSetT for ValueSetDateTime {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
||||||
let mut iter = self.set.iter().copied();
|
self.set.iter().next().copied().map(|v| v.into())
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
@ -200,6 +210,9 @@ mod tests {
|
||||||
let odt = OffsetDateTime::UNIX_EPOCH + Duration::from_secs(69_420);
|
let odt = OffsetDateTime::UNIX_EPOCH + Duration::from_secs(69_420);
|
||||||
let vs: ValueSet = ValueSetDateTime::new(odt);
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
use crate::valueset::ScimResolveStatus;
|
|
||||||
use std::iter::{self};
|
|
||||||
|
|
||||||
use super::ValueSet;
|
use super::ValueSet;
|
||||||
use crate::be::dbvalue::DbValueSetV2;
|
use crate::be::dbvalue::DbValueSetV2;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::value::{PartialValue, SyntaxType, Value};
|
use crate::value::{PartialValue, SyntaxType, Value};
|
||||||
|
use crate::valueset::ScimResolveStatus;
|
||||||
use openssl::ec::EcKey;
|
use openssl::ec::EcKey;
|
||||||
use openssl::pkey::{Private, Public};
|
use openssl::pkey::{Private, Public};
|
||||||
|
use std::iter::{self};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct EcKeyPrivate {
|
struct EcKeyPrivate {
|
||||||
|
|
|
@ -183,6 +183,9 @@ mod tests {
|
||||||
fn test_scim_hexstring() {
|
fn test_scim_hexstring() {
|
||||||
let vs: ValueSet =
|
let vs: ValueSet =
|
||||||
ValueSetHexString::new("D68475C760A7A0F6A924C28F095573A967F600D6".to_string());
|
ValueSetHexString::new("D68475C760A7A0F6A924C28F095573A967F600D6".to_string());
|
||||||
crate::valueset::scim_json_reflexive(vs, r#""D68475C760A7A0F6A924C28F095573A967F600D6""#);
|
crate::valueset::scim_json_reflexive(
|
||||||
|
vs.clone(),
|
||||||
|
r#""D68475C760A7A0F6A924C28F095573A967F600D6""#,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -458,6 +458,7 @@ impl ValueSetT for ValueSetImage {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
// use super::ValueSetImage;
|
||||||
use super::{ImageType, ImageValue, ImageValueThings};
|
use super::{ImageType, ImageValue, ImageValueThings};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -511,8 +512,8 @@ mod tests {
|
||||||
assert!(!image.hash_imagevalue().is_empty());
|
assert!(!image.hash_imagevalue().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
// This test is broken on github as it appears to be changing the binary image hash.
|
// This test is broken on github as it appears to be changing the binary image hash.
|
||||||
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_imagevalue() {
|
fn test_scim_imagevalue() {
|
||||||
let filename = format!(
|
let filename = format!(
|
||||||
|
@ -531,7 +532,7 @@ mod tests {
|
||||||
"142dc7984dd548dd5dacfe2ad30f8473e3217e39b3b6c8d17a0cf6e4e24b02e0"
|
"142dc7984dd548dd5dacfe2ad30f8473e3217e39b3b6c8d17a0cf6e4e24b02e0"
|
||||||
]"#;
|
]"#;
|
||||||
|
|
||||||
crate::valueset::scim_json_reflexive(vs, data);
|
crate::valueset::scim_json_reflexive(vs.clone(), data);
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@ use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::utils::trigraph_iter;
|
use crate::utils::trigraph_iter;
|
||||||
use crate::valueset::ScimResolveStatus;
|
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;
|
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 {
|
impl ValueSetT for ValueSetIname {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -209,6 +226,9 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_iname() {
|
fn test_scim_iname() {
|
||||||
let vs: ValueSet = ValueSetIname::new("stevo");
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::valueset::ScimResolveStatus;
|
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;
|
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 {
|
impl ValueSetT for ValueSetIndex {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -159,6 +183,9 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_index() {
|
fn test_scim_index() {
|
||||||
let vs: ValueSet = ValueSetIndex::new(IndexType::Equality);
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::utils::trigraph_iter;
|
use crate::utils::trigraph_iter;
|
||||||
use crate::valueset::ScimResolveStatus;
|
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;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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 {
|
impl ValueSetT for ValueSetIutf8 {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -128,9 +144,11 @@ impl ValueSetT for ValueSetIutf8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
self.set
|
self.set.iter().all(|s| {
|
||||||
.iter()
|
Value::validate_str_escapes(s) && Value::validate_singleline(s) &&
|
||||||
.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> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
@ -209,6 +227,9 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_iutf8() {
|
fn test_scim_iutf8() {
|
||||||
let vs: ValueSet = ValueSetIutf8::new("lowercase string");
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ use crate::schema::SchemaAttribute;
|
||||||
use crate::valueset::ScimResolveStatus;
|
use crate::valueset::ScimResolveStatus;
|
||||||
use crate::valueset::{DbValueSetV2, ValueSet};
|
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||||
use kanidm_proto::internal::Filter as ProtoFilter;
|
use kanidm_proto::internal::Filter as ProtoFilter;
|
||||||
|
|
||||||
use smolset::SmolSet;
|
use smolset::SmolSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -206,6 +205,9 @@ mod tests {
|
||||||
"{\"pres\":\"class\"}"
|
"{\"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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
|
use crate::be::dbvalue::{DbValueKeyInternal, DbValueKeyStatus, DbValueKeyUsage};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::valueset::ScimResolveStatus;
|
|
||||||
|
|
||||||
use crate::server::keys::KeyId;
|
use crate::server::keys::KeyId;
|
||||||
use crate::value::{KeyStatus, KeyUsage};
|
use crate::value::{KeyStatus, KeyUsage};
|
||||||
|
use crate::valueset::ScimResolveStatus;
|
||||||
use crate::be::dbvalue::{DbValueKeyInternal, DbValueKeyStatus, DbValueKeyUsage};
|
|
||||||
use crate::valueset::{DbValueSetV2, ValueSet};
|
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||||
|
use kanidm_proto::scim_v1::server::ScimKeyInternal;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use kanidm_proto::scim_v1::server::ScimKeyInternal;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
pub struct KeyInternalData {
|
pub struct KeyInternalData {
|
||||||
pub usage: KeyUsage,
|
pub usage: KeyUsage,
|
||||||
|
|
|
@ -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::be::dbvalue::DbValueSetV2;
|
||||||
use crate::credential::{apppwd::ApplicationPassword, totp::Totp, Credential};
|
use crate::credential::{apppwd::ApplicationPassword, totp::Totp, Credential};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::repl::cid::Cid;
|
use crate::repl::cid::Cid;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::server::keys::KeyId;
|
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::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};
|
pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
|
||||||
use self::apppwd::ValueSetApplicationPassword;
|
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 {
|
impl PartialEq for ValueSet {
|
||||||
fn eq(&self, other: &ValueSet) -> bool {
|
fn eq(&self, other: &ValueSet) -> bool {
|
||||||
self.equal(other)
|
self.equal(other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
pub struct UnresolvedScimValueOauth2ClaimMap {
|
||||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
|
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 {
|
pub enum ScimValueIntermediate {
|
||||||
Refer(Uuid),
|
References(Vec<Uuid>),
|
||||||
ReferMany(Vec<Uuid>),
|
Oauth2ClaimMap(Vec<UnresolvedScimValueOauth2ClaimMap>),
|
||||||
|
Oauth2ScopeMap(Vec<UnresolvedScimValueOauth2ScopeMap>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ScimResolveStatus {
|
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 {
|
pub fn uuid_to_proto_string(u: Uuid) -> String {
|
||||||
u.as_hyphenated().to_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();
|
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();
|
let expect: serde_json::Value = serde_json::from_str(data).unwrap();
|
||||||
|
|
||||||
assert_eq!(json_value, expect);
|
assert_eq!(json_value, expect);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn scim_json_reflexive_unresolved(vs: ValueSet, data: &str) {
|
pub(crate) fn scim_json_reflexive_unresolved(
|
||||||
let scim_value = vs.to_scim_value().unwrap().assume_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();
|
let strout = serde_json::to_string_pretty(&scim_value).unwrap();
|
||||||
eprintln!("{}", strout);
|
eprintln!("{}", strout);
|
||||||
|
@ -943,3 +1027,50 @@ pub(crate) fn scim_json_reflexive_unresolved(vs: ValueSet, data: &str) {
|
||||||
|
|
||||||
assert_eq!(json_value, expect);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@ use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::value::NSUNIQUEID_RE;
|
use crate::value::NSUNIQUEID_RE;
|
||||||
use crate::valueset::ScimResolveStatus;
|
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;
|
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 {
|
impl ValueSetT for ValueSetNsUniqueId {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -172,6 +189,12 @@ mod tests {
|
||||||
fn test_scim_nsuniqueid() {
|
fn test_scim_nsuniqueid() {
|
||||||
let vs: ValueSet =
|
let vs: ValueSet =
|
||||||
ValueSetNsUniqueId::new("3a163ca0-47624620-a18806b7-50c84c86".to_string());
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,17 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||||
use crate::be::dbvalue::{DbValueOauthClaimMap, DbValueOauthScopeMapV1};
|
use crate::be::dbvalue::{DbValueOauthClaimMap, DbValueOauthScopeMapV1};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::utils::str_join;
|
|
||||||
use crate::value::{OauthClaimMapJoin, OAUTHSCOPE_RE};
|
use crate::value::{OauthClaimMapJoin, OAUTHSCOPE_RE};
|
||||||
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ValueSet};
|
use crate::valueset::{
|
||||||
|
uuid_to_proto_string, DbValueSetV2, ResolvedValueSetOauth2ClaimMap,
|
||||||
use kanidm_proto::scim_v1::server::ScimOAuth2ClaimMap;
|
ResolvedValueSetOauth2ScopeMap, ScimValueIntermediate, UnresolvedScimValueOauth2ClaimMap,
|
||||||
use kanidm_proto::scim_v1::server::ScimOAuth2ScopeMap;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetOauthScope {
|
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 {
|
impl ValueSetT for ValueSetOauthScope {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -114,7 +132,9 @@ impl ValueSetT for ValueSetOauthScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
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 {
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
@ -200,6 +220,58 @@ impl ValueSetOauthScopeMap {
|
||||||
let map = iter.into_iter().collect();
|
let map = iter.into_iter().collect();
|
||||||
Some(Box::new(ValueSetOauthScopeMap { map }))
|
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 {
|
impl ValueSetT for ValueSetOauthScopeMap {
|
||||||
|
@ -291,18 +363,18 @@ impl ValueSetT for ValueSetOauthScopeMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
||||||
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
|
let unresolved_maps = self
|
||||||
self.map
|
.map
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(uuid, scopes)| {
|
.map(|(group_uuid, scopes)| UnresolvedScimValueOauth2ScopeMap {
|
||||||
ScimOAuth2ScopeMap {
|
group_uuid: *group_uuid,
|
||||||
uuid: *uuid,
|
scopes: scopes.clone(),
|
||||||
// Flattened to a space separated list.
|
})
|
||||||
scopes: scopes.clone(),
|
.collect::<Vec<_>>();
|
||||||
}
|
|
||||||
})
|
Some(ScimResolveStatus::NeedsResolution(
|
||||||
.collect::<Vec<_>>(),
|
ScimValueIntermediate::Oauth2ScopeMap(unresolved_maps),
|
||||||
)))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
@ -420,6 +492,45 @@ impl ValueSetOauthClaimMap {
|
||||||
Ok(Box::new(ValueSetOauthClaimMap { map }))
|
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) {
|
fn trim(&mut self) {
|
||||||
self.map
|
self.map
|
||||||
.values_mut()
|
.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 {
|
impl ValueSetT for ValueSetOauthClaimMap {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -622,22 +786,24 @@ impl ValueSetT for ValueSetOauthClaimMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
||||||
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
|
let unresolved_maps = self
|
||||||
self.map
|
.map
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|(claim_name, mappings)| {
|
.flat_map(|(claim_name, mappings)| {
|
||||||
mappings
|
mappings.values.iter().map(|(group_uuid, claim_values)| {
|
||||||
.values
|
UnresolvedScimValueOauth2ClaimMap {
|
||||||
.iter()
|
group_uuid: *group_uuid,
|
||||||
.map(|(group_uuid, claim_values)| ScimOAuth2ClaimMap {
|
claim: claim_name.to_string(),
|
||||||
group: *group_uuid,
|
join_char: mappings.join.into(),
|
||||||
claim: claim_name.to_string(),
|
values: claim_values.clone(),
|
||||||
join_char: mappings.join.to_str().to_string(),
|
}
|
||||||
values: claim_values.clone(),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
})
|
||||||
)))
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Some(ScimResolveStatus::NeedsResolution(
|
||||||
|
ScimValueIntermediate::Oauth2ClaimMap(unresolved_maps),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
@ -704,8 +870,7 @@ impl ValueSetT for ValueSetOauthClaimMap {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{ValueSetOauthClaimMap, ValueSetOauthScope, ValueSetOauthScopeMap};
|
use super::{ValueSetOauthClaimMap, ValueSetOauthScope, ValueSetOauthScopeMap};
|
||||||
use crate::prelude::ValueSet;
|
use crate::prelude::*;
|
||||||
use crate::valueset::ValueSetT;
|
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -726,43 +891,88 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_oauth2_scope() {
|
fn test_scim_oauth2_scope() {
|
||||||
let vs: ValueSet = ValueSetOauthScope::new("fully_sick_scope_m8".to_string());
|
let vs: ValueSet = ValueSetOauthScope::new("fully_sick_scope_m8".to_string());
|
||||||
let data = r#""fully_sick_scope_m8""#;
|
let data = r#"["fully_sick_scope_m8"]"#;
|
||||||
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::<ValueSetOauthScope>(vs, &[])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[qs_test]
|
||||||
fn test_scim_oauth2_scope_map() {
|
async fn test_scim_oauth2_scope_map(server: &QueryServer) {
|
||||||
let u = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
|
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 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#"
|
let data = r#"
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"scopes": "read write",
|
"scopes": ["read", "write"],
|
||||||
"uuid": "3a163ca0-4762-4620-a188-06b750c84c86"
|
"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]
|
#[qs_test]
|
||||||
fn test_scim_oauth2_claim_map() {
|
async fn test_scim_oauth2_claim_map(server: &QueryServer) {
|
||||||
let u = uuid::uuid!("3a163ca0-4762-4620-a188-06b750c84c86");
|
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 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#"
|
let data = r#"
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"claim": "claim",
|
"claim": "claim",
|
||||||
"group": "3a163ca0-4762-4620-a188-06b750c84c86",
|
"group": "testgroup@example.com",
|
||||||
|
"groupUuid": "4d21d04a-dc0e-42eb-b850-34dd180b107f",
|
||||||
"joinChar": ";",
|
"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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
use std::collections::btree_map::Entry as BTreeEntry;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
use crate::be::dbvalue::{
|
use crate::be::dbvalue::{
|
||||||
DbCidV1, DbValueAccessScopeV1, DbValueApiToken, DbValueApiTokenScopeV1, DbValueAuthTypeV1,
|
DbCidV1, DbValueAccessScopeV1, DbValueApiToken, DbValueApiTokenScopeV1, DbValueAuthTypeV1,
|
||||||
DbValueIdentityId, DbValueOauth2Session, DbValueSession, DbValueSessionStateV1,
|
DbValueIdentityId, DbValueOauth2Session, DbValueSession, DbValueSessionStateV1,
|
||||||
|
@ -14,10 +9,12 @@ use crate::value::{
|
||||||
ApiToken, ApiTokenScope, AuthType, Oauth2Session, Session, SessionScope, SessionState,
|
ApiToken, ApiTokenScope, AuthType, Oauth2Session, Session, SessionScope, SessionState,
|
||||||
};
|
};
|
||||||
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ScimResolveStatus, ValueSet};
|
use crate::valueset::{uuid_to_proto_string, DbValueSetV2, ScimResolveStatus, ValueSet};
|
||||||
|
|
||||||
use kanidm_proto::scim_v1::server::ScimApiToken;
|
use kanidm_proto::scim_v1::server::ScimApiToken;
|
||||||
use kanidm_proto::scim_v1::server::ScimAuthSession;
|
use kanidm_proto::scim_v1::server::ScimAuthSession;
|
||||||
use kanidm_proto::scim_v1::server::ScimOAuth2Session;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetSession {
|
pub struct ValueSetSession {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
use std::collections::btree_map::Entry as BTreeEntry;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use crate::be::dbvalue::DbValueTaggedStringV1;
|
use crate::be::dbvalue::DbValueTaggedStringV1;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::utils::trigraph_iter;
|
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 sshkey_attest::proto::PublicKey as SshPublicKey;
|
||||||
|
use std::collections::btree_map::Entry as BTreeEntry;
|
||||||
use kanidm_proto::scim_v1::server::ScimSshPublicKey;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetSshKey {
|
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 {
|
impl ValueSetT for ValueSetSshKey {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
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;
|
use smolset::SmolSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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>> {
|
impl FromIterator<SyntaxType> for Option<Box<ValueSetSyntax>> {
|
||||||
fn from_iter<T>(iter: T) -> Option<Box<ValueSetSyntax>>
|
fn from_iter<T>(iter: T) -> Option<Box<ValueSetSyntax>>
|
||||||
where
|
where
|
||||||
|
@ -106,9 +131,10 @@ impl ValueSetT for ValueSetSyntax {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
||||||
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
|
self.set
|
||||||
self.set.iter().map(|u| u.to_string()).collect::<Vec<_>>(),
|
.iter()
|
||||||
)))
|
.next()
|
||||||
|
.map(|u| ScimResolveStatus::Resolved(ScimValueKanidm::from(u.to_string())))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
@ -162,6 +188,9 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_syntax() {
|
fn test_scim_syntax() {
|
||||||
let vs: ValueSet = ValueSetSyntax::new(SyntaxType::Uuid);
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
|
use crate::be::dbvalue::DbTotpV1;
|
||||||
use crate::credential::totp::Totp;
|
use crate::credential::totp::Totp;
|
||||||
use crate::prelude::*;
|
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::schema::SchemaAttribute;
|
||||||
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
|
use crate::valueset::{DbValueSetV2, ScimResolveStatus, ValueSet};
|
||||||
|
use std::collections::btree_map::Entry as BTreeEntry;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetTotpSecret {
|
pub struct ValueSetTotpSecret {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
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::internal::UiHint;
|
||||||
|
use kanidm_proto::scim_v1::JsonValue;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetUiHint {
|
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 {
|
impl ValueSetT for ValueSetUiHint {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -96,7 +112,7 @@ impl ValueSetT for ValueSetUiHint {
|
||||||
|
|
||||||
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
||||||
Some(ScimResolveStatus::Resolved(ScimValueKanidm::from(
|
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]
|
#[test]
|
||||||
fn test_scim_uihint() {
|
fn test_scim_uihint() {
|
||||||
let vs: ValueSet = ValueSetUiHint::new(UiHint::PosixAccount);
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
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;
|
use smolset::SmolSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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 {
|
impl ValueSetT for ValueSetUint32 {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -178,6 +196,9 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_uint32() {
|
fn test_scim_uint32() {
|
||||||
let vs: ValueSet = ValueSetUint32::new(69);
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
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;
|
use smolset::SmolSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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 {
|
impl ValueSetT for ValueSetUrl {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -165,6 +183,9 @@ mod tests {
|
||||||
fn test_scim_url() {
|
fn test_scim_url() {
|
||||||
let u = Url::parse("https://idm.example.com").unwrap();
|
let u = Url::parse("https://idm.example.com").unwrap();
|
||||||
let vs: ValueSet = ValueSetUrl::new(u);
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::utils::trigraph_iter;
|
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;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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 {
|
impl ValueSetT for ValueSetUtf8 {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -227,6 +245,10 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_utf8() {
|
fn test_scim_utf8() {
|
||||||
let vs: ValueSet = ValueSetUtf8::new("Test".to_string());
|
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, &[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::valueset::{
|
use crate::valueset::{
|
||||||
uuid_to_proto_string, DbValueSetV2, ScimResolveStatus, ScimValueIntermediate, ValueSet,
|
uuid_to_proto_string, DbValueSetV2, ScimResolveStatus, ScimValueIntermediate, ValueSet,
|
||||||
|
ValueSetIntermediate, ValueSetResolveStatus, ValueSetScimPut,
|
||||||
};
|
};
|
||||||
|
use kanidm_proto::scim_v1::JsonValue;
|
||||||
use smolset::SmolSet;
|
use smolset::SmolSet;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetUuid {
|
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 {
|
impl ValueSetT for ValueSetUuid {
|
||||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||||
match value {
|
match value {
|
||||||
|
@ -119,7 +135,8 @@ impl ValueSetT for ValueSetUuid {
|
||||||
.iter()
|
.iter()
|
||||||
.next()
|
.next()
|
||||||
.copied()
|
.copied()
|
||||||
.map(|uuid| ScimResolveStatus::NeedsResolution(ScimValueIntermediate::Refer(uuid)))
|
.map(ScimValueKanidm::Uuid)
|
||||||
|
.map(ScimResolveStatus::Resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||||
|
@ -211,6 +228,55 @@ impl ValueSetRefer {
|
||||||
Some(Box::new(ValueSetRefer { set }))
|
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 {
|
impl ValueSetT for ValueSetRefer {
|
||||||
|
@ -290,7 +356,7 @@ impl ValueSetT for ValueSetRefer {
|
||||||
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
fn to_scim_value(&self) -> Option<ScimResolveStatus> {
|
||||||
let uuids = self.set.iter().copied().collect::<Vec<_>>();
|
let uuids = self.set.iter().copied().collect::<Vec<_>>();
|
||||||
Some(ScimResolveStatus::NeedsResolution(
|
Some(ScimResolveStatus::NeedsResolution(
|
||||||
ScimValueIntermediate::ReferMany(uuids),
|
ScimValueIntermediate::References(uuids),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,23 +414,50 @@ impl ValueSetT for ValueSetRefer {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{ValueSetRefer, ValueSetUuid};
|
use super::{ValueSetRefer, ValueSetUuid};
|
||||||
use crate::prelude::ValueSet;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_scim_uuid() {
|
fn test_scim_uuid() {
|
||||||
let vs: ValueSet = ValueSetUuid::new(uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f"));
|
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]
|
#[qs_test]
|
||||||
fn test_scim_refer() {
|
async fn test_scim_refer(server: &QueryServer) {
|
||||||
let vs: ValueSet = ValueSetRefer::new(uuid::uuid!("4d21d04a-dc0e-42eb-b850-34dd180b107f"));
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -365,7 +365,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
||||||
.expect("Unable to decode AccessTokenIntrospectResponse");
|
.expect("Unable to decode AccessTokenIntrospectResponse");
|
||||||
|
|
||||||
assert!(tir.active);
|
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.client_id.as_deref(), Some(TEST_INTEGRATION_RS_ID));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tir.username.as_deref(),
|
tir.username.as_deref(),
|
||||||
|
@ -469,7 +469,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
||||||
.expect("Unable to decode AccessTokenIntrospectResponse");
|
.expect("Unable to decode AccessTokenIntrospectResponse");
|
||||||
|
|
||||||
assert!(tir.active);
|
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.client_id.as_deref(), Some(TEST_INTEGRATION_RS_ID));
|
||||||
assert_eq!(tir.username.as_deref(), Some("test_integration@localhost"));
|
assert_eq!(tir.username.as_deref(), Some("test_integration@localhost"));
|
||||||
assert_eq!(tir.token_type, Some(AccessTokenType::Bearer));
|
assert_eq!(tir.token_type, Some(AccessTokenType::Bearer));
|
||||||
|
|
Loading…
Reference in a new issue