about summary refs log tree commit diff
path: root/pkgs/by-name/ka
diff options
context:
space:
mode:
authorAdam C. Stephens2024-08-16 10:15:22 -0400
committerGitHub2024-08-16 10:15:22 -0400
commitc49d0387e0b2ee9a53f5298eaaa6b2d37809962f (patch)
treee6088ac8e71d4377102de4c85060a15b1ec9bde3 /pkgs/by-name/ka
parent5c30512e09c15ff9d5a214d75093eb368e83829c (diff)
parent558fa6abc6d7ea107003d3be64c7861ec7aa2a1f (diff)
Merge pull request #251598 from oddlama/feat-kanidm-provision
nixos/kanidm: add basic provisioning
Diffstat (limited to 'pkgs/by-name/ka')
-rw-r--r--pkgs/by-name/ka/kanidm-provision/package.nix29
-rw-r--r--pkgs/by-name/ka/kanidm/package.nix17
-rw-r--r--pkgs/by-name/ka/kanidm/patches/oauth2-basic-secret-modify.patch303
-rw-r--r--pkgs/by-name/ka/kanidm/patches/recover-account.patch173
4 files changed, 521 insertions, 1 deletions
diff --git a/pkgs/by-name/ka/kanidm-provision/package.nix b/pkgs/by-name/ka/kanidm-provision/package.nix
new file mode 100644
index 000000000000..47ccb70f86f6
--- /dev/null
+++ b/pkgs/by-name/ka/kanidm-provision/package.nix
@@ -0,0 +1,29 @@
+{
+  lib,
+  rustPlatform,
+  fetchFromGitHub,
+}:
+rustPlatform.buildRustPackage rec {
+  pname = "kanidm-provision";
+  version = "1.1.1";
+
+  src = fetchFromGitHub {
+    owner = "oddlama";
+    repo = "kanidm-provision";
+    rev = "v${version}";
+    hash = "sha256-tX24cszmWu7kB5Eoa3OrPqU1bayD62OpAV12U0ayoEo=";
+  };
+
+  cargoHash = "sha256-Ok8A47z5Z3QW4teql/4RyDlox/nrhkdA6IN/qJm13bM=";
+
+  meta = with lib; {
+    description = "A small utility to help with kanidm provisioning";
+    homepage = "https://github.com/oddlama/kanidm-provision";
+    license = with licenses; [
+      asl20
+      mit
+    ];
+    maintainers = with maintainers; [ oddlama ];
+    mainProgram = "kanidm-provision";
+  };
+}
diff --git a/pkgs/by-name/ka/kanidm/package.nix b/pkgs/by-name/ka/kanidm/package.nix
index febfec0c16f9..dc032583ae07 100644
--- a/pkgs/by-name/ka/kanidm/package.nix
+++ b/pkgs/by-name/ka/kanidm/package.nix
@@ -13,6 +13,14 @@
 , pam
 , bashInteractive
 , rust-jemalloc-sys
+, kanidm
+# If this is enabled, kanidm will be built with two patches allowing both
+# oauth2 basic secrets and admin credentials to be provisioned.
+# This is NOT officially supported (and will likely never be),
+# see https://github.com/kanidm/kanidm/issues/1747.
+# Please report any provisioning-related errors to
+# https://github.com/oddlama/kanidm-provision/issues/ instead.
+, enableSecretProvisioning ? false
 }:
 
 let
@@ -33,6 +41,11 @@ rustPlatform.buildRustPackage rec {
 
   KANIDM_BUILD_PROFILE = "release_nixos_${arch}";
 
+  patches = lib.optionals enableSecretProvisioning [
+    ./patches/oauth2-basic-secret-modify.patch
+    ./patches/recover-account.patch
+  ];
+
   postPatch =
     let
       format = (formats.toml { }).generate "${KANIDM_BUILD_PROFILE}.toml";
@@ -94,10 +107,12 @@ rustPlatform.buildRustPackage rec {
 
   passthru = {
     tests = {
-      inherit (nixosTests) kanidm;
+      inherit (nixosTests) kanidm kanidm-provisioning;
     };
 
     updateScript = nix-update-script { };
+    inherit enableSecretProvisioning;
+    withSecretProvisioning = kanidm.override { enableSecretProvisioning = true; };
   };
 
   meta = with lib; {
diff --git a/pkgs/by-name/ka/kanidm/patches/oauth2-basic-secret-modify.patch b/pkgs/by-name/ka/kanidm/patches/oauth2-basic-secret-modify.patch
new file mode 100644
index 000000000000..afff94ca51e9
--- /dev/null
+++ b/pkgs/by-name/ka/kanidm/patches/oauth2-basic-secret-modify.patch
@@ -0,0 +1,303 @@
+From 44dfbc2b9dccce86c7d7e7b54db4c989344b8c56 Mon Sep 17 00:00:00 2001
+From: oddlama <oddlama@oddlama.org>
+Date: Mon, 12 Aug 2024 23:17:25 +0200
+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    |  6 +++++
+ 4 files changed, 82 insertions(+), 1 deletion(-)
+
+diff --git a/server/core/src/actors/v1_write.rs b/server/core/src/actors/v1_write.rs
+index e00a969fb..1cacc67b8 100644
+--- a/server/core/src/actors/v1_write.rs
++++ b/server/core/src/actors/v1_write.rs
+@@ -315,20 +315,62 @@ impl QueryServerWriteV1 {
+         };
+ 
+         trace!(?del, "Begin delete event");
+ 
+         idms_prox_write
+             .qs_write
+             .delete(&del)
+             .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,
+         fields(uuid = ?eventid)
+     )]
+     pub async fn handle_reviverecycled(
+         &self,
+         client_auth_info: ClientAuthInfo,
+         filter: Filter<FilterInvalid>,
+         eventid: Uuid,
+diff --git a/server/core/src/https/v1.rs b/server/core/src/https/v1.rs
+index 8aba83bb2..f1f815026 100644
+--- a/server/core/src/https/v1.rs
++++ b/server/core/src/https/v1.rs
+@@ -1,17 +1,17 @@
+ //! The V1 API things!
+ 
+ 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};
+ use kanidm_proto::constants::uri::V1_AUTH_VALID;
+ use std::net::IpAddr;
+ use uuid::Uuid;
+ 
+ use kanidm_proto::internal::{
+     ApiToken, AppLink, CUIntentToken, CURequest, CUSessionToken, CUStatus, CreateRequest,
+     CredentialStatus, DeleteRequest, IdentifyUserRequest, IdentifyUserResponse, ModifyRequest,
+@@ -3119,20 +3119,24 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
+         )
+         .route(
+             "/v1/oauth2/:rs_name/_image",
+             post(super::v1_oauth2::oauth2_id_image_post)
+                 .delete(super::v1_oauth2::oauth2_id_image_delete),
+         )
+         .route(
+             "/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)
+                 .delete(super::v1_oauth2::oauth2_id_scopemap_delete),
+         )
+         .route(
+             "/v1/oauth2/:rs_name/_sup_scopemap/:group",
+             post(super::v1_oauth2::oauth2_id_sup_scopemap_post)
+                 .delete(super::v1_oauth2::oauth2_id_sup_scopemap_delete),
+         )
+diff --git a/server/core/src/https/v1_oauth2.rs b/server/core/src/https/v1_oauth2.rs
+index 5e481afab..a771aed04 100644
+--- a/server/core/src/https/v1_oauth2.rs
++++ b/server/core/src/https/v1_oauth2.rs
+@@ -144,20 +144,49 @@ pub(crate) async fn oauth2_id_get_basic_secret(
+ ) -> Result<Json<Option<String>>, WebError> {
+     let filter = oauth2_id(&rs_name);
+     state
+         .qe_r_ref
+         .handle_oauth2_basic_secret_read(client_auth_info, filter, kopid.eventid)
+         .await
+         .map(Json::from)
+         .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}",
+     request_body=ProtoEntry,
+     responses(
+         DefaultApiResponse,
+     ),
+     security(("token_jwt" = [])),
+     tag = "v1/oauth2",
+     operation_id = "oauth2_id_patch"
+diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs
+index f3409649d..42e407b7d 100644
+--- a/server/lib/src/constants/acp.rs
++++ b/server/lib/src/constants/acp.rs
+@@ -645,34 +645,36 @@ lazy_static! {
+             Attribute::Image,
+         ],
+         modify_present_attrs: vec![
+             Attribute::Description,
+             Attribute::DisplayName,
+             Attribute::OAuth2RsName,
+             Attribute::OAuth2RsOrigin,
+             Attribute::OAuth2RsOriginLanding,
+             Attribute::OAuth2RsSupScopeMap,
+             Attribute::OAuth2RsScopeMap,
++            Attribute::OAuth2RsBasicSecret,
+             Attribute::OAuth2AllowInsecureClientDisablePkce,
+             Attribute::OAuth2JwtLegacyCryptoEnable,
+             Attribute::OAuth2PreferShortUsername,
+             Attribute::Image,
+         ],
+         create_attrs: vec![
+             Attribute::Class,
+             Attribute::Description,
+             Attribute::DisplayName,
+             Attribute::OAuth2RsName,
+             Attribute::OAuth2RsOrigin,
+             Attribute::OAuth2RsOriginLanding,
+             Attribute::OAuth2RsSupScopeMap,
+             Attribute::OAuth2RsScopeMap,
++            Attribute::OAuth2RsBasicSecret,
+             Attribute::OAuth2AllowInsecureClientDisablePkce,
+             Attribute::OAuth2JwtLegacyCryptoEnable,
+             Attribute::OAuth2PreferShortUsername,
+             Attribute::Image,
+         ],
+         create_classes: vec![
+             EntryClass::Object,
+             EntryClass::OAuth2ResourceServer,
+             EntryClass::OAuth2ResourceServerBasic,
+             EntryClass::OAuth2ResourceServerPublic,
+@@ -739,36 +741,38 @@ lazy_static! {
+             Attribute::Image,
+         ],
+         modify_present_attrs: vec![
+             Attribute::Description,
+             Attribute::DisplayName,
+             Attribute::OAuth2RsName,
+             Attribute::OAuth2RsOrigin,
+             Attribute::OAuth2RsOriginLanding,
+             Attribute::OAuth2RsSupScopeMap,
+             Attribute::OAuth2RsScopeMap,
++            Attribute::OAuth2RsBasicSecret,
+             Attribute::OAuth2AllowInsecureClientDisablePkce,
+             Attribute::OAuth2JwtLegacyCryptoEnable,
+             Attribute::OAuth2PreferShortUsername,
+             Attribute::OAuth2AllowLocalhostRedirect,
+             Attribute::OAuth2RsClaimMap,
+             Attribute::Image,
+         ],
+         create_attrs: vec![
+             Attribute::Class,
+             Attribute::Description,
+             Attribute::DisplayName,
+             Attribute::OAuth2RsName,
+             Attribute::OAuth2RsOrigin,
+             Attribute::OAuth2RsOriginLanding,
+             Attribute::OAuth2RsSupScopeMap,
+             Attribute::OAuth2RsScopeMap,
++            Attribute::OAuth2RsBasicSecret,
+             Attribute::OAuth2AllowInsecureClientDisablePkce,
+             Attribute::OAuth2JwtLegacyCryptoEnable,
+             Attribute::OAuth2PreferShortUsername,
+             Attribute::OAuth2AllowLocalhostRedirect,
+             Attribute::OAuth2RsClaimMap,
+             Attribute::Image,
+         ],
+         create_classes: vec![
+             EntryClass::Object,
+             EntryClass::OAuth2ResourceServer,
+@@ -840,36 +844,38 @@ lazy_static! {
+             Attribute::Image,
+         ],
+         modify_present_attrs: vec![
+             Attribute::Description,
+             Attribute::DisplayName,
+             Attribute::Name,
+             Attribute::OAuth2RsOrigin,
+             Attribute::OAuth2RsOriginLanding,
+             Attribute::OAuth2RsSupScopeMap,
+             Attribute::OAuth2RsScopeMap,
++            Attribute::OAuth2RsBasicSecret,
+             Attribute::OAuth2AllowInsecureClientDisablePkce,
+             Attribute::OAuth2JwtLegacyCryptoEnable,
+             Attribute::OAuth2PreferShortUsername,
+             Attribute::OAuth2AllowLocalhostRedirect,
+             Attribute::OAuth2RsClaimMap,
+             Attribute::Image,
+         ],
+         create_attrs: vec![
+             Attribute::Class,
+             Attribute::Description,
+             Attribute::Name,
+             Attribute::OAuth2RsName,
+             Attribute::OAuth2RsOrigin,
+             Attribute::OAuth2RsOriginLanding,
+             Attribute::OAuth2RsSupScopeMap,
+             Attribute::OAuth2RsScopeMap,
++            Attribute::OAuth2RsBasicSecret,
+             Attribute::OAuth2AllowInsecureClientDisablePkce,
+             Attribute::OAuth2JwtLegacyCryptoEnable,
+             Attribute::OAuth2PreferShortUsername,
+             Attribute::OAuth2AllowLocalhostRedirect,
+             Attribute::OAuth2RsClaimMap,
+             Attribute::Image,
+         ],
+         create_classes: vec![
+             EntryClass::Object,
+             EntryClass::Account,
+-- 
+2.45.2
+
diff --git a/pkgs/by-name/ka/kanidm/patches/recover-account.patch b/pkgs/by-name/ka/kanidm/patches/recover-account.patch
new file mode 100644
index 000000000000..a344f5a2086f
--- /dev/null
+++ b/pkgs/by-name/ka/kanidm/patches/recover-account.patch
@@ -0,0 +1,173 @@
+From cc8269489b56755714f07eee4671f8aa2659c014 Mon Sep 17 00:00:00 2001
+From: oddlama <oddlama@oddlama.org>
+Date: Mon, 12 Aug 2024 23:17:42 +0200
+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 40c18777f..40d553b40 100644
+--- a/server/core/src/actors/internal.rs
++++ b/server/core/src/actors/internal.rs
+@@ -153,25 +153,26 @@ impl QueryServerWriteV1 {
+     }
+ 
+     #[instrument(
+         level = "info",
+         skip(self, 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 577995615..a967928c9 100644
+--- a/server/daemon/src/main.rs
++++ b/server/daemon/src/main.rs
+@@ -894,27 +894,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.45.2
+