kanidm_1_8: init at 1.8.0

Changelog: https://github.com/kanidm/kanidm/releases/tag/v1.8.0
This commit is contained in:
Adam C. Stephens
2025-11-08 10:06:13 -05:00
parent a4402b2610
commit 00ec07d9e4
7 changed files with 295 additions and 2 deletions

View File

@@ -2,5 +2,5 @@ import ./generic.nix {
version = "1.7.4";
hash = "sha256-nWwwcRmCfKJECYN/5w30W3sDu9BqIGonF4ke8F04x3E=";
cargoHash = "sha256-h5jeQxvYzHRVfNYYh9qKukE4h4nhDyuRou2xuZq4AdM=";
patches = [ ];
eolDate = "2025-12-12";
}

View File

@@ -0,0 +1,5 @@
import ./generic.nix {
version = "1.8.0";
hash = "sha256-KDXiveeAIsWJLJWhqSUBJjd6bDnww046XKd+iXrWJO0=";
cargoHash = "sha256-UDEOXOjfLTEd9j5ey+lbIAS1bAm+JgzIN9LTbYfHdB8=";
}

View File

@@ -16,7 +16,7 @@ For example, when upgrading from 1.4 -> 1.5
### Init new version
1. `cp pkgs/by-name/ka/kanidm/1_4.nix pkgs/by-name/ka/kanidm/1_5.nix`
1. `cp -r pkgs/by-name/ka/kanidm/patches/1_4 pkgs/by-name/ka/kanidm/patches/1_5`
1. `cp -r pkgs/by-name/ka/kanidm/provision-patches/1_4 pkgs/by-name/ka/kanidm/provision-patches/1_5`
1. Update `1_5.nix` hashes/paths, and as needed for upstream changes, `generic.nix`
1. Update `all-packages.nix` to add `kanidm_1_5` and `kanidmWithSecretProvisioning_1_5`, leave default
1. Update the previous release, e.g. `1_4.nix` and set `eolDate = "YYYY-MM-DD"` where the date is 30 days from release of 1.5.

View File

@@ -82,6 +82,9 @@ rustPlatform.buildRustPackage (finalAttrs: {
server_admin_bind_path = socket_path;
server_config_path = "/etc/kanidm/server.toml";
server_ui_pkg_path = "@htmx_ui_pkg_path@";
}
// lib.optionalAttrs (lib.versionAtLeast finalAttrs.version "1.8") {
resolver_service_account_token_path = "/etc/kanidm/token";
};
in
''

View File

@@ -0,0 +1,159 @@
From bebd0ae51344eba2bc9bb8e8bd88f279daf09581 Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Mon, 10 Nov 2025 19:58:39 +0100
Subject: [PATCH 1/2] oauth2 basic secret modify
---
server/core/src/actors/v1_write.rs | 42 +++++++++++++++++++++++++++++
server/core/src/https/v1.rs | 6 ++++-
server/core/src/https/v1_oauth2.rs | 29 ++++++++++++++++++++
server/lib/src/server/migrations.rs | 16 +++++++++++
4 files changed, 92 insertions(+), 1 deletion(-)
diff --git a/server/core/src/actors/v1_write.rs b/server/core/src/actors/v1_write.rs
index 732e826c8..a2b8e503f 100644
--- a/server/core/src/actors/v1_write.rs
+++ b/server/core/src/actors/v1_write.rs
@@ -324,6 +324,48 @@ impl QueryServerWriteV1 {
.and_then(|_| idms_prox_write.commit().map(|_| ()))
}
+ #[instrument(
+ level = "info",
+ skip_all,
+ fields(uuid = ?eventid)
+ )]
+ pub async fn handle_oauth2_basic_secret_write(
+ &self,
+ client_auth_info: ClientAuthInfo,
+ filter: Filter<FilterInvalid>,
+ new_secret: String,
+ eventid: Uuid,
+ ) -> Result<(), OperationError> {
+ // Given a protoEntry, turn this into a modification set.
+ let ct = duration_from_epoch_now();
+ let mut idms_prox_write = self.idms.proxy_write(ct).await?;
+ let ident = idms_prox_write
+ .validate_client_auth_info_to_ident(client_auth_info, ct)
+ .map_err(|e| {
+ admin_error!(err = ?e, "Invalid identity");
+ e
+ })?;
+
+ let modlist = ModifyList::new_purge_and_set(
+ Attribute::OAuth2RsBasicSecret,
+ Value::SecretValue(new_secret),
+ );
+
+ let mdf =
+ ModifyEvent::from_internal_parts(ident, &modlist, &filter, &idms_prox_write.qs_write)
+ .map_err(|e| {
+ admin_error!(err = ?e, "Failed to begin modify during handle_oauth2_basic_secret_write");
+ e
+ })?;
+
+ trace!(?mdf, "Begin modify event");
+
+ idms_prox_write
+ .qs_write
+ .modify(&mdf)
+ .and_then(|_| idms_prox_write.commit())
+ }
+
#[instrument(
level = "info",
skip_all,
diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs
index 7d5beb1f0..210147e0a 100644
--- a/server/core/src/https/v1.rs
+++ b/server/core/src/https/v1.rs
@@ -10,7 +10,7 @@ use axum::extract::{Path, State};
use axum::http::{HeaderMap, HeaderValue};
use axum::middleware::from_fn;
use axum::response::{IntoResponse, Response};
-use axum::routing::{delete, get, post, put};
+use axum::routing::{delete, get, post, put, patch};
use axum::{Extension, Json, Router};
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
use compact_jwt::{Jwk, Jws, JwsSigner};
@@ -3113,6 +3113,10 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
"/v1/oauth2/{rs_name}/_basic_secret",
get(super::v1_oauth2::oauth2_id_get_basic_secret),
)
+ .route(
+ "/v1/oauth2/{rs_name}/_basic_secret",
+ patch(super::v1_oauth2::oauth2_id_patch_basic_secret),
+ )
.route(
"/v1/oauth2/{rs_name}/_scopemap/{group}",
post(super::v1_oauth2::oauth2_id_scopemap_post)
diff --git a/server/core/src/https/v1_oauth2.rs b/server/core/src/https/v1_oauth2.rs
index f399539bc..ffad9921e 100644
--- a/server/core/src/https/v1_oauth2.rs
+++ b/server/core/src/https/v1_oauth2.rs
@@ -151,6 +151,35 @@ pub(crate) async fn oauth2_id_get_basic_secret(
.map_err(WebError::from)
}
+#[utoipa::path(
+ patch,
+ path = "/v1/oauth2/{rs_name}/_basic_secret",
+ request_body=ProtoEntry,
+ responses(
+ DefaultApiResponse,
+ ),
+ security(("token_jwt" = [])),
+ tag = "v1/oauth2",
+ operation_id = "oauth2_id_patch_basic_secret"
+)]
+/// Overwrite the basic secret for a given OAuth2 Resource Server.
+#[instrument(level = "info", skip(state, new_secret))]
+pub(crate) async fn oauth2_id_patch_basic_secret(
+ State(state): State<ServerState>,
+ Extension(kopid): Extension<KOpId>,
+ VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
+ Path(rs_name): Path<String>,
+ Json(new_secret): Json<String>,
+) -> Result<Json<()>, WebError> {
+ let filter = oauth2_id(&rs_name);
+ state
+ .qe_w_ref
+ .handle_oauth2_basic_secret_write(client_auth_info, filter, new_secret, kopid.eventid)
+ .await
+ .map(Json::from)
+ .map_err(WebError::from)
+}
+
#[utoipa::path(
patch,
path = "/v1/oauth2/{rs_name}",
diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs
index a916eced2..94327e938 100644
--- a/server/lib/src/server/migrations.rs
+++ b/server/lib/src/server/migrations.rs
@@ -172,6 +172,22 @@ impl QueryServer {
reload_required = true;
};
+ // secret provisioning: allow idm_admin to modify OAuth2RsBasicSecret.
+ write_txn.internal_modify_uuid(
+ UUID_IDM_ACP_OAUTH2_MANAGE_V1,
+ &ModifyList::new_append(
+ Attribute::AcpCreateAttr,
+ Attribute::OAuth2RsBasicSecret.into(),
+ ),
+ )?;
+ write_txn.internal_modify_uuid(
+ UUID_IDM_ACP_OAUTH2_MANAGE_V1,
+ &ModifyList::new_append(
+ Attribute::AcpModifyPresentAttr,
+ Attribute::OAuth2RsBasicSecret.into(),
+ ),
+ )?;
+
// Execute whatever operations we have batched up and ready to go. This is needed
// to preserve ordering of the operations - if we reloaded after a remigrate then
// we would have skipped the patch level fix which needs to have occurred *first*.
--
2.51.0

View File

@@ -0,0 +1,122 @@
From 29dab03201185675d116dd5da6928c6ca3ad30ff Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Mon, 10 Nov 2025 20:01:07 +0100
Subject: [PATCH 2/2] recover account
---
server/core/src/actors/internal.rs | 5 +++--
server/core/src/admin.rs | 6 +++---
server/daemon/src/main.rs | 23 ++++++++++++++++++++++-
server/daemon/src/opt.rs | 7 +++++++
4 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/server/core/src/actors/internal.rs b/server/core/src/actors/internal.rs
index b3708f36d..6a52735fc 100644
--- a/server/core/src/actors/internal.rs
+++ b/server/core/src/actors/internal.rs
@@ -186,17 +186,18 @@ impl QueryServerWriteV1 {
#[instrument(
level = "info",
- skip(self, eventid),
+ skip(self, password, eventid),
fields(uuid = ?eventid)
)]
pub(crate) async fn handle_admin_recover_account(
&self,
name: String,
+ password: Option<String>,
eventid: Uuid,
) -> Result<String, OperationError> {
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write(ct).await?;
- let pw = idms_prox_write.recover_account(name.as_str(), None)?;
+ let pw = idms_prox_write.recover_account(name.as_str(), password.as_deref())?;
idms_prox_write.commit().map(|()| pw)
}
diff --git a/server/core/src/admin.rs b/server/core/src/admin.rs
index b74cc90c5..660e3de8f 100644
--- a/server/core/src/admin.rs
+++ b/server/core/src/admin.rs
@@ -24,7 +24,7 @@ pub use kanidm_proto::internal::{
#[derive(Serialize, Deserialize, Debug)]
pub enum AdminTaskRequest {
- RecoverAccount { name: String },
+ RecoverAccount { name: String, password: Option<String> },
DisableAccount { name: String },
ShowReplicationCertificate,
RenewReplicationCertificate,
@@ -334,8 +334,8 @@ async fn handle_client(
let resp = async {
match req {
- AdminTaskRequest::RecoverAccount { name } => {
- match server_rw.handle_admin_recover_account(name, eventid).await {
+ AdminTaskRequest::RecoverAccount { name, password } => {
+ match server_rw.handle_admin_recover_account(name, password, eventid).await {
Ok(password) => AdminTaskResponse::RecoverAccount { password },
Err(e) => {
error!(err = ?e, "error during recover-account");
diff --git a/server/daemon/src/main.rs b/server/daemon/src/main.rs
index 2ad7830cc..52fa8d2d9 100644
--- a/server/daemon/src/main.rs
+++ b/server/daemon/src/main.rs
@@ -832,13 +832,34 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
.await;
}
}
- KanidmdOpt::RecoverAccount { name } => {
+ KanidmdOpt::RecoverAccount { name, from_environment } => {
info!("Running account recovery ...");
let output_mode: ConsoleOutputMode = opt.output_mode.into();
+ let password = if *from_environment {
+ match std::env::var("KANIDM_RECOVER_ACCOUNT_PASSWORD_FILE") {
+ Ok(path) => match tokio::fs::read_to_string(&path).await {
+ Ok(contents) => Some(contents),
+ Err(e) => {
+ error!("Failed to read password file '{}': {}", path, e);
+ return ExitCode::FAILURE;
+ }
+ },
+ Err(_) => match std::env::var("KANIDM_RECOVER_ACCOUNT_PASSWORD") {
+ Ok(val) => Some(val),
+ Err(_) => {
+ error!("Neither KANIDM_RECOVER_ACCOUNT_PASSWORD_FILE nor KANIDM_RECOVER_ACCOUNT_PASSWORD was set");
+ return ExitCode::FAILURE;
+ }
+ }
+ }
+ } else {
+ None
+ };
submit_admin_req(
config.adminbindpath.as_str(),
AdminTaskRequest::RecoverAccount {
name: name.to_owned(),
+ password,
},
output_mode,
)
diff --git a/server/daemon/src/opt.rs b/server/daemon/src/opt.rs
index 05c5b9fb3..834b8f9cf 100644
--- a/server/daemon/src/opt.rs
+++ b/server/daemon/src/opt.rs
@@ -158,6 +158,13 @@ enum KanidmdOpt {
#[clap(value_parser)]
/// The account name to recover credentials for.
name: String,
+ /// Use a password given via an environment variable.
+ /// - `KANIDM_RECOVER_ACCOUNT_PASSWORD_FILE` takes precedence and reads the desired
+ /// password from the given file
+ /// - `KANIDM_RECOVER_ACCOUNT_PASSWORD` directly takes a
+ /// password - beware that this will leave the password in the environment
+ #[clap(long = "from-environment")]
+ from_environment: bool,
},
#[clap(name = "disable-account")]
/// Disable an account so that it can not be used. This can be reset with `recover-account`.
--
2.51.0

View File

@@ -9214,10 +9214,14 @@ with pkgs;
kanidm_1_7 = callPackage ../servers/kanidm/1_7.nix {
kanidmWithSecretProvisioning = kanidmWithSecretProvisioning_1_7;
};
kanidm_1_8 = callPackage ../servers/kanidm/1_8.nix {
kanidmWithSecretProvisioning = kanidmWithSecretProvisioning_1_8;
};
kanidmWithSecretProvisioning_1_5 = kanidm_1_5.override { enableSecretProvisioning = true; };
kanidmWithSecretProvisioning_1_6 = kanidm_1_6.override { enableSecretProvisioning = true; };
kanidmWithSecretProvisioning_1_7 = kanidm_1_7.override { enableSecretProvisioning = true; };
kanidmWithSecretProvisioning_1_8 = kanidm_1_8.override { enableSecretProvisioning = true; };
knot-resolver = callPackage ../servers/dns/knot-resolver {
systemd = systemdMinimal; # in closure already anyway