feat(fleet-auth): unify Zitadel role extraction + request roles via scope (Ch1) #327
@@ -11,9 +11,10 @@ public client). Distinct from the agent/callout machine auth
|
||||
secret), redirect URI `https://fleet-stg.<base>/auth/callback`, post-logout URI
|
||||
`https://fleet-stg.<base>/`. Copy its **Client ID**.
|
||||
1b. **Roles** — the dashboard requires the **`fleet-admin`** project role. Create that
|
||||
role on the project, enable the project's **Assert Roles on Authentication** (so the
|
||||
role lands in the ID token), and grant it to each operator user. Without it they
|
||||
authenticate but get **403 Access denied**.
|
||||
role on the project and grant it to each operator user. The login flow requests
|
||||
roles **in-band** via the OIDC scope `urn:zitadel:iam:org:project:roles`, so the
|
||||
project's **Assert Roles on Authentication** checkbox is **not** needed. Without
|
||||
the role grant, users authenticate but get **403 Access denied**.
|
||||
2. **Seed config** in OpenBao (namespace `fleet-staging`) — the deploy derives every
|
||||
host from `base_domain`, so you set only:
|
||||
- `FleetDeployConfig.operator_oidc_client_id` = the Client ID
|
||||
@@ -49,8 +50,9 @@ on the app's **Development Mode** (Zitadel rejects non-HTTPS redirects otherwise
|
||||
The operator reads `ZitadelAuthConfig` + `OperatorCookieKey` via ConfigClient. The
|
||||
deploy derives `zitadel_base` / `base_url` / `logout_redirect_uri` from `base_domain`
|
||||
(`https://sso-stg.<base>`, `https://fleet-stg.<base>`, `…/`) and fixes
|
||||
`scope = openid profile email`; you supply `client_id`, `trusted_audiences`,
|
||||
`cookie_key_b64`. All endpoints derive from `zitadel_base`:
|
||||
`scope = openid profile email` (the login flow appends
|
||||
`urn:zitadel:iam:org:project:roles` so roles are always asserted); you supply
|
||||
`client_id`, `trusted_audiences`, `cookie_key_b64`. All endpoints derive from `zitadel_base`:
|
||||
`/.well-known/openid-configuration`, `/oauth/v2/authorize`, `/oauth/v2/token`,
|
||||
`/oidc/v1/end_session`.
|
||||
|
||||
|
||||
@@ -134,6 +134,22 @@ pub fn build_logout_url(config: &ZitadelAuthConfig, id_token: &str) -> Result<Ur
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
/// Zitadel's reserved scope that asserts the caller's project roles in the
|
||||
/// token — the in-band alternative to the project's "Assert Roles on
|
||||
/// Authentication" checkbox, which is out-of-band and silently broke the gate.
|
||||
const ZITADEL_PROJECT_ROLES_SCOPE: &str = "urn:zitadel:iam:org:project:roles";
|
||||
|
||||
fn ensure_roles_scope(scope: &str) -> String {
|
||||
if scope
|
||||
.split_whitespace()
|
||||
.any(|s| s == ZITADEL_PROJECT_ROLES_SCOPE)
|
||||
{
|
||||
scope.to_string()
|
||||
} else {
|
||||
format!("{scope} {ZITADEL_PROJECT_ROLES_SCOPE}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_login_attempt(config: &ZitadelAuthConfig) -> Result<LoginAttempt> {
|
||||
let state = random_url_token(32);
|
||||
let pkce_code_verifier = random_url_token(32);
|
||||
@@ -145,7 +161,7 @@ pub fn build_login_attempt(config: &ZitadelAuthConfig) -> Result<LoginAttempt> {
|
||||
.append_pair("client_id", &config.client_id)
|
||||
.append_pair("redirect_uri", &config.redirect_uri())
|
||||
.append_pair("response_type", "code")
|
||||
.append_pair("scope", &config.scope)
|
||||
.append_pair("scope", &ensure_roles_scope(&config.scope))
|
||||
.append_pair("code_challenge", &code_challenge)
|
||||
.append_pair("code_challenge_method", "S256")
|
||||
.append_pair("state", &state)
|
||||
@@ -225,4 +241,18 @@ mod tests {
|
||||
let challenge = pkce_s256_challenge(code_verifier);
|
||||
assert_eq!(challenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_roles_scope_appends_when_absent() {
|
||||
assert_eq!(
|
||||
ensure_roles_scope("openid profile email"),
|
||||
"openid profile email urn:zitadel:iam:org:project:roles"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_roles_scope_is_idempotent() {
|
||||
let s = "openid urn:zitadel:iam:org:project:roles email";
|
||||
assert_eq!(ensure_roles_scope(s), s);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user