mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
Add new scim/sync files (#1152)
This commit is contained in:
parent
d6105c051a
commit
15c3bde00e
|
@ -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 <container name>
|
||||
|
||||
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 <container name>
|
||||
|
||||
# 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"]}'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
34
kanidm_client/src/sync_account.rs
Normal file
34
kanidm_client/src/sync_account.rs
Normal file
|
@ -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<Vec<Entry>, ClientError> {
|
||||
self.perform_get_request("/v1/sync_account").await
|
||||
}
|
||||
|
||||
pub async fn idm_sync_account_get(&self, id: &str) -> Result<Option<Entry>, 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
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
pub mod constants;
|
||||
pub mod messages;
|
||||
pub mod oauth2;
|
||||
pub mod scim_v1;
|
||||
pub mod utils;
|
||||
pub mod v1;
|
||||
|
||||
|
|
9
kanidm_proto/src/scim_v1.rs
Normal file
9
kanidm_proto/src/scim_v1.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use base64urlsafedata::Base64UrlSafeData;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ScimSyncState {
|
||||
Initial,
|
||||
Active { cookie: Base64UrlSafeData },
|
||||
}
|
|
@ -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<String> = results.into_iter().flatten().collect();
|
||||
|
||||
info!(
|
||||
"{} passwords passed zxcvbn, uploading ...",
|
||||
filt_pwset.len()
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
//! if they are read or write transactions internally.
|
||||
|
||||
pub mod v1_read;
|
||||
pub mod v1_scim;
|
||||
pub mod v1_write;
|
||||
|
|
80
kanidmd/core/src/actors/v1_scim.rs
Normal file
80
kanidmd/core/src/actors/v1_scim.rs
Normal file
|
@ -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<String>,
|
||||
uuid_or_name: String,
|
||||
label: String,
|
||||
eventid: Uuid,
|
||||
) -> Result<String, 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
|
||||
})?;
|
||||
|
||||
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<String>,
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ use kanidmd_lib::{
|
|||
use kanidmd_lib::prelude::*;
|
||||
|
||||
pub struct QueryServerWriteV1 {
|
||||
idms: Arc<IdmServer>,
|
||||
pub(crate) idms: Arc<IdmServer>,
|
||||
}
|
||||
|
||||
impl QueryServerWriteV1 {
|
||||
|
|
|
@ -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<String>;
|
||||
|
||||
fn get_auth_bearer(&self) -> Option<&str>;
|
||||
|
||||
fn get_current_auth_session_id(&self) -> Option<Uuid>;
|
||||
|
||||
fn get_url_param(&self, param: &str) -> Result<String, tide::Error>;
|
||||
|
@ -80,6 +84,21 @@ pub trait RequestExtensions {
|
|||
}
|
||||
|
||||
impl RequestExtensions for tide::Request<AppState> {
|
||||
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::<UserAuthToken>("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<String> {
|
||||
// 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");
|
||||
|
|
|
@ -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<AppState>) -> 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);
|
||||
}
|
||||
|
|
263
kanidmd/core/src/https/v1_scim.rs
Normal file
263
kanidmd/core/src/https/v1_scim.rs
Normal file
|
@ -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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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<AppState>) -> 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#"
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="theme-color" content="white" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Sink!</title>
|
||||
<link rel="icon" href="/pkg/img/favicon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
</head>
|
||||
<body>
|
||||
<pre>
|
||||
___
|
||||
.' _ '.
|
||||
/ /` `\ \
|
||||
| | [__]
|
||||
| | {{
|
||||
| | }}
|
||||
_ | | _ {{
|
||||
___________<_>_| |_<_>}}________
|
||||
.=======^=(___)=^={{====.
|
||||
/ .----------------}}---. \
|
||||
/ / {{ \ \
|
||||
/ / }} \ \
|
||||
( '=========================' )
|
||||
'-----------------------------'
|
||||
</pre>
|
||||
</body>
|
||||
</html>"#,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -377,6 +377,8 @@ pub enum DbValueIdentityId {
|
|||
V1Internal,
|
||||
#[serde(rename = "v1u")]
|
||||
V1Uuid(Uuid),
|
||||
#[serde(rename = "v1s")]
|
||||
V1Sync(Uuid),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}"#;
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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!();
|
||||
|
|
|
@ -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<String>> {
|
||||
match &self.origin {
|
||||
IdentType::Internal => None,
|
||||
IdentType::Internal | IdentType::Synch(_) => None,
|
||||
IdentType::User(u) => u
|
||||
.entry
|
||||
.get_ava_as_oauthscopemaps("oauth2_consent_scope_map")
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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(),
|
||||
}))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
500
kanidmd/lib/src/idm/scim.rs
Normal file
500
kanidmd/lib/src/idm/scim.rs
Normal file
|
@ -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<Uuid, Session>,
|
||||
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<EntrySealed, EntryCommitted>,
|
||||
// qs: &mut QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
// 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<EntrySealed, EntryCommitted>,
|
||||
) -> 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<String, OperationError> {
|
||||
// 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<ScimSyncState, OperationError> {
|
||||
// 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.
|
||||
}
|
|
@ -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<Identity, OperationError> {
|
||||
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<ScimSyncToken>| 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,
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -28,7 +28,7 @@ pub enum LdapResponseState {
|
|||
BindMultiPartResponse(LdapBoundToken, Vec<LdapMsg>),
|
||||
}
|
||||
|
||||
#[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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -164,7 +164,8 @@ impl ModifyList<ModifyInvalid> {
|
|||
.expect("Critical: Core schema corrupt or missing. To initiate a core transfer, please deposit substitute core in receptacle.");
|
||||
*/
|
||||
|
||||
let res: Result<Vec<Modify>, _> = (&self.mods)
|
||||
let res: Result<Vec<Modify>, _> = self
|
||||
.mods
|
||||
.iter()
|
||||
.map(|m| match m {
|
||||
Modify::Present(attr, value) => {
|
||||
|
|
|
@ -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<Uuid, Filter<FilterInvalid>>,
|
||||
}
|
||||
|
||||
impl Default for DynGroupCache {
|
||||
fn default() -> Self {
|
||||
DynGroupCache {
|
||||
insts: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DynGroup;
|
||||
|
||||
impl DynGroup {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,7 +23,7 @@ impl ValueSetJwsKeyEs256 {
|
|||
self.set.insert(k)
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<Vec<u8>>) -> Result<ValueSet, OperationError> {
|
||||
pub fn from_dbvs2(data: &[Vec<u8>]) -> Result<ValueSet, OperationError> {
|
||||
let set = data
|
||||
.iter()
|
||||
.map(|b| {
|
||||
|
@ -179,7 +179,7 @@ impl ValueSetJwsKeyRs256 {
|
|||
self.set.insert(k)
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<Vec<u8>>) -> Result<ValueSet, OperationError> {
|
||||
pub fn from_dbvs2(data: &[Vec<u8>]) -> Result<ValueSet, OperationError> {
|
||||
let set = data
|
||||
.iter()
|
||||
.map(|b| {
|
||||
|
|
|
@ -644,8 +644,8 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
|||
DbValueSetV2::Passkey(set) => 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!()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
43
kanidmd/testkit/tests/scim_test.rs
Normal file
43
kanidmd/testkit/tests/scim_test.rs
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue