mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Working scim entry get for person (#3088)
This commit is contained in:
parent
50e513b30b
commit
2075125439
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
Loading…
Reference in a new issue