901cc69ba3
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_operator (push) Successful in 20s
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Has been skipped
ci / test_platform_api (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / tc_portal (push) Successful in 26s
ci / build_platform_api (push) Has been skipped
ci / build_operator (push) Successful in 31s
ci / build_portal (push) Successful in 39s
ci / deploy (push) Successful in 41s
Idle sessions died and left a broken page: when the access token expired, nuxt-oidc-auth's automatic refresh had no refresh token to use — neither Authentik provider carried the offline_access scope mapping (and the operator never requested the scope), so the module cleared the session and every /api call 401'd until a manual F5 happened to re-auth through Authentik's still-alive SSO session. Fix 1: offline_access end to end — scope mapping attached to both live providers (and blueprints, prod + dev), operator now requests the scope. Sessions renew server-side for up to 30 days of activity (Redis store + pinned token key from earlier make the refresh tokens durable). Fix 2: client plugin in both apps — a 401 from /api sends the browser through /auth/oidc/login instead of leaving dead buttons; invisible when Authentik's session is alive, a clean sign-in screen when it isn't. Loop-guarded. Full sign-out behavior unchanged.
145 lines
6.5 KiB
YAML
145 lines
6.5 KiB
YAML
# Provisions the dezky-operator OIDC application AND restricts it to platform
|
|
# admins — the IdP-level half of operator isolation, captured as code instead
|
|
# of manual UI clicks.
|
|
#
|
|
# WHY: The operator portal's first line of defense is *which Authentik users
|
|
# may complete its OIDC flow*. Without an application policy binding, ANY
|
|
# Authentik user — a partner, a tenant admin, an end user — can authorize
|
|
# against dezky-operator and land on the operator UI. platform-api's
|
|
# OperatorGuard still 403s their data, and the operator app's
|
|
# require-platform-admin middleware signs them out, but those are layers 2 and
|
|
# 3. This blueprint is layer 1: Authentik itself refuses the login unless the
|
|
# user is in `dezky-platform-admins` (the same group platform-api treats as the
|
|
# source of truth for platformAdmin).
|
|
#
|
|
# CREATE-ONLY for the provider + application. The dezky-operator provider was
|
|
# originally created by hand; its client_secret and scope mappings are live and
|
|
# must not be clobbered. `state: created` means "create if absent, never update
|
|
# if present" — so a FRESH environment gets the app built from this file, while
|
|
# an EXISTING environment keeps its hand-made provider exactly as-is. The group,
|
|
# policy, and binding use `state: present` so they reconcile (and the binding is
|
|
# enforced) on both fresh and existing environments.
|
|
#
|
|
# The provider/app are matched by client_id / slug (both globally unique and
|
|
# stable) rather than display name, so `state: created` reliably recognizes the
|
|
# existing objects regardless of what they were named in the UI.
|
|
#
|
|
# Applied by the authentik-worker, which mounts this dir at /blueprints/custom
|
|
# and reads OPERATOR_OIDC_CLIENT_ID / OPERATOR_OIDC_CLIENT_SECRET from its env
|
|
# (see docker-compose.yml). On a fresh env the !Env client_secret below must
|
|
# match what the operator container uses (same .env var) so the two agree.
|
|
#
|
|
# VERIFY after apply (Authentik UI):
|
|
# Applications → dezky-operator → Bindings lists
|
|
# "operator-require-platform-admin" (enabled). Then: a non-admin test user is
|
|
# denied at login; an admin user still gets in.
|
|
|
|
version: 1
|
|
metadata:
|
|
name: dezky-operator-application
|
|
labels:
|
|
blueprints.goauthentik.io/instantiate: "true"
|
|
|
|
entries:
|
|
# ── Source of truth for operator privilege ───────────────────────────────
|
|
# find-or-create; safe to reconcile whether made by hand or by platform-api's
|
|
# bootstrap. Identified by name to match platform-api's
|
|
# PLATFORM_ADMIN_BOOTSTRAP_GROUP (default "dezky-platform-admins").
|
|
- model: authentik_core.group
|
|
state: present
|
|
identifiers:
|
|
name: dezky-platform-admins
|
|
attrs:
|
|
name: dezky-platform-admins
|
|
|
|
# ── OAuth2 / OIDC provider ───────────────────────────────────────────────
|
|
# Create-only: never mutate the live, hand-made provider. Mirrors the portal
|
|
# provider's shape (see docs/AUTHENTIK-SETUP.md §3.3): implicit-consent flow,
|
|
# the default self-signed signing key, the standard openid/email/profile
|
|
# scope mappings (the default `profile` mapping carries the groups claim that
|
|
# platformAdmin bootstrap relies on), hashed sub, per-provider issuer.
|
|
- id: operator-oauth2-provider
|
|
model: authentik_providers_oauth2.oauth2provider
|
|
state: created
|
|
identifiers:
|
|
client_id: !Env [OPERATOR_OIDC_CLIENT_ID, dezky-operator]
|
|
attrs:
|
|
name: dezky-operator
|
|
client_type: confidential
|
|
client_id: !Env [OPERATOR_OIDC_CLIENT_ID, dezky-operator]
|
|
client_secret: !Env OPERATOR_OIDC_CLIENT_SECRET
|
|
authorization_flow:
|
|
!Find [
|
|
authentik_flows.flow,
|
|
[slug, default-provider-authorization-implicit-consent],
|
|
]
|
|
signing_key:
|
|
!Find [
|
|
authentik_crypto.certificatekeypair,
|
|
[name, "authentik Self-signed Certificate"],
|
|
]
|
|
redirect_uris:
|
|
- matching_mode: strict
|
|
url: https://operator.dezky.local/auth/oidc/callback
|
|
property_mappings:
|
|
- !Find [
|
|
authentik_providers_oauth2.scopemapping,
|
|
[managed, "goauthentik.io/providers/oauth2/scope-openid"],
|
|
]
|
|
- !Find [
|
|
authentik_providers_oauth2.scopemapping,
|
|
[managed, "goauthentik.io/providers/oauth2/scope-email"],
|
|
]
|
|
- !Find [
|
|
authentik_providers_oauth2.scopemapping,
|
|
[managed, "goauthentik.io/providers/oauth2/scope-profile"],
|
|
]
|
|
# offline_access -> refresh tokens for the apps' silent session renewal.
|
|
- !Find [
|
|
authentik_providers_oauth2.scopemapping,
|
|
[managed, "goauthentik.io/providers/oauth2/scope-offline_access"],
|
|
]
|
|
sub_mode: hashed_user_id
|
|
issuer_mode: per_provider
|
|
|
|
# ── Application ──────────────────────────────────────────────────────────
|
|
# Create-only. Slug must stay `dezky-operator` — the issuer URL
|
|
# (.../application/o/dezky-operator/) and platform-api's accepted audience
|
|
# both derive from it.
|
|
- id: operator-application
|
|
model: authentik_core.application
|
|
state: created
|
|
identifiers:
|
|
slug: dezky-operator
|
|
attrs:
|
|
name: Dezky Operator
|
|
slug: dezky-operator
|
|
provider: !KeyOf operator-oauth2-provider
|
|
meta_launch_url: https://operator.dezky.local
|
|
meta_description: Internal Dezky operator control plane. Platform admins only.
|
|
|
|
# ── Access policy: require membership in dezky-platform-admins ────────────
|
|
- id: operator-require-platform-admin
|
|
model: authentik_policies_expression.expressionpolicy
|
|
state: present
|
|
identifiers:
|
|
name: operator-require-platform-admin
|
|
attrs:
|
|
name: operator-require-platform-admin
|
|
expression: |
|
|
return ak_is_group_member(request.user, name="dezky-platform-admins")
|
|
|
|
# ── Bind the policy to the application ────────────────────────────────────
|
|
# Authentik evaluates application policy bindings before issuing the
|
|
# authorization, so a non-member is denied during the OIDC flow and never
|
|
# reaches the operator app. `present` so it's enforced on fresh AND existing
|
|
# environments.
|
|
- model: authentik_policies.policybinding
|
|
state: present
|
|
identifiers:
|
|
target: !KeyOf operator-application
|
|
policy: !KeyOf operator-require-platform-admin
|
|
attrs:
|
|
enabled: true
|
|
order: 0
|