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
