feat: allow granting Platform Admin by OIDC subject in gateway config
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 8.7k
- Forks
- 1.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 243
Description
Problem Statement
Today, granting Platform Admin privileges on an OpenShell gateway requires the IdP to assign a specific role (e.g. openshell-admin) in the JWT claims. There is no way to designate a specific OIDC subject (sub claim) as a Platform Admin directly in the gateway configuration. This creates friction when bootstrapping a new gateway (chicken-and-egg: you need an admin to configure roles, but the admin role must come from the IdP), when the IdP doesn't support custom roles, or when an operator wants gateway-local admin overrides independent of IdP role assignments.
A gateway-level admin_subjects configuration option would grant Platform Admin to specific OIDC subjects regardless of their JWT role claims, working alongside the existing role-based admin check — either mechanism grants admin access.
Technical Context
The gateway's authorization system has two layers that enforce admin status:
- Middleware-level RBAC (
AuthzPolicy.check()) — evaluates whether an authenticated identity's roles include the configuredadmin_roleto gate access to platform-admin-scoped gRPC methods. - Handler-level workspace authorization (
workspace_authz::is_platform_admin()) — called directly by gRPC handlers to bypass workspace membership checks for platform admins.
Both layers currently check only the identity's JWT roles against the configured admin_role string. The OIDC subject (sub claim) — already available on the Identity struct — is never consulted for admin status determination.
Affected Components
| Component | Key Files | Role |
|---|---|---|
| Config (core) | crates/openshell-core/src/config.rs |
OidcConfig struct — holds OIDC settings including admin_role |
| CLI args | crates/openshell-server/src/cli.rs |
Clap arg definitions, env var bindings, config-file overlay logic |
| Server state | crates/openshell-server/src/lib.rs |
ServerState — carries admin_role: String used by all handlers |
| AuthzPolicy (middleware) | crates/openshell-server/src/auth/authz.rs |
AuthzPolicy.check() — method-level RBAC using roles |
| Workspace authz (handler) | crates/openshell-server/src/auth/workspace_authz.rs |
is_platform_admin(), require_platform_admin(), authorize_workspace() |
| Multiplex layer | crates/openshell-server/src/multiplex.rs |
Builds AuthzPolicy from OIDC config for the gRPC auth middleware |
| gRPC handlers | crates/openshell-server/src/grpc/{sandbox,workspace,service,provider,policy}.rs |
Pass &state.admin_role to authorization functions |
| Inference handlers | crates/openshell-server/src/inference.rs |
Also passes &state.admin_role to authorization functions |
| Helm chart | deploy/helm/openshell/values.yaml, templates/gateway-config.yaml |
OIDC config rendered into gateway.toml |
| Docs | docs/reference/gateway-config.mdx, docs/reference/gateway-auth.mdx |
Gateway config reference and auth guide |
Technical Investigation
Architecture Overview
Authentication and authorization are separated per RFC 0001. Authentication providers (OIDC, mTLS, etc.) produce an Identity with subject, display_name, roles, and scopes. The authorization layer then evaluates this identity without knowing which provider authenticated the caller.
The admin check flows through two paths:
Path 1 — Middleware RBAC (authz.rs):
OidcConfig.admin_role → AuthzPolicy.admin_role (constructed in multiplex.rs:287) → AuthzPolicy.check() compares identity.roles against admin_role to gate platform-admin-scoped gRPC methods.
Path 2 — Handler workspace authorization (workspace_authz.rs):
OidcConfig.admin_role → ServerState.admin_role (set in lib.rs:208) → passed as &state.admin_role to authorize_workspace() / require_platform_admin() → calls is_platform_admin(identity_roles, admin_role) which checks admin_role.is_empty() || identity_roles.iter().any(|r| r == admin_role).
Both paths have access to Identity.subject but never check it for admin determination.
Code References
| Location | Description |
|---|---|
crates/openshell-core/src/config.rs:522 |
OidcConfig struct — #[serde(deny_unknown_fields)], holds admin_role: String |
crates/openshell-core/src/config.rs:541 |
admin_role field with default_admin_role() → "openshell-admin" |
crates/openshell-server/src/lib.rs:176 |
ServerState.admin_role: String field |
crates/openshell-server/src/lib.rs:205-208 |
admin_role populated from config.oidc.admin_role in ServerState::new() |
crates/openshell-server/src/auth/authz.rs:30 |
AuthzPolicy struct with admin_role: String |
crates/openshell-server/src/auth/authz.rs:75-95 |
AuthzPolicy.check() — role-based method authorization |
crates/openshell-server/src/auth/workspace_authz.rs:170-172 |
is_platform_admin() — core admin check: admin_role.is_empty() || identity_roles.iter().any(|r| r == admin_role) |
crates/openshell-server/src/auth/workspace_authz.rs:149-157 |
require_platform_admin() — called by handlers for cross-workspace ops |
crates/openshell-server/src/auth/workspace_authz.rs:73-86 |
authorize_workspace() — checks is_platform_admin() to bypass membership |
crates/openshell-server/src/multiplex.rs:287-290 |
AuthzPolicy construction from oidc config |
crates/openshell-server/src/cli.rs:172-175 |
CLI --oidc-admin-role arg, env OPENSHELL_OIDC_ADMIN_ROLE |
crates/openshell-server/src/cli.rs:659-673 |
Config file overlay applies TOML admin_role to CLI args |
deploy/helm/openshell/values.yaml:305 |
oidc.adminRole Helm value |
deploy/helm/openshell/templates/gateway-config.yaml:103-104 |
Renders admin_role into [openshell.gateway.oidc] TOML section |
Current Behavior
The core admin determination happens in workspace_authz.rs:
fn is_platform_admin(identity_roles: &[String], admin_role: &str) -> bool {
admin_role.is_empty() || identity_roles.iter().any(|r| r == admin_role)
}
When admin_role is empty (OIDC not configured or auth-only mode), every authenticated user is treated as platform admin. Otherwise, the user must have the admin_role in their JWT roles claim.
At the middleware level, AuthzPolicy.check() similarly compares identity.roles against admin_role to determine if the caller can access platform-admin-scoped methods. Admin role holders implicitly satisfy user-role requirements.
There are ~30+ call sites across grpc/sandbox.rs, grpc/workspace.rs, grpc/service.rs, grpc/provider.rs, grpc/policy.rs, and inference.rs that pass &state.admin_role to authorize_workspace(), authorize_sandbox_workspace(), or require_platform_admin().
What Would Need to Change
Config layer (openshell-core): Add admin_subjects: Vec<String> to OidcConfig. The deny_unknown_fields serde attribute means this must be an explicit field.
CLI layer (openshell-server/cli.rs): Add --oidc-admin-subjects CLI arg with OPENSHELL_OIDC_ADMIN_SUBJECTS env var. Add config-file overlay logic in the same pattern as admin_role.
Server state (openshell-server/lib.rs): Add admin_subjects field to ServerState, populated from config.oidc.admin_subjects in ServerState::new().
Middleware RBAC (authz.rs): Add admin_subjects: HashSet<String> to AuthzPolicy. Modify check() to also grant admin access when identity.subject is in admin_subjects.
Handler authorization (workspace_authz.rs): Modify is_platform_admin() signature to accept the caller's subject and the admin_subjects set. Update require_platform_admin(), authorize_workspace(), and is_platform_admin_principal() accordingly.
Call site churn mitigation: The ~30+ call sites that pass &state.admin_role would need updating. A recommended approach: introduce an AdminPolicy struct (or similar) that bundles admin_role and admin_subjects, then change ServerState to carry admin_policy: AdminPolicy and update the authorization function signatures to take &AdminPolicy. This localizes the change and prevents future churn if more admin criteria are added.
Multiplex (multiplex.rs): Update AuthzPolicy construction to include admin_subjects.
Helm chart: Add adminSubjects: [] to values.yaml under server.oidc. Render as admin_subjects = ["sub1", "sub2"] in the gateway-config.yaml template.
Docs: Update docs/reference/gateway-config.mdx and docs/reference/gateway-auth.mdx for the new config field, CLI flag, and env var.
Alternative Approaches Considered
-
Flat approach — add
admin_subjectsas a sibling field toadmin_rolein all signatures. Simpler to implement but results in ~30+ call-site changes and makes future admin-criteria additions equally expensive. -
AdminPolicy struct — bundle
admin_role+admin_subjectsinto one struct, thread that through instead of separate fields. More upfront work but reduces call-site churn to a type change and prevents future N+1 field threading. Recommended. -
Synthetic role injection — when the subject matches
admin_subjects, inject theadmin_roleinto the identity's roles list at the authentication layer, before authorization runs. This would require zero changes to the authorization layer but mixes concerns: the auth layer would be modifying identity claims, which could confuse audit logging andGetCurrentUserresponses. Not recommended.
Patterns to Follow
- Config fields: Follow the
admin_rolepattern —#[serde(default)]with a default function, matching CLI arg + env var + config-file overlay. - CLI env vars: Use
OPENSHELL_OIDC_ADMIN_SUBJECTSwith comma-separated values (consistent with how list-type env vars are handled elsewhere). - Helm values: Follow the existing OIDC block pattern with conditional rendering in the template.
- Tests: The
workspace_authz.rstest module has thorough coverage of admin scenarios. New tests should followplatform_admin_bypasses_membership_checkandauth_disabled_empty_admin_role_is_platform_adminpatterns.authz.rshas role-based tests that should be extended for subject-based admin access.
Proposed Approach
Introduce an AdminPolicy struct that bundles the admin role name and admin subjects set, replacing the flat admin_role: String threading. Add admin_subjects to OidcConfig (config), CLI args (with env var OPENSHELL_OIDC_ADMIN_SUBJECTS), and the Helm chart. Modify is_platform_admin() and AuthzPolicy.check() to grant admin access when the caller's OIDC subject is in the configured admin_subjects set, alongside the existing role-based check. Update gateway config and auth documentation.
Scope Assessment
- Complexity: Medium — well-scoped changes across multiple files, but the pattern is clear and mechanical
- Confidence: High — clear path forward, no architectural unknowns
- Estimated files to change: ~15 (config, CLI, server state, 2 auth modules, multiplex, Helm values + template, 2 docs pages, plus test updates)
- Issue type:
feat
Risks & Open Questions
- Revocation latency: Admin subjects are static in gateway config — revoking an admin subject requires a gateway restart (or config reload if supported). Should be documented as an operational consideration.
- Audit trail: When a user is granted admin via
admin_subjectsrather than IdP roles, theGetCurrentUserresponse won't show an admin role inroles[]. Operators may want to distinguish how admin access was granted. Consider whether the auth grant path should be surfaced (e.g. in logs or response metadata). - Auth-only mode interaction: When both
admin_roleanduser_roleare empty (auth-only mode), everyone is already admin.admin_subjectsshould be a no-op in that mode — document this. - CLI arg format: Comma-separated
OPENSHELL_OIDC_ADMIN_SUBJECTS=sub1,sub2or multi-value--oidc-admin-subject sub1 --oidc-admin-subject sub2? Clap supports both; comma-separated env var + multi-value CLI flag is the most ergonomic. - Large subject lists: For gateways with many admin subjects, a
HashSet<String>is appropriate for O(1) lookup. The current role check is O(n) on roles which is fine for small role lists.
Disposition Readiness
- State: validated
- Assessment: The investigation identified all affected code paths, confirmed feasibility, and mapped the implementation surface. The feature is a well-scoped addition to an existing, well-tested authorization system with clear patterns to follow.
- Missing evidence: None
Test Considerations
- Unit tests in
workspace_authz.rs: Add tests for subject-based admin grant, subject-based admin with workspace membership bypass, subject-based admin + role-based admin coexistence, and empty admin_subjects (no effect). - Unit tests in
authz.rs: Add tests for admin_subjects granting access to platform-admin-scoped methods. - Config serialization tests: Ensure
admin_subjectsround-trips through TOML serialization/deserialization. - CLI integration tests: Verify
OPENSHELL_OIDC_ADMIN_SUBJECTSenv var and--oidc-admin-subjectsCLI arg are parsed correctly. - Helm template tests: Extend
deploy/helm/openshell/tests/gateway_config_test.yamlto verifyadmin_subjectsrendering. - E2e tests (optional): The existing
e2e/rust/tests/oidc_pkce.rstests could be extended with a Keycloak user whose subject (not role) is configured as admin_subjects, but this is a stretch goal given the e2e test infrastructure required. - Existing test patterns: Follow the
platform_admin_bypasses_membership_checkandauth_disabled_empty_admin_role_is_platform_adminpatterns inworkspace_authz.rs. The test module createsuser_principal(subject, roles)helpers that can be reused.
Created by spike investigation. Validated means the issue is ready for human disposition. A human applies state:accepted if OpenShell should pursue the work and places it on the roadmap separately. To queue unattended agent planning, a human applies agent:plan-requested; a direct request to an agent does not require that label.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with OidcConfig in crates/openshell-core/src/config.rs, then trace admin_role through cli.rs, lib.rs, auth/authz.rs, auth/workspace_authz.rs, and multiplex.rs. Use the existing workspace_authz.rs and authz.rs admin tests as the starting point, and inspect the Helm templates and gateway documentation. Done means configured OIDC subjects receive the same admin access as the configured role, with CLI, Helm, tests, and docs updated.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- helm, rust
- Domain
- authentication, authorization, documentation
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100