Unverified Commit 59a463f5 authored by oddlama's avatar oddlama
Browse files

kanidm: add optional secret provisioning patches

parent f67d395a
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -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";
@@ -98,6 +111,8 @@ rustPlatform.buildRustPackage rec {
    };

    updateScript = nix-update-script { };
    inherit enableSecretProvisioning;
    withSecretProvisioning = kanidm.override { enableSecretProvisioning = true; };
  };

  meta = with lib; {
+303 −0
Original line number Diff line number Diff line
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
+173 −0
Original line number Diff line number Diff line
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