mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
20240810 SCIM entry basic (#3032)
This commit is contained in:
parent
f053ff7fba
commit
d3891e301f
|
@ -1,5 +1,5 @@
|
||||||
use crate::{ClientError, KanidmClient};
|
use crate::{ClientError, KanidmClient};
|
||||||
use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState};
|
use kanidm_proto::scim_v1::{ScimEntryGeneric, ScimSyncRequest, ScimSyncState};
|
||||||
|
|
||||||
impl KanidmClient {
|
impl KanidmClient {
|
||||||
// TODO: testing for this
|
// TODO: testing for this
|
||||||
|
@ -15,4 +15,14 @@ impl KanidmClient {
|
||||||
self.perform_post_request("/scim/v1/Sync", scim_sync_request)
|
self.perform_post_request("/scim/v1/Sync", scim_sync_request)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve a Generic SCIM Entry as a JSON Value. This can retrieve any
|
||||||
|
/// type of entry that Kanidm supports.
|
||||||
|
pub async fn scim_v1_entry_get(
|
||||||
|
&self,
|
||||||
|
name_or_uuid: &str,
|
||||||
|
) -> Result<ScimEntryGeneric, ClientError> {
|
||||||
|
self.perform_get_request(format!("/scim/v1/Entry/{}", name_or_uuid).as_str())
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ pub enum OperationError {
|
||||||
Backend,
|
Backend,
|
||||||
NoMatchingEntries,
|
NoMatchingEntries,
|
||||||
NoMatchingAttributes,
|
NoMatchingAttributes,
|
||||||
|
UniqueConstraintViolation,
|
||||||
CorruptedEntry(u64),
|
CorruptedEntry(u64),
|
||||||
CorruptedIndex(String),
|
CorruptedIndex(String),
|
||||||
// TODO: this should just be a vec of the ConsistencyErrors, surely?
|
// TODO: this should just be a vec of the ConsistencyErrors, surely?
|
||||||
|
@ -256,6 +257,7 @@ impl OperationError {
|
||||||
Self::Backend => None,
|
Self::Backend => None,
|
||||||
Self::NoMatchingEntries => None,
|
Self::NoMatchingEntries => None,
|
||||||
Self::NoMatchingAttributes => None,
|
Self::NoMatchingAttributes => None,
|
||||||
|
Self::UniqueConstraintViolation => Some("A unique constraint was violated resulting in multiple conflicting results."),
|
||||||
Self::CorruptedEntry(_) => None,
|
Self::CorruptedEntry(_) => None,
|
||||||
Self::CorruptedIndex(_) => None,
|
Self::CorruptedIndex(_) => None,
|
||||||
Self::ConsistencyError(_) => None,
|
Self::ConsistencyError(_) => None,
|
||||||
|
|
|
@ -16,15 +16,29 @@
|
||||||
//! The server module, which describes how a server should transmit entries and
|
//! The server module, which describes how a server should transmit entries and
|
||||||
//! how it should recieve them.
|
//! how it should recieve them.
|
||||||
|
|
||||||
|
use crate::attribute::Attribute;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub use self::synch::*;
|
||||||
|
pub use scim_proto::prelude::*;
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
mod synch;
|
mod synch;
|
||||||
|
|
||||||
pub use scim_proto::prelude::*;
|
/// A generic ScimEntry. This retains attribute
|
||||||
|
/// values in a generic state awaiting processing by schema aware transforms
|
||||||
pub use self::synch::*;
|
/// either by the server or the client.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||||
//
|
pub struct ScimEntryGeneric {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub header: ScimEntryHeader,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub attrs: BTreeMap<Attribute, JsonValue>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use crate::attribute::Attribute;
|
use crate::attribute::Attribute;
|
||||||
use scim_proto::ScimEntryHeader;
|
use scim_proto::ScimEntryHeader;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Serialize;
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
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, StringWithSeparator};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
@ -10,16 +9,6 @@ use url::Url;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// A generic ScimEntry that we receive from a client. This retains attribute
|
|
||||||
/// values in a generic state awaiting processing by schema aware transforms
|
|
||||||
#[derive(Deserialize, Debug, Clone, ToSchema)]
|
|
||||||
pub struct ScimEntryGeneric {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub header: ScimEntryHeader,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub attrs: BTreeMap<Attribute, JsonValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A strongly typed ScimEntry that is for transmission to clients. This uses
|
/// A strongly typed ScimEntry that is for transmission to clients. This uses
|
||||||
/// Kanidm internal strong types for values allowing direct serialisation and
|
/// Kanidm internal strong types for values allowing direct serialisation and
|
||||||
/// transmission.
|
/// transmission.
|
||||||
|
|
|
@ -5,7 +5,7 @@ use kanidmd_lib::idm::scim::{
|
||||||
};
|
};
|
||||||
use kanidmd_lib::idm::server::IdmServerTransaction;
|
use kanidmd_lib::idm::server::IdmServerTransaction;
|
||||||
|
|
||||||
use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState};
|
use kanidm_proto::scim_v1::{server::ScimEntryKanidm, ScimSyncRequest, ScimSyncState};
|
||||||
|
|
||||||
use super::{QueryServerReadV1, QueryServerWriteV1};
|
use super::{QueryServerReadV1, QueryServerWriteV1};
|
||||||
|
|
||||||
|
@ -197,4 +197,36 @@ impl QueryServerReadV1 {
|
||||||
|
|
||||||
idms_prox_read.scim_sync_get_state(&ident)
|
idms_prox_read.scim_sync_get_state(&ident)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
level = "info",
|
||||||
|
skip_all,
|
||||||
|
fields(uuid = ?eventid)
|
||||||
|
)]
|
||||||
|
pub async fn scim_entry_id_get(
|
||||||
|
&self,
|
||||||
|
client_auth_info: ClientAuthInfo,
|
||||||
|
eventid: Uuid,
|
||||||
|
uuid_or_name: String,
|
||||||
|
) -> Result<ScimEntryKanidm, OperationError> {
|
||||||
|
let ct = duration_from_epoch_now();
|
||||||
|
let mut idms_prox_read = self.idms.proxy_read().await?;
|
||||||
|
let ident = idms_prox_read
|
||||||
|
.validate_client_auth_info_to_ident(client_auth_info, ct)
|
||||||
|
.inspect_err(|err| {
|
||||||
|
error!(?err, "Invalid identity");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let target_uuid = idms_prox_read
|
||||||
|
.qs_read
|
||||||
|
.name_to_uuid(uuid_or_name.as_str())
|
||||||
|
.inspect_err(|err| {
|
||||||
|
error!(?err, "Error resolving id to target");
|
||||||
|
})?;
|
||||||
|
|
||||||
|
idms_prox_read
|
||||||
|
.qs_read
|
||||||
|
.impersonate_search_ext_uuid(target_uuid, &ident)
|
||||||
|
.and_then(|entry| entry.to_scim_kanidm())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,8 +73,10 @@ impl Modify for SecurityAddon {
|
||||||
super::v1_oauth2::oauth2_id_claimmap_join_post,
|
super::v1_oauth2::oauth2_id_claimmap_join_post,
|
||||||
super::v1_oauth2::oauth2_id_claimmap_post,
|
super::v1_oauth2::oauth2_id_claimmap_post,
|
||||||
super::v1_oauth2::oauth2_id_claimmap_delete,
|
super::v1_oauth2::oauth2_id_claimmap_delete,
|
||||||
|
|
||||||
super::v1_scim::scim_sync_post,
|
super::v1_scim::scim_sync_post,
|
||||||
super::v1_scim::scim_sync_get,
|
super::v1_scim::scim_sync_get,
|
||||||
|
super::v1_scim::scim_entry_id_get,
|
||||||
|
|
||||||
super::v1::schema_get,
|
super::v1::schema_get,
|
||||||
super::v1::whoami,
|
super::v1::whoami,
|
||||||
|
|
|
@ -11,7 +11,7 @@ use axum::extract::{Path, State};
|
||||||
use axum::response::Html;
|
use axum::response::Html;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{Extension, Json, Router};
|
use axum::{Extension, Json, Router};
|
||||||
use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState};
|
use kanidm_proto::scim_v1::{server::ScimEntryKanidm, ScimSyncRequest, ScimSyncState};
|
||||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||||
use kanidmd_lib::prelude::*;
|
use kanidmd_lib::prelude::*;
|
||||||
|
|
||||||
|
@ -206,6 +206,54 @@ pub async fn sync_account_token_delete(
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/sync_account/{id}/_attr/{attr}",
|
||||||
|
responses(
|
||||||
|
(status = 200, body=Option<Vec<String>>, content_type="application/json"),
|
||||||
|
ApiResponseWithout200,
|
||||||
|
),
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/sync_account",
|
||||||
|
operation_id = "sync_account_id_attr_get"
|
||||||
|
)]
|
||||||
|
pub async fn sync_account_id_attr_get(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
Path((id, attr)): Path<(String, String)>,
|
||||||
|
) -> Result<Json<Option<Vec<String>>>, WebError> {
|
||||||
|
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||||
|
json_rest_event_get_id_attr(state, id, attr, filter, kopid, client_auth_info).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/sync_account/{id}/_attr/{attr}",
|
||||||
|
request_body=Vec<String>,
|
||||||
|
responses(
|
||||||
|
DefaultApiResponse,
|
||||||
|
),
|
||||||
|
security(("token_jwt" = [])),
|
||||||
|
tag = "v1/sync_account",
|
||||||
|
operation_id = "sync_account_id_attr_put"
|
||||||
|
)]
|
||||||
|
pub async fn sync_account_id_attr_put(
|
||||||
|
State(state): State<ServerState>,
|
||||||
|
Extension(kopid): Extension<KOpId>,
|
||||||
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
|
Path((id, attr)): Path<(String, String)>,
|
||||||
|
Json(values): Json<Vec<String>>,
|
||||||
|
) -> Result<Json<()>, WebError> {
|
||||||
|
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||||
|
json_rest_event_put_attr(state, id, attr, filter, values, kopid, client_auth_info).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When you want the kitchen Sink
|
||||||
|
async fn scim_sink_get() -> Html<&'static str> {
|
||||||
|
Html::from(include_str!("scim/sink.html"))
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/scim/v1/Sync",
|
path = "/scim/v1/Sync",
|
||||||
|
@ -255,56 +303,58 @@ async fn scim_sync_get(
|
||||||
.map(Json::from)
|
.map(Json::from)
|
||||||
.map_err(WebError::from)
|
.map_err(WebError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/v1/sync_account/{id}/_attr/{attr}",
|
path = "/scim/v1/Entry/{id}",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body=Option<Vec<String>>, content_type="application/json"),
|
(status = 200, content_type="application/json", body=ScimEntry),
|
||||||
ApiResponseWithout200,
|
ApiResponseWithout200,
|
||||||
),
|
),
|
||||||
security(("token_jwt" = [])),
|
security(("token_jwt" = [])),
|
||||||
tag = "v1/sync_account",
|
tag = "scim",
|
||||||
operation_id = "sync_account_id_attr_get"
|
operation_id = "scim_entry_id_get"
|
||||||
)]
|
)]
|
||||||
pub async fn sync_account_id_attr_get(
|
async fn scim_entry_id_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
||||||
Path((id, attr)): Path<(String, String)>,
|
) -> Result<Json<ScimEntryKanidm>, WebError> {
|
||||||
) -> Result<Json<Option<Vec<String>>>, WebError> {
|
state
|
||||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
.qe_r_ref
|
||||||
json_rest_event_get_id_attr(state, id, attr, filter, kopid, client_auth_info).await
|
.scim_entry_id_get(client_auth_info, kopid.eventid, id)
|
||||||
}
|
.await
|
||||||
|
.map(Json::from)
|
||||||
#[utoipa::path(
|
.map_err(WebError::from)
|
||||||
post,
|
|
||||||
path = "/v1/sync_account/{id}/_attr/{attr}",
|
|
||||||
request_body=Vec<String>,
|
|
||||||
responses(
|
|
||||||
DefaultApiResponse,
|
|
||||||
),
|
|
||||||
security(("token_jwt" = [])),
|
|
||||||
tag = "v1/sync_account",
|
|
||||||
operation_id = "sync_account_id_attr_put"
|
|
||||||
)]
|
|
||||||
pub async fn sync_account_id_attr_put(
|
|
||||||
State(state): State<ServerState>,
|
|
||||||
Extension(kopid): Extension<KOpId>,
|
|
||||||
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
|
|
||||||
Path((id, attr)): Path<(String, String)>,
|
|
||||||
Json(values): Json<Vec<String>>,
|
|
||||||
) -> Result<Json<()>, WebError> {
|
|
||||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
|
||||||
json_rest_event_put_attr(state, id, attr, filter, values, kopid, client_auth_info).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When you want the kitchen Sink
|
|
||||||
async fn scim_sink_get() -> Html<&'static str> {
|
|
||||||
Html::from(include_str!("scim/sink.html"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn route_setup() -> Router<ServerState> {
|
pub fn route_setup() -> Router<ServerState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/v1/sync_account",
|
||||||
|
get(sync_account_get).post(sync_account_post),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/sync_account/:id",
|
||||||
|
get(sync_account_id_get).patch(sync_account_id_patch),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/sync_account/:id/_attr/:attr",
|
||||||
|
get(sync_account_id_attr_get).put(sync_account_id_attr_put),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/sync_account/:id/_finalise",
|
||||||
|
get(sync_account_id_finalise_get),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/sync_account/:id/_terminate",
|
||||||
|
get(sync_account_id_terminate_get),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/v1/sync_account/:id/_sync_token",
|
||||||
|
post(sync_account_token_post).delete(sync_account_token_delete),
|
||||||
|
)
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.2
|
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.2
|
||||||
//
|
//
|
||||||
// HTTP SCIM Usage
|
// HTTP SCIM Usage
|
||||||
|
@ -366,6 +416,11 @@ pub fn route_setup() -> Router<ServerState> {
|
||||||
// POST.
|
// POST.
|
||||||
// -- Kanidm Resources
|
// -- Kanidm Resources
|
||||||
//
|
//
|
||||||
|
// Entry /Entry/{id} GET Retrieve a generic entry
|
||||||
|
// of any kind from the database.
|
||||||
|
// {id} is any unique id.
|
||||||
|
.route("/scim/v1/Entry/:id", get(scim_entry_id_get))
|
||||||
|
//
|
||||||
// Sync /Sync GET Retrieve the current
|
// Sync /Sync GET Retrieve the current
|
||||||
// sync state associated
|
// sync state associated
|
||||||
// with the authenticated
|
// with the authenticated
|
||||||
|
@ -373,30 +428,6 @@ pub fn route_setup() -> Router<ServerState> {
|
||||||
//
|
//
|
||||||
// POST Send a sync update
|
// POST Send a sync update
|
||||||
//
|
//
|
||||||
.route(
|
|
||||||
"/v1/sync_account",
|
|
||||||
get(sync_account_get).post(sync_account_post),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/v1/sync_account/:id",
|
|
||||||
get(sync_account_id_get).patch(sync_account_id_patch),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/v1/sync_account/:id/_attr/:attr",
|
|
||||||
get(sync_account_id_attr_get).put(sync_account_id_attr_put),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/v1/sync_account/:id/_finalise",
|
|
||||||
get(sync_account_id_finalise_get),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/v1/sync_account/:id/_terminate",
|
|
||||||
get(sync_account_id_terminate_get),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/v1/sync_account/:id/_sync_token",
|
|
||||||
post(sync_account_token_post).delete(sync_account_token_delete),
|
|
||||||
)
|
|
||||||
.route("/scim/v1/Sync", post(scim_sync_post).get(scim_sync_get))
|
.route("/scim/v1/Sync", post(scim_sync_post).get(scim_sync_get))
|
||||||
.route("/scim/v1/Sink", get(scim_sink_get)) // skip_route_check
|
.route("/scim/v1/Sink", get(scim_sink_get)) // skip_route_check
|
||||||
}
|
}
|
||||||
|
|
|
@ -2433,6 +2433,8 @@ impl Entry<EntryReduced, EntryCommitted> {
|
||||||
let attrs = self
|
let attrs = self
|
||||||
.attrs
|
.attrs
|
||||||
.iter()
|
.iter()
|
||||||
|
// We want to skip some attributes as they are already in the header.
|
||||||
|
.filter(|(k, _vs)| **k != Attribute::Uuid)
|
||||||
.filter_map(|(k, vs)| vs.to_scim_value().map(|scim_value| (k.clone(), scim_value)))
|
.filter_map(|(k, vs)| vs.to_scim_value().map(|scim_value| (k.clone(), scim_value)))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
@ -586,7 +586,14 @@ pub trait QueryServerTransaction<'a> {
|
||||||
let mut vs = self.impersonate_search_ext(filter, filter_intent, event)?;
|
let mut vs = self.impersonate_search_ext(filter, filter_intent, event)?;
|
||||||
match vs.pop() {
|
match vs.pop() {
|
||||||
Some(entry) if vs.is_empty() => Ok(entry),
|
Some(entry) if vs.is_empty() => Ok(entry),
|
||||||
_ => Err(OperationError::NoMatchingEntries),
|
_ => {
|
||||||
|
if vs.is_empty() {
|
||||||
|
Err(OperationError::NoMatchingEntries)
|
||||||
|
} else {
|
||||||
|
// Multiple entries matched, should not be possible!
|
||||||
|
Err(OperationError::UniqueConstraintViolation)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
|
use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
|
||||||
use kanidm_client::KanidmClient;
|
use kanidm_client::KanidmClient;
|
||||||
use kanidm_proto::internal::ScimSyncToken;
|
use kanidm_proto::internal::ScimSyncToken;
|
||||||
|
use kanidmd_lib::prelude::{Attribute, BUILTIN_GROUP_IDM_ADMINS_V1};
|
||||||
use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
|
use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
|
||||||
use reqwest::header::HeaderValue;
|
use reqwest::header::HeaderValue;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -158,3 +159,39 @@ async fn test_scim_sync_get(rsclient: KanidmClient) {
|
||||||
assert!(content_type.to_str().unwrap().contains("text/html"));
|
assert!(content_type.to_str().unwrap().contains("text/html"));
|
||||||
assert!(response.text().await.unwrap().contains("Sink"));
|
assert!(response.text().await.unwrap().contains("Sink"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[kanidmd_testkit::test]
|
||||||
|
async fn test_scim_sync_entry_get(rsclient: KanidmClient) {
|
||||||
|
let res = rsclient
|
||||||
|
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
|
||||||
|
// All admin to create persons.
|
||||||
|
rsclient
|
||||||
|
.idm_group_add_members(BUILTIN_GROUP_IDM_ADMINS_V1.name, &["admin"])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
rsclient
|
||||||
|
.idm_person_account_create("demo_account", "Deeeeemo")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// This will be as raw json, not the strongly typed version the server sees
|
||||||
|
// internally.
|
||||||
|
let scim_entry = rsclient.scim_v1_entry_get("demo_account").await.unwrap();
|
||||||
|
|
||||||
|
tracing::info!("{:#?}", scim_entry);
|
||||||
|
|
||||||
|
assert!(scim_entry.attrs.contains_key(&Attribute::Class));
|
||||||
|
assert!(scim_entry.attrs.contains_key(&Attribute::Name));
|
||||||
|
assert_eq!(
|
||||||
|
scim_entry
|
||||||
|
.attrs
|
||||||
|
.get(&Attribute::Name)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap(),
|
||||||
|
"demo_account".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue