feat/v0-3-dashboard-role-enforcement #293
Open
johnride
wants to merge 2 commits from
feat/v0-3-dashboard-role-enforcement into feat/smoke-test-contract
pull from: feat/v0-3-dashboard-role-enforcement
merge into: NationTech:feat/smoke-test-contract
NationTech:master
NationTech:feat/fleet-ch2-operator-recovery
NationTech:feat/fleet-device-exec-logs
NationTech:feat/zitadel-web-pkce-and-human-user
NationTech:feat/jwt-bearer-openbao-auth
NationTech:feat/fleet-ch5-graceful-deploy-upgrade
NationTech:feat/fleet-ch4-agent-upgrade
NationTech:feat/fleet-ch3-log-streaming
NationTech:feat/add-claims-for-openbao
NationTech:refactor/move-zitadel-jwt-to-module
NationTech:feat/fleet-operator-real-data
NationTech:docs/fleet-secrets-device-access
NationTech:chore/fleet-operator-prune-mock-dtos
NationTech:chore/rename-release-to-publish
NationTech:refactor/config-namespace-env-var
NationTech:feat/fleet-staging-openbao
NationTech:feat/auth-add-next-url-redirect
NationTech:pr/harmony-sso-example
NationTech:feat/unified-config-and-secrets
NationTech:ci/fleet-argo-cd
NationTech:ci/fleet-operator-release-pipeline
NationTech:feat/on-device-key-gen
NationTech:feat/install-gitea
NationTech:feat/v0-3-logs-companion
NationTech:refactor/smoke-companion-minimal
NationTech:feat/smoke-test-contract
NationTech:feat/iobench-redpanda-profile
NationTech:feat/v0-3-init-containers
NationTech:feat/v0-3-operator-restart-baseline
NationTech:feat/fleet-e2e-x86
NationTech:feat/ceph-score
NationTech:feat/opnsense-bootstrap-score
NationTech:feat/fleet-e2e
NationTech:feat/fleet-e2e-harness-and-ping
NationTech:feat/dashboard-auth
NationTech:feat/fleet-operator-web-frontend
NationTech:feat/deploy_fleet_server_side
NationTech:feat/openwebui
NationTech:feat/iot-aggregation-scale
NationTech:feat/iot-operator-helm-chart
NationTech:feat/removesideeffect
NationTech:feat/test-alert-receivers-sttest
NationTech:feat/brocade-client-add-vlans
NationTech:feat/agent-desired-state
NationTech:feat/opnsense-dns-implementation
NationTech:feat/named-config-instances
NationTech:worktree-bridge-cse_012j1jB37XfjXvDGHUjHrKSj
NationTech:chore/leftover-adr
NationTech:feat/config_e2e_zitadel_openbao
NationTech:example/vllm
NationTech:feat/config_sqlite
NationTech:chore/roadmap
NationTech:feature/kvm-module
NationTech:feat/rustfs
NationTech:feat/harmony_assets
NationTech:feat/brocade_assisted_setup
NationTech:feat/cluster_alerting_score
NationTech:e2e-tests-multicluster
NationTech:fix/refactor_alert_receivers
NationTech:feat/change-node-readiness-strategy
NationTech:feat/zitadel
NationTech:feat/improve-inventory-discovery
NationTech:fix/monitoring_abstractions_openshift
NationTech:feat/nats-jetstream
NationTech:adr-nats-creds
NationTech:feat/st_test
NationTech:feat/dockerAutoinstall
NationTech:chore/cleanup_hacluster
NationTech:doc/cert-management
NationTech:feat/certificate_management
NationTech:adr/017-staleness-failover
NationTech:fix/nats_non_root
NationTech:feat/rebuild_inventory
NationTech:fix/opnsense_update
NationTech:feat/unshedulable_control_planes
NationTech:feat/worker_okd_install
NationTech:doc-and-braindump
NationTech:fix/pxe_install
NationTech:switch-client
NationTech:okd_enable_user_workload_monitoring
NationTech:configure-switch
NationTech:fix/clippy
NationTech:feat/gen-ca-cert
NationTech:feat/okd_default_ingress_class
NationTech:fix/add_routes_to_domain
NationTech:secrets-prompt-editor
NationTech:feat/multisiteApplication
NationTech:feat/ceph-install-score
NationTech:feat/ceph-osd-score
NationTech:feat/ceph_validate_health
NationTech:better-indicatif-progress-grouped
NationTech:feat/crd-alertmanager-configs
NationTech:better-cli
NationTech:opnsense_upgrade
NationTech:feat/monitoring-application-feature
NationTech:dev/postgres
NationTech:feat/cd/localdeploymentdemo
NationTech:feat/webhook_receiver
NationTech:feat/kube-prometheus
NationTech:feat/init_k8s_tenant
NationTech:feat/discord-webhook-receiver
NationTech:feat/kube-prometheus-monitor
NationTech:feat/tenantScore
NationTech:feat/teams-integration
NationTech:feat/slack-notifs
NationTech:monitoring
NationTech:runtime-profiles
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| e9522464af |
refactor(zitadel-auth): typed Role/Roles, drop string-path role lookup
All checks were successful
Run Check Script / check (pull_request) Successful in 2m21s
Removes the brittle pieces that landed in the original role-enforcement
commit: `extract_roles(value, path)` walking `serde_json::Value` with
a `path.contains("urn:")`-aware splitter, an env-driven
`FLEET_AUTH_ROLES_CLAIM`, and `VerifiedSession.roles: Vec<String>`
compared with `iter().any(|r| r == "fleet-admin")` in middleware.
This is a security-relevant code path; runtime string heuristics inside
it hide injection-shaped bugs (a misconfigured env or a custom Zitadel
mapper shifts the lookup to a path the attacker controls).
Replaced with end-to-end typed serde decoding:
* `Role` — closed enum (one variant today: `FleetAdmin`). Adding a
variant is a deliberate code change to the security path. Unknown
role names emitted by the IdP cannot be represented and therefore
cannot grant access.
* `RoleClaims` — wire-side struct, flattened into the JWT `Claims`.
The two well-known role-claim locations are matched verbatim by
`#[serde(rename = "urn:zitadel:iam:org:project:roles")]` and
`#[serde(rename = "roles")]`. No dotted-path navigation. No env
string. If a future issuer adds a third location it is an additive
`#[serde(rename = ...)]` field inside the security boundary.
* `Roles` — domain value on `VerifiedSession`. Construction is
restricted to `RoleClaims::into_roles` plus a typed
`FromIterator<Role>` for test fixtures. `Roles::has(Role::FleetAdmin)`
is the only check the middleware needs; no string comparison exists
anywhere downstream.
* Malformed shapes (scalar at the URN path, mixed-type array) now
ERROR at decode rather than degrading to an empty-vec "closed door".
Fail-loud is the security-correct default when the IdP misbehaves
— the user re-logs in, the operator notices.
Callout side: reverted the shared-extract_roles delegation. The
callout retains its own, unchanged role-extraction logic. We do NOT
need cross-crate sharing here, and the shared extractor was the entry
point we were trying to delete — the callout's own behaviour was the
status quo and is preserved verbatim.
Dropped exports: `DEFAULT_ADMIN_ROLE`, `DEFAULT_ROLES_CLAIM`,
`extract_roles`, `ROLES_CLAIM_ENV`, `ZitadelAuthConfig.roles_claim`.
None had external consumers in the workspace.
Tests:
* 12 tests on `RoleClaims` deserialization: both shapes resolve,
Zitadel URN wins precedence, unknown roles dropped, malformed
scalar/mixed-type arrays error at decode, missing claim → empty,
empty object/array → empty, extra unrelated claims are ignored,
display matches wire spelling.
* 4 middleware tests on the typed `require_role(Role::FleetAdmin, …)`
path. Dropped the redundant "admin-only vs admin+other" test —
with a single-variant enum it duplicated the positive case.
|
|||
| aad14cd04d |
feat(fleet-operator): require fleet-admin role on dashboard routes
Previously the dashboard granted full access to any Zitadel user with a valid JWT — issuer, audience and signature were the only checks. Operators routinely give non-admin users an account in Zitadel for SSO into adjacent apps, so a valid JWT is not by itself evidence the user should manage a fleet. This change closes that gap. Behaviour: - Verified sessions now carry a `roles: Vec<String>` extracted from a configurable JWT claim (env `FLEET_AUTH_ROLES_CLAIM`, default the Zitadel role URN). The extractor accepts both OIDC array shape and Zitadel's object-map shape; missing/malformed claim yields empty vec (closed door, never wildcard). - `require_fleet_admin` middleware layers above `require_auth` on the private routes. A logged-in user without the role gets a human- readable 403 page that names the missing role and offers a sign-out link. - `/logout` is in `public_routes` so a non-admin can switch accounts without being trapped by the gate that rendered the 403. Shared role extraction: - `harmony_zitadel_auth::roles::extract_roles` is the single source of truth for Zitadel role parsing. - The NATS auth callout's `ZitadelValidator::extract_roles` now delegates to it. Any future quirk in Zitadel's role-emission shape is fixed once. Tests: - 9 unit tests on the role extractor: both claim shapes, empty/missing fail closed, non-string array entries skipped, dotted paths, Zitadel URN paths. - 5 axum middleware tests via `tower::ServiceExt::oneshot`: no session → unauthenticated redirect, role-less session → 403 with expected role name + logout link, fleet-admin session → handler reached, HTML (not JSON) content-type on 403. Defers to follow-ups: a `fleet-viewer` read-only role; unifying the dashboard role policy with the NATS callout's per-device scoping. |