Working scim entry get for person (#3088)

This commit is contained in:
Firstyear 2024-10-15 14:29:45 +10:00 committed by GitHub
parent 50e513b30b
commit 2075125439
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 347 additions and 93 deletions

2
Cargo.lock generated
View file

@ -3265,6 +3265,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"time", "time",
"tokio", "tokio",
"toml", "toml",
@ -3314,6 +3315,7 @@ dependencies = [
"scim_proto", "scim_proto",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"serde_with", "serde_with",
"smartstring", "smartstring",
"sshkey-attest", "sshkey-attest",

View file

@ -255,6 +255,7 @@ selinux = "^0.4.6"
serde = "^1.0.210" serde = "^1.0.210"
serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" } serde_cbor = { version = "0.12.0-dev", package = "serde_cbor_2" }
serde_json = "^1.0.128" serde_json = "^1.0.128"
serde_urlencoded = "^0.7.1"
serde-wasm-bindgen = "0.5" serde-wasm-bindgen = "0.5"
serde_with = "3.11.0" serde_with = "3.11.0"
sha-crypt = "0.5.0" sha-crypt = "0.5.0"

View file

@ -94,8 +94,6 @@ Stable APIs are:
- LDAP protocol operations - LDAP protocol operations
- JSON HTTP end points which use elements from - JSON HTTP end points which use elements from
[`proto/src/v1`](https://github.com/kanidm/kanidm/blob/master/proto/src/v1) [`proto/src/v1`](https://github.com/kanidm/kanidm/blob/master/proto/src/v1)
- SCIM operations from
[`proto/src/scim_v1`](https://github.com/kanidm/kanidm/blob/master/proto/src/scim_v1)
All other APIs and interactions are not considered stable. Changes will be minimised if possible. All other APIs and interactions are not considered stable. Changes will be minimised if possible.
This includes but is not limited to: This includes but is not limited to:
@ -107,6 +105,8 @@ This includes but is not limited to:
- CLI interface of any command provided by kanidm unless otherwise noted above - CLI interface of any command provided by kanidm unless otherwise noted above
- JSON HTTP end points which use elements from - JSON HTTP end points which use elements from
[`proto/src/internal.rs`](https://github.com/kanidm/kanidm/blob/master/proto/src/internal.rs) [`proto/src/internal.rs`](https://github.com/kanidm/kanidm/blob/master/proto/src/internal.rs)
- SCIM operations from
[`proto/src/scim_v1`](https://github.com/kanidm/kanidm/blob/master/proto/src/scim_v1)
### Deprecation Policy ### Deprecation Policy

View file

@ -28,6 +28,7 @@ http = { workspace = true }
hyper = { workspace = true } hyper = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_urlencoded = { workspace = true }
time = { workspace = true, features = ["serde", "std"] } time = { workspace = true, features = ["serde", "std"] }
tokio = { workspace = true, features = [ tokio = { workspace = true, features = [
"rt", "rt",

View file

@ -14,7 +14,7 @@
extern crate tracing; extern crate tracing;
use std::collections::{BTreeMap, BTreeSet as Set}; use std::collections::{BTreeMap, BTreeSet as Set};
use std::fmt::{Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::fs::File; use std::fs::File;
#[cfg(target_family = "unix")] // not needed for windows builds #[cfg(target_family = "unix")] // not needed for windows builds
use std::fs::{metadata, Metadata}; use std::fs::{metadata, Metadata};
@ -41,6 +41,7 @@ pub use reqwest::StatusCode;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::error::Error as SerdeJsonError; use serde_json::error::Error as SerdeJsonError;
use serde_urlencoded::ser::Error as UrlEncodeError;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
@ -72,6 +73,7 @@ pub enum ClientError {
JsonDecode(reqwest::Error, String), JsonDecode(reqwest::Error, String),
InvalidResponseFormat(String), InvalidResponseFormat(String),
JsonEncode(SerdeJsonError), JsonEncode(SerdeJsonError),
UrlEncode(UrlEncodeError),
SystemError, SystemError,
ConfigParseIssue(String), ConfigParseIssue(String),
CertParseIssue(String), CertParseIssue(String),
@ -1003,7 +1005,27 @@ impl KanidmClient {
&self, &self,
dest: &str, dest: &str,
) -> Result<T, ClientError> { ) -> Result<T, ClientError> {
let response = self.client.get(self.make_url(dest)); let query: Option<()> = None;
self.perform_get_request_query(dest, query).await
}
#[instrument(level = "debug", skip(self))]
pub async fn perform_get_request_query<T: DeserializeOwned, Q: Serialize + Debug>(
&self,
dest: &str,
query: Option<Q>,
) -> Result<T, ClientError> {
let mut dest_url = self.make_url(dest);
if let Some(query) = query {
let txt = serde_urlencoded::to_string(&query).map_err(ClientError::UrlEncode)?;
if !txt.is_empty() {
dest_url.set_query(Some(txt.as_str()));
}
}
let response = self.client.get(dest_url);
let response = { let response = {
let tguard = self.bearer_token.read().await; let tguard = self.bearer_token.read().await;
if let Some(token) = &(*tguard) { if let Some(token) = &(*tguard) {

View file

@ -1,5 +1,5 @@
use crate::{ClientError, KanidmClient}; use crate::{ClientError, KanidmClient};
use kanidm_proto::scim_v1::{ScimEntryGeneric, ScimSyncRequest, ScimSyncState}; use kanidm_proto::scim_v1::{ScimEntryGeneric, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState};
impl KanidmClient { impl KanidmClient {
// TODO: testing for this // TODO: testing for this
@ -21,8 +21,19 @@ impl KanidmClient {
pub async fn scim_v1_entry_get( pub async fn scim_v1_entry_get(
&self, &self,
name_or_uuid: &str, name_or_uuid: &str,
query: Option<ScimEntryGetQuery>,
) -> Result<ScimEntryGeneric, ClientError> { ) -> Result<ScimEntryGeneric, ClientError> {
self.perform_get_request(format!("/scim/v1/Entry/{}", name_or_uuid).as_str()) self.perform_get_request_query(format!("/scim/v1/Entry/{}", name_or_uuid).as_str(), query)
.await
}
/// Retrieve a Person as a SCIM JSON Value.
pub async fn scim_v1_person_get(
&self,
name_or_uuid: &str,
query: Option<ScimEntryGetQuery>,
) -> Result<ScimEntryGeneric, ClientError> {
self.perform_get_request_query(format!("/scim/v1/Person/{}", name_or_uuid).as_str(), query)
.await .await
} }
} }

View file

@ -26,6 +26,7 @@ num_enum = { workspace = true }
scim_proto = { workspace = true } scim_proto = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_urlencoded = { workspace = true }
serde_with = { workspace = true, features = ["time_0_3", "base64", "hex"] } serde_with = { workspace = true, features = ["time_0_3", "base64", "hex"] }
smartstring = { workspace = true, features = ["serde"] } smartstring = { workspace = true, features = ["serde"] }
time = { workspace = true, features = ["serde", "std"] } time = { workspace = true, features = ["serde", "std"] }

View file

@ -3,7 +3,9 @@ use utoipa::ToSchema;
use crate::constants::*; use crate::constants::*;
use crate::internal::OperationError; use crate::internal::OperationError;
use std::convert::Infallible;
use std::fmt; use std::fmt;
use std::str::FromStr;
pub use smartstring::alias::String as AttrString; pub use smartstring::alias::String as AttrString;
@ -205,13 +207,13 @@ impl TryFrom<&AttrString> for Attribute {
type Error = OperationError; type Error = OperationError;
fn try_from(value: &AttrString) -> Result<Self, Self::Error> { fn try_from(value: &AttrString) -> Result<Self, Self::Error> {
Ok(Attribute::from_str(value.as_str())) Ok(Attribute::inner_from_str(value.as_str()))
} }
} }
impl From<&str> for Attribute { impl From<&str> for Attribute {
fn from(value: &str) -> Self { fn from(value: &str) -> Self {
Self::from_str(value) Self::inner_from_str(value)
} }
} }
@ -227,6 +229,14 @@ impl From<Attribute> for AttrString {
} }
} }
impl FromStr for Attribute {
type Err = Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self::inner_from_str(value))
}
}
impl Attribute { impl Attribute {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
@ -406,7 +416,7 @@ impl Attribute {
// We allow this because the standard lib from_str is fallible, and we want an infallible version. // We allow this because the standard lib from_str is fallible, and we want an infallible version.
#[allow(clippy::should_implement_trait)] #[allow(clippy::should_implement_trait)]
pub fn from_str(value: &str) -> Self { fn inner_from_str(value: &str) -> Self {
// Could this be something like heapless to save allocations? Also gives a way // Could this be something like heapless to save allocations? Also gives a way
// to limit length of str? // to limit length of str?
match value.to_lowercase().as_str() { match value.to_lowercase().as_str() {
@ -603,9 +613,9 @@ mod test {
#[test] #[test]
fn test_valueattribute_from_str() { fn test_valueattribute_from_str() {
assert_eq!(Attribute::Uuid, Attribute::from_str("UUID")); assert_eq!(Attribute::Uuid, Attribute::from("UUID"));
assert_eq!(Attribute::Uuid, Attribute::from_str("UuiD")); assert_eq!(Attribute::Uuid, Attribute::from("UuiD"));
assert_eq!(Attribute::Uuid, Attribute::from_str("uuid")); assert_eq!(Attribute::Uuid, Attribute::from("uuid"));
} }
#[test] #[test]

View file

@ -1 +1,11 @@
use serde::{Deserialize, Serialize};
use sshkey_attest::proto::PublicKey as SshPublicKey;
pub type ScimSshPublicKeys = Vec<ScimSshPublicKey>;
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ScimSshPublicKey {
pub label: String,
pub value: SshPublicKey,
}

View file

@ -22,10 +22,13 @@ use serde_json::Value as JsonValue;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use utoipa::ToSchema; use utoipa::ToSchema;
use serde_with::formats::CommaSeparator;
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::*;
mod client; pub mod client;
pub mod server; pub mod server;
mod synch; mod synch;
@ -40,28 +43,37 @@ pub struct ScimEntryGeneric {
pub attrs: BTreeMap<Attribute, JsonValue>, pub attrs: BTreeMap<Attribute, JsonValue>,
} }
/// SCIM Query Parameters used during the get of a single entry
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct ScimEntryGetQuery {
#[serde_as(as = "Option<StringWithSeparator::<CommaSeparator, Attribute>>")]
pub attributes: Option<Vec<Attribute>>,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// use super::*; // use super::*;
#[test] #[test]
fn test_scim_rfc_to_generic() { fn scim_rfc_to_generic() {
// Assert that we can transition from the rfc generic entries to the // Assert that we can transition from the rfc generic entries to the
// kanidm types. // kanidm types.
} }
#[test] #[test]
fn test_scim_kani_to_generic() { fn scim_kani_to_generic() {
// Assert that a kanidm strong entry can convert to generic. // Assert that a kanidm strong entry can convert to generic.
} }
#[test] #[test]
fn test_scim_kani_to_rfc() { fn scim_kani_to_rfc() {
// Assert that a kanidm strong entry can convert to rfc. // Assert that a kanidm strong entry can convert to rfc.
} }
#[test] #[test]
fn test_scim_sync_kani_to_rfc() { fn scim_sync_kani_to_rfc() {
use super::*; use super::*;
// Group // Group
@ -114,4 +126,29 @@ mod tests {
assert!(entry.is_ok()); assert!(entry.is_ok());
} }
#[test]
fn scim_entry_get_query() {
use super::*;
let q = ScimEntryGetQuery { attributes: None };
let txt = serde_urlencoded::to_string(&q).unwrap();
assert_eq!(txt, "");
let q = ScimEntryGetQuery {
attributes: Some(vec![Attribute::Name]),
};
let txt = serde_urlencoded::to_string(&q).unwrap();
assert_eq!(txt, "attributes=name");
let q = ScimEntryGetQuery {
attributes: Some(vec![Attribute::Name, Attribute::Spn]),
};
let txt = serde_urlencoded::to_string(&q).unwrap();
assert_eq!(txt, "attributes=name%2Cspn");
}
} }

View file

@ -1,13 +1,12 @@
use kanidmd_lib::prelude::*; use super::{QueryServerReadV1, QueryServerWriteV1};
use kanidm_proto::scim_v1::{
server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
};
use kanidmd_lib::idm::scim::{ use kanidmd_lib::idm::scim::{
GenerateScimSyncTokenEvent, ScimSyncFinaliseEvent, ScimSyncTerminateEvent, ScimSyncUpdateEvent, GenerateScimSyncTokenEvent, ScimSyncFinaliseEvent, ScimSyncTerminateEvent, ScimSyncUpdateEvent,
}; };
use kanidmd_lib::idm::server::IdmServerTransaction; use kanidmd_lib::idm::server::IdmServerTransaction;
use kanidmd_lib::prelude::*;
use kanidm_proto::scim_v1::{server::ScimEntryKanidm, ScimSyncRequest, ScimSyncState};
use super::{QueryServerReadV1, QueryServerWriteV1};
impl QueryServerWriteV1 { impl QueryServerWriteV1 {
#[instrument( #[instrument(
@ -208,6 +207,8 @@ impl QueryServerReadV1 {
client_auth_info: ClientAuthInfo, client_auth_info: ClientAuthInfo,
eventid: Uuid, eventid: Uuid,
uuid_or_name: String, uuid_or_name: String,
class: EntryClass,
query: ScimEntryGetQuery,
) -> Result<ScimEntryKanidm, OperationError> { ) -> Result<ScimEntryKanidm, OperationError> {
let ct = duration_from_epoch_now(); let ct = duration_from_epoch_now();
let mut idms_prox_read = self.idms.proxy_read().await?; let mut idms_prox_read = self.idms.proxy_read().await?;
@ -226,7 +227,6 @@ impl QueryServerReadV1 {
idms_prox_read idms_prox_read
.qs_read .qs_read
.impersonate_search_ext_uuid(target_uuid, &ident) .scim_entry_id_get_ext(target_uuid, class, query, ident)
.and_then(|entry| entry.to_scim_kanidm(idms_prox_read.qs_read))
} }
} }

View file

@ -77,6 +77,7 @@ impl Modify for SecurityAddon {
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_scim::scim_entry_id_get,
super::v1_scim::scim_person_id_get,
super::v1::schema_get, super::v1::schema_get,
super::v1::whoami, super::v1::whoami,

View file

@ -7,11 +7,13 @@ use super::v1::{
}; };
use super::ServerState; use super::ServerState;
use crate::https::extractors::VerifiedClientInformation; use crate::https::extractors::VerifiedClientInformation;
use axum::extract::{Path, State}; use axum::extract::{Path, Query, 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::{server::ScimEntryKanidm, ScimSyncRequest, ScimSyncState}; use kanidm_proto::scim_v1::{
server::ScimEntryKanidm, ScimEntryGetQuery, ScimSyncRequest, ScimSyncState,
};
use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::Entry as ProtoEntry;
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
@ -320,10 +322,49 @@ async fn scim_entry_id_get(
Path(id): Path<String>, Path(id): Path<String>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation, VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Query(scim_entry_get_query): Query<ScimEntryGetQuery>,
) -> Result<Json<ScimEntryKanidm>, WebError> { ) -> Result<Json<ScimEntryKanidm>, WebError> {
state state
.qe_r_ref .qe_r_ref
.scim_entry_id_get(client_auth_info, kopid.eventid, id) .scim_entry_id_get(
client_auth_info,
kopid.eventid,
id,
EntryClass::Object,
scim_entry_get_query,
)
.await
.map(Json::from)
.map_err(WebError::from)
}
#[utoipa::path(
get,
path = "/scim/v1/Person/{id}",
responses(
(status = 200, content_type="application/json", body=ScimEntry),
ApiResponseWithout200,
),
security(("token_jwt" = [])),
tag = "scim",
operation_id = "scim_entry_id_get"
)]
async fn scim_person_id_get(
State(state): State<ServerState>,
Path(id): Path<String>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Query(scim_entry_get_query): Query<ScimEntryGetQuery>,
) -> Result<Json<ScimEntryKanidm>, WebError> {
state
.qe_r_ref
.scim_entry_id_get(
client_auth_info,
kopid.eventid,
id,
EntryClass::Person,
scim_entry_get_query,
)
.await .await
.map(Json::from) .map(Json::from)
.map_err(WebError::from) .map_err(WebError::from)
@ -420,6 +461,10 @@ pub fn route_setup() -> Router<ServerState> {
// of any kind from the database. // of any kind from the database.
// {id} is any unique id. // {id} is any unique id.
.route("/scim/v1/Entry/:id", get(scim_entry_id_get)) .route("/scim/v1/Entry/:id", get(scim_entry_id_get))
// Person /Person/{id} GET Retrieve a a person from the
// database.
// {id} is any unique id.
.route("/scim/v1/Person/:id", get(scim_person_id_get))
// //
// Sync /Sync GET Retrieve the current // Sync /Sync GET Retrieve the current
// sync state associated // sync state associated

View file

@ -47,7 +47,6 @@ use kanidm_proto::internal::ImageValue;
use kanidm_proto::internal::{ use kanidm_proto::internal::{
ConsistencyError, Filter as ProtoFilter, OperationError, SchemaError, UiHint, ConsistencyError, Filter as ProtoFilter, OperationError, SchemaError, UiHint,
}; };
use kanidm_proto::scim_v1::server::ScimReference;
use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::Entry as ProtoEntry;
use ldap3_proto::simple::{LdapPartialAttribute, LdapSearchResultEntry}; use ldap3_proto::simple::{LdapPartialAttribute, LdapSearchResultEntry};
use openssl::ec::EcKey; use openssl::ec::EcKey;
@ -64,7 +63,7 @@ use crate::value::{
ApiToken, CredentialType, IndexType, IntentTokenState, Oauth2Session, PartialValue, Session, ApiToken, CredentialType, IndexType, IntentTokenState, Oauth2Session, PartialValue, Session,
SyntaxType, Value, SyntaxType, Value,
}; };
use crate::valueset::{self, ScimResolveStatus, ScimValueIntermediate, ValueSet}; use crate::valueset::{self, ScimResolveStatus, ValueSet};
pub type EntryInitNew = Entry<EntryInit, EntryNew>; pub type EntryInitNew = Entry<EntryInit, EntryNew>;
pub type EntryInvalidNew = Entry<EntryInvalid, EntryNew>; pub type EntryInvalidNew = Entry<EntryInvalid, EntryNew>;
@ -2232,7 +2231,7 @@ impl Entry<EntryReduced, EntryCommitted> {
pub fn to_scim_kanidm( pub fn to_scim_kanidm(
&self, &self,
mut read_txn: QueryServerReadTransaction, read_txn: &mut QueryServerReadTransaction,
) -> Result<ScimEntryKanidm, OperationError> { ) -> Result<ScimEntryKanidm, OperationError> {
let result: Result<BTreeMap<Attribute, ScimValueKanidm>, OperationError> = self let result: Result<BTreeMap<Attribute, ScimValueKanidm>, OperationError> = self
.attrs .attrs
@ -2245,7 +2244,7 @@ impl Entry<EntryReduced, EntryCommitted> {
None => Ok(None), None => Ok(None),
Some(ScimResolveStatus::Resolved(scim_value_kani)) => Ok(Some(scim_value_kani)), Some(ScimResolveStatus::Resolved(scim_value_kani)) => Ok(Some(scim_value_kani)),
Some(ScimResolveStatus::NeedsResolution(scim_value_interim)) => { Some(ScimResolveStatus::NeedsResolution(scim_value_interim)) => {
resolve_scim_interim(scim_value_interim, &mut read_txn) read_txn.resolve_scim_interim(scim_value_interim)
} }
}; };
res_opt_scim_value res_opt_scim_value
@ -2370,37 +2369,6 @@ impl Entry<EntryReduced, EntryCommitted> {
} }
} }
fn resolve_scim_interim(
scim_value_intermediate: ScimValueIntermediate,
read_txn: &mut QueryServerReadTransaction,
) -> Result<Option<ScimValueKanidm>, OperationError> {
match scim_value_intermediate {
ScimValueIntermediate::Refer(uuid) => {
if let Some(option) = read_txn.uuid_to_spn(uuid)? {
Ok(Some(ScimValueKanidm::EntryReference(ScimReference {
uuid,
value: option.to_proto_string_clone(),
})))
} else {
// TODO: didn't have spn, fallback to uuid.to_string ?
Ok(None)
}
}
ScimValueIntermediate::ReferMany(uuids) => {
let mut scim_references = vec![];
for uuid in uuids {
if let Some(option) = read_txn.uuid_to_spn(uuid)? {
scim_references.push(ScimReference {
uuid,
value: option.to_proto_string_clone(),
})
}
}
Ok(Some(ScimValueKanidm::EntryReferences(scim_references)))
}
}
}
// impl<STATE> Entry<EntryValid, STATE> { // impl<STATE> Entry<EntryValid, STATE> {
impl<VALID, STATE> Entry<VALID, STATE> { impl<VALID, STATE> Entry<VALID, STATE> {
/// This internally adds an AVA to the entry. If the entry was newly added, then true is returned. /// This internally adds an AVA to the entry. If the entry was newly added, then true is returned.

View file

@ -1,20 +1,30 @@
//! `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 std::str::FromStr; use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction};
use std::sync::Arc;
use concread::arcache::{ARCacheBuilder, ARCacheReadTxn}; use concread::arcache::{ARCacheBuilder, ARCacheReadTxn};
use concread::cowcell::*; use concread::cowcell::*;
use hashbrown::{HashMap, HashSet}; 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::collections::BTreeSet;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::{Semaphore, SemaphorePermit}; use tokio::sync::{Semaphore, SemaphorePermit};
use tracing::trace; use tracing::trace;
use kanidm_proto::internal::{DomainInfo as ProtoDomainInfo, ImageValue, UiHint};
use crate::be::{Backend, BackendReadTransaction, BackendTransaction, BackendWriteTransaction};
// We use so many, we just import them all ... // We use so many, we just import them all ...
use self::access::{
profiles::{
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
},
AccessControls, AccessControlsReadTransaction, AccessControlsTransaction,
AccessControlsWriteTransaction,
};
use self::keys::{
KeyObject, KeyProvider, KeyProviders, KeyProvidersReadTransaction, KeyProvidersTransaction,
KeyProvidersWriteTransaction,
};
use crate::filter::{ use crate::filter::{
Filter, FilterInvalid, FilterValid, FilterValidResolved, ResolveFilterCache, Filter, FilterInvalid, FilterValid, FilterValidResolved, ResolveFilterCache,
ResolveFilterCacheReadTxn, ResolveFilterCacheReadTxn,
@ -31,19 +41,7 @@ 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 self::access::{
profiles::{
AccessControlCreate, AccessControlDelete, AccessControlModify, AccessControlSearch,
},
AccessControls, AccessControlsReadTransaction, AccessControlsTransaction,
AccessControlsWriteTransaction,
};
use self::keys::{
KeyObject, KeyProvider, KeyProviders, KeyProvidersReadTransaction, KeyProvidersTransaction,
KeyProvidersWriteTransaction,
};
pub(crate) mod access; pub(crate) mod access;
pub mod batch_modify; pub mod batch_modify;
@ -838,6 +836,37 @@ pub trait QueryServerTransaction<'a> {
} }
} }
fn resolve_scim_interim(
&mut self,
scim_value_intermediate: ScimValueIntermediate,
) -> Result<Option<ScimValueKanidm>, OperationError> {
match scim_value_intermediate {
ScimValueIntermediate::Refer(uuid) => {
if let Some(option) = self.uuid_to_spn(uuid)? {
Ok(Some(ScimValueKanidm::EntryReference(ScimReference {
uuid,
value: option.to_proto_string_clone(),
})))
} else {
// TODO: didn't have spn, fallback to uuid.to_string ?
Ok(None)
}
}
ScimValueIntermediate::ReferMany(uuids) => {
let mut scim_references = vec![];
for uuid in uuids {
if let Some(option) = self.uuid_to_spn(uuid)? {
scim_references.push(ScimReference {
uuid,
value: option.to_proto_string_clone(),
})
}
}
Ok(Some(ScimValueKanidm::EntryReferences(scim_references)))
}
}
}
// In the opposite direction, we can resolve values for presentation // In the opposite direction, we can resolve values for presentation
fn resolve_valueset(&mut self, value: &ValueSet) -> Result<Vec<String>, OperationError> { fn resolve_valueset(&mut self, value: &ValueSet) -> Result<Vec<String>, OperationError> {
if let Some(r_set) = value.as_refer_set() { if let Some(r_set) = value.as_refer_set() {
@ -1206,6 +1235,50 @@ impl<'a> QueryServerReadTransaction<'a> {
results results
} }
#[instrument(level = "debug", skip_all)]
pub fn scim_entry_id_get_ext(
&mut self,
uuid: Uuid,
class: EntryClass,
query: ScimEntryGetQuery,
ident: Identity,
) -> Result<ScimEntryKanidm, OperationError> {
let filter_intent = filter!(f_and!([
f_eq(Attribute::Uuid, PartialValue::Uuid(uuid)),
f_eq(Attribute::Class, class.into())
]));
let f_intent_valid = filter_intent
.validate(self.get_schema())
.map_err(OperationError::SchemaViolation)?;
let f_valid = f_intent_valid.clone().into_ignore_hidden();
let r_attrs = query
.attributes
.map(|attr_set| attr_set.into_iter().collect());
let se = SearchEvent {
ident,
filter: f_valid,
filter_orig: f_intent_valid,
attrs: r_attrs,
};
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)
}
}
}
}
} }
impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> { impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
@ -2626,7 +2699,7 @@ mod tests {
// Convert entry into scim // Convert entry into scim
let reduced = entry.as_ref().clone().into_reduced(); let reduced = entry.as_ref().clone().into_reduced();
let scim_entry = reduced.to_scim_kanidm(read_txn).unwrap(); let scim_entry = reduced.to_scim_kanidm(&mut read_txn).unwrap();
// Assert scim entry attributes are as expected // Assert scim entry attributes are as expected
assert_eq!(scim_entry.header.id, UUID_IDM_PEOPLE_SELF_NAME_WRITE); assert_eq!(scim_entry.header.id, UUID_IDM_PEOPLE_SELF_NAME_WRITE);

View file

@ -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 kanidm_proto::scim_v1::ScimEntryGetQuery;
use kanidmd_lib::prelude::{Attribute, BUILTIN_GROUP_IDM_ADMINS_V1}; 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;
@ -180,7 +181,10 @@ async fn test_scim_sync_entry_get(rsclient: KanidmClient) {
// This will be as raw json, not the strongly typed version the server sees // This will be as raw json, not the strongly typed version the server sees
// internally. // internally.
let scim_entry = rsclient.scim_v1_entry_get("demo_account").await.unwrap(); let scim_entry = rsclient
.scim_v1_entry_get("demo_account", None)
.await
.unwrap();
tracing::info!("{:#?}", scim_entry); tracing::info!("{:#?}", scim_entry);
@ -194,4 +198,56 @@ async fn test_scim_sync_entry_get(rsclient: KanidmClient) {
.unwrap(), .unwrap(),
"demo_account".to_string() "demo_account".to_string()
); );
// Limit the attributes we want.
let query = ScimEntryGetQuery {
attributes: Some(vec![Attribute::Name]),
};
let scim_entry = rsclient
.scim_v1_entry_get("demo_account", Some(query))
.await
.unwrap();
tracing::info!("{:#?}", scim_entry);
// Should not be present now.
assert!(!scim_entry.attrs.contains_key(&Attribute::Class));
assert!(scim_entry.attrs.contains_key(&Attribute::Name));
// ==========================================
// Same, but via the Person API
let scim_entry = rsclient
.scim_v1_person_get("demo_account", None)
.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()
);
// Limit the attributes we want.
let query = ScimEntryGetQuery {
attributes: Some(vec![Attribute::Name]),
};
let scim_entry = rsclient
.scim_v1_person_get("demo_account", Some(query))
.await
.unwrap();
tracing::info!("{:#?}", scim_entry);
// Should not be present now.
assert!(!scim_entry.attrs.contains_key(&Attribute::Class));
assert!(scim_entry.attrs.contains_key(&Attribute::Name));
} }

View file

@ -6,9 +6,8 @@ use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, Input, Password, Select}; use dialoguer::{Confirm, Input, Password, Select};
use kanidm_client::ClientError::Http as ClientErrorHttp; use kanidm_client::ClientError::Http as ClientErrorHttp;
use kanidm_client::KanidmClient; use kanidm_client::KanidmClient;
use kanidm_proto::constants::{ use kanidm_proto::attribute::Attribute;
ATTR_ACCOUNT_EXPIRE, ATTR_ACCOUNT_VALID_FROM, ATTR_GIDNUMBER, ATTR_SSH_PUBLICKEY, use kanidm_proto::constants::{ATTR_ACCOUNT_EXPIRE, ATTR_ACCOUNT_VALID_FROM, ATTR_GIDNUMBER};
};
use kanidm_proto::internal::OperationError::{ use kanidm_proto::internal::OperationError::{
DuplicateKey, DuplicateLabel, InvalidLabel, NoMatchingEntries, PasswordQuality, DuplicateKey, DuplicateLabel, InvalidLabel, NoMatchingEntries, PasswordQuality,
}; };
@ -18,6 +17,7 @@ use kanidm_proto::internal::{
}; };
use kanidm_proto::internal::{CredentialDetail, CredentialDetailType}; use kanidm_proto::internal::{CredentialDetail, CredentialDetailType};
use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus}; use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus};
use kanidm_proto::scim_v1::{client::ScimSshPublicKeys, ScimEntryGetQuery};
use qrcode::render::unicode; use qrcode::render::unicode;
use qrcode::QrCode; use qrcode::QrCode;
use time::format_description::well_known::Rfc3339; use time::format_description::well_known::Rfc3339;
@ -233,15 +233,31 @@ impl PersonOpt {
AccountSsh::List(aopt) => { AccountSsh::List(aopt) => {
let client = aopt.copt.to_client(OpType::Read).await; let client = aopt.copt.to_client(OpType::Read).await;
match client let mut entry = match client
.idm_person_account_get_attr( .scim_v1_person_get(
aopt.aopts.account_id.as_str(), aopt.aopts.account_id.as_str(),
ATTR_SSH_PUBLICKEY, Some(ScimEntryGetQuery {
attributes: Some(vec![Attribute::SshPublicKey]),
}),
) )
.await .await
{ {
Ok(pkeys) => pkeys.iter().flatten().for_each(|pkey| println!("{}", pkey)), Ok(entry) => entry,
Err(e) => handle_client_error(e, aopt.copt.output_mode), Err(e) => return handle_client_error(e, aopt.copt.output_mode),
};
let Some(pkeys) = entry.attrs.remove(&Attribute::SshPublicKey) else {
println!("No ssh public keys");
return;
};
let Ok(keys) = serde_json::from_value::<ScimSshPublicKeys>(pkeys) else {
eprintln!("Invalid ssh public key format");
return;
};
for key in keys {
println!("{}: {}", key.label, key.value);
} }
} }
AccountSsh::Add(aopt) => { AccountSsh::Add(aopt) => {