# 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
