From 15c3bde00ebe55bcef0629c894bc7450aefce8c8 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Sat, 29 Oct 2022 19:07:54 +1000 Subject: [PATCH] Add new scim/sync files (#1152) --- kanidm_book/src/administrivia.md | 53 -- .../designs/scim_migration_planning.md | 4 +- kanidm_client/src/lib.rs | 1 + kanidm_client/src/sync_account.rs | 34 ++ kanidm_proto/src/lib.rs | 1 + kanidm_proto/src/scim_v1.rs | 9 + kanidm_tools/src/cli/badlist.rs | 5 +- kanidmd/core/src/actors/mod.rs | 1 + kanidmd/core/src/actors/v1_scim.rs | 80 +++ kanidmd/core/src/actors/v1_write.rs | 2 +- kanidmd/core/src/https/mod.rs | 72 +-- kanidmd/core/src/https/oauth2.rs | 51 ++ kanidmd/core/src/https/v1_scim.rs | 263 +++++++++ kanidmd/lib-macros/src/entry.rs | 2 +- kanidmd/lib-macros/src/lib.rs | 4 +- kanidmd/lib/src/access.rs | 37 +- kanidmd/lib/src/be/dbvalue.rs | 2 + kanidmd/lib/src/constants/acp.rs | 48 ++ kanidmd/lib/src/constants/entries.rs | 16 + kanidmd/lib/src/constants/schema.rs | 92 ++++ kanidmd/lib/src/constants/uuids.rs | 9 + kanidmd/lib/src/constants/values.rs | 1 + kanidmd/lib/src/credential/mod.rs | 6 +- kanidmd/lib/src/identity.rs | 11 +- kanidmd/lib/src/idm/account.rs | 8 +- kanidmd/lib/src/idm/authsession.rs | 2 +- kanidmd/lib/src/idm/credupdatesession.rs | 2 +- kanidmd/lib/src/idm/group.rs | 9 +- kanidmd/lib/src/idm/mod.rs | 1 + kanidmd/lib/src/idm/scim.rs | 500 ++++++++++++++++++ kanidmd/lib/src/idm/server.rs | 100 +++- kanidmd/lib/src/idm/serviceaccount.rs | 6 +- kanidmd/lib/src/ldap.rs | 2 +- kanidmd/lib/src/lib.rs | 4 +- kanidmd/lib/src/macros.rs | 4 +- kanidmd/lib/src/modify.rs | 3 +- kanidmd/lib/src/plugins/dyngroup.rs | 10 +- kanidmd/lib/src/plugins/jwskeygen.rs | 4 +- kanidmd/lib/src/server.rs | 10 +- kanidmd/lib/src/valueset/jws.rs | 4 +- kanidmd/lib/src/valueset/mod.rs | 4 +- kanidmd/lib/src/valueset/session.rs | 2 + kanidmd/testkit-macros/src/entry.rs | 6 +- kanidmd/testkit-macros/src/lib.rs | 2 +- kanidmd/testkit/tests/scim_test.rs | 43 ++ orca/src/preprocess.rs | 2 +- 46 files changed, 1360 insertions(+), 172 deletions(-) create mode 100644 kanidm_client/src/sync_account.rs create mode 100644 kanidm_proto/src/scim_v1.rs create mode 100644 kanidmd/core/src/actors/v1_scim.rs create mode 100644 kanidmd/core/src/https/v1_scim.rs create mode 100644 kanidmd/lib/src/idm/scim.rs create mode 100644 kanidmd/testkit/tests/scim_test.rs diff --git a/kanidm_book/src/administrivia.md b/kanidm_book/src/administrivia.md index ea443b48f..27a3b3b14 100644 --- a/kanidm_book/src/administrivia.md +++ b/kanidm_book/src/administrivia.md @@ -5,56 +5,3 @@ a Kanidm server, such as making backups and restoring from backups, testing server configuration, reindexing, verifying data consistency, and renaming your domain. -# Rename the domain - -There are some cases where you may need to rename the domain. You should have configured -this initially in the setup, however you may have a situation where a business is changing -name, merging, or other needs which may prompt this needing to be changed. - -> **WARNING:** This WILL break ALL u2f/webauthn tokens that have been enrolled, which MAY cause -> accounts to be locked out and unrecoverable until further action is taken. DO NOT CHANGE -> the domain name unless REQUIRED and have a plan on how to manage these issues. - -> **WARNING:** This operation can take an extensive amount of time as ALL accounts and groups -> in the domain MUST have their Security Principal Names (SPNs) regenerated. This WILL also cause -> a large delay in replication once the system is restarted. - -You should make a backup before proceeding with this operation. - -When you have a created a migration plan and strategy on handling the invalidation of webauthn, -you can then rename the domain. - -First, stop the instance. - - docker stop - -Second, change `domain` and `origin` in `server.toml`. - -Third, trigger the database domain rename process. - - docker run --rm -i -t -v kanidmd:/data \ - kanidm/server:latest /sbin/kanidmd domain rename -c /data/server.toml - -Finally, you can now start your instance again. - - docker start - -# Raw actions - -The server has a low-level stateful API you can use for more complex or advanced tasks on large numbers -of entries at once. Some examples are below, but generally we advise you to use the APIs as listed -above. - - # Create from json (group or account) - kanidm raw create -H https://localhost:8443 -C ../insecure/ca.pem -D admin example.create.account.json - kanidm raw create -H https://localhost:8443 -C ../insecure/ca.pem -D idm_admin example.create.group.json - - # Apply a json stateful modification to all entries matching a filter - kanidm raw modify -H https://localhost:8443 -C ../insecure/ca.pem -D admin '{"or": [ {"eq": ["name", "idm_person_account_create_priv"]}, {"eq": ["name", "idm_service_account_create_priv"]}, {"eq": ["name", "idm_account_write_priv"]}, {"eq": ["name", "idm_group_write_priv"]}, {"eq": ["name", "idm_people_write_priv"]}, {"eq": ["name", "idm_group_create_priv"]} ]}' example.modify.idm_admin.json - kanidm raw modify -H https://localhost:8443 -C ../insecure/ca.pem -D idm_admin '{"eq": ["name", "idm_admins"]}' example.modify.idm_admin.json - - # Search and show the database representations - kanidm raw search -H https://localhost:8443 -C ../insecure/ca.pem -D admin '{"eq": ["name", "idm_admin"]}' - - # Delete all entries matching a filter - kanidm raw delete -H https://localhost:8443 -C ../insecure/ca.pem -D idm_admin '{"eq": ["name", "test_account_delete_me"]}' diff --git a/kanidm_book/src/developers/designs/scim_migration_planning.md b/kanidm_book/src/developers/designs/scim_migration_planning.md index b9b168eac..1e1255f1d 100644 --- a/kanidm_book/src/developers/designs/scim_migration_planning.md +++ b/kanidm_book/src/developers/designs/scim_migration_planning.md @@ -31,7 +31,7 @@ In these processes there may be a need to "reset" the synchronsied data. The dia ├───────────────────────┬─────────────────────┐ │ │ │ ┌─────────────┐ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ │ │ │ │───┘ │ │ │ │ - │ │ Initial │ │ │ Partial │ │ │ Final │ │ + │ │ Initial │ │ │ Active │ │ │ Final │ │ └─▶│ Synchronise │──────┴──▶│ Synchronise │───────┴──▶│ Synchronise │──┤ │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ @@ -46,7 +46,7 @@ In these processes there may be a need to "reset" the synchronsied data. The dia Kanidm starts in a "detached" state from the extern IDM source. For Kanidm as a "read only" application source the Initial synchronisation is performed followed by periodic -partial synchronisations. At anytime a full initial synchronisation can re-occur to reset the data of the +active (partial) synchronisations. At anytime a full initial synchronisation can re-occur to reset the data of the provider. The provider can be reset and removed by a purge which reset's Kanidm to a detached state. For a gradual migration, this process is the same as the read only application. However when ready diff --git a/kanidm_client/src/lib.rs b/kanidm_client/src/lib.rs index e2444ae10..dfc0ec11d 100644 --- a/kanidm_client/src/lib.rs +++ b/kanidm_client/src/lib.rs @@ -39,6 +39,7 @@ use webauthn_rs_proto::{ mod person; mod service_account; +mod sync_account; mod system; pub const APPLICATION_JSON: &str = "application/json"; diff --git a/kanidm_client/src/sync_account.rs b/kanidm_client/src/sync_account.rs new file mode 100644 index 000000000..ccec69091 --- /dev/null +++ b/kanidm_client/src/sync_account.rs @@ -0,0 +1,34 @@ +use crate::{ClientError, KanidmClient}; +use kanidm_proto::v1::Entry; +use std::collections::BTreeMap; + +impl KanidmClient { + pub async fn idm_sync_account_list(&self) -> Result, ClientError> { + self.perform_get_request("/v1/sync_account").await + } + + pub async fn idm_sync_account_get(&self, id: &str) -> Result, ClientError> { + self.perform_get_request(format!("/v1/sync_account/{}", id).as_str()) + .await + } + + pub async fn idm_sync_account_create( + &self, + name: &str, + description: Option<&str>, + ) -> Result<(), ClientError> { + let mut new_acct = Entry { + attrs: BTreeMap::new(), + }; + new_acct + .attrs + .insert("name".to_string(), vec![name.to_string()]); + if let Some(description) = description { + new_acct + .attrs + .insert("description".to_string(), vec![description.to_string()]); + } + self.perform_post_request("/v1/sync_account", new_acct) + .await + } +} diff --git a/kanidm_proto/src/lib.rs b/kanidm_proto/src/lib.rs index 936a166c0..1c6ebe6d9 100644 --- a/kanidm_proto/src/lib.rs +++ b/kanidm_proto/src/lib.rs @@ -11,6 +11,7 @@ pub mod constants; pub mod messages; pub mod oauth2; +pub mod scim_v1; pub mod utils; pub mod v1; diff --git a/kanidm_proto/src/scim_v1.rs b/kanidm_proto/src/scim_v1.rs new file mode 100644 index 000000000..6e4a0eefb --- /dev/null +++ b/kanidm_proto/src/scim_v1.rs @@ -0,0 +1,9 @@ +use base64urlsafedata::Base64UrlSafeData; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub enum ScimSyncState { + Initial, + Active { cookie: Base64UrlSafeData }, +} diff --git a/kanidm_tools/src/cli/badlist.rs b/kanidm_tools/src/cli/badlist.rs index 5fe586b77..7fb083741 100644 --- a/kanidm_tools/src/cli/badlist.rs +++ b/kanidm_tools/src/cli/badlist.rs @@ -101,13 +101,12 @@ impl PwBadlistOpt { let results = task_handles.join().await; - let results: Vec<_> = results + let filt_pwset: Vec<_> = results .into_iter() .map(|res| res.expect("Thread join failure")) + .flatten() .collect(); - let filt_pwset: Vec = results.into_iter().flatten().collect(); - info!( "{} passwords passed zxcvbn, uploading ...", filt_pwset.len() diff --git a/kanidmd/core/src/actors/mod.rs b/kanidmd/core/src/actors/mod.rs index b52b81738..64d5b59a1 100644 --- a/kanidmd/core/src/actors/mod.rs +++ b/kanidmd/core/src/actors/mod.rs @@ -3,4 +3,5 @@ //! if they are read or write transactions internally. pub mod v1_read; +pub mod v1_scim; pub mod v1_write; diff --git a/kanidmd/core/src/actors/v1_scim.rs b/kanidmd/core/src/actors/v1_scim.rs new file mode 100644 index 000000000..4c140ba55 --- /dev/null +++ b/kanidmd/core/src/actors/v1_scim.rs @@ -0,0 +1,80 @@ +use kanidmd_lib::prelude::*; + +use crate::QueryServerWriteV1; +use kanidmd_lib::idm::scim::GenerateScimSyncTokenEvent; +use kanidmd_lib::idm::server::IdmServerTransaction; + +impl QueryServerWriteV1 { + #[instrument( + level = "info", + skip_all, + fields(uuid = ?eventid) + )] + pub async fn handle_sync_account_token_generate( + &self, + uat: Option, + uuid_or_name: String, + label: String, + eventid: Uuid, + ) -> Result { + let ct = duration_from_epoch_now(); + let mut idms_prox_write = self.idms.proxy_write(ct).await; + let ident = idms_prox_write + .validate_and_parse_token_to_ident(uat.as_deref(), ct) + .map_err(|e| { + admin_error!(err = ?e, "Invalid identity"); + e + })?; + + let target = idms_prox_write + .qs_write + .name_to_uuid(uuid_or_name.as_str()) + .map_err(|e| { + admin_error!(err = ?e, "Error resolving id to target"); + e + })?; + + let gte = GenerateScimSyncTokenEvent { + ident, + target, + label, + }; + + idms_prox_write + .scim_sync_generate_token(>e, ct) + .and_then(|r| idms_prox_write.commit().map(|_| r)) + } + + #[instrument( + level = "info", + skip_all, + fields(uuid = ?eventid) + )] + pub async fn handle_sync_account_token_destroy( + &self, + uat: Option, + uuid_or_name: String, + eventid: Uuid, + ) -> Result<(), OperationError> { + let ct = duration_from_epoch_now(); + let mut idms_prox_write = self.idms.proxy_write(ct).await; + let ident = idms_prox_write + .validate_and_parse_token_to_ident(uat.as_deref(), ct) + .map_err(|e| { + admin_error!(err = ?e, "Invalid identity"); + e + })?; + + let target = idms_prox_write + .qs_write + .name_to_uuid(uuid_or_name.as_str()) + .map_err(|e| { + admin_error!(err = ?e, "Error resolving id to target"); + e + })?; + + idms_prox_write + .sync_account_destroy_token(&ident, target, ct) + .and_then(|r| idms_prox_write.commit().map(|_| r)) + } +} diff --git a/kanidmd/core/src/actors/v1_write.rs b/kanidmd/core/src/actors/v1_write.rs index b5213350f..67e56e86b 100644 --- a/kanidmd/core/src/actors/v1_write.rs +++ b/kanidmd/core/src/actors/v1_write.rs @@ -34,7 +34,7 @@ use kanidmd_lib::{ use kanidmd_lib::prelude::*; pub struct QueryServerWriteV1 { - idms: Arc, + pub(crate) idms: Arc, } impl QueryServerWriteV1 { diff --git a/kanidmd/core/src/https/mod.rs b/kanidmd/core/src/https/mod.rs index 81010d0b2..05b56b41c 100644 --- a/kanidmd/core/src/https/mod.rs +++ b/kanidmd/core/src/https/mod.rs @@ -3,6 +3,7 @@ pub mod middleware; mod oauth2; mod routemaps; mod v1; +mod v1_scim; use std::fs::canonicalize; use std::path::PathBuf; @@ -22,6 +23,7 @@ use self::middleware::*; use self::oauth2::*; use self::routemaps::{RouteMap, RouteMaps}; use self::v1::*; +use self::v1_scim::*; use crate::actors::v1_read::QueryServerReadV1; use crate::actors::v1_write::QueryServerWriteV1; use crate::config::{ServerRole, TlsConfiguration}; @@ -70,6 +72,8 @@ pub struct AppState { pub trait RequestExtensions { fn get_current_uat(&self) -> Option; + fn get_auth_bearer(&self) -> Option<&str>; + fn get_current_auth_session_id(&self) -> Option; fn get_url_param(&self, param: &str) -> Result; @@ -80,6 +84,21 @@ pub trait RequestExtensions { } impl RequestExtensions for tide::Request { + fn get_auth_bearer(&self) -> Option<&str> { + // Contact the QS to get it to validate wtf is up. + // let kref = &self.state().bundy_handle; + // self.session().get::("uat") + self.header(tide::http::headers::AUTHORIZATION) + .and_then(|hv| { + // Get the first header value. + hv.get(0) + }) + .and_then(|h| { + // Turn it to a &str, and then check the prefix + h.as_str().strip_prefix("Bearer ") + }) + } + fn get_current_uat(&self) -> Option { // Contact the QS to get it to validate wtf is up. // let kref = &self.state().bundy_handle; @@ -474,55 +493,12 @@ pub fn create_https_server( appserver .at("/status") .mapped_get(&mut routemap, self::status); + // == oauth endpoints. + oauth2_route_setup(&mut appserver, &mut routemap); - let mut oauth2_process = appserver.at("/oauth2"); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/authorise") - .mapped_post(&mut routemap, oauth2_authorise_post) - .mapped_get(&mut routemap, oauth2_authorise_get); - - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/authorise/permit") - .mapped_post(&mut routemap, oauth2_authorise_permit_post) - .mapped_get(&mut routemap, oauth2_authorise_permit_get); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/authorise/reject") - .mapped_post(&mut routemap, oauth2_authorise_reject_post) - .mapped_get(&mut routemap, oauth2_authorise_reject_get); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/token") - .mapped_post(&mut routemap, oauth2_token_post); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - oauth2_process - .at("/token/introspect") - .mapped_post(&mut routemap, oauth2_token_introspect_post); - - let mut openid_process = appserver.at("/oauth2/openid"); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - openid_process - .at("/:client_id/.well-known/openid-configuration") - .mapped_get(&mut routemap, oauth2_openid_discovery_get); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - openid_process - .at("/:client_id/userinfo") - .mapped_get(&mut routemap, oauth2_openid_userinfo_get); - // ⚠️ ⚠️ WARNING ⚠️ ⚠️ - // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS - openid_process - .at("/:client_id/public_key.jwk") - .mapped_get(&mut routemap, oauth2_openid_publickey_get); + // == scim endpoints. + scim_route_setup(&mut appserver, &mut routemap); let mut raw_route = appserver.at("/v1/raw"); raw_route.at("/create").mapped_post(&mut routemap, create); @@ -741,8 +717,6 @@ pub fn create_https_server( .at("/:id/_unix") .mapped_post(&mut routemap, account_post_id_unix); - // TODO: Apis for token management - // Shared account features only - mainly this is for unix-like // features. let mut account_route = appserver.at("/v1/account"); diff --git a/kanidmd/core/src/https/oauth2.rs b/kanidmd/core/src/https/oauth2.rs index 27e835915..a2db70c5d 100644 --- a/kanidmd/core/src/https/oauth2.rs +++ b/kanidmd/core/src/https/oauth2.rs @@ -7,6 +7,7 @@ use kanidmd_lib::idm::oauth2::{ use kanidmd_lib::prelude::*; use serde::{Deserialize, Serialize}; +use super::routemaps::{RouteMap, RouteMaps}; use super::v1::{json_rest_event_get, json_rest_event_post}; use super::{to_tide_response, AppState, RequestExtensions}; @@ -717,3 +718,53 @@ pub async fn oauth2_token_introspect_post(mut req: tide::Request) -> t res }) } + +pub fn oauth2_route_setup(appserver: &mut tide::Route<'_, AppState>, routemap: &mut RouteMap) { + let mut oauth2_process = appserver.at("/oauth2"); + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + oauth2_process + .at("/authorise") + .mapped_post(routemap, oauth2_authorise_post) + .mapped_get(routemap, oauth2_authorise_get); + + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + oauth2_process + .at("/authorise/permit") + .mapped_post(routemap, oauth2_authorise_permit_post) + .mapped_get(routemap, oauth2_authorise_permit_get); + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + oauth2_process + .at("/authorise/reject") + .mapped_post(routemap, oauth2_authorise_reject_post) + .mapped_get(routemap, oauth2_authorise_reject_get); + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + oauth2_process + .at("/token") + .mapped_post(routemap, oauth2_token_post); + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + oauth2_process + .at("/token/introspect") + .mapped_post(routemap, oauth2_token_introspect_post); + + let mut openid_process = appserver.at("/oauth2/openid"); + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + openid_process + .at("/:client_id/.well-known/openid-configuration") + .mapped_get(routemap, oauth2_openid_discovery_get); + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + openid_process + .at("/:client_id/userinfo") + .mapped_get(routemap, oauth2_openid_userinfo_get); + // ⚠️ ⚠️ WARNING ⚠️ ⚠️ + // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS + openid_process + .at("/:client_id/public_key.jwk") + .mapped_get(routemap, oauth2_openid_publickey_get); +} diff --git a/kanidmd/core/src/https/v1_scim.rs b/kanidmd/core/src/https/v1_scim.rs new file mode 100644 index 000000000..243003f62 --- /dev/null +++ b/kanidmd/core/src/https/v1_scim.rs @@ -0,0 +1,263 @@ +use super::routemaps::{RouteMap, RouteMaps}; +use super::{to_tide_response, AppState, RequestExtensions}; +use kanidm_proto::v1::Entry as ProtoEntry; +use kanidmd_lib::prelude::*; + +use super::v1::{ + json_rest_event_delete_id, json_rest_event_get, json_rest_event_get_id, json_rest_event_post, +}; + +pub async fn sync_account_get(req: tide::Request) -> tide::Result { + let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); + json_rest_event_get(req, filter, None).await +} + +pub async fn sync_account_post(req: tide::Request) -> tide::Result { + let classes = vec!["sync_account".to_string(), "object".to_string()]; + json_rest_event_post(req, classes).await +} + +pub async fn sync_account_id_get(req: tide::Request) -> tide::Result { + let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); + json_rest_event_get_id(req, filter, None).await +} + +pub async fn sync_account_id_delete(req: tide::Request) -> tide::Result { + let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); + json_rest_event_delete_id(req, filter).await +} + +pub async fn sync_account_id_patch(mut req: tide::Request) -> tide::Result { + // Update a value / attrs + let uat = req.get_current_uat(); + let id = req.get_url_param("id")?; + + let obj: ProtoEntry = req.body_json().await?; + + let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account"))); + let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str()))); + + let (eventid, hvalue) = req.new_eventid(); + + let res = req + .state() + .qe_w_ref + .handle_internalpatch(uat, filter, obj, eventid) + .await; + to_tide_response(res, hvalue) +} + +/* +pub async fn sync_account_token_get(req: tide::Request) -> tide::Result { + let uat = req.get_current_uat(); + let uuid_or_name = req.get_url_param("id")?; + + let (eventid, hvalue) = req.new_eventid(); + + let res = req + .state() + .qe_r_ref + .handle_service_account_api_token_get(uat, uuid_or_name, eventid) + .await; + to_tide_response(res, hvalue) +} +*/ + +pub async fn sync_account_token_post(mut req: tide::Request) -> tide::Result { + let uat = req.get_current_uat(); + let uuid_or_name = req.get_url_param("id")?; + + let label: String = req.body_json().await?; + + let (eventid, hvalue) = req.new_eventid(); + + let res = req + .state() + .qe_w_ref + .handle_sync_account_token_generate(uat, uuid_or_name, label, eventid) + .await; + to_tide_response(res, hvalue) +} + +pub async fn sync_account_token_delete(req: tide::Request) -> tide::Result { + let uat = req.get_current_uat(); + let uuid_or_name = req.get_url_param("id")?; + + let (eventid, hvalue) = req.new_eventid(); + + let res = req + .state() + .qe_w_ref + .handle_sync_account_token_destroy(uat, uuid_or_name, eventid) + .await; + to_tide_response(res, hvalue) +} + +async fn scim_sync_post(_req: tide::Request) -> tide::Result { + // let (eventid, hvalue) = req.new_eventid(); + /* + let ApiTokenGenerate { + label, + expiry, + read_write, + } = req.body_json().await?; + */ + + // We need to deserialise the body. + + Ok(tide::Response::new(500)) +} + +async fn scim_sync_get(_req: tide::Request) -> tide::Result { + // let (eventid, hvalue) = req.new_eventid(); + + // let bearer = req.get_auth_bearer(); + + // Given the token + // What is the connected sync session + // Issue it's current state (version) cookie. + + // todo!(); + Ok(tide::Response::new(500)) +} + +async fn scim_sink_get(req: tide::Request) -> tide::Result { + let (_, hvalue) = req.new_eventid(); + let mut res = tide::Response::new(200); + + res.insert_header("X-KANIDM-OPID", hvalue); + res.set_content_type("text/html;charset=utf-8"); + + res.set_body( + r#" + + + + + + + Sink! + + + + +
+                    ___
+                  .' _ '.
+                 / /` `\ \
+                 | |   [__]
+                 | |    {{
+                 | |    }}
+              _  | |  _ {{
+  ___________<_>_| |_<_>}}________
+      .=======^=(___)=^={{====.
+     / .----------------}}---. \
+    / /                 {{    \ \
+   / /                  }}     \ \
+  (  '========================='  )
+   '-----------------------------'
+        
+ +"#, + ); + + Ok(res) +} + +pub fn scim_route_setup(appserver: &mut tide::Route<'_, AppState>, routemap: &mut RouteMap) { + let mut scim_process = appserver.at("/scim/v1"); + + // https://datatracker.ietf.org/doc/html/rfc7644#section-3.2 + // + // HTTP SCIM Usage + // Method + // ------ -------------------------------------------------------------- + // GET Retrieves one or more complete or partial resources. + // + // POST Depending on the endpoint, creates new resources, creates a + // search request, or MAY be used to bulk-modify resources. + // + // PUT Modifies a resource by replacing existing attributes with a + // specified set of replacement attributes (replace). PUT + // MUST NOT be used to create new resources. + // + // PATCH Modifies a resource with a set of client-specified changes + // (partial update). + // + // DELETE Deletes a resource. + // + // Resource Endpoint Operations Description + // -------- ---------------- ---------------------- -------------------- + // User /Users GET (Section 3.4.1), Retrieve, add, + // POST (Section 3.3), modify Users. + // PUT (Section 3.5.1), + // PATCH (Section 3.5.2), + // DELETE (Section 3.6) + // + // Group /Groups GET (Section 3.4.1), Retrieve, add, + // POST (Section 3.3), modify Groups. + // PUT (Section 3.5.1), + // PATCH (Section 3.5.2), + // DELETE (Section 3.6) + // + // Self /Me GET, POST, PUT, PATCH, Alias for operations + // DELETE (Section 3.11) against a resource + // mapped to an + // authenticated + // subject (e.g., + // User). + // + // Service /ServiceProvider GET (Section 4) Retrieve service + // provider Config provider's + // config. configuration. + // + // Resource /ResourceTypes GET (Section 4) Retrieve supported + // type resource types. + // + // Schema /Schemas GET (Section 4) Retrieve one or more + // supported schemas. + // + // Bulk /Bulk POST (Section 3.7) Bulk updates to one + // or more resources. + // + // Search [prefix]/.search POST (Section 3.4.3) Search from system + // root or within a + // resource endpoint + // for one or more + // resource types using + // POST. + // -- Kanidm Resources + // + // Sync /Sync GET Retrieve the current + // sync state associated + // with the authenticated + // session + // + // POST Send a sync update + // + + scim_process + .at("/Sync") + .mapped_post(routemap, scim_sync_post) + .mapped_get(routemap, scim_sync_get); + + scim_process.at("/Sink").mapped_get(routemap, scim_sink_get); + + let mut sync_account_route = appserver.at("/v1/sync_account"); + sync_account_route + .at("/") + .mapped_get(routemap, sync_account_get) + .mapped_post(routemap, sync_account_post); + + sync_account_route + .at("/:id") + .mapped_get(routemap, sync_account_id_get) + .mapped_patch(routemap, sync_account_id_patch) + .mapped_delete(routemap, sync_account_id_delete); + + sync_account_route + .at("/:id/_sync_token") + // .mapped_get(&mut routemap, sync_account_token_get) + .mapped_post(routemap, sync_account_token_post) + .mapped_delete(routemap, sync_account_token_delete); +} diff --git a/kanidmd/lib-macros/src/entry.rs b/kanidmd/lib-macros/src/entry.rs index 7ed1d8105..df3d4f2ab 100644 --- a/kanidmd/lib-macros/src/entry.rs +++ b/kanidmd/lib-macros/src/entry.rs @@ -8,7 +8,7 @@ fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenS tokens } -pub(crate) fn qs_test(_args: TokenStream, item: TokenStream, with_init: bool) -> TokenStream { +pub(crate) fn qs_test(_args: &TokenStream, item: TokenStream, with_init: bool) -> TokenStream { let input: syn::ItemFn = match syn::parse(item.clone()) { Ok(it) => it, Err(e) => return token_stream_with_error(item, e), diff --git a/kanidmd/lib-macros/src/lib.rs b/kanidmd/lib-macros/src/lib.rs index a36607def..5f495f7ce 100644 --- a/kanidmd/lib-macros/src/lib.rs +++ b/kanidmd/lib-macros/src/lib.rs @@ -19,10 +19,10 @@ use proc_macro::TokenStream; #[proc_macro_attribute] pub fn qs_test(args: TokenStream, item: TokenStream) -> TokenStream { - entry::qs_test(args, item, true) + entry::qs_test(&args, item, true) } #[proc_macro_attribute] pub fn qs_test_no_init(args: TokenStream, item: TokenStream) -> TokenStream { - entry::qs_test(args, item, false) + entry::qs_test(&args, item, false) } diff --git a/kanidmd/lib/src/access.rs b/kanidmd/lib/src/access.rs index 981019ad0..20f6cf92b 100644 --- a/kanidmd/lib/src/access.rs +++ b/kanidmd/lib/src/access.rs @@ -456,12 +456,17 @@ pub trait AccessControlsTransaction<'a> { // A possible solution is to change the filter resolve function // such that it takes an entry, rather than an event, but that // would create issues in search. - match (&acs.acp.receiver).resolve(ident, None, Some(acp_resolve_filter_cache)) { + match acs + .acp + .receiver + .resolve(ident, None, Some(acp_resolve_filter_cache)) + { Ok(f_res) => { if rec_entry.entry_match_no_index(&f_res) { // Now, for each of the acp's that apply to our receiver, resolve their // related target filters. - (&acs.acp.targetscope) + acs.acp + .targetscope .resolve(ident, None, Some(acp_resolve_filter_cache)) .map_err(|e| { admin_error!( @@ -509,6 +514,10 @@ pub trait AccessControlsTransaction<'a> { // No need to check ACS return Ok(entries); } + IdentType::Synch(_) => { + security_critical!("Blocking sync check"); + return Err(OperationError::InvalidState); + } IdentType::User(u) => &u.entry, }; info!(event = %se.ident, "Access check for search (filter) event"); @@ -612,6 +621,10 @@ pub trait AccessControlsTransaction<'a> { return Ok(Vec::new()); } } + IdentType::Synch(_) => { + security_critical!("Blocking sync check"); + return Err(OperationError::InvalidState); + } IdentType::User(u) => &u.entry, }; @@ -724,10 +737,10 @@ pub trait AccessControlsTransaction<'a> { modify_state .iter() .filter_map(|acs| { - match (&acs.acp.receiver).resolve(ident, None, Some(acp_resolve_filter_cache)) { + match acs.acp.receiver.resolve(ident, None, Some(acp_resolve_filter_cache)) { Ok(f_res) => { if rec_entry.entry_match_no_index(&f_res) { - (&acs.acp.targetscope) + acs.acp.targetscope .resolve(ident, None, Some(acp_resolve_filter_cache)) .map_err(|e| { admin_error!( @@ -769,6 +782,10 @@ pub trait AccessControlsTransaction<'a> { // No need to check ACS return Ok(true); } + IdentType::Synch(_) => { + security_critical!("Blocking sync check"); + return Err(OperationError::InvalidState); + } IdentType::User(u) => &u.entry, }; info!(event = %me.ident, "Access check for modify event"); @@ -940,6 +957,10 @@ pub trait AccessControlsTransaction<'a> { // No need to check ACS return Ok(true); } + IdentType::Synch(_) => { + security_critical!("Blocking sync check"); + return Err(OperationError::InvalidState); + } IdentType::User(u) => &u.entry, }; info!(event = %ce.ident, "Access check for create event"); @@ -1081,6 +1102,10 @@ pub trait AccessControlsTransaction<'a> { // No need to check ACS return Ok(true); } + IdentType::Synch(_) => { + security_critical!("Blocking sync check"); + return Err(OperationError::InvalidState); + } IdentType::User(u) => &u.entry, }; info!(event = %de.ident, "Access check for delete event"); @@ -1194,6 +1219,10 @@ pub trait AccessControlsTransaction<'a> { // No need to check ACS return Err(OperationError::InvalidState); } + IdentType::Synch(_) => { + security_critical!("Blocking sync check"); + return Err(OperationError::InvalidState); + } IdentType::User(u) => &u.entry, }; diff --git a/kanidmd/lib/src/be/dbvalue.rs b/kanidmd/lib/src/be/dbvalue.rs index 4614413ee..271b1b5a5 100644 --- a/kanidmd/lib/src/be/dbvalue.rs +++ b/kanidmd/lib/src/be/dbvalue.rs @@ -377,6 +377,8 @@ pub enum DbValueIdentityId { V1Internal, #[serde(rename = "v1u")] V1Uuid(Uuid), + #[serde(rename = "v1s")] + V1Sync(Uuid), } #[derive(Serialize, Deserialize, Debug)] diff --git a/kanidmd/lib/src/constants/acp.rs b/kanidmd/lib/src/constants/acp.rs index 7ba437b60..0935d85e4 100644 --- a/kanidmd/lib/src/constants/acp.rs +++ b/kanidmd/lib/src/constants/acp.rs @@ -1290,3 +1290,51 @@ pub const JSON_IDM_ACP_OAUTH2_READ_PRIV_V1: &str = r#"{ ] } }"#; + +pub const JSON_IDM_HP_ACP_SYNC_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{ + "attrs": { + "class": [ + "object", + "access_control_profile", + "access_control_search", + "access_control_modify", + "access_control_delete", + "access_control_create" + ], + "name": ["idm_acp_hp_sync_account_manage_priv"], + "uuid": ["00000000-0000-0000-0000-ffffff000044"], + "description": ["Builtin IDM Control for managing IDM synchronisation accounts / connections"], + "acp_receiver": [ + "{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000037\"]}" + ], + "acp_targetscope": [ + "{\"and\": [{\"eq\": [\"class\",\"sync_account\"]},{\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}" + ], + "acp_search_attr": [ + "class", + "name", + "description", + "jws_es256_private_key", + "sync_token_session", + "sync_cookie" + ], + "acp_modify_removedattr": [ + "name", + "description", + "jws_es256_private_key", + "sync_token_session", + "sync_cookie" + ], + "acp_modify_presentattr": [ + "name", + "description" + ], + "acp_modify_class": [], + "acp_create_attr": [ + "class", + "name", + "description" + ], + "acp_create_class": ["sync_account", "object"] + } +}"#; diff --git a/kanidmd/lib/src/constants/entries.rs b/kanidmd/lib/src/constants/entries.rs index dba8adfcb..d237b7ff0 100644 --- a/kanidmd/lib/src/constants/entries.rs +++ b/kanidmd/lib/src/constants/entries.rs @@ -438,6 +438,21 @@ pub const JSON_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV: &str = r#"{ } }"#; +/// Builtin System Admin account. +pub const JSON_IDM_HP_SYNC_ACCOUNT_MANAGE_PRIV: &str = r#"{ + "attrs": { + "class": ["group", "object"], + "name": ["idm_hp_sync_account_manage_priv"], + "uuid": ["00000000-0000-0000-0000-000000000037"], + "description": ["Builtin IDM Group for managing sychronisation from external identity sources"], + "member": [ + "00000000-0000-0000-0000-000000000019" + ] + } +}"#; + +// == dyn groups + pub const JSON_IDM_ALL_PERSONS: &str = r#"{ "attrs": { "class": ["dyngroup", "group", "object"], @@ -497,6 +512,7 @@ pub const JSON_IDM_HIGH_PRIVILEGE_V1: &str = r#"{ "00000000-0000-0000-0000-000000000031", "00000000-0000-0000-0000-000000000032", "00000000-0000-0000-0000-000000000034", + "00000000-0000-0000-0000-000000000037", "00000000-0000-0000-0000-000000001000" ] } diff --git a/kanidmd/lib/src/constants/schema.rs b/kanidmd/lib/src/constants/schema.rs index e8ec4e070..7cef20b4b 100644 --- a/kanidmd/lib/src/constants/schema.rs +++ b/kanidmd/lib/src/constants/schema.rs @@ -1171,6 +1171,66 @@ pub const JSON_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: &str = r#"{ } }"#; +pub const JSON_SCHEMA_ATTR_SYNC_TOKEN_SESSION: &str = r#"{ + "attrs": { + "class": [ + "object", + "system", + "attributetype" + ], + "description": [ + "A session entry related to an issued sync token" + ], + "index": [ + "EQUALITY" + ], + "unique": [ + "true" + ], + "multivalue": [ + "false" + ], + "attributename": [ + "sync_token_session" + ], + "syntax": [ + "SESSION" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000115" + ] + } +}"#; + +pub const JSON_SCHEMA_ATTR_SYNC_COOKIE: &str = r#"{ + "attrs": { + "class": [ + "object", + "system", + "attributetype" + ], + "description": [ + "A private sync cookie for a remote IDM source" + ], + "index": [], + "unique": [ + "false" + ], + "multivalue": [ + "false" + ], + "attributename": [ + "sync_cookie" + ], + "syntax": [ + "PRIVATE_BINARY" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000116" + ] + } +}"#; + // === classes === pub const JSON_SCHEMA_CLASS_PERSON: &str = r#" @@ -1359,6 +1419,38 @@ pub const JSON_SCHEMA_CLASS_SERVICE_ACCOUNT: &str = r#" } "#; +pub const JSON_SCHEMA_CLASS_SYNC_ACCOUNT: &str = r#" + { + "attrs": { + "class": [ + "object", + "system", + "classtype" + ], + "description": [ + "Object representation of sync account" + ], + "classname": [ + "sync_account" + ], + "systemmust": [ + "name", + "jws_es256_private_key" + ], + "systemmay": [ + "sync_token_session", + "sync_cookie" + ], + "uuid": [ + "00000000-0000-0000-0000-ffff00000114" + ], + "systemexcludes": [ + "account" + ] + } + } +"#; + // domain_info type // domain_uuid // domain_name <- should be the dns name? diff --git a/kanidmd/lib/src/constants/uuids.rs b/kanidmd/lib/src/constants/uuids.rs index ff35975c3..7006854dd 100644 --- a/kanidmd/lib/src/constants/uuids.rs +++ b/kanidmd/lib/src/constants/uuids.rs @@ -49,6 +49,9 @@ pub const _UUID_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV: Uuid = pub const UUID_IDM_ALL_PERSONS: Uuid = uuid!("00000000-0000-0000-0000-000000000035"); pub const UUID_IDM_ALL_ACCOUNTS: Uuid = uuid!("00000000-0000-0000-0000-000000000036"); +pub const _UUID_IDM_HP_SYNC_ACCOUNT_MANAGE_PRIV: Uuid = + uuid!("00000000-0000-0000-0000-000000000037"); + // pub const _UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000"); @@ -194,6 +197,10 @@ pub const _UUID_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP: Uuid = uuid!("00000000-0000-0000-0000-ffff00000112"); pub const _UUID_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: Uuid = uuid!("00000000-0000-0000-0000-ffff00000113"); +pub const _UUID_SCHEMA_CLASS_SYNC_ACCOUNT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000114"); +pub const _UUID_SCHEMA_ATTR_SYNC_TOKEN_SESSION: Uuid = + uuid!("00000000-0000-0000-0000-ffff00000115"); +pub const _UUID_SCHEMA_ATTR_SYNC_COOKIE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000116"); // System and domain infos // I'd like to strongly criticise william of the past for making poor choices about these allocations. @@ -271,6 +278,8 @@ pub const _UUID_IDM_PEOPLE_SELF_ACP_WRITE_MAIL_V1: Uuid = pub const _UUID_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000042"); pub const _UUID_IDM_ACP_OAUTH2_READ_PRIV_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000043"); +pub const _UUID_IDM_HP_ACP_SYNC_ACCOUNT_MANAGE_PRIV_V1: Uuid = + uuid!("00000000-0000-0000-0000-ffffff000044"); // End of system ranges pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe"); diff --git a/kanidmd/lib/src/constants/values.rs b/kanidmd/lib/src/constants/values.rs index 052257b6f..4e2a73d5b 100644 --- a/kanidmd/lib/src/constants/values.rs +++ b/kanidmd/lib/src/constants/values.rs @@ -30,6 +30,7 @@ lazy_static! { pub static ref PVCLASS_RECYCLED: PartialValue = PartialValue::new_class("recycled"); pub static ref PVCLASS_SERVICE_ACCOUNT: PartialValue = PartialValue::new_class("service_account"); + pub static ref PVCLASS_SYNC_ACCOUNT: PartialValue = PartialValue::new_class("sync_account"); pub static ref PVCLASS_SYSTEM: PartialValue = PartialValue::new_class("system"); pub static ref PVCLASS_SYSTEM_INFO: PartialValue = PartialValue::new_class("system_info"); pub static ref PVCLASS_SYSTEM_CONFIG: PartialValue = PartialValue::new_class("system_config"); diff --git a/kanidmd/lib/src/credential/mod.rs b/kanidmd/lib/src/credential/mod.rs index 4b93d6ab3..1119fe58c 100644 --- a/kanidmd/lib/src/credential/mod.rs +++ b/kanidmd/lib/src/credential/mod.rs @@ -154,7 +154,7 @@ impl TryFrom<&str> for Password { } if value.starts_with("ipaNTHash: ") { - let nt_md4 = match value.split_once(" ") { + let nt_md4 = match value.split_once(' ') { Some((_, v)) => v, None => { unreachable!(); @@ -168,7 +168,7 @@ impl TryFrom<&str> for Password { } if value.starts_with("sambaNTPassword: ") { - let nt_md4 = match value.split_once(" ") { + let nt_md4 = match value.split_once(' ') { Some((_, v)) => v, None => { unreachable!(); @@ -199,7 +199,7 @@ impl TryFrom<&str> for Password { || value.starts_with("{PBKDF2-SHA256}") || value.starts_with("{PBKDF2-SHA512}") { - let ol_pbkdf2 = match value.split_once("}") { + let ol_pbkdf2 = match value.split_once('}') { Some((_, v)) => v, None => { unreachable!(); diff --git a/kanidmd/lib/src/identity.rs b/kanidmd/lib/src/identity.rs index 720ea7fed..ee330e869 100644 --- a/kanidmd/lib/src/identity.rs +++ b/kanidmd/lib/src/identity.rs @@ -123,6 +123,7 @@ pub struct IdentUser { /// The type of Identity that is related to this session. pub enum IdentType { User(IdentUser), + Synch(Uuid), Internal, } @@ -133,6 +134,7 @@ pub enum IdentityId { // Time stamp of the originating event. // The uuid of the originiating user User(Uuid), + Synch(Uuid), Internal, } @@ -141,6 +143,7 @@ impl From<&IdentType> for IdentityId { match idt { IdentType::Internal => IdentityId::Internal, IdentType::User(u) => IdentityId::User(u.entry.get_uuid()), + IdentType::Synch(u) => IdentityId::Synch(*u), } } } @@ -162,6 +165,7 @@ impl std::fmt::Display for Identity { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match &self.origin { IdentType::Internal => write!(f, "Internal ({})", self.scope), + IdentType::Synch(u) => write!(f, "Synchronise ({}) ({})", u, self.scope), IdentType::User(u) => { let nv = u.entry.get_uuid2spn(); write!( @@ -245,6 +249,7 @@ impl Identity { match &self.origin { IdentType::Internal => None, IdentType::User(u) => Some(u.entry.get_uuid()), + IdentType::Synch(u) => Some(*u), } } @@ -255,7 +260,7 @@ impl Identity { #[cfg(test)] pub fn has_claim(&self, claim: &str) -> bool { match &self.origin { - IdentType::Internal => false, + IdentType::Internal | IdentType::Synch(_) => false, IdentType::User(u) => u .entry .attribute_equality("claim", &PartialValue::new_iutf8(claim)), @@ -264,7 +269,7 @@ impl Identity { pub fn is_memberof(&self, group: Uuid) -> bool { match &self.origin { - IdentType::Internal => false, + IdentType::Internal | IdentType::Synch(_) => false, IdentType::User(u) => u .entry .attribute_equality("memberof", &PartialValue::new_refer(group)), @@ -273,7 +278,7 @@ impl Identity { pub fn get_oauth2_consent_scopes(&self, oauth2_rs: Uuid) -> Option<&BTreeSet> { match &self.origin { - IdentType::Internal => None, + IdentType::Internal | IdentType::Synch(_) => None, IdentType::User(u) => u .entry .get_ava_as_oauthscopemaps("oauth2_consent_scope_map") diff --git a/kanidmd/lib/src/idm/account.rs b/kanidmd/lib/src/idm/account.rs index a89dbfd7b..9c712d54f 100644 --- a/kanidmd/lib/src/idm/account.rs +++ b/kanidmd/lib/src/idm/account.rs @@ -199,9 +199,7 @@ impl Account { let expiry = OffsetDateTime::unix_epoch() + ct + Duration::from_secs(AUTH_SESSION_EXPIRY); let issued_at = OffsetDateTime::unix_epoch() + ct; // TODO: Apply priv expiry. - let purpose = UatPurpose::ReadWrite { - expiry: expiry.clone(), - }; + let purpose = UatPurpose::ReadWrite { expiry: expiry }; Some(UserAuthToken { session_id, @@ -646,8 +644,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> { .map(|purpose| UatStatus { account_id, session_id: *u, - expiry: s.expiry.clone(), - issued_at: s.issued_at.clone(), + expiry: s.expiry, + issued_at: s.issued_at, purpose, }) .map_err(|e| { diff --git a/kanidmd/lib/src/idm/authsession.rs b/kanidmd/lib/src/idm/authsession.rs index a97d9ded0..8763bbb8e 100644 --- a/kanidmd/lib/src/idm/authsession.rs +++ b/kanidmd/lib/src/idm/authsession.rs @@ -778,7 +778,7 @@ impl AuthSession { session_id, label: "Auth Session".to_string(), expiry: uat.expiry, - issued_at: uat.issued_at.clone(), + issued_at: uat.issued_at, issued_by: IdentityId::User(self.account.uuid), scope: (&uat.purpose).into(), })) diff --git a/kanidmd/lib/src/idm/credupdatesession.rs b/kanidmd/lib/src/idm/credupdatesession.rs index b7883e1d2..3c8e51d2b 100644 --- a/kanidmd/lib/src/idm/credupdatesession.rs +++ b/kanidmd/lib/src/idm/credupdatesession.rs @@ -1074,7 +1074,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> { // check a password badlist to eliminate more content // we check the password as "lower case" to help eliminate possibilities // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase) - if (&*self.pw_badlist_cache).contains(&cleartext.to_lowercase()) { + if (*self.pw_badlist_cache).contains(&cleartext.to_lowercase()) { security_info!("Password found in badlist, rejecting"); Err(PasswordQuality::BadListed) } else { diff --git a/kanidmd/lib/src/idm/group.rs b/kanidmd/lib/src/idm/group.rs index 05ad100f1..40f23cb9c 100644 --- a/kanidmd/lib/src/idm/group.rs +++ b/kanidmd/lib/src/idm/group.rs @@ -105,12 +105,9 @@ impl Group { OperationError::InvalidAccountState("Missing attribute: name".to_string()) })?; */ - let spn = - value - .get_ava_single_proto_string("spn") - .ok_or(OperationError::InvalidAccountState( - "Missing attribute: spn".to_string(), - ))?; + let spn = value.get_ava_single_proto_string("spn").ok_or_else(|| { + OperationError::InvalidAccountState("Missing attribute: spn".to_string()) + })?; let uuid = value.get_uuid(); diff --git a/kanidmd/lib/src/idm/mod.rs b/kanidmd/lib/src/idm/mod.rs index 2e4057aec..5937d378d 100644 --- a/kanidmd/lib/src/idm/mod.rs +++ b/kanidmd/lib/src/idm/mod.rs @@ -11,6 +11,7 @@ pub mod event; pub mod group; pub mod oauth2; pub mod radius; +pub mod scim; pub mod server; pub mod serviceaccount; pub mod unix; diff --git a/kanidmd/lib/src/idm/scim.rs b/kanidmd/lib/src/idm/scim.rs new file mode 100644 index 000000000..efb2e13e4 --- /dev/null +++ b/kanidmd/lib/src/idm/scim.rs @@ -0,0 +1,500 @@ +use std::collections::BTreeMap; +use std::time::Duration; + +use base64urlsafedata::Base64UrlSafeData; + +use compact_jwt::{Jws, JwsSigner}; +use kanidm_proto::scim_v1::*; +use kanidm_proto::v1::ApiTokenPurpose; +use serde::{Deserialize, Serialize}; + +use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction}; +use crate::prelude::*; +use crate::value::Session; + +// Internals of a Scim Sync token + +#[allow(dead_code)] +pub(crate) struct SyncAccount { + pub name: String, + pub uuid: Uuid, + pub sync_tokens: BTreeMap, + pub jws_key: JwsSigner, +} + +macro_rules! try_from_entry { + ($value:expr) => {{ + // Check the classes + if !$value.attribute_equality("class", &PVCLASS_SYNC_ACCOUNT) { + return Err(OperationError::InvalidAccountState( + "Missing class: sync account".to_string(), + )); + } + + let name = $value + .get_ava_single_iname("name") + .map(|s| s.to_string()) + .ok_or(OperationError::InvalidAccountState( + "Missing attribute: name".to_string(), + ))?; + + let jws_key = $value + .get_ava_single_jws_key_es256("jws_es256_private_key") + .cloned() + .ok_or(OperationError::InvalidAccountState( + "Missing attribute: jws_es256_private_key".to_string(), + ))?; + + let sync_tokens = $value + .get_ava_as_session_map("sync_token_session") + .cloned() + .unwrap_or_default(); + + let uuid = $value.get_uuid().clone(); + + Ok(SyncAccount { + name, + uuid, + sync_tokens, + jws_key, + }) + }}; +} + +impl SyncAccount { + #[instrument(level = "debug", skip_all)] + pub(crate) fn try_from_entry_rw( + value: &Entry, + // qs: &mut QueryServerWriteTransaction, + ) -> Result { + // let groups = Group::try_from_account_entry_rw(value, qs)?; + try_from_entry!(value) + } + + pub(crate) fn check_sync_token_valid( + _ct: Duration, + sst: &ScimSyncToken, + entry: &Entry, + ) -> bool { + let valid_purpose = matches!(sst.purpose, ApiTokenPurpose::Synchronise); + + // Get the sessions. There are no gracewindows on sync, we are much stricter. + let session_present = entry + .get_ava_as_session_map("sync_token_session") + .map(|session_map| session_map.get(&sst.token_id).is_some()) + .unwrap_or(false); + + debug!(?session_present, valid_purpose); + + session_present && valid_purpose + } +} + +// Need to create a Sync input source +// + +pub struct GenerateScimSyncTokenEvent { + // Who initiated this? + pub ident: Identity, + // Who is it targetting? + pub target: Uuid, + // The label + pub label: String, +} + +impl GenerateScimSyncTokenEvent { + #[cfg(test)] + pub fn new_internal(target: Uuid, label: &str) -> Self { + GenerateScimSyncTokenEvent { + ident: Identity::from_internal(), + target, + label: label.to_string(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "lowercase")] +pub(crate) struct ScimSyncToken { + // uuid of the token? + pub token_id: Uuid, + #[serde(with = "time::serde::timestamp")] + pub issued_at: time::OffsetDateTime, + #[serde(default)] + pub purpose: ApiTokenPurpose, +} + +impl<'a> IdmServerProxyWriteTransaction<'a> { + pub fn scim_sync_generate_token( + &mut self, + gte: &GenerateScimSyncTokenEvent, + ct: Duration, + ) -> Result { + // Get the target signing key. + let sync_account = self + .qs_write + .internal_search_uuid(>e.target) + .and_then(|entry| SyncAccount::try_from_entry_rw(&entry)) + .map_err(|e| { + admin_error!(?e, "Failed to search service account"); + e + })?; + + let session_id = Uuid::new_v4(); + let issued_at = time::OffsetDateTime::unix_epoch() + ct; + + let purpose = ApiTokenPurpose::Synchronise; + + let session = Value::Session( + session_id, + Session { + label: gte.label.clone(), + expiry: None, + // Need the other inner bits? + // for the gracewindow. + issued_at, + // Who actually created this? + issued_by: gte.ident.get_event_origin_id(), + // What is the access scope of this session? This is + // for auditing purposes. + scope: (&purpose).into(), + }, + ); + + let token = Jws::new(ScimSyncToken { + token_id: session_id, + issued_at, + purpose, + }); + + let modlist = ModifyList::new_list(vec![Modify::Present( + AttrString::from("sync_token_session"), + session, + )]); + + self.qs_write + .impersonate_modify( + // Filter as executed + &filter!(f_eq("uuid", PartialValue::new_uuid(gte.target))), + // Filter as intended (acp) + &filter_all!(f_eq("uuid", PartialValue::new_uuid(gte.target))), + &modlist, + // Provide the event to impersonate + >e.ident, + ) + .and_then(|_| { + // The modify succeeded and was allowed, now sign the token for return. + token + .sign(&sync_account.jws_key) + .map(|jws_signed| jws_signed.to_string()) + .map_err(|e| { + admin_error!(err = ?e, "Unable to sign sync token"); + OperationError::CryptographyError + }) + }) + .map_err(|e| { + admin_error!("Failed to generate sync token {:?}", e); + e + }) + // Done! + } + + pub fn sync_account_destroy_token( + &mut self, + ident: &Identity, + target: Uuid, + _ct: Duration, + ) -> Result<(), OperationError> { + let modlist = + ModifyList::new_list(vec![Modify::Purged(AttrString::from("sync_token_session"))]); + + self.qs_write + .impersonate_modify( + // Filter as executed + &filter!(f_eq("uuid", PartialValue::Uuid(target))), + // Filter as intended (acp) + &filter!(f_eq("uuid", PartialValue::Uuid(target))), + &modlist, + // Provide the event to impersonate + ident, + ) + .map_err(|e| { + admin_error!("Failed to destroy api token {:?}", e); + e + }) + } +} + +pub struct ScimSyncUpdateEvent {} + +impl<'a> IdmServerProxyWriteTransaction<'a> { + pub fn scim_sync_update( + &mut self, + _sse: &ScimSyncUpdateEvent, + _ct: Duration, + ) -> Result<(), OperationError> { + // Only update entries related to this uuid + // Make a sync_authority uuid to relate back to on creates. + + // How to check for re-use of a cookie? + + // How to handle delete then re-add of same syncuuid? + // Syncuuid could be a seperate attr so that we avoid this? + + // Should deleted by synced item be exempt on recycle purge? Should + // it just go direct to tombstone? + + Err(OperationError::AccessDenied) + } +} + +impl<'a> IdmServerProxyReadTransaction<'a> { + pub fn scim_sync_get_state(&self, ident: &Identity) -> Result { + // We must be *extra* careful in these functions since we do *internal* searches + // which are *bypassing* normal access checks! + + // The ident *must* be a synchronise session. + let sync_uuid = match &ident.origin { + IdentType::User(_) | IdentType::Internal => { + warn!("Ident type is not synchronise"); + return Err(OperationError::AccessDenied); + } + IdentType::Synch(u) => { + // Ok! + u + } + }; + + match ident.access_scope() { + AccessScope::IdentityOnly | AccessScope::ReadOnly | AccessScope::ReadWrite => { + warn!("Ident access scope is not synchronise"); + return Err(OperationError::AccessDenied); + } + AccessScope::Synchronise => { + // As you were + } + }; + + // Get the sync cookie of that session. + let sync_entry = self.qs_read.internal_search_uuid(sync_uuid)?; + + Ok( + match sync_entry.get_ava_single_private_binary("sync_cookie") { + Some(b) => ScimSyncState::Active { + cookie: Base64UrlSafeData(b.to_vec()), + }, + None => ScimSyncState::Initial, + }, + ) + } +} + +#[cfg(test)] +mod tests { + use crate::event::CreateEvent; + use crate::event::ModifyEvent; + use crate::idm::server::{IdmServerProxyWriteTransaction, IdmServerTransaction}; + use crate::prelude::*; + use compact_jwt::Jws; + use kanidm_proto::scim_v1::*; + use kanidm_proto::v1::ApiTokenPurpose; + use std::time::Duration; + + use super::{GenerateScimSyncTokenEvent, ScimSyncToken}; + + use async_std::task; + + const TEST_CURRENT_TIME: u64 = 6000; + + fn create_scim_sync_account( + idms_prox_write: &mut IdmServerProxyWriteTransaction<'_>, + ct: Duration, + ) -> (Uuid, String) { + let sync_uuid = Uuid::new_v4(); + + let e1 = entry_init!( + ("class", Value::new_class("object")), + ("class", Value::new_class("sync_account")), + ("name", Value::new_iname("test_scim_sync")), + ("uuid", Value::new_uuid(sync_uuid)), + ("description", Value::new_utf8s("A test sync agreement")) + ); + + let ce = CreateEvent::new_internal(vec![e1]); + let cr = idms_prox_write.qs_write.create(&ce); + assert!(cr.is_ok()); + + let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector"); + + let sync_token = idms_prox_write + .scim_sync_generate_token(>e, ct) + .expect("failed to generate new scim sync token"); + + (sync_uuid, sync_token) + } + + #[test] + fn test_idm_scim_sync_basic_function() { + run_idm_test!(|_qs: &QueryServer, + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed| { + let ct = Duration::from_secs(TEST_CURRENT_TIME); + + let mut idms_prox_write = task::block_on(idms.proxy_write(ct)); + let (sync_uuid, sync_token) = create_scim_sync_account(&mut idms_prox_write, ct); + + assert!(idms_prox_write.commit().is_ok()); + + // Do a get_state to get the current "state cookie" if any. + let idms_prox_read = task::block_on(idms.proxy_read()); + + let ident = idms_prox_read + .validate_and_parse_sync_token_to_ident(Some(sync_token.as_str()), ct) + .expect("Failed to validate sync token"); + + assert!(Some(sync_uuid) == ident.get_uuid()); + + let sync_state = idms_prox_read + .scim_sync_get_state(&ident) + .expect("Failed to get current sync state"); + trace!(?sync_state); + + assert!(matches!(sync_state, ScimSyncState::Initial)); + + drop(idms_prox_read); + + // Use the current state and update. + + // TODO!!! + }) + } + + #[test] + fn test_idm_scim_sync_token_security() { + run_idm_test!(|_qs: &QueryServer, + idms: &IdmServer, + _idms_delayed: &mut IdmServerDelayed| { + let ct = Duration::from_secs(TEST_CURRENT_TIME); + + let mut idms_prox_write = task::block_on(idms.proxy_write(ct)); + + let sync_uuid = Uuid::new_v4(); + + let e1 = entry_init!( + ("class", Value::new_class("object")), + ("class", Value::new_class("sync_account")), + ("name", Value::new_iname("test_scim_sync")), + ("uuid", Value::new_uuid(sync_uuid)), + ("description", Value::new_utf8s("A test sync agreement")) + ); + + let ce = CreateEvent::new_internal(vec![e1]); + let cr = idms_prox_write.qs_write.create(&ce); + assert!(cr.is_ok()); + + let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector"); + + let sync_token = idms_prox_write + .scim_sync_generate_token(>e, ct) + .expect("failed to generate new scim sync token"); + + assert!(idms_prox_write.commit().is_ok()); + + // -- Check the happy path. + let idms_prox_read = task::block_on(idms.proxy_read()); + let ident = idms_prox_read + .validate_and_parse_sync_token_to_ident(Some(sync_token.as_str()), ct) + .expect("Failed to validate sync token"); + assert!(Some(sync_uuid) == ident.get_uuid()); + drop(idms_prox_read); + + // -- Revoke the session + + let mut idms_prox_write = task::block_on(idms.proxy_write(ct)); + let me_inv_m = unsafe { + ModifyEvent::new_internal_invalid( + filter!(f_eq("name", PartialValue::new_iname("test_scim_sync"))), + ModifyList::new_list(vec![Modify::Purged(AttrString::from( + "sync_token_session", + ))]), + ) + }; + assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok()); + assert!(idms_prox_write.commit().is_ok()); + + // Must fail + let idms_prox_read = task::block_on(idms.proxy_read()); + let fail = idms_prox_read + .validate_and_parse_sync_token_to_ident(Some(sync_token.as_str()), ct); + assert!(matches!(fail, Err(OperationError::NotAuthenticated))); + drop(idms_prox_read); + + // -- New session, reset the JWS + let mut idms_prox_write = task::block_on(idms.proxy_write(ct)); + + let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector"); + let sync_token = idms_prox_write + .scim_sync_generate_token(>e, ct) + .expect("failed to generate new scim sync token"); + + let me_inv_m = unsafe { + ModifyEvent::new_internal_invalid( + filter!(f_eq("name", PartialValue::new_iname("test_scim_sync"))), + ModifyList::new_list(vec![Modify::Purged(AttrString::from( + "jws_es256_private_key", + ))]), + ) + }; + assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok()); + assert!(idms_prox_write.commit().is_ok()); + + let idms_prox_read = task::block_on(idms.proxy_read()); + let fail = idms_prox_read + .validate_and_parse_sync_token_to_ident(Some(sync_token.as_str()), ct); + assert!(matches!(fail, Err(OperationError::NotAuthenticated))); + + // -- Forge a session, use wrong types + + let sync_entry = idms_prox_read + .qs_read + .internal_search_uuid(&sync_uuid) + .expect("Unable to access sync entry"); + + let jws_key = sync_entry + .get_ava_single_jws_key_es256("jws_es256_private_key") + .cloned() + .expect("Missing attribute: jws_es256_private_key"); + + let sync_tokens = sync_entry + .get_ava_as_session_map("sync_token_session") + .cloned() + .unwrap_or_default(); + + // Steal these from the legit sesh. + let (token_id, issued_at) = sync_tokens + .iter() + .next() + .map(|(k, v)| (*k, v.issued_at.clone())) + .expect("No sync tokens present"); + + let purpose = ApiTokenPurpose::ReadWrite; + + let token = Jws::new(ScimSyncToken { + token_id, + issued_at, + purpose, + }); + + let forged_token = token + .sign(&jws_key) + .map(|jws_signed| jws_signed.to_string()) + .expect("Unable to sign forged token"); + + let fail = idms_prox_read + .validate_and_parse_sync_token_to_ident(Some(forged_token.as_str()), ct); + assert!(matches!(fail, Err(OperationError::NotAuthenticated))); + }) + } + + // Need to delete different phases such as conflictn and end of the agreement. +} diff --git a/kanidmd/lib/src/idm/server.rs b/kanidmd/lib/src/idm/server.rs index 200a1d20b..68f7c4608 100644 --- a/kanidmd/lib/src/idm/server.rs +++ b/kanidmd/lib/src/idm/server.rs @@ -53,6 +53,7 @@ use crate::idm::oauth2::{ Oauth2ResourceServersWriteTransaction, OidcDiscoveryResponse, OidcToken, }; use crate::idm::radius::RadiusAccount; +use crate::idm::scim::{ScimSyncToken, SyncAccount}; use crate::idm::serviceaccount::ServiceAccount; use crate::idm::unix::{UnixGroup, UnixUserAccount}; use crate::idm::AuthState; @@ -466,7 +467,7 @@ pub trait IdmServerTransaction<'a> { .get_qs_txn() .internal_search(filter!(f_eq( "jws_es256_private_key", - PartialValue::new_iutf8(&kid) + PartialValue::new_iutf8(kid) ))) .and_then(|mut vs| match vs.pop() { Some(entry) if vs.is_empty() => Ok(entry), @@ -691,7 +692,7 @@ pub trait IdmServerTransaction<'a> { let entry = if uuid == &UUID_ANONYMOUS { anon_entry.clone() } else { - self.get_qs_txn().internal_search_uuid(&uuid).map_err(|e| { + self.get_qs_txn().internal_search_uuid(uuid).map_err(|e| { admin_error!("Failed to start auth ldap -> {:?}", e); e })? @@ -717,7 +718,7 @@ pub trait IdmServerTransaction<'a> { Err(OperationError::SessionExpired) } } - LdapSession::UserAuthToken(uat) => self.process_uat_to_identity(&uat, ct), + LdapSession::UserAuthToken(uat) => self.process_uat_to_identity(uat, ct), LdapSession::ApiToken(apit) => { let entry = self .get_qs_txn() @@ -727,10 +728,93 @@ pub trait IdmServerTransaction<'a> { e })?; - self.process_apit_to_identity(&apit, entry, ct) + self.process_apit_to_identity(apit, entry, ct) } } } + + #[instrument(level = "info", skip_all)] + fn validate_and_parse_sync_token_to_ident( + &self, + token: Option<&str>, + ct: Duration, + ) -> Result { + let jwsu = token + .ok_or_else(|| { + security_info!("No token provided"); + OperationError::NotAuthenticated + }) + .and_then(|s| { + JwsUnverified::from_str(s).map_err(|e| { + security_info!(?e, "Unable to decode token"); + OperationError::NotAuthenticated + }) + })?; + + let kid = jwsu.get_jwk_kid().ok_or_else(|| { + security_info!("Token does not contain a valid kid"); + OperationError::NotAuthenticated + })?; + + let entry = self + .get_qs_txn() + .internal_search(filter!(f_eq( + "jws_es256_private_key", + PartialValue::new_iutf8(kid) + ))) + .and_then(|mut vs| match vs.pop() { + Some(entry) if vs.is_empty() => Ok(entry), + _ => { + admin_error!( + ?kid, + "entries was empty, or matched multiple results for kid" + ); + Err(OperationError::NotAuthenticated) + } + })?; + + let user_signer = entry + .get_ava_single_jws_key_es256("jws_es256_private_key") + .ok_or_else(|| { + admin_error!( + ?kid, + "A kid was present on entry {} but it does not contain a signing key", + entry.get_uuid() + ); + OperationError::NotAuthenticated + })?; + + let user_validator = user_signer.get_validator().map_err(|e| { + security_info!(?e, "Unable to access token verifier"); + OperationError::NotAuthenticated + })?; + + let sync_token = jwsu + .validate(&user_validator) + .map_err(|e| { + security_info!(?e, "Unable to verify token"); + OperationError::NotAuthenticated + }) + .map(|t: Jws| t.into_inner())?; + + let valid = SyncAccount::check_sync_token_valid(ct, &sync_token, &entry); + + if !valid { + security_info!("Unable to proceed with invalid sync token"); + return Err(OperationError::NotAuthenticated); + } + + // If scope is not Synchronise, then fail. + let scope = (&sync_token.purpose).into(); + + let limits = Limits::unlimited(); + Ok(Identity { + origin: IdentType::Synch(entry.get_uuid()), + session_id: sync_token.token_id, + scope, + limits, + }) + } } impl<'a> IdmServerTransaction<'a> for IdmServerAuthTransaction<'a> { @@ -1140,9 +1224,9 @@ impl<'a> IdmServerAuthTransaction<'a> { })) } Token::ApiToken(apit, entry) => { - let spn = entry.get_ava_single_proto_string("spn").ok_or( - OperationError::InvalidAccountState("Missing attribute: spn".to_string()), - )?; + let spn = entry.get_ava_single_proto_string("spn").ok_or_else(|| { + OperationError::InvalidAccountState("Missing attribute: spn".to_string()) + })?; Ok(Some(LdapBoundToken { session_id: apit.token_id, @@ -1520,7 +1604,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { // check a password badlist to eliminate more content // we check the password as "lower case" to help eliminate possibilities // also, when pw_badlist_cache is read from DB, it is read as Value (iutf8 lowercase) - if (&*self.pw_badlist_cache).contains(&cleartext.to_lowercase()) { + if (*self.pw_badlist_cache).contains(&cleartext.to_lowercase()) { security_info!("Password found in badlist, rejecting"); Err(OperationError::PasswordQuality(vec![ PasswordFeedback::BadListed, diff --git a/kanidmd/lib/src/idm/serviceaccount.rs b/kanidmd/lib/src/idm/serviceaccount.rs index b23d82d8f..dfa4b003c 100644 --- a/kanidmd/lib/src/idm/serviceaccount.rs +++ b/kanidmd/lib/src/idm/serviceaccount.rs @@ -238,7 +238,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> { account_id: service_account.uuid, token_id: session_id, label: gte.label.clone(), - expiry: gte.expiry.clone(), + expiry: gte.expiry, issued_at, purpose, }); @@ -344,8 +344,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> { account_id, token_id: *u, label: s.label.clone(), - expiry: s.expiry.clone(), - issued_at: s.issued_at.clone(), + expiry: s.expiry, + issued_at: s.issued_at, purpose, }) .map_err(|e| { diff --git a/kanidmd/lib/src/ldap.rs b/kanidmd/lib/src/ldap.rs index c6d2e6f56..3a08c698c 100644 --- a/kanidmd/lib/src/ldap.rs +++ b/kanidmd/lib/src/ldap.rs @@ -28,7 +28,7 @@ pub enum LdapResponseState { BindMultiPartResponse(LdapBoundToken, Vec), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum LdapSession { // Maps through and provides anon read, but allows us to check the validity // of the account still. diff --git a/kanidmd/lib/src/lib.rs b/kanidmd/lib/src/lib.rs index 9741a2820..fafe8d7bb 100644 --- a/kanidmd/lib/src/lib.rs +++ b/kanidmd/lib/src/lib.rs @@ -9,10 +9,10 @@ #![deny(clippy::unwrap_used)] #![deny(clippy::expect_used)] #![deny(clippy::panic)] -#![deny(clippy::unreachable)] #![deny(clippy::await_holding_lock)] #![deny(clippy::needless_pass_by_value)] #![deny(clippy::trivially_copy_pass_by_ref)] +#![allow(clippy::unreachable)] #[cfg(all(jemallocator, test, not(target_family = "windows")))] #[global_allocator] @@ -78,7 +78,7 @@ pub mod prelude { f_and, f_andnot, f_eq, f_id, f_inc, f_lt, f_or, f_pres, f_self, f_spn_name, f_sub, Filter, FilterInvalid, FC, }; - pub use crate::identity::{AccessScope, Identity}; + pub use crate::identity::{AccessScope, IdentType, Identity}; pub use crate::modify::{m_pres, m_purge, m_remove, Modify, ModifyInvalid, ModifyList}; pub use crate::server::{ QueryServer, QueryServerReadTransaction, QueryServerTransaction, diff --git a/kanidmd/lib/src/macros.rs b/kanidmd/lib/src/macros.rs index f1f333400..89482ffe6 100644 --- a/kanidmd/lib/src/macros.rs +++ b/kanidmd/lib/src/macros.rs @@ -120,8 +120,8 @@ where &crate::idm::server::IdmServerDelayed, ), { - let _ = sketching::test_init(); - let _ = run_idm_test_inner!(test_fn); + sketching::test_init(); + run_idm_test_inner!(test_fn); } // Test helpers for all plugins. diff --git a/kanidmd/lib/src/modify.rs b/kanidmd/lib/src/modify.rs index c1fba5005..038dfe12d 100644 --- a/kanidmd/lib/src/modify.rs +++ b/kanidmd/lib/src/modify.rs @@ -164,7 +164,8 @@ impl ModifyList { .expect("Critical: Core schema corrupt or missing. To initiate a core transfer, please deposit substitute core in receptacle."); */ - let res: Result, _> = (&self.mods) + let res: Result, _> = self + .mods .iter() .map(|m| match m { Modify::Present(attr, value) => { diff --git a/kanidmd/lib/src/plugins/dyngroup.rs b/kanidmd/lib/src/plugins/dyngroup.rs index 6f3ce7065..5d0633f88 100644 --- a/kanidmd/lib/src/plugins/dyngroup.rs +++ b/kanidmd/lib/src/plugins/dyngroup.rs @@ -7,19 +7,11 @@ use crate::event::{CreateEvent, ModifyEvent}; use crate::filter::FilterInvalid; use crate::prelude::*; -#[derive(Clone)] +#[derive(Clone, Default)] pub struct DynGroupCache { insts: BTreeMap>, } -impl Default for DynGroupCache { - fn default() -> Self { - DynGroupCache { - insts: BTreeMap::default(), - } - } -} - pub struct DynGroup; impl DynGroup { diff --git a/kanidmd/lib/src/plugins/jwskeygen.rs b/kanidmd/lib/src/plugins/jwskeygen.rs index bf28544a8..37c84c9fb 100644 --- a/kanidmd/lib/src/plugins/jwskeygen.rs +++ b/kanidmd/lib/src/plugins/jwskeygen.rs @@ -49,7 +49,9 @@ macro_rules! keygen_transform { } } - if $e.attribute_equality("class", &PVCLASS_SERVICE_ACCOUNT) { + if $e.attribute_equality("class", &PVCLASS_SERVICE_ACCOUNT) || + $e.attribute_equality("class", &PVCLASS_SYNC_ACCOUNT) + { if !$e.attribute_pres("jws_es256_private_key") { security_info!("regenerating jws es256 private key"); let jwssigner = JwsSigner::generate_es256() diff --git a/kanidmd/lib/src/server.rs b/kanidmd/lib/src/server.rs index 63d69c996..ed07889b5 100644 --- a/kanidmd/lib/src/server.rs +++ b/kanidmd/lib/src/server.rs @@ -2112,6 +2112,7 @@ impl<'a> QueryServerWriteTransaction<'a> { Ok(()) } + #[allow(clippy::mut_from_ref)] pub(crate) fn get_dyngroup_cache(&self) -> &mut DynGroupCache { unsafe { let mptr = self.dyngroup_cache.as_ptr(); @@ -2649,6 +2650,10 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_ATTR_API_TOKEN_SESSION, JSON_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP, JSON_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION, + JSON_SCHEMA_ATTR_NSUNIQUEID, + JSON_SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME, + JSON_SCHEMA_ATTR_SYNC_TOKEN_SESSION, + JSON_SCHEMA_ATTR_SYNC_COOKIE, JSON_SCHEMA_CLASS_PERSON, JSON_SCHEMA_CLASS_ORGPERSON, JSON_SCHEMA_CLASS_GROUP, @@ -2661,8 +2666,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_SCHEMA_CLASS_SYSTEM_CONFIG, JSON_SCHEMA_CLASS_OAUTH2_RS, JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC, - JSON_SCHEMA_ATTR_NSUNIQUEID, - JSON_SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME, + JSON_SCHEMA_CLASS_SYNC_ACCOUNT, ]; let r = idm_schema @@ -2757,6 +2761,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_DOMAIN_ADMINS, JSON_IDM_HP_OAUTH2_MANAGE_PRIV_V1, JSON_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV, + JSON_IDM_HP_SYNC_ACCOUNT_MANAGE_PRIV, // All members must exist before we write HP JSON_IDM_HIGH_PRIVILEGE_V1, // Built in access controls. @@ -2800,6 +2805,7 @@ impl<'a> QueryServerWriteTransaction<'a> { JSON_IDM_ACP_RADIUS_SECRET_WRITE_PRIV_V1, JSON_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1, JSON_IDM_ACP_OAUTH2_READ_PRIV_V1, + JSON_IDM_HP_ACP_SYNC_ACCOUNT_MANAGE_PRIV_V1, ]; let res: Result<(), _> = idm_entries diff --git a/kanidmd/lib/src/valueset/jws.rs b/kanidmd/lib/src/valueset/jws.rs index 063099050..5adc0b7a0 100644 --- a/kanidmd/lib/src/valueset/jws.rs +++ b/kanidmd/lib/src/valueset/jws.rs @@ -23,7 +23,7 @@ impl ValueSetJwsKeyEs256 { self.set.insert(k) } - pub fn from_dbvs2(data: Vec>) -> Result { + pub fn from_dbvs2(data: &[Vec]) -> Result { let set = data .iter() .map(|b| { @@ -179,7 +179,7 @@ impl ValueSetJwsKeyRs256 { self.set.insert(k) } - pub fn from_dbvs2(data: Vec>) -> Result { + pub fn from_dbvs2(data: &[Vec]) -> Result { let set = data .iter() .map(|b| { diff --git a/kanidmd/lib/src/valueset/mod.rs b/kanidmd/lib/src/valueset/mod.rs index 65ce03f6f..97407d2ca 100644 --- a/kanidmd/lib/src/valueset/mod.rs +++ b/kanidmd/lib/src/valueset/mod.rs @@ -644,8 +644,8 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result ValueSetPasskey::from_dbvs2(set), DbValueSetV2::DeviceKey(set) => ValueSetDeviceKey::from_dbvs2(set), DbValueSetV2::Session(set) => ValueSetSession::from_dbvs2(set), - DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(set), - DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(set), + DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set), + DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set), DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => { unimplemented!() } diff --git a/kanidmd/lib/src/valueset/session.rs b/kanidmd/lib/src/valueset/session.rs index 3de1188f5..5f99d4837 100644 --- a/kanidmd/lib/src/valueset/session.rs +++ b/kanidmd/lib/src/valueset/session.rs @@ -77,6 +77,7 @@ impl ValueSetSession { let issued_by = match issued_by { DbValueIdentityId::V1Internal => IdentityId::Internal, DbValueIdentityId::V1Uuid(u) => IdentityId::User(u), + DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u), }; let scope = match scope { @@ -201,6 +202,7 @@ impl ValueSetT for ValueSetSession { issued_by: match m.issued_by { IdentityId::Internal => DbValueIdentityId::V1Internal, IdentityId::User(u) => DbValueIdentityId::V1Uuid(u), + IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u), }, scope: match m.scope { AccessScope::IdentityOnly => DbValueAccessScopeV1::IdentityOnly, diff --git a/kanidmd/testkit-macros/src/entry.rs b/kanidmd/testkit-macros/src/entry.rs index f8b0c1bec..d1d90e256 100644 --- a/kanidmd/testkit-macros/src/entry.rs +++ b/kanidmd/testkit-macros/src/entry.rs @@ -4,7 +4,7 @@ use syn::spanned::Spanned; use quote::{quote, quote_spanned, ToTokens}; -fn parse_knobs(input: syn::ItemFn) -> TokenStream { +fn parse_knobs(input: &syn::ItemFn) -> TokenStream { // If type mismatch occurs, the current rustc points to the last statement. let (last_stmt_start_span, _last_stmt_end_span) = { let mut last_stmt = input @@ -65,7 +65,7 @@ fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenS tokens } -pub(crate) fn test(_args: TokenStream, item: TokenStream) -> TokenStream { +pub(crate) fn test(_args: &TokenStream, item: TokenStream) -> TokenStream { // If any of the steps for this macro fail, we still want to expand to an item that is as close // to the expected output as possible. This helps out IDEs such that completions and other // related features keep working. @@ -84,5 +84,5 @@ pub(crate) fn test(_args: TokenStream, item: TokenStream) -> TokenStream { return token_stream_with_error(item, syn::Error::new_spanned(input.sig.fn_token, msg)); } - parse_knobs(input) + parse_knobs(&input) } diff --git a/kanidmd/testkit-macros/src/lib.rs b/kanidmd/testkit-macros/src/lib.rs index 42af50275..463b6c0b0 100644 --- a/kanidmd/testkit-macros/src/lib.rs +++ b/kanidmd/testkit-macros/src/lib.rs @@ -19,5 +19,5 @@ use proc_macro::TokenStream; #[proc_macro_attribute] pub fn test(args: TokenStream, item: TokenStream) -> TokenStream { - entry::test(args, item) + entry::test(&args, item) } diff --git a/kanidmd/testkit/tests/scim_test.rs b/kanidmd/testkit/tests/scim_test.rs new file mode 100644 index 000000000..3b1b25086 --- /dev/null +++ b/kanidmd/testkit/tests/scim_test.rs @@ -0,0 +1,43 @@ +use kanidm_client::KanidmClient; +use kanidmd_testkit::ADMIN_TEST_PASSWORD; + +#[kanidmd_testkit::test] +async fn test_sync_account_lifecycle(rsclient: KanidmClient) { + let a_res = rsclient + .auth_simple_password("admin", ADMIN_TEST_PASSWORD) + .await; + assert!(a_res.is_ok()); + + let a_list = rsclient.idm_sync_account_list().await.unwrap(); + assert!(a_list.is_empty()); + + rsclient + .idm_sync_account_create("ipa_sync_account", Some("Demo of a sync account")) + .await + .unwrap(); + + let a_list = rsclient.idm_sync_account_list().await.unwrap(); + assert!(!a_list.is_empty()); + + let a = rsclient + .idm_sync_account_get("ipa_sync_account") + .await + .unwrap(); + assert!(a.is_some()); + println!("{:?}", a); + + // Get a token + + // List sessions? + + // Reset Sign Key + // Get New token + + // Get sync state + + // Delete session + + // Sync state fails. + + // Delete account +} diff --git a/orca/src/preprocess.rs b/orca/src/preprocess.rs index 24ca190e3..5c79970ab 100644 --- a/orca/src/preprocess.rs +++ b/orca/src/preprocess.rs @@ -275,7 +275,7 @@ pub fn doit(input: &Path, output: &Path) { } else { // Choose the number of members: let m = rng.gen_range(0..max_m); - let members = (&precreate).choose_multiple(&mut rng, m).cloned().collect(); + let members = (precreate).choose_multiple(&mut rng, m).cloned().collect(); Entity::Group(Group::generate(*id, members)) }; (*id, ent)