mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 16:03:21 +00:00
[ee] feat(scim): OAuth2 client-credentials grant for SCIM provisioning
Support OAuth 2.0 client-credentials authentication for SCIM provisioning (e.g. Microsoft Entra ID) alongside the existing static bearer token, so identity providers can use short-lived, rotatable access tokens. Backend (EE companion PR modifies scim_ee.rs): - Unauthenticated token endpoint POST /api/scim_token/token issues a short-lived scope:scim JWT via the client-credentials grant. - has_scim_token validates SCIM JWTs (when OAuth is configured) in addition to the static token — fully backward compatible. - Super-admin config endpoints (generate/rotate/disable) store the client secret hashed (SHA-256). New scim_oauth global setting is agent-worker blocked and live-reloaded. Frontend: - OAuth 2.0 client-credentials section in the SCIM/SAML instance settings: enable toggle, generate-secret (shown once), copyable token endpoint and client ID, regenerate/disable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
710a13a59d
commit
a89fcf72fa
@@ -1475,6 +1475,74 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/scim_token/config:
|
||||
get:
|
||||
summary: get SCIM OAuth client-credentials config status
|
||||
operationId: getScimOauthConfig
|
||||
tags:
|
||||
- setting
|
||||
responses:
|
||||
"200":
|
||||
description: SCIM OAuth config status (never returns the secret)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- enabled
|
||||
- token_endpoint
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
client_id:
|
||||
type: string
|
||||
token_endpoint:
|
||||
type: string
|
||||
delete:
|
||||
summary: disable SCIM OAuth client-credentials
|
||||
operationId: disableScimOauthConfig
|
||||
tags:
|
||||
- setting
|
||||
responses:
|
||||
"204":
|
||||
description: disabled
|
||||
|
||||
/scim_token/config/generate:
|
||||
post:
|
||||
summary: generate or rotate the SCIM OAuth client secret
|
||||
operationId: generateScimOauthSecret
|
||||
tags:
|
||||
- setting
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
token_ttl_secs:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
description: generated credentials, client_secret returned once
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
- token_endpoint
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
client_secret:
|
||||
type: string
|
||||
token_endpoint:
|
||||
type: string
|
||||
|
||||
/settings_u/ruff_config:
|
||||
get:
|
||||
summary: get instance ruff config (unauthenticated)
|
||||
|
||||
@@ -203,6 +203,10 @@ lazy_static::lazy_static! {
|
||||
pub static ref REQUEST_SIZE_LIMIT: Arc<RwLock<usize>> = Arc::new(RwLock::new(DEFAULT_BODY_LIMIT));
|
||||
|
||||
pub static ref SCIM_TOKEN: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
// Raw JSON of the SCIM OAuth2 client-credentials config (client_id +
|
||||
// hashed client_secret + optional token TTL). Parsed lazily at the token
|
||||
// endpoint; `None` disables the OAuth grant entirely (see has_scim_token).
|
||||
pub static ref SCIM_OAUTH_CONFIG: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
pub static ref SAML_METADATA: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
|
||||
|
||||
@@ -748,6 +752,10 @@ pub async fn run_server(
|
||||
scim_oss::global_service()
|
||||
.route_layer(axum::middleware::from_fn(has_scim_token)),
|
||||
)
|
||||
// Unauthenticated: the OAuth2 client-credentials token endpoint
|
||||
// authenticates itself via client_id/client_secret, so it must
|
||||
// NOT sit behind has_scim_token.
|
||||
.nest("/scim_token", scim_oss::token_service())
|
||||
.nest("/tokens", token::global_service())
|
||||
.nest("/concurrency_groups", concurrency_groups::global_service())
|
||||
.nest("/scripts_u", scripts::global_unauthed_service())
|
||||
|
||||
@@ -18,6 +18,11 @@ pub fn global_service() -> Router {
|
||||
Router::new().route("/ee", get(ee))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub fn token_service() -> Router {
|
||||
Router::new().route("/ee", get(ee))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub async fn ee() -> String {
|
||||
return "Enterprise Edition".to_string();
|
||||
|
||||
Reference in New Issue
Block a user