20250110 eo fixes ()

While preparing for everything open, I found a small number of doc/book issues, some logging issues, and some minor performance wins. This pr is just small bits of various polish around the place.
This commit is contained in:
Firstyear 2025-01-12 13:53:31 +10:00 committed by GitHub
parent c4bc1ff546
commit e7d91ed55d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 137 additions and 28 deletions
book/src/sync
examples
server
core/src/https
lib/src

View file

@ -33,7 +33,8 @@ You can find the name of your 389 Directory Server instance with:
```bash ```bash
# Run on the FreeIPA server # Run on the FreeIPA server
dsconf --list dsctl --list
> slapd-DEV-KANIDM-COM
``` ```
Using this you can show the current status of the retro changelog plugin to see if you need to Using this you can show the current status of the retro changelog plugin to see if you need to
@ -83,6 +84,20 @@ kanidm-ipa-sync [-c /path/to/kanidm/config] -i /path/to/kanidm-ipa-sync -n
kanidm-ipa-sync -i /etc/kanidm/ipa-sync -n kanidm-ipa-sync -i /etc/kanidm/ipa-sync -n
``` ```
As the sync tool is part of the tools container, you can run this with:
```bash
docker run --rm -i -t \
--user uid:gid \
-p 12345:12345 \
-v /etc/kanidm/config:/etc/kanidm/config:ro \
-v /path/to/kanidm.ca.pem:/path/to/kanidm.ca.pem:ro
-v /path/to/ipa-ca.pem:/etc/kanidm/ipa-ca.pem:ro \
-v /path/to/ipa-sync:/etc/kanidm/ipa-sync:ro \
kanidm/tools:latest \
kanidm-ipa-sync -i /etc/kanidm/ipa-sync -
```
## Running the Sync Tool Automatically ## Running the Sync Tool Automatically
The sync tool can be run on a schedule if you configure the `schedule` parameter, and provide the The sync tool can be run on a schedule if you configure the `schedule` parameter, and provide the
@ -96,11 +111,14 @@ kanidm-ipa-sync -i /etc/kanidm/ipa-sync --schedule
As the sync tool is part of the tools container, you can run this with: As the sync tool is part of the tools container, you can run this with:
```bash ```bash
docker create --name kanidm-ipa-sync \ docker run --name kanidm-ipa-sync \
--user uid:gid \ --user uid:gid \
-p 12345:12345 \ -p 12345:12345 \
-v /etc/kanidm/config:/etc/kanidm/config:ro \ -v /etc/kanidm/config:/etc/kanidm/config:ro \
-v /path/to/kanidm.ca.pem:/path/to/kanidm.ca.pem:ro
-v /path/to/ipa-ca.pem:/etc/kanidm/ipa-ca.pem:ro \
-v /path/to/ipa-sync:/etc/kanidm/ipa-sync:ro \ -v /path/to/ipa-sync:/etc/kanidm/ipa-sync:ro \
kanidm/tools:latest \
kanidm-ipa-sync -i /etc/kanidm/ipa-sync --schedule kanidm-ipa-sync -i /etc/kanidm/ipa-sync --schedule
``` ```

View file

@ -17,8 +17,9 @@ sync_token = "eyJhb..."
# server in the IPA topology rather than via a load balancer or dns srv records. This # server in the IPA topology rather than via a load balancer or dns srv records. This
# is to prevent replication conflicts and issues due to how 389-ds content sync works. # is to prevent replication conflicts and issues due to how 389-ds content sync works.
ipa_uri = "ldaps://specific-server.ipa.dev.kanidm.com" ipa_uri = "ldaps://specific-server.ipa.dev.kanidm.com"
# Path to the IPA CA certificate in PEM format. # Path to the IPA CA certificate in PEM format. This can be found on an IPA server
ipa_ca = "/path/to/kanidm-ipa-ca.pem" # in the file `/etc/ipa/ca.crt`
ipa_ca = "/path/to/ipa-ca.pem"
# The DN of an account with content sync rights. By default cn=Directory Manager has # The DN of an account with content sync rights. By default cn=Directory Manager has
# this access. # this access.
ipa_sync_dn = "cn=Directory Manager" ipa_sync_dn = "cn=Directory Manager"

View file

@ -7,8 +7,8 @@ use super::v1::{
}; };
use super::ServerState; use super::ServerState;
use crate::https::extractors::VerifiedClientInformation; use crate::https::extractors::VerifiedClientInformation;
use axum::extract::{Path, Query, State}; use axum::extract::{rejection::JsonRejection, DefaultBodyLimit, Path, Query, State};
use axum::response::Html; use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::{Extension, Json, Router}; use axum::{Extension, Json, Router};
use kanidm_proto::scim_v1::{ use kanidm_proto::scim_v1::{
@ -17,6 +17,8 @@ use kanidm_proto::scim_v1::{
use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::Entry as ProtoEntry;
use kanidmd_lib::prelude::*; use kanidmd_lib::prelude::*;
const DEFAULT_SCIM_SYNC_BYTES: usize = 1024 * 1024 * 32;
#[utoipa::path( #[utoipa::path(
get, get,
path = "/v1/sync_account", path = "/v1/sync_account",
@ -271,14 +273,25 @@ async fn scim_sync_post(
State(state): State<ServerState>, State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>, Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation, VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Json(changes): Json<ScimSyncRequest>, payload: Result<Json<ScimSyncRequest>, JsonRejection>,
) -> Result<Json<()>, WebError> { ) -> Response {
state match payload {
Ok(Json(changes)) => {
let res = state
.qe_w_ref .qe_w_ref
.handle_scim_sync_apply(client_auth_info, changes, kopid.eventid) .handle_scim_sync_apply(client_auth_info, changes, kopid.eventid)
.await .await;
.map(Json::from)
.map_err(WebError::from) match res {
Ok(data) => Json::from(data).into_response(),
Err(err) => WebError::from(err).into_response(),
}
}
Err(rejection) => {
error!(?rejection, "Unable to process JSON");
rejection.into_response()
}
}
} }
#[utoipa::path( #[utoipa::path(
@ -473,6 +486,11 @@ pub fn route_setup() -> Router<ServerState> {
// //
// POST Send a sync update // POST Send a sync update
// //
.route("/scim/v1/Sync", post(scim_sync_post).get(scim_sync_get)) .route(
"/scim/v1/Sync",
post(scim_sync_post)
.layer(DefaultBodyLimit::max(DEFAULT_SCIM_SYNC_BYTES))
.get(scim_sync_get),
)
.route("/scim/v1/Sink", get(scim_sink_get)) // skip_route_check .route("/scim/v1/Sink", get(scim_sink_get)) // skip_route_check
} }

View file

@ -94,11 +94,22 @@ impl Limits {
} }
} }
/// The result of a key value request containing the list of entry IDs that
/// match the filter/query condition.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum IdList { pub enum IdList {
/// The value is not indexed, and must be assumed that all entries may match.
AllIds, AllIds,
PartialThreshold(IDLBitRange), /// The index is "fuzzy" like a bloom filter (perhaps superset is a better description) -
/// it containes all elements that do match, but may have extra elements that don't.
/// This requires the caller to perform a filter test to assert that all
/// returned entries match all assertions within the filter.
Partial(IDLBitRange), Partial(IDLBitRange),
/// The set was indexed and is below the filter test threshold. This is because it's
/// now faster to test with the filter than to continue to access indexes at this point.
/// Like a partial set, this is a super set of the entries that match the query.
PartialThreshold(IDLBitRange),
/// The value is indexed and accurately represents the set of entries that precisely match.
Indexed(IDLBitRange), Indexed(IDLBitRange),
} }
@ -640,7 +651,7 @@ pub trait BackendTransaction {
let (idl, fplan) = trace_span!("be::search -> filter2idl") let (idl, fplan) = trace_span!("be::search -> filter2idl")
.in_scope(|| self.filter2idl(filt.to_inner(), FILTER_SEARCH_TEST_THRESHOLD))?; .in_scope(|| self.filter2idl(filt.to_inner(), FILTER_SEARCH_TEST_THRESHOLD))?;
debug!(search_filter_executed_plan = ?fplan); debug!(search_filter_executed_plan = %fplan);
match &idl { match &idl {
IdList::AllIds => { IdList::AllIds => {
@ -736,7 +747,7 @@ pub trait BackendTransaction {
let (idl, fplan) = trace_span!("be::exists -> filter2idl") let (idl, fplan) = trace_span!("be::exists -> filter2idl")
.in_scope(|| self.filter2idl(filt.to_inner(), FILTER_EXISTS_TEST_THRESHOLD))?; .in_scope(|| self.filter2idl(filt.to_inner(), FILTER_EXISTS_TEST_THRESHOLD))?;
debug!(exist_filter_executed_plan = ?fplan); debug!(exist_filter_executed_plan = %fplan);
// Apply limits to the IdList. // Apply limits to the IdList.
match &idl { match &idl {

View file

@ -358,6 +358,54 @@ pub enum FilterPlan {
InclusionIndexed(Vec<FilterPlan>), InclusionIndexed(Vec<FilterPlan>),
} }
// This difference in this is that we want to show unindexed elements more prominently
// in the execution.
fn fmt_filterplan_set(f: &mut fmt::Formatter<'_>, name: &str, plan: &[FilterPlan]) -> fmt::Result {
write!(f, "{name}(")?;
for item in plan {
write!(f, "{}, ", item)?;
}
write!(f, ")")
}
impl fmt::Display for FilterPlan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid => write!(f, "Invalid"),
Self::EqIndexed(attr, _) => write!(f, "EqIndexed({attr})"),
Self::EqCorrupt(attr) => write!(f, "EqCorrupt({attr})"),
Self::EqUnindexed(attr) => write!(f, "EqUnindexed({attr})"),
Self::SubIndexed(attr, _) => write!(f, "SubIndexed({attr})"),
Self::SubCorrupt(attr) => write!(f, "SubCorrupt({attr})"),
Self::SubUnindexed(attr) => write!(f, "SubUnindexed({attr})"),
Self::PresIndexed(attr) => write!(f, "PresIndexed({attr})"),
Self::PresCorrupt(attr) => write!(f, "PresCorrupt({attr})"),
Self::PresUnindexed(attr) => write!(f, "PresUnindexed({attr})"),
Self::LessThanUnindexed(attr) => write!(f, "LessThanUnindexed({attr})"),
Self::OrUnindexed(plan) => fmt_filterplan_set(f, "OrUnindexed", plan),
Self::OrIndexed(plan) => write!(f, "OrIndexed(len={})", plan.len()),
Self::OrPartial(plan) => fmt_filterplan_set(f, "OrPartial", plan),
Self::OrPartialThreshold(plan) => fmt_filterplan_set(f, "OrPartialThreshold", plan),
Self::AndEmptyCand(plan) => write!(f, "AndEmptyCand(len={})", plan.len()),
Self::AndUnindexed(plan) => fmt_filterplan_set(f, "AndUnindexed", plan),
Self::AndIndexed(plan) => write!(f, "AndIndexed(len={})", plan.len()),
Self::AndPartial(plan) => fmt_filterplan_set(f, "AndPartial", plan),
Self::AndPartialThreshold(plan) => fmt_filterplan_set(f, "AndPartialThreshold", plan),
Self::AndNot(plan) => write!(f, "AndNot({plan})"),
Self::InclusionInvalid(plan) => fmt_filterplan_set(f, "InclusionInvalid", plan),
Self::InclusionIndexed(plan) => write!(f, "InclusionIndexed(len={})", plan.len()),
}
}
}
/// A `Filter` is a logical set of assertions about the state of an [`Entry`] and /// A `Filter` is a logical set of assertions about the state of an [`Entry`] and
/// it's avas. `Filter`s are built from a set of possible assertions. /// it's avas. `Filter`s are built from a set of possible assertions.
/// ///

View file

@ -1,6 +1,9 @@
use std::time::Duration; use std::time::Duration;
use base64::{engine::general_purpose::STANDARD, Engine as _}; use base64::{
engine::general_purpose::{STANDARD, URL_SAFE},
Engine as _,
};
use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner}; use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
use kanidm_proto::internal::{ApiTokenPurpose, ScimSyncToken}; use kanidm_proto::internal::{ApiTokenPurpose, ScimSyncToken};
@ -532,10 +535,14 @@ impl IdmServerProxyWriteTransaction<'_> {
self.scim_sync_apply_phase_4(&changes.retain, sync_uuid)?; self.scim_sync_apply_phase_4(&changes.retain, sync_uuid)?;
// Final house keeping. Commit the new sync state. // Final house keeping. Commit the new sync state.
self.scim_sync_apply_phase_5(sync_uuid, &changes.to_state) self.scim_sync_apply_phase_5(sync_uuid, &changes.to_state)?;
info!("success");
Ok(())
} }
#[instrument(level = "debug", skip_all)] #[instrument(level = "info", skip_all)]
fn scim_sync_apply_phase_1<'b>( fn scim_sync_apply_phase_1<'b>(
&mut self, &mut self,
sse: &'b ScimSyncUpdateEvent, sse: &'b ScimSyncUpdateEvent,
@ -630,7 +637,7 @@ impl IdmServerProxyWriteTransaction<'_> {
Ok((sync_uuid, sync_authority_set, change_entries, sync_refresh)) Ok((sync_uuid, sync_authority_set, change_entries, sync_refresh))
} }
#[instrument(level = "debug", skip_all)] #[instrument(level = "info", skip_all)]
pub(crate) fn scim_sync_apply_phase_2( pub(crate) fn scim_sync_apply_phase_2(
&mut self, &mut self,
change_entries: &BTreeMap<Uuid, &ScimEntry>, change_entries: &BTreeMap<Uuid, &ScimEntry>,
@ -749,7 +756,7 @@ impl IdmServerProxyWriteTransaction<'_> {
Ok(()) Ok(())
} }
#[instrument(level = "debug", skip_all)] #[instrument(level = "info", skip_all)]
pub(crate) fn scim_sync_apply_phase_refresh_cleanup( pub(crate) fn scim_sync_apply_phase_refresh_cleanup(
&mut self, &mut self,
change_entries: &BTreeMap<Uuid, &ScimEntry>, change_entries: &BTreeMap<Uuid, &ScimEntry>,
@ -922,7 +929,9 @@ impl IdmServerProxyWriteTransaction<'_> {
}) })
.and_then(|secret| match secret { .and_then(|secret| match secret {
ScimAttr::String(value) => { ScimAttr::String(value) => {
STANDARD.decode(value.as_str()) URL_SAFE.decode(value.as_str()).or_else(
|_| STANDARD.decode(value.as_str())
)
.map_err(|_| { .map_err(|_| {
error!("Invalid secret attribute - must be base64 string"); error!("Invalid secret attribute - must be base64 string");
OperationError::InvalidAttribute(format!( OperationError::InvalidAttribute(format!(
@ -1247,7 +1256,7 @@ impl IdmServerProxyWriteTransaction<'_> {
Ok(ModifyList::new_list(mods)) Ok(ModifyList::new_list(mods))
} }
#[instrument(level = "debug", skip_all)] #[instrument(level = "info", skip_all)]
pub(crate) fn scim_sync_apply_phase_3( pub(crate) fn scim_sync_apply_phase_3(
&mut self, &mut self,
change_entries: &BTreeMap<Uuid, &ScimEntry>, change_entries: &BTreeMap<Uuid, &ScimEntry>,
@ -1333,7 +1342,7 @@ impl IdmServerProxyWriteTransaction<'_> {
}) })
} }
#[instrument(level = "debug", skip_all)] #[instrument(level = "info", skip_all)]
pub(crate) fn scim_sync_apply_phase_4( pub(crate) fn scim_sync_apply_phase_4(
&mut self, &mut self,
retain: &ScimSyncRetentionMode, retain: &ScimSyncRetentionMode,
@ -1439,7 +1448,7 @@ impl IdmServerProxyWriteTransaction<'_> {
} }
} }
#[instrument(level = "debug", skip_all)] #[instrument(level = "info", skip_all)]
pub(crate) fn scim_sync_apply_phase_5( pub(crate) fn scim_sync_apply_phase_5(
&mut self, &mut self,
sync_uuid: Uuid, sync_uuid: Uuid,

View file

@ -508,6 +508,8 @@ pub trait AccessControlsTransaction<'a> {
let sync_agmts = self.get_sync_agreements(); let sync_agmts = self.get_sync_agreements();
let r = entries.iter().all(|e| { let r = entries.iter().all(|e| {
debug!(entry_id = %e.get_display_id());
match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) { match apply_modify_access(&me.ident, related_acp.as_slice(), sync_agmts, e) {
ModifyResult::Denied => false, ModifyResult::Denied => false,
ModifyResult::Grant => true, ModifyResult::Grant => true,
@ -634,6 +636,7 @@ pub trait AccessControlsTransaction<'a> {
debug!(?requested_pres, "Requested present set"); debug!(?requested_pres, "Requested present set");
debug!(?requested_rem, "Requested remove set"); debug!(?requested_rem, "Requested remove set");
debug!(?requested_classes, "Requested class set"); debug!(?requested_classes, "Requested class set");
debug!(entry_id = %e.get_display_id());
let sync_agmts = self.get_sync_agreements(); let sync_agmts = self.get_sync_agreements();

View file

@ -39,7 +39,8 @@ impl QueryServerWriteTransaction<'_> {
// Is the candidate set empty? // Is the candidate set empty?
if pre_candidates.is_empty() { if pre_candidates.is_empty() {
warn!(filter = ?de.filter, "delete: no candidates match filter"); warn!("delete: no candidates match filter");
debug!(delete_filter = ?de.filter);
return Err(OperationError::NoMatchingEntries); return Err(OperationError::NoMatchingEntries);
}; };