# 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"], ] 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