kanidm: cleanup versions before 1.10 and improve release process (#569150)

This commit is contained in:
Tom Herbers
2026-10-13 22:50:29 +00:00
committed by GitHub
18 changed files with 354 additions and 1580 deletions

View File

@@ -1,6 +0,0 @@
import ./generic.nix {
version = "1.8.6";
hash = "sha256-+NZs8HHHmWGmlNNymIlbV7E28RiaGLmvtZkCWZJQvMk=";
cargoHash = "sha256-NKCQRIxvWAT5saMO85noYZtm7h0OPFhWFqudvkQPYTE=";
unsupported = true;
}

View File

@@ -1,6 +0,0 @@
import ./generic.nix {
version = "1.9.4";
hash = "sha256-sz4jbnaRU+mUCBwxODMGkKnk9DMAmD6cyPyCbYp6aOQ=";
cargoHash = "sha256-1U262qRtYzRTQZLNF027LlCASCvbxmuBvZJpDPnI/vU=";
unsupported = true;
}

View File

@@ -6,36 +6,36 @@ Version upgrades are only supported on adjacent releases, with no support for ju
To ensure we provide sufficient coverage for upgrading, we will aim to have two or three releases in tree at any given time.
Unsupported versions will be marked as vulnerable (lacking an "unsupported" mechanism), but built by hydra to avoid pushing the very large rebuild on users.
The default version will be updated with each new version, but the default will not be backported.
It is expected that stable users will have to manually specify the version, and update that version, throughout the lifecycle of a NixOS release.
## New release
For example, when upgrading from 1.4 -> 1.5
For example, when upgrading from 1.11 to 1.12:
### 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/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.
1. Update default in `nixos/tests/kanidm.nix` and `nixos/tests/kanidm-provisioning.nix`
1. Create commit, `kanidm_1_5: init at 1.5.0` - this is the only commit that will be backported
```sh
./pkgs/servers/kanidm/release.py new-release 1.12 [--commit]
```
### Backport to stable
1. Manually create a backport using _only_ the init commit
Latest point release will be found, eolDate for previous version computed from .0 release.
## Remove release
Kanidm versions are supported for 30 days after the release of new versions. Following the example above, 1.5.x superseding 1.4.x in 30 days, do the following near the end of the 30-day window:
### Mark unsupported and backport
1. Update `pkgs/by-name/ka/kanidm/1_4.nix` by adding `unsupported = true;`
1. Update `pkgs/top-level/release.nix` and add `kanidm_1_4-1.4.6` and `kanidmWithSecretProvisioning_1_4-1.4.6` to `permittedInsecurePackages`
1. Create commit `kanidm_1_4: mark EOL`, this commit alone should be backported
```sh
./pkgs/servers/kanidm/release.py mark-unsupported 1.11 [--commit]
```
1. Remove the third oldest release from `all-packages.nix`, e.g. 1.3.x continuing the example. Remove `kanidm_1_3` and `kanidmWithSecretProvisioning_1_3`
1. Update `pkgs/top-level/release.nix` and remove `kanidm_1_3*` from `permittedInsecurePackages`
1. Update `pkgs/top-level/aliases.nix` and add `kanidm_1_4` and `kanidmWithSecretProvisioning_1_4`
1. Remove `pkgs/by-name/ka/kanidm/1_3.nix`
Create(s) commit `kanidm_1_11: mark EOL`; this commit alone should be backported.
### Cleanup master
Retain the two newest releases for adjacent upgrades. Remove older releases explicitly:
```sh
./pkgs/servers/kanidm/release.py remove-release 1.10 [--commit]
```
Do not backport

View File

@@ -18,7 +18,6 @@
nix-update-script,
pkg-config,
udev,
openssl,
sqlite,
pam,
bashInteractive,
@@ -76,38 +75,19 @@ rustPlatform.buildRustPackage (finalAttrs: {
socket_path = if stdenv.hostPlatform.isLinux then "/run/kanidmd/sock" else "/var/run/kanidm.socket";
profile = {
cpu_flags = if stdenv.hostPlatform.isx86_64 then "x86_64_legacy" else "none";
}
// lib.optionalAttrs (lib.versionAtLeast finalAttrs.version "1.5") {
client_config_path = "/etc/kanidm/config";
resolver_config_path = "/etc/kanidm/unixd";
resolver_unix_shell_path = "${lib.getBin bashInteractive}/bin/bash";
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";
}
// lib.optionalAttrs (lib.versionAtLeast finalAttrs.version "1.9") {
server_migration_path = "/etc/kanidm/migrations.d";
};
# lower required rust-version in Cargo.toml to allow backporting
rustVersion =
if lib.versionAtLeast finalAttrs.version "1.11" then
{
from = "1.96";
to = "1.95";
}
else
null;
in
''
cp ${format profile} libs/profiles/${finalAttrs.env.KANIDM_BUILD_PROFILE}.toml
substituteInPlace libs/profiles/${finalAttrs.env.KANIDM_BUILD_PROFILE}.toml --replace-fail '@htmx_ui_pkg_path@' "$out/ui/hpkg"
''
+ lib.optionalString (rustVersion != null) ''
substituteInPlace Cargo.toml \
--replace-fail 'rust-version = "${rustVersion.from}"' 'rust-version = "${rustVersion.to}"'
'';
nativeBuildInputs = [
@@ -122,9 +102,6 @@ rustPlatform.buildRustPackage (finalAttrs: {
]
++ lib.optionals stdenv.hostPlatform.isLinux [
udev
]
++ lib.optionals (lib.versionOlder finalAttrs.version "1.10") [
openssl
];
# The UI needs to be in place before the tests are run.

View File

@@ -1,199 +0,0 @@
From cf50a972b446b0ae051cfa4b01d82a4f8077386e Mon Sep 17 00:00:00 2001
From: Benjamin Bädorf <hello@benjaminbaedorf.eu>
Date: Fri, 28 Mar 2025 19:27:42 +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/constants/acp.rs | 8 ++++++
4 files changed, 84 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 c410a4b5d..cc67cac6c 100644
--- a/server/core/src/https/v1.rs
+++ b/server/core/src/https/v1.rs
@@ -4,7 +4,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};
@@ -3127,6 +3127,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/constants/acp.rs b/server/lib/src/constants/acp.rs
index 7c0487745..3cd83ad52 100644
--- a/server/lib/src/constants/acp.rs
+++ b/server/lib/src/constants/acp.rs
@@ -665,6 +665,7 @@ lazy_static! {
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
+ Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
@@ -681,6 +682,7 @@ lazy_static! {
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
+ Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
@@ -766,6 +768,7 @@ lazy_static! {
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
+ Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
@@ -783,6 +786,7 @@ lazy_static! {
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
+ Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
@@ -871,6 +875,7 @@ lazy_static! {
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
+ Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
@@ -889,6 +894,7 @@ lazy_static! {
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
+ Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
@@ -980,6 +986,7 @@ lazy_static! {
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
+ Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
@@ -999,6 +1006,7 @@ lazy_static! {
Attribute::OAuth2RsOriginLanding,
Attribute::OAuth2RsSupScopeMap,
Attribute::OAuth2RsScopeMap,
+ Attribute::OAuth2RsBasicSecret,
Attribute::OAuth2AllowInsecureClientDisablePkce,
Attribute::OAuth2JwtLegacyCryptoEnable,
Attribute::OAuth2PreferShortUsername,
--
2.47.2

View File

@@ -1,174 +0,0 @@
From c8ed69efe3f702b19834c2659be1dd3ec2d41c17 Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Fri, 1 Nov 2024 12:27:43 +0100
Subject: [PATCH 2/2] recover account
---
server/core/src/actors/internal.rs | 3 ++-
server/core/src/admin.rs | 6 +++---
server/daemon/src/main.rs | 14 +++++++++++++-
server/daemon/src/opt.rs | 4 ++++
4 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/server/core/src/actors/internal.rs b/server/core/src/actors/internal.rs
index 420e72c6c..5c4353116 100644
--- a/server/core/src/actors/internal.rs
+++ b/server/core/src/actors/internal.rs
@@ -171,25 +171,26 @@ 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)
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
pub(crate) async fn handle_domain_raise(&self, eventid: Uuid) -> Result<u32, OperationError> {
diff --git a/server/core/src/admin.rs b/server/core/src/admin.rs
index 90ccb1927..85e31ddef 100644
--- a/server/core/src/admin.rs
+++ b/server/core/src/admin.rs
@@ -17,21 +17,21 @@ use tokio_util::codec::{Decoder, Encoder, Framed};
use tracing::{span, Instrument, Level};
use uuid::Uuid;
pub use kanidm_proto::internal::{
DomainInfo as ProtoDomainInfo, DomainUpgradeCheckReport as ProtoDomainUpgradeCheckReport,
DomainUpgradeCheckStatus as ProtoDomainUpgradeCheckStatus,
};
#[derive(Serialize, Deserialize, Debug)]
pub enum AdminTaskRequest {
- RecoverAccount { name: String },
+ RecoverAccount { name: String, password: Option<String> },
ShowReplicationCertificate,
RenewReplicationCertificate,
RefreshReplicationConsumer,
DomainShow,
DomainUpgradeCheck,
DomainRaise,
DomainRemigrate { level: Option<u32> },
}
#[derive(Serialize, Deserialize, Debug)]
@@ -302,22 +302,22 @@ async fn handle_client(
let mut reqs = Framed::new(sock, ServerCodec);
trace!("Waiting for requests ...");
while let Some(Ok(req)) = reqs.next().await {
// Setup the logging span
let eventid = Uuid::new_v4();
let nspan = span!(Level::INFO, "handle_admin_client_request", uuid = ?eventid);
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");
AdminTaskResponse::Error
}
}
}
AdminTaskRequest::ShowReplicationCertificate => match repl_ctrl_tx.as_mut() {
Some(ctrl_tx) => show_replication_certificate(ctrl_tx).await,
None => {
diff --git a/server/daemon/src/main.rs b/server/daemon/src/main.rs
index 7486d34a8..784106352 100644
--- a/server/daemon/src/main.rs
+++ b/server/daemon/src/main.rs
@@ -903,27 +903,39 @@ async fn kanidm_main(
} else {
let output_mode: ConsoleOutputMode = commonopts.output_mode.to_owned().into();
submit_admin_req(
config.adminbindpath.as_str(),
AdminTaskRequest::RefreshReplicationConsumer,
output_mode,
)
.await;
}
}
- KanidmdOpt::RecoverAccount { name, commonopts } => {
+ KanidmdOpt::RecoverAccount { name, from_environment, commonopts } => {
info!("Running account recovery ...");
let output_mode: ConsoleOutputMode = commonopts.output_mode.to_owned().into();
+ let password = if *from_environment {
+ match std::env::var("KANIDM_RECOVER_ACCOUNT_PASSWORD") {
+ Ok(val) => Some(val),
+ _ => {
+ error!("Environment variable KANIDM_RECOVER_ACCOUNT_PASSWORD not set");
+ return ExitCode::FAILURE;
+ }
+ }
+ } else {
+ None
+ };
submit_admin_req(
config.adminbindpath.as_str(),
AdminTaskRequest::RecoverAccount {
name: name.to_owned(),
+ password,
},
output_mode,
)
.await;
}
KanidmdOpt::Database {
commands: DbCommands::Reindex(_copt),
} => {
info!("Running in reindex mode ...");
reindex_server_core(&config).await;
diff --git a/server/daemon/src/opt.rs b/server/daemon/src/opt.rs
index f1b45a5b3..9c013e32e 100644
--- a/server/daemon/src/opt.rs
+++ b/server/daemon/src/opt.rs
@@ -229,20 +229,24 @@ enum KanidmdOpt {
/// Create a self-signed ca and tls certificate in the locations listed from the
/// configuration. These certificates should *not* be used in production, they
/// are for testing and evaluation only!
CertGenerate(CommonOpt),
#[clap(name = "recover-account")]
/// Recover an account's password
RecoverAccount {
#[clap(value_parser)]
/// The account name to recover credentials for.
name: String,
+ /// Use the password given in the environment variable
+ /// `KANIDM_RECOVER_ACCOUNT_PASSWORD` instead of generating one.
+ #[clap(long = "from-environment")]
+ from_environment: bool,
#[clap(flatten)]
commonopts: CommonOpt,
},
/// Display this server's replication certificate
ShowReplicationCertificate {
#[clap(flatten)]
commonopts: CommonOpt,
},
/// Renew this server's replication certificate
RenewReplicationCertificate {
--
2.46.1

View File

@@ -1,159 +0,0 @@
From fc26fe5ac9e9cd65af82609c5a4966c8f756ea0f Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Fri, 21 Mar 2025 16:07:54 +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 30de387b8..a11aa8ecd 100644
--- a/server/core/src/https/v1.rs
+++ b/server/core/src/https/v1.rs
@@ -4,7 +4,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};
@@ -3129,6 +3129,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 fd0bca8db..8621714f2 100644
--- a/server/lib/src/server/migrations.rs
+++ b/server/lib/src/server/migrations.rs
@@ -171,6 +171,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.49.0

View File

@@ -1,122 +0,0 @@
From 229165abe5be596fc2be8e285884813a1b5a38c8 Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Fri, 21 Mar 2025 16:08:15 +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 420e72c6c..e252bca51 100644
--- a/server/core/src/actors/internal.rs
+++ b/server/core/src/actors/internal.rs
@@ -172,17 +172,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 90ccb1927..85e31ddef 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> },
ShowReplicationCertificate,
RenewReplicationCertificate,
RefreshReplicationConsumer,
@@ -309,8 +309,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 c3b40faa0..2a57a307c 100644
--- a/server/daemon/src/main.rs
+++ b/server/daemon/src/main.rs
@@ -923,13 +923,34 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
.await;
}
}
- KanidmdOpt::RecoverAccount { name, commonopts } => {
+ KanidmdOpt::RecoverAccount { name, from_environment, commonopts } => {
info!("Running account recovery ...");
let output_mode: ConsoleOutputMode = commonopts.output_mode.to_owned().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 f1b45a5b3..ca19fb6a5 100644
--- a/server/daemon/src/opt.rs
+++ b/server/daemon/src/opt.rs
@@ -236,6 +236,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(flatten)]
commonopts: CommonOpt,
},
--
2.49.0

View File

@@ -1,159 +0,0 @@
From fc26fe5ac9e9cd65af82609c5a4966c8f756ea0f Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Fri, 21 Mar 2025 16:07:54 +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 30de387b8..a11aa8ecd 100644
--- a/server/core/src/https/v1.rs
+++ b/server/core/src/https/v1.rs
@@ -4,7 +4,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};
@@ -3129,6 +3129,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 fd0bca8db..8621714f2 100644
--- a/server/lib/src/server/migrations.rs
+++ b/server/lib/src/server/migrations.rs
@@ -171,6 +171,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.49.0

View File

@@ -1,122 +0,0 @@
From 229165abe5be596fc2be8e285884813a1b5a38c8 Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Fri, 21 Mar 2025 16:08:15 +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 420e72c6c..e252bca51 100644
--- a/server/core/src/actors/internal.rs
+++ b/server/core/src/actors/internal.rs
@@ -172,17 +172,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 90ccb1927..85e31ddef 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,
@@ -309,8 +309,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 c3b40faa0..2a57a307c 100644
--- a/server/daemon/src/main.rs
+++ b/server/daemon/src/main.rs
@@ -923,13 +923,34 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
.await;
}
}
- KanidmdOpt::RecoverAccount { name, commonopts } => {
+ KanidmdOpt::RecoverAccount { name, from_environment, commonopts } => {
info!("Running account recovery ...");
let output_mode: ConsoleOutputMode = commonopts.output_mode.to_owned().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 f1b45a5b3..ca19fb6a5 100644
--- a/server/daemon/src/opt.rs
+++ b/server/daemon/src/opt.rs
@@ -236,6 +236,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(flatten)]
commonopts: CommonOpt,
},
--
2.49.0

View File

@@ -1,159 +0,0 @@
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

@@ -1,122 +0,0 @@
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

@@ -1,159 +0,0 @@
From 3ba8714f99d57e1434aeed316f6fd9cd115198dc Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Sat, 14 Feb 2026 12:23:49 +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 47be2002e..4e3522e1a 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 97be6d666..33778ae08 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};
@@ -3123,6 +3123,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 c6209c750..16dd3e348 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 e5dcdfc04..add51fba5 100644
--- a/server/lib/src/server/migrations.rs
+++ b/server/lib/src/server/migrations.rs
@@ -220,6 +220,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.52.0

View File

@@ -1,128 +0,0 @@
From 8db3febfce8057011156e80a371c4312a79be4cc Mon Sep 17 00:00:00 2001
From: oddlama <oddlama@oddlama.org>
Date: Sat, 14 Feb 2026 12:27:00 +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 | 24 +++++++++++++++++++++++-
server/daemon/src/opt.rs | 7 +++++++
4 files changed, 36 insertions(+), 6 deletions(-)
diff --git a/server/core/src/actors/internal.rs b/server/core/src/actors/internal.rs
index abcc1b27c..2a63d0e9d 100644
--- a/server/core/src/actors/internal.rs
+++ b/server/core/src/actors/internal.rs
@@ -189,17 +189,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 e00eb0476..175a6f661 100644
--- a/server/core/src/admin.rs
+++ b/server/core/src/admin.rs
@@ -23,7 +23,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,
@@ -341,8 +341,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 611022a63..0b2f863e4 100644
--- a/server/daemon/src/main.rs
+++ b/server/daemon/src/main.rs
@@ -370,11 +370,32 @@ fn check_file_ownership(opt: &KanidmdParser) -> Result<(), ExitCode> {
async fn scripting_command(cmd: ScriptingCommand, config: Configuration) -> ExitCode {
match cmd {
- ScriptingCommand::RecoverAccount { name } => {
+ ScriptingCommand::RecoverAccount { name, from_environment } => {
+ 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_json(
config.adminbindpath.as_str(),
AdminTaskRequest::RecoverAccount {
name: name.to_owned(),
+ password,
},
)
.await;
@@ -998,6 +1019,7 @@ async fn kanidm_main(config: Configuration, opt: KanidmdParser) -> ExitCode {
config.adminbindpath.as_str(),
AdminTaskRequest::RecoverAccount {
name: name.to_owned(),
+ password: None,
},
)
.await;
diff --git a/server/daemon/src/opt.rs b/server/daemon/src/opt.rs
index ba5d00fc7..f1497f6dc 100644
--- a/server/daemon/src/opt.rs
+++ b/server/daemon/src/opt.rs
@@ -128,6 +128,13 @@ enum ScriptingCommand {
#[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,
},
/// Backup
Backup {
--
2.52.0

330
pkgs/servers/kanidm/release.py Executable file
View File

@@ -0,0 +1,330 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 --packages python3 nix nix-update nixfmt jujutsu git
# ruff: noqa: EXE005
import argparse
import json
import re
import shutil
import subprocess
from datetime import UTC, date, datetime, timedelta
from pathlib import Path
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parents[3]
PACKAGE_DIR = ROOT / "pkgs/servers/kanidm"
ALL_PACKAGES = ROOT / "pkgs/top-level/all-packages.nix"
HYDRA_RELEASE = ROOT / "pkgs/top-level/release.nix"
ALIASES = ROOT / "pkgs/top-level/aliases.nix"
def run(*command):
subprocess.run(command, cwd=ROOT, check=True)
def format_nix(*paths):
run("nixfmt", *(str(path) for path in paths))
def commit_phase(args, message, *paths):
if not args.commit:
print(f"Commit separately:\n{message}")
return
files = [str(path.relative_to(ROOT)) for path in paths]
if (ROOT / ".jj").is_dir():
run("jj", "commit", "--message", message, *files)
else:
run("git", "add", "--", *files)
run("git", "commit", "--only", "--message", message, "--", *files)
print(f"Committed: {message.splitlines()[0]}.")
def github_json(endpoint):
request = Request(
f"https://api.github.com/repos/kanidm/kanidm/releases{endpoint}",
headers={
"Accept": "application/vnd.github+json",
"User-Agent": "nixpkgs-kanidm-release",
},
)
with urlopen(request) as response:
return json.load(response)
def release_info(release):
releases = github_json("?per_page=10")
versions = [
item["tag_name"].removeprefix("v")
for item in releases
if not item["draft"]
and not item["prerelease"]
and re.fullmatch(
rf"{re.escape(release)}\.[0-9]+",
item["tag_name"].removeprefix("v"),
)
]
if not versions:
raise ValueError(
f"No {release}.x release found in the ten latest GitHub releases"
)
version = max(versions, key=lambda value: int(value.rsplit(".", 1)[1]))
initial = github_json(f"/tags/v{release}.0")
return version, date.fromisoformat(initial["published_at"][:10])
def replace_once(text, old, new):
if text.count(old) != 1:
raise ValueError(f"Expected one occurrence of {old!r}")
return text.replace(old, new, 1)
def set_assignment(path, name, value):
text = path.read_text()
pattern = rf"(?m)^ {name} = [^\n]+;\n"
assignment = f" {name} = {value};\n"
if re.search(pattern, text):
text = re.sub(pattern, assignment, text)
else:
text = replace_once(text, "\n}", f"\n{assignment}}}")
path.write_text(text)
def version_key(version):
return "_".join(version.split(".")[:2])
def bindings(key):
return (
(
f" kanidm_{key} = callPackage ../servers/kanidm/{key}.nix {{\n"
f" kanidmWithSecretProvisioning = kanidmWithSecretProvisioning_{key};\n"
" };\n"
),
f" kanidmWithSecretProvisioning_{key} = kanidm_{key}.override {{ enableSecretProvisioning = true; }};\n",
f" kanidm_{key}\n",
f" kanidmWithSecretProvisioning_{key}\n",
)
def package_info(key):
expression = f"(import ./. {{ config.allowInsecure = true; }}).kanidm_{key}.version"
result = subprocess.run(
["nix", "eval", "--impure", "--json", "--expr", expression],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
version = json.loads(result.stdout)
return {
"version": version,
"names": [
f"kanidm_{key}-{version}",
f"kanidmWithSecretProvisioning_{key}-{version}",
],
}
def without_allowlist_entries(text, key, info):
names = [
f"kanidm_{key}-{info['version']}",
f"kanidmWithSecretProvisioning_{key}-{info['version']}",
]
for name in names:
text = re.sub(rf'(?m)^ "{re.escape(name)}"\n', "", text)
return text
def new_release(args):
previous = max(
(path.stem for path in PACKAGE_DIR.glob("[0-9]*_[0-9]*.nix")),
key=lambda key: tuple(map(int, key.split("_"))),
)
key = version_key(args.release)
source = PACKAGE_DIR / f"{previous}.nix"
target = PACKAGE_DIR / f"{key}.nix"
if key == previous or target.exists():
raise ValueError(f"Release {key} already exists")
version, published_date = release_info(args.release)
text = re.sub(r"(?m)^ (?:unsupported|eolDate) = [^\n]+;\n", "", source.read_text())
shutil.copytree(
PACKAGE_DIR / "provision-patches" / previous,
PACKAGE_DIR / "provision-patches" / key,
)
target.write_text(text)
text = ALL_PACKAGES.read_text()
for old, new in zip(bindings(previous), bindings(key)):
text = replace_once(text, old, old + new)
ALL_PACKAGES.write_text(text)
run(
"nix-update",
f"kanidm_{key}",
"--version",
version,
"--override-filename",
str(target.relative_to(ROOT)),
)
eol_date = published_date + timedelta(days=30)
set_assignment(source, "eolDate", json.dumps(eol_date.isoformat()))
tests = []
for filename, attribute in (
("kanidm.nix", "kanidm"),
("kanidm-provisioning.nix", "kanidmWithSecretProvisioning"),
):
path = ROOT / "nixos/tests" / filename
text = path.read_text()
old = re.search(
rf"_module\.args\.kanidmPackage = pkgs\.lib\.mkDefault pkgs\.{attribute}_[0-9]+_[0-9]+;",
text,
).group(0)
new = f"_module.args.kanidmPackage = pkgs.lib.mkDefault pkgs.{attribute}_{key};"
path.write_text(replace_once(text, old, new))
tests.append(path)
format_nix(target, source, ALL_PACKAGES, *tests)
print(f"Prepared kanidm_{key} at {version}; predecessor EOL: {eol_date}.")
print(
"Review provisioning patches and generic.nix, then build both packages and their passthru.tests."
)
commit_phase(
args,
(
f"kanidm_{key}: init at {version}\n\n"
f"Changelog: https://github.com/kanidm/kanidm/releases/tag/v{version}"
),
target,
PACKAGE_DIR / "provision-patches" / key,
source,
ALL_PACKAGES,
*tests,
)
def mark_unsupported(args):
key = version_key(args.release)
info = package_info(key)
path = PACKAGE_DIR / f"{key}.nix"
set_assignment(path, "unsupported", "true")
text = HYDRA_RELEASE.read_text()
anchor = " permittedInsecurePackages = [\n"
entries = "".join(
f' "{name}"\n' for name in info["names"] if f'"{name}"' not in text
)
HYDRA_RELEASE.write_text(replace_once(text, anchor, anchor + entries))
format_nix(path, HYDRA_RELEASE)
print(f"Marked kanidm_{key} unsupported and permitted both derivations for Hydra.")
commit_phase(args, f"kanidm_{key}: mark EOL", path, HYDRA_RELEASE)
print("Backport the EOL commit.")
def alias_sort_key(name):
return [
int(part) if part.isdigit() else part.lower()
for part in re.split(r"([0-9]+)", name)
]
def remove_release(args):
key = version_key(args.release)
info = package_info(key)
for filename, attribute in (
("kanidm.nix", "kanidm"),
("kanidm-provisioning.nix", "kanidmWithSecretProvisioning"),
):
text = (ROOT / "nixos/tests" / filename).read_text()
if f"pkgs.{attribute}_{key};" in text:
raise ValueError(f"Release {key} is still the default in {filename}")
text = ALL_PACKAGES.read_text()
for binding in bindings(key):
text = replace_once(text, binding, "")
ALL_PACKAGES.write_text(text)
HYDRA_RELEASE.write_text(
without_allowlist_entries(HYDRA_RELEASE.read_text(), key, info)
)
(PACKAGE_DIR / f"{key}.nix").unlink()
shutil.rmtree(PACKAGE_DIR / "provision-patches" / key)
text = ALIASES.read_text()
for attribute in ("kanidm", "kanidmWithSecretProvisioning"):
name = f"{attribute}_{key}"
alias = (
f" {name} = throw \"'{name}' has been removed as it has reached end of life\";"
f" # Added {datetime.now(UTC).date().isoformat()}\n"
)
aliases = list(
re.finditer(
r"(?m)^ (kanidm(?:WithSecretProvisioning)?(?:_[0-9]+_[0-9]+)?)"
r" = [^\n]*\n",
text,
)
)
sort_key = alias_sort_key(name)
position = next(
(
match.start()
for match in aliases
if alias_sort_key(match.group(1)) > sort_key
),
aliases[-1].end(),
)
text = text[:position] + alias + text[position:]
ALIASES.write_text(text)
format_nix(ALL_PACKAGES, HYDRA_RELEASE, ALIASES)
print(f"Removed kanidm_{key}, its provisioning variant, and provisioning patches.")
commit_phase(
args,
f"kanidm_{key}: remove EOL release",
ALL_PACKAGES,
HYDRA_RELEASE,
ALIASES,
PACKAGE_DIR / f"{key}.nix",
PACKAGE_DIR / "provision-patches" / key,
)
print("Do not backport the cleanup commit.")
def minor_release(value):
if not re.fullmatch(r"[0-9]+\.[0-9]+", value):
raise argparse.ArgumentTypeError("Expected a release such as 1.11")
return value
def main():
parser = argparse.ArgumentParser(
description="Execute Kanidm packaging release phases."
)
phases = parser.add_subparsers(dest="phase", required=True)
new = phases.add_parser(
"new-release", help="Add a release and schedule predecessor EOL"
)
new.add_argument("release", type=minor_release)
new.add_argument(
"--commit", action="store_true", help="Commit this phase's changes"
)
new.set_defaults(action=new_release)
unsupported = phases.add_parser(
"mark-unsupported", help="Mark EOL and permit Hydra builds"
)
unsupported.add_argument("release", type=minor_release)
unsupported.add_argument(
"--commit", action="store_true", help="Commit this phase's changes"
)
unsupported.set_defaults(action=mark_unsupported)
remove = phases.add_parser(
"remove-release", help="Remove an obsolete release from master"
)
remove.add_argument("release", type=minor_release)
remove.add_argument(
"--commit", action="store_true", help="Commit this phase's changes"
)
remove.set_defaults(action=remove_release)
args = parser.parse_args()
args.action(args)
if __name__ == "__main__":
main()

View File

@@ -1301,13 +1301,11 @@ mapAliases {
kak-lsp = throw "'kak-lsp' has been renamed to/replaced by 'kakoune-lsp'"; # Converted to throw 2025-10-27
kanagawa-gtk-theme = throw "'kanagawa-gtk-theme' has been removed because it depended on 'gtk-engine-murrine', which was removed because it was unmaintained upstream and depended on GTK 2."; # Added 2026-07-22
kanidm = throw "'kanidm' alias has been removed. You must use a versioned package, e.g. 'kanidm_1_x'."; # Added 2026-01-29
kanidm_1_4 = throw "'kanidm_1_4' has been removed as it has reached end of life"; # Added 2025-06-18
kanidm_1_5 = throw "'kanidm_1_5' has been removed as it has reached end of life"; # Added 2026-01-29
kanidm_1_6 = throw "'kanidm_1_6' has been removed as it has reached end of life"; # Added 2026-01-29
kanidm_1_8 = throw "'kanidm_1_8' has been removed as it has reached end of life"; # Added 2026-10-02
kanidm_1_9 = throw "'kanidm_1_9' has been removed as it has reached end of life"; # Added 2026-10-02
kanidmWithSecretProvisioning = throw "'kanidmWithSecretProvisioning' was removed. You must use a versioned package, e.g. 'kanidmWithSecretProvisioning_1_x'."; # Added 2026-04-30
kanidmWithSecretProvisioning_1_4 = throw "'kanidmWithSecretProvisioning_1_4' has been removed as it has reached end of life"; # Added 2025-06-18
kanidmWithSecretProvisioning_1_5 = throw "'kanidmWithSecretProvisioning_1_5' has been removed as it has reached end of life"; # Added 2026-01-29
kanidmWithSecretProvisioning_1_6 = throw "'kanidmWithSecretProvisioning_1_6' has been removed as it has reached end of life"; # Added 2026-01-29
kanidmWithSecretProvisioning_1_8 = throw "'kanidmWithSecretProvisioning_1_8' has been removed as it has reached end of life"; # Added 2026-10-02
kanidmWithSecretProvisioning_1_9 = throw "'kanidmWithSecretProvisioning_1_9' has been removed as it has reached end of life"; # Added 2026-10-02
kapitano = throw "'kapitano' has been removed, as it is unmaintained upstream"; # Added 2025-10-29
karing = throw "'karing' has been removed, as it is unmaintained in nixpkgs"; # Added 2026-01-31
katawa-shoujo = throw "'katawa-shoujo' has been removed, as it bundles insecure Python 2. You may install 'katawa-shoujo-re-engineered' instead, which is an updated version remade by Fleeting Heartbeat Studios."; # Added 2026-03-27

View File

@@ -7194,12 +7194,6 @@ with pkgs;
inherit
({
kanidm_1_8 = callPackage ../servers/kanidm/1_8.nix {
kanidmWithSecretProvisioning = kanidmWithSecretProvisioning_1_8;
};
kanidm_1_9 = callPackage ../servers/kanidm/1_9.nix {
kanidmWithSecretProvisioning = kanidmWithSecretProvisioning_1_9;
};
kanidm_1_10 = callPackage ../servers/kanidm/1_10.nix {
kanidmWithSecretProvisioning = kanidmWithSecretProvisioning_1_10;
};
@@ -7207,17 +7201,11 @@ with pkgs;
kanidmWithSecretProvisioning = kanidmWithSecretProvisioning_1_11;
};
kanidmWithSecretProvisioning_1_8 = kanidm_1_8.override { enableSecretProvisioning = true; };
kanidmWithSecretProvisioning_1_9 = kanidm_1_9.override { enableSecretProvisioning = true; };
kanidmWithSecretProvisioning_1_10 = kanidm_1_10.override { enableSecretProvisioning = true; };
kanidmWithSecretProvisioning_1_11 = kanidm_1_11.override { enableSecretProvisioning = true; };
})
kanidm_1_8
kanidm_1_9
kanidm_1_10
kanidm_1_11
kanidmWithSecretProvisioning_1_8
kanidmWithSecretProvisioning_1_9
kanidmWithSecretProvisioning_1_10
kanidmWithSecretProvisioning_1_11
;

View File

@@ -44,10 +44,6 @@
# so users choosing to allow don't have to rebuild them every time.
permittedInsecurePackages = [
"olm-3.2.16" # see PR #347899
"kanidm_1_8-1.8.6"
"kanidmWithSecretProvisioning_1_8-1.8.6"
"kanidm_1_9-1.9.4"
"kanidmWithSecretProvisioning_1_9-1.9.4"
"kanidm_1_10-1.10.5"
"kanidmWithSecretProvisioning_1_10-1.10.5"
];