Add new scim/sync files (#1152)

This commit is contained in:
Firstyear 2022-10-29 19:07:54 +10:00 committed by GitHub
parent d6105c051a
commit 15c3bde00e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1360 additions and 172 deletions

View file

@ -5,56 +5,3 @@ a Kanidm server, such as making backups and restoring from backups, testing
server configuration, reindexing, verifying data consistency, and renaming server configuration, reindexing, verifying data consistency, and renaming
your domain. 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"]}'

View file

@ -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 │──┤ └─▶│ 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. 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 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. 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 For a gradual migration, this process is the same as the read only application. However when ready

View file

@ -39,6 +39,7 @@ use webauthn_rs_proto::{
mod person; mod person;
mod service_account; mod service_account;
mod sync_account;
mod system; mod system;
pub const APPLICATION_JSON: &str = "application/json"; pub const APPLICATION_JSON: &str = "application/json";

View 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
}
}

View file

@ -11,6 +11,7 @@
pub mod constants; pub mod constants;
pub mod messages; pub mod messages;
pub mod oauth2; pub mod oauth2;
pub mod scim_v1;
pub mod utils; pub mod utils;
pub mod v1; pub mod v1;

View file

@ -0,0 +1,9 @@
use base64urlsafedata::Base64UrlSafeData;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum ScimSyncState {
Initial,
Active { cookie: Base64UrlSafeData },
}

View file

@ -101,13 +101,12 @@ impl PwBadlistOpt {
let results = task_handles.join().await; let results = task_handles.join().await;
let results: Vec<_> = results let filt_pwset: Vec<_> = results
.into_iter() .into_iter()
.map(|res| res.expect("Thread join failure")) .map(|res| res.expect("Thread join failure"))
.flatten()
.collect(); .collect();
let filt_pwset: Vec<String> = results.into_iter().flatten().collect();
info!( info!(
"{} passwords passed zxcvbn, uploading ...", "{} passwords passed zxcvbn, uploading ...",
filt_pwset.len() filt_pwset.len()

View file

@ -3,4 +3,5 @@
//! if they are read or write transactions internally. //! if they are read or write transactions internally.
pub mod v1_read; pub mod v1_read;
pub mod v1_scim;
pub mod v1_write; pub mod v1_write;

View 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(&gte, 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))
}
}

View file

@ -34,7 +34,7 @@ use kanidmd_lib::{
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
pub struct QueryServerWriteV1 { pub struct QueryServerWriteV1 {
idms: Arc<IdmServer>, pub(crate) idms: Arc<IdmServer>,
} }
impl QueryServerWriteV1 { impl QueryServerWriteV1 {

View file

@ -3,6 +3,7 @@ pub mod middleware;
mod oauth2; mod oauth2;
mod routemaps; mod routemaps;
mod v1; mod v1;
mod v1_scim;
use std::fs::canonicalize; use std::fs::canonicalize;
use std::path::PathBuf; use std::path::PathBuf;
@ -22,6 +23,7 @@ use self::middleware::*;
use self::oauth2::*; use self::oauth2::*;
use self::routemaps::{RouteMap, RouteMaps}; use self::routemaps::{RouteMap, RouteMaps};
use self::v1::*; use self::v1::*;
use self::v1_scim::*;
use crate::actors::v1_read::QueryServerReadV1; use crate::actors::v1_read::QueryServerReadV1;
use crate::actors::v1_write::QueryServerWriteV1; use crate::actors::v1_write::QueryServerWriteV1;
use crate::config::{ServerRole, TlsConfiguration}; use crate::config::{ServerRole, TlsConfiguration};
@ -70,6 +72,8 @@ pub struct AppState {
pub trait RequestExtensions { pub trait RequestExtensions {
fn get_current_uat(&self) -> Option<String>; 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_current_auth_session_id(&self) -> Option<Uuid>;
fn get_url_param(&self, param: &str) -> Result<String, tide::Error>; fn get_url_param(&self, param: &str) -> Result<String, tide::Error>;
@ -80,6 +84,21 @@ pub trait RequestExtensions {
} }
impl RequestExtensions for tide::Request<AppState> { 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> { fn get_current_uat(&self) -> Option<String> {
// Contact the QS to get it to validate wtf is up. // Contact the QS to get it to validate wtf is up.
// let kref = &self.state().bundy_handle; // let kref = &self.state().bundy_handle;
@ -474,55 +493,12 @@ pub fn create_https_server(
appserver appserver
.at("/status") .at("/status")
.mapped_get(&mut routemap, self::status); .mapped_get(&mut routemap, self::status);
// == oauth endpoints. // == oauth endpoints.
oauth2_route_setup(&mut appserver, &mut routemap);
let mut oauth2_process = appserver.at("/oauth2"); // == scim endpoints.
// ⚠️ ⚠️ WARNING ⚠️ ⚠️ scim_route_setup(&mut appserver, &mut routemap);
// 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);
let mut raw_route = appserver.at("/v1/raw"); let mut raw_route = appserver.at("/v1/raw");
raw_route.at("/create").mapped_post(&mut routemap, create); raw_route.at("/create").mapped_post(&mut routemap, create);
@ -741,8 +717,6 @@ pub fn create_https_server(
.at("/:id/_unix") .at("/:id/_unix")
.mapped_post(&mut routemap, account_post_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 // Shared account features only - mainly this is for unix-like
// features. // features.
let mut account_route = appserver.at("/v1/account"); let mut account_route = appserver.at("/v1/account");

View file

@ -7,6 +7,7 @@ use kanidmd_lib::idm::oauth2::{
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::routemaps::{RouteMap, RouteMaps};
use super::v1::{json_rest_event_get, json_rest_event_post}; use super::v1::{json_rest_event_get, json_rest_event_post};
use super::{to_tide_response, AppState, RequestExtensions}; 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 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);
}

View 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);
}

View file

@ -8,7 +8,7 @@ fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenS
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()) { let input: syn::ItemFn = match syn::parse(item.clone()) {
Ok(it) => it, Ok(it) => it,
Err(e) => return token_stream_with_error(item, e), Err(e) => return token_stream_with_error(item, e),

View file

@ -19,10 +19,10 @@ use proc_macro::TokenStream;
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn qs_test(args: TokenStream, item: TokenStream) -> TokenStream { pub fn qs_test(args: TokenStream, item: TokenStream) -> TokenStream {
entry::qs_test(args, item, true) entry::qs_test(&args, item, true)
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn qs_test_no_init(args: TokenStream, item: TokenStream) -> TokenStream { pub fn qs_test_no_init(args: TokenStream, item: TokenStream) -> TokenStream {
entry::qs_test(args, item, false) entry::qs_test(&args, item, false)
} }

View file

@ -456,12 +456,17 @@ pub trait AccessControlsTransaction<'a> {
// A possible solution is to change the filter resolve function // A possible solution is to change the filter resolve function
// such that it takes an entry, rather than an event, but that // such that it takes an entry, rather than an event, but that
// would create issues in search. // 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) => { Ok(f_res) => {
if rec_entry.entry_match_no_index(&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 // Now, for each of the acp's that apply to our receiver, resolve their
// related target filters. // related target filters.
(&acs.acp.targetscope) acs.acp
.targetscope
.resolve(ident, None, Some(acp_resolve_filter_cache)) .resolve(ident, None, Some(acp_resolve_filter_cache))
.map_err(|e| { .map_err(|e| {
admin_error!( admin_error!(
@ -509,6 +514,10 @@ pub trait AccessControlsTransaction<'a> {
// No need to check ACS // No need to check ACS
return Ok(entries); return Ok(entries);
} }
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return Err(OperationError::InvalidState);
}
IdentType::User(u) => &u.entry, IdentType::User(u) => &u.entry,
}; };
info!(event = %se.ident, "Access check for search (filter) event"); info!(event = %se.ident, "Access check for search (filter) event");
@ -612,6 +621,10 @@ pub trait AccessControlsTransaction<'a> {
return Ok(Vec::new()); return Ok(Vec::new());
} }
} }
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return Err(OperationError::InvalidState);
}
IdentType::User(u) => &u.entry, IdentType::User(u) => &u.entry,
}; };
@ -724,10 +737,10 @@ pub trait AccessControlsTransaction<'a> {
modify_state modify_state
.iter() .iter()
.filter_map(|acs| { .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) => { Ok(f_res) => {
if rec_entry.entry_match_no_index(&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)) .resolve(ident, None, Some(acp_resolve_filter_cache))
.map_err(|e| { .map_err(|e| {
admin_error!( admin_error!(
@ -769,6 +782,10 @@ pub trait AccessControlsTransaction<'a> {
// No need to check ACS // No need to check ACS
return Ok(true); return Ok(true);
} }
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return Err(OperationError::InvalidState);
}
IdentType::User(u) => &u.entry, IdentType::User(u) => &u.entry,
}; };
info!(event = %me.ident, "Access check for modify event"); info!(event = %me.ident, "Access check for modify event");
@ -940,6 +957,10 @@ pub trait AccessControlsTransaction<'a> {
// No need to check ACS // No need to check ACS
return Ok(true); return Ok(true);
} }
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return Err(OperationError::InvalidState);
}
IdentType::User(u) => &u.entry, IdentType::User(u) => &u.entry,
}; };
info!(event = %ce.ident, "Access check for create event"); info!(event = %ce.ident, "Access check for create event");
@ -1081,6 +1102,10 @@ pub trait AccessControlsTransaction<'a> {
// No need to check ACS // No need to check ACS
return Ok(true); return Ok(true);
} }
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return Err(OperationError::InvalidState);
}
IdentType::User(u) => &u.entry, IdentType::User(u) => &u.entry,
}; };
info!(event = %de.ident, "Access check for delete event"); info!(event = %de.ident, "Access check for delete event");
@ -1194,6 +1219,10 @@ pub trait AccessControlsTransaction<'a> {
// No need to check ACS // No need to check ACS
return Err(OperationError::InvalidState); return Err(OperationError::InvalidState);
} }
IdentType::Synch(_) => {
security_critical!("Blocking sync check");
return Err(OperationError::InvalidState);
}
IdentType::User(u) => &u.entry, IdentType::User(u) => &u.entry,
}; };

View file

@ -377,6 +377,8 @@ pub enum DbValueIdentityId {
V1Internal, V1Internal,
#[serde(rename = "v1u")] #[serde(rename = "v1u")]
V1Uuid(Uuid), V1Uuid(Uuid),
#[serde(rename = "v1s")]
V1Sync(Uuid),
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -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"]
}
}"#;

View file

@ -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#"{ pub const JSON_IDM_ALL_PERSONS: &str = r#"{
"attrs": { "attrs": {
"class": ["dyngroup", "group", "object"], "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-000000000031",
"00000000-0000-0000-0000-000000000032", "00000000-0000-0000-0000-000000000032",
"00000000-0000-0000-0000-000000000034", "00000000-0000-0000-0000-000000000034",
"00000000-0000-0000-0000-000000000037",
"00000000-0000-0000-0000-000000001000" "00000000-0000-0000-0000-000000001000"
] ]
} }

View file

@ -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 === // === classes ===
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#" 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_info type
// domain_uuid // domain_uuid
// domain_name <- should be the dns name? // domain_name <- should be the dns name?

View file

@ -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_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_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"); 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"); uuid!("00000000-0000-0000-0000-ffff00000112");
pub const _UUID_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: Uuid = pub const _UUID_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000113"); 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 // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // 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 = pub const _UUID_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1: Uuid =
uuid!("00000000-0000-0000-0000-ffffff000042"); 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_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 // End of system ranges
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe"); pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");

View file

@ -30,6 +30,7 @@ lazy_static! {
pub static ref PVCLASS_RECYCLED: PartialValue = PartialValue::new_class("recycled"); pub static ref PVCLASS_RECYCLED: PartialValue = PartialValue::new_class("recycled");
pub static ref PVCLASS_SERVICE_ACCOUNT: PartialValue = pub static ref PVCLASS_SERVICE_ACCOUNT: PartialValue =
PartialValue::new_class("service_account"); 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: PartialValue = PartialValue::new_class("system");
pub static ref PVCLASS_SYSTEM_INFO: PartialValue = PartialValue::new_class("system_info"); pub static ref PVCLASS_SYSTEM_INFO: PartialValue = PartialValue::new_class("system_info");
pub static ref PVCLASS_SYSTEM_CONFIG: PartialValue = PartialValue::new_class("system_config"); pub static ref PVCLASS_SYSTEM_CONFIG: PartialValue = PartialValue::new_class("system_config");

View file

@ -154,7 +154,7 @@ impl TryFrom<&str> for Password {
} }
if value.starts_with("ipaNTHash: ") { if value.starts_with("ipaNTHash: ") {
let nt_md4 = match value.split_once(" ") { let nt_md4 = match value.split_once(' ') {
Some((_, v)) => v, Some((_, v)) => v,
None => { None => {
unreachable!(); unreachable!();
@ -168,7 +168,7 @@ impl TryFrom<&str> for Password {
} }
if value.starts_with("sambaNTPassword: ") { if value.starts_with("sambaNTPassword: ") {
let nt_md4 = match value.split_once(" ") { let nt_md4 = match value.split_once(' ') {
Some((_, v)) => v, Some((_, v)) => v,
None => { None => {
unreachable!(); unreachable!();
@ -199,7 +199,7 @@ impl TryFrom<&str> for Password {
|| value.starts_with("{PBKDF2-SHA256}") || value.starts_with("{PBKDF2-SHA256}")
|| value.starts_with("{PBKDF2-SHA512}") || value.starts_with("{PBKDF2-SHA512}")
{ {
let ol_pbkdf2 = match value.split_once("}") { let ol_pbkdf2 = match value.split_once('}') {
Some((_, v)) => v, Some((_, v)) => v,
None => { None => {
unreachable!(); unreachable!();

View file

@ -123,6 +123,7 @@ pub struct IdentUser {
/// The type of Identity that is related to this session. /// The type of Identity that is related to this session.
pub enum IdentType { pub enum IdentType {
User(IdentUser), User(IdentUser),
Synch(Uuid),
Internal, Internal,
} }
@ -133,6 +134,7 @@ pub enum IdentityId {
// Time stamp of the originating event. // Time stamp of the originating event.
// The uuid of the originiating user // The uuid of the originiating user
User(Uuid), User(Uuid),
Synch(Uuid),
Internal, Internal,
} }
@ -141,6 +143,7 @@ impl From<&IdentType> for IdentityId {
match idt { match idt {
IdentType::Internal => IdentityId::Internal, IdentType::Internal => IdentityId::Internal,
IdentType::User(u) => IdentityId::User(u.entry.get_uuid()), 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 { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.origin { match &self.origin {
IdentType::Internal => write!(f, "Internal ({})", self.scope), IdentType::Internal => write!(f, "Internal ({})", self.scope),
IdentType::Synch(u) => write!(f, "Synchronise ({}) ({})", u, self.scope),
IdentType::User(u) => { IdentType::User(u) => {
let nv = u.entry.get_uuid2spn(); let nv = u.entry.get_uuid2spn();
write!( write!(
@ -245,6 +249,7 @@ impl Identity {
match &self.origin { match &self.origin {
IdentType::Internal => None, IdentType::Internal => None,
IdentType::User(u) => Some(u.entry.get_uuid()), IdentType::User(u) => Some(u.entry.get_uuid()),
IdentType::Synch(u) => Some(*u),
} }
} }
@ -255,7 +260,7 @@ impl Identity {
#[cfg(test)] #[cfg(test)]
pub fn has_claim(&self, claim: &str) -> bool { pub fn has_claim(&self, claim: &str) -> bool {
match &self.origin { match &self.origin {
IdentType::Internal => false, IdentType::Internal | IdentType::Synch(_) => false,
IdentType::User(u) => u IdentType::User(u) => u
.entry .entry
.attribute_equality("claim", &PartialValue::new_iutf8(claim)), .attribute_equality("claim", &PartialValue::new_iutf8(claim)),
@ -264,7 +269,7 @@ impl Identity {
pub fn is_memberof(&self, group: Uuid) -> bool { pub fn is_memberof(&self, group: Uuid) -> bool {
match &self.origin { match &self.origin {
IdentType::Internal => false, IdentType::Internal | IdentType::Synch(_) => false,
IdentType::User(u) => u IdentType::User(u) => u
.entry .entry
.attribute_equality("memberof", &PartialValue::new_refer(group)), .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>> { pub fn get_oauth2_consent_scopes(&self, oauth2_rs: Uuid) -> Option<&BTreeSet<String>> {
match &self.origin { match &self.origin {
IdentType::Internal => None, IdentType::Internal | IdentType::Synch(_) => None,
IdentType::User(u) => u IdentType::User(u) => u
.entry .entry
.get_ava_as_oauthscopemaps("oauth2_consent_scope_map") .get_ava_as_oauthscopemaps("oauth2_consent_scope_map")

View file

@ -199,9 +199,7 @@ impl Account {
let expiry = OffsetDateTime::unix_epoch() + ct + Duration::from_secs(AUTH_SESSION_EXPIRY); let expiry = OffsetDateTime::unix_epoch() + ct + Duration::from_secs(AUTH_SESSION_EXPIRY);
let issued_at = OffsetDateTime::unix_epoch() + ct; let issued_at = OffsetDateTime::unix_epoch() + ct;
// TODO: Apply priv expiry. // TODO: Apply priv expiry.
let purpose = UatPurpose::ReadWrite { let purpose = UatPurpose::ReadWrite { expiry: expiry };
expiry: expiry.clone(),
};
Some(UserAuthToken { Some(UserAuthToken {
session_id, session_id,
@ -646,8 +644,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
.map(|purpose| UatStatus { .map(|purpose| UatStatus {
account_id, account_id,
session_id: *u, session_id: *u,
expiry: s.expiry.clone(), expiry: s.expiry,
issued_at: s.issued_at.clone(), issued_at: s.issued_at,
purpose, purpose,
}) })
.map_err(|e| { .map_err(|e| {

View file

@ -778,7 +778,7 @@ impl AuthSession {
session_id, session_id,
label: "Auth Session".to_string(), label: "Auth Session".to_string(),
expiry: uat.expiry, expiry: uat.expiry,
issued_at: uat.issued_at.clone(), issued_at: uat.issued_at,
issued_by: IdentityId::User(self.account.uuid), issued_by: IdentityId::User(self.account.uuid),
scope: (&uat.purpose).into(), scope: (&uat.purpose).into(),
})) }))

View file

@ -1074,7 +1074,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
// check a password badlist to eliminate more content // check a password badlist to eliminate more content
// we check the password as "lower case" to help eliminate possibilities // 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) // 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"); security_info!("Password found in badlist, rejecting");
Err(PasswordQuality::BadListed) Err(PasswordQuality::BadListed)
} else { } else {

View file

@ -105,12 +105,9 @@ impl Group {
OperationError::InvalidAccountState("Missing attribute: name".to_string()) OperationError::InvalidAccountState("Missing attribute: name".to_string())
})?; })?;
*/ */
let spn = let spn = value.get_ava_single_proto_string("spn").ok_or_else(|| {
value OperationError::InvalidAccountState("Missing attribute: spn".to_string())
.get_ava_single_proto_string("spn") })?;
.ok_or(OperationError::InvalidAccountState(
"Missing attribute: spn".to_string(),
))?;
let uuid = value.get_uuid(); let uuid = value.get_uuid();

View file

@ -11,6 +11,7 @@ pub mod event;
pub mod group; pub mod group;
pub mod oauth2; pub mod oauth2;
pub mod radius; pub mod radius;
pub mod scim;
pub mod server; pub mod server;
pub mod serviceaccount; pub mod serviceaccount;
pub mod unix; pub mod unix;

500
kanidmd/lib/src/idm/scim.rs Normal file
View 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(&gte.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
&gte.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(&gte, 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(&gte, 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(&gte, 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.
}

View file

@ -53,6 +53,7 @@ use crate::idm::oauth2::{
Oauth2ResourceServersWriteTransaction, OidcDiscoveryResponse, OidcToken, Oauth2ResourceServersWriteTransaction, OidcDiscoveryResponse, OidcToken,
}; };
use crate::idm::radius::RadiusAccount; use crate::idm::radius::RadiusAccount;
use crate::idm::scim::{ScimSyncToken, SyncAccount};
use crate::idm::serviceaccount::ServiceAccount; use crate::idm::serviceaccount::ServiceAccount;
use crate::idm::unix::{UnixGroup, UnixUserAccount}; use crate::idm::unix::{UnixGroup, UnixUserAccount};
use crate::idm::AuthState; use crate::idm::AuthState;
@ -466,7 +467,7 @@ pub trait IdmServerTransaction<'a> {
.get_qs_txn() .get_qs_txn()
.internal_search(filter!(f_eq( .internal_search(filter!(f_eq(
"jws_es256_private_key", "jws_es256_private_key",
PartialValue::new_iutf8(&kid) PartialValue::new_iutf8(kid)
))) )))
.and_then(|mut vs| match vs.pop() { .and_then(|mut vs| match vs.pop() {
Some(entry) if vs.is_empty() => Ok(entry), Some(entry) if vs.is_empty() => Ok(entry),
@ -691,7 +692,7 @@ pub trait IdmServerTransaction<'a> {
let entry = if uuid == &UUID_ANONYMOUS { let entry = if uuid == &UUID_ANONYMOUS {
anon_entry.clone() anon_entry.clone()
} else { } 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); admin_error!("Failed to start auth ldap -> {:?}", e);
e e
})? })?
@ -717,7 +718,7 @@ pub trait IdmServerTransaction<'a> {
Err(OperationError::SessionExpired) 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) => { LdapSession::ApiToken(apit) => {
let entry = self let entry = self
.get_qs_txn() .get_qs_txn()
@ -727,10 +728,93 @@ pub trait IdmServerTransaction<'a> {
e 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> { impl<'a> IdmServerTransaction<'a> for IdmServerAuthTransaction<'a> {
@ -1140,9 +1224,9 @@ impl<'a> IdmServerAuthTransaction<'a> {
})) }))
} }
Token::ApiToken(apit, entry) => { Token::ApiToken(apit, entry) => {
let spn = entry.get_ava_single_proto_string("spn").ok_or( let spn = entry.get_ava_single_proto_string("spn").ok_or_else(|| {
OperationError::InvalidAccountState("Missing attribute: spn".to_string()), OperationError::InvalidAccountState("Missing attribute: spn".to_string())
)?; })?;
Ok(Some(LdapBoundToken { Ok(Some(LdapBoundToken {
session_id: apit.token_id, session_id: apit.token_id,
@ -1520,7 +1604,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// check a password badlist to eliminate more content // check a password badlist to eliminate more content
// we check the password as "lower case" to help eliminate possibilities // 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) // 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"); security_info!("Password found in badlist, rejecting");
Err(OperationError::PasswordQuality(vec![ Err(OperationError::PasswordQuality(vec![
PasswordFeedback::BadListed, PasswordFeedback::BadListed,

View file

@ -238,7 +238,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
account_id: service_account.uuid, account_id: service_account.uuid,
token_id: session_id, token_id: session_id,
label: gte.label.clone(), label: gte.label.clone(),
expiry: gte.expiry.clone(), expiry: gte.expiry,
issued_at, issued_at,
purpose, purpose,
}); });
@ -344,8 +344,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
account_id, account_id,
token_id: *u, token_id: *u,
label: s.label.clone(), label: s.label.clone(),
expiry: s.expiry.clone(), expiry: s.expiry,
issued_at: s.issued_at.clone(), issued_at: s.issued_at,
purpose, purpose,
}) })
.map_err(|e| { .map_err(|e| {

View file

@ -28,7 +28,7 @@ pub enum LdapResponseState {
BindMultiPartResponse(LdapBoundToken, Vec<LdapMsg>), BindMultiPartResponse(LdapBoundToken, Vec<LdapMsg>),
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum LdapSession { pub enum LdapSession {
// Maps through and provides anon read, but allows us to check the validity // Maps through and provides anon read, but allows us to check the validity
// of the account still. // of the account still.

View file

@ -9,10 +9,10 @@
#![deny(clippy::unwrap_used)] #![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)] #![deny(clippy::expect_used)]
#![deny(clippy::panic)] #![deny(clippy::panic)]
#![deny(clippy::unreachable)]
#![deny(clippy::await_holding_lock)] #![deny(clippy::await_holding_lock)]
#![deny(clippy::needless_pass_by_value)] #![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)] #![deny(clippy::trivially_copy_pass_by_ref)]
#![allow(clippy::unreachable)]
#[cfg(all(jemallocator, test, not(target_family = "windows")))] #[cfg(all(jemallocator, test, not(target_family = "windows")))]
#[global_allocator] #[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, 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, 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::modify::{m_pres, m_purge, m_remove, Modify, ModifyInvalid, ModifyList};
pub use crate::server::{ pub use crate::server::{
QueryServer, QueryServerReadTransaction, QueryServerTransaction, QueryServer, QueryServerReadTransaction, QueryServerTransaction,

View file

@ -120,8 +120,8 @@ where
&crate::idm::server::IdmServerDelayed, &crate::idm::server::IdmServerDelayed,
), ),
{ {
let _ = sketching::test_init(); sketching::test_init();
let _ = run_idm_test_inner!(test_fn); run_idm_test_inner!(test_fn);
} }
// Test helpers for all plugins. // Test helpers for all plugins.

View file

@ -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."); .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() .iter()
.map(|m| match m { .map(|m| match m {
Modify::Present(attr, value) => { Modify::Present(attr, value) => {

View file

@ -7,19 +7,11 @@ use crate::event::{CreateEvent, ModifyEvent};
use crate::filter::FilterInvalid; use crate::filter::FilterInvalid;
use crate::prelude::*; use crate::prelude::*;
#[derive(Clone)] #[derive(Clone, Default)]
pub struct DynGroupCache { pub struct DynGroupCache {
insts: BTreeMap<Uuid, Filter<FilterInvalid>>, insts: BTreeMap<Uuid, Filter<FilterInvalid>>,
} }
impl Default for DynGroupCache {
fn default() -> Self {
DynGroupCache {
insts: BTreeMap::default(),
}
}
}
pub struct DynGroup; pub struct DynGroup;
impl DynGroup { impl DynGroup {

View file

@ -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") { if !$e.attribute_pres("jws_es256_private_key") {
security_info!("regenerating jws es256 private key"); security_info!("regenerating jws es256 private key");
let jwssigner = JwsSigner::generate_es256() let jwssigner = JwsSigner::generate_es256()

View file

@ -2112,6 +2112,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
Ok(()) Ok(())
} }
#[allow(clippy::mut_from_ref)]
pub(crate) fn get_dyngroup_cache(&self) -> &mut DynGroupCache { pub(crate) fn get_dyngroup_cache(&self) -> &mut DynGroupCache {
unsafe { unsafe {
let mptr = self.dyngroup_cache.as_ptr(); let mptr = self.dyngroup_cache.as_ptr();
@ -2649,6 +2650,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_ATTR_API_TOKEN_SESSION, JSON_SCHEMA_ATTR_API_TOKEN_SESSION,
JSON_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP, JSON_SCHEMA_ATTR_OAUTH2_RS_SUP_SCOPE_MAP,
JSON_SCHEMA_ATTR_USER_AUTH_TOKEN_SESSION, 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_PERSON,
JSON_SCHEMA_CLASS_ORGPERSON, JSON_SCHEMA_CLASS_ORGPERSON,
JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_GROUP,
@ -2661,8 +2666,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_CLASS_SYSTEM_CONFIG, JSON_SCHEMA_CLASS_SYSTEM_CONFIG,
JSON_SCHEMA_CLASS_OAUTH2_RS, JSON_SCHEMA_CLASS_OAUTH2_RS,
JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC, JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC,
JSON_SCHEMA_ATTR_NSUNIQUEID, JSON_SCHEMA_CLASS_SYNC_ACCOUNT,
JSON_SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME,
]; ];
let r = idm_schema let r = idm_schema
@ -2757,6 +2761,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_DOMAIN_ADMINS, JSON_DOMAIN_ADMINS,
JSON_IDM_HP_OAUTH2_MANAGE_PRIV_V1, JSON_IDM_HP_OAUTH2_MANAGE_PRIV_V1,
JSON_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV, JSON_IDM_HP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_PRIV,
JSON_IDM_HP_SYNC_ACCOUNT_MANAGE_PRIV,
// All members must exist before we write HP // All members must exist before we write HP
JSON_IDM_HIGH_PRIVILEGE_V1, JSON_IDM_HIGH_PRIVILEGE_V1,
// Built in access controls. // Built in access controls.
@ -2800,6 +2805,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_IDM_ACP_RADIUS_SECRET_WRITE_PRIV_V1, JSON_IDM_ACP_RADIUS_SECRET_WRITE_PRIV_V1,
JSON_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1, JSON_IDM_HP_ACP_SERVICE_ACCOUNT_INTO_PERSON_MIGRATE_V1,
JSON_IDM_ACP_OAUTH2_READ_PRIV_V1, JSON_IDM_ACP_OAUTH2_READ_PRIV_V1,
JSON_IDM_HP_ACP_SYNC_ACCOUNT_MANAGE_PRIV_V1,
]; ];
let res: Result<(), _> = idm_entries let res: Result<(), _> = idm_entries

View file

@ -23,7 +23,7 @@ impl ValueSetJwsKeyEs256 {
self.set.insert(k) 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 let set = data
.iter() .iter()
.map(|b| { .map(|b| {
@ -179,7 +179,7 @@ impl ValueSetJwsKeyRs256 {
self.set.insert(k) 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 let set = data
.iter() .iter()
.map(|b| { .map(|b| {

View file

@ -644,8 +644,8 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
DbValueSetV2::Passkey(set) => ValueSetPasskey::from_dbvs2(set), DbValueSetV2::Passkey(set) => ValueSetPasskey::from_dbvs2(set),
DbValueSetV2::DeviceKey(set) => ValueSetDeviceKey::from_dbvs2(set), DbValueSetV2::DeviceKey(set) => ValueSetDeviceKey::from_dbvs2(set),
DbValueSetV2::Session(set) => ValueSetSession::from_dbvs2(set), DbValueSetV2::Session(set) => ValueSetSession::from_dbvs2(set),
DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(set), DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(set), DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(&set),
DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => { DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => {
unimplemented!() unimplemented!()
} }

View file

@ -77,6 +77,7 @@ impl ValueSetSession {
let issued_by = match issued_by { let issued_by = match issued_by {
DbValueIdentityId::V1Internal => IdentityId::Internal, DbValueIdentityId::V1Internal => IdentityId::Internal,
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u), DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
DbValueIdentityId::V1Sync(u) => IdentityId::Synch(u),
}; };
let scope = match scope { let scope = match scope {
@ -201,6 +202,7 @@ impl ValueSetT for ValueSetSession {
issued_by: match m.issued_by { issued_by: match m.issued_by {
IdentityId::Internal => DbValueIdentityId::V1Internal, IdentityId::Internal => DbValueIdentityId::V1Internal,
IdentityId::User(u) => DbValueIdentityId::V1Uuid(u), IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
IdentityId::Synch(u) => DbValueIdentityId::V1Sync(u),
}, },
scope: match m.scope { scope: match m.scope {
AccessScope::IdentityOnly => DbValueAccessScopeV1::IdentityOnly, AccessScope::IdentityOnly => DbValueAccessScopeV1::IdentityOnly,

View file

@ -4,7 +4,7 @@ use syn::spanned::Spanned;
use quote::{quote, quote_spanned, ToTokens}; 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. // If type mismatch occurs, the current rustc points to the last statement.
let (last_stmt_start_span, _last_stmt_end_span) = { let (last_stmt_start_span, _last_stmt_end_span) = {
let mut last_stmt = input let mut last_stmt = input
@ -65,7 +65,7 @@ fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenS
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 // 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 // to the expected output as possible. This helps out IDEs such that completions and other
// related features keep working. // 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)); return token_stream_with_error(item, syn::Error::new_spanned(input.sig.fn_token, msg));
} }
parse_knobs(input) parse_knobs(&input)
} }

View file

@ -19,5 +19,5 @@ use proc_macro::TokenStream;
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn test(args: TokenStream, item: TokenStream) -> TokenStream { pub fn test(args: TokenStream, item: TokenStream) -> TokenStream {
entry::test(args, item) entry::test(&args, item)
} }

View 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
}

View file

@ -275,7 +275,7 @@ pub fn doit(input: &Path, output: &Path) {
} else { } else {
// Choose the number of members: // Choose the number of members:
let m = rng.gen_range(0..max_m); 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)) Entity::Group(Group::generate(*id, members))
}; };
(*id, ent) (*id, ent)