diff --git a/ansible/roles/noble_authentik/README.md b/ansible/roles/noble_authentik/README.md index 8aa21e0..16a72ab 100644 --- a/ansible/roles/noble_authentik/README.md +++ b/ansible/roles/noble_authentik/README.md @@ -42,7 +42,7 @@ Network isolation is enforced at **DNS and the tunnel**, not inside Authentik. O **Two-Brand model:** - **Lab Brand** — domain equals **`noble_authentik_host`**: use a **restricted authentication flow** so only operator groups (defaults: **`noble-admins`**, **`authentik Admins`**) can complete sign-in on that hostname. Your bootstrap / break-glass account must remain in one of those groups (see **`noble_authentik_ensure_admin_ui_access`** and **`configure_authentik.py`** group membership). -- **Public Brand(s)** — one Brand per FQDN in **`noble_authentik_ingress_extra_hosts`**: use the stock **`default-authentication-flow`** (or replace with your own flow slug via a forked blueprint later). Assign general users to **`noble_authentik_blueprint_public_groups`** (defaults **`noble-public-users`**, **`noble-public-admins`**) for app policies and OAuth claims; **`noble-admins`** / **`noble-editors`** remain for cluster / Argo / Grafana as today. +- **Public Brand(s)** — one Brand per FQDN in **`noble_authentik_ingress_extra_hosts`**: use the stock **`default-authentication-flow`** (or replace with your own flow slug via a forked blueprint later). Assign people to **`noble_authentik_blueprint_public_groups`** and/or **`noble_authentik_blueprint_nikflix_groups`** (defaults include **`nikflix-users`** / **`nikflix-admins`** for the Nikflix hostname); **`noble-admins`** / **`noble-editors`** remain for cluster / Argo / Grafana as today. **OAuth note:** Redirect URIs and **`iss`** must stay consistent with the hostname clients use (internal issuer for in-cluster apps vs public issuer is a deliberate choice — avoid mixing both for the same app). @@ -50,12 +50,33 @@ Network isolation is enforced at **DNS and the tunnel**, not inside Authentik. O | Key | Purpose | | --- | --- | -| **`10-noble-public-groups.yaml.j2`** | Ensures **`noble_authentik_blueprint_public_groups`** exist. | +| **`10-noble-public-groups.yaml.j2`** | **`noble_authentik_blueprint_public_groups`** ∪ **`noble_authentik_blueprint_extra_directory_groups`** ∪ **`noble_authentik_blueprint_nikflix_groups`** → **Group** objects (see **Blueprint: directory groups**). | | **`20-noble-lab-operator-authentication-flow.yaml.j2`** | Flow **`noble_authentik_blueprint_lab_flow_slug`** + expression policy **`noble_authentik_blueprint_operator_policy_name`** (allowed groups **`noble_authentik_blueprint_lab_operator_groups`**). | | **`30-noble-brands-domain-split.yaml.j2`** | Brand for **`noble_authentik_host`** → lab flow; one Brand per **`noble_authentik_ingress_extra_hosts`** → default authentication. | Tune titles via **`noble_authentik_blueprint_lab_brand_title`** and **`noble_authentik_blueprint_public_brand_title_prefix`**. After the worker applies blueprints, confirm **System → Brands** and **Flows** in the admin UI; fix any **`!Find`** failures if upstream default stage **names** change between Authentik versions. +#### Blueprint: directory groups + +Three inventory lists are concatenated **in this order** into **`10-noble-public-groups.yaml.j2`**: + +1. **`noble_authentik_blueprint_public_groups`** — generic “public hostname” audience (defaults **`noble-public-users`** / **`noble-public-admins`**). +2. **`noble_authentik_blueprint_extra_directory_groups`** — any other groups (empty by default). +3. **`noble_authentik_blueprint_nikflix_groups`** — Nikflix-facing groups (defaults **`nikflix-users`** / **`nikflix-admins`** with **`noble.ak/brand: nikflix`**). Listed last so **`parents`** can reference groups from (1) or (2) if you choose. + +Each item may be: + +| Form | Example | +| --- | --- | +| **String** | **`my-app-operators`** — creates a group with that **name** only. | +| **Mapping** | **`name`** (required), optional **`is_superuser`**: **`true`** (use sparingly), **`attributes`**: dict (JSON on the group; useful in expression policies), **`parents`**: list of **existing** group **names** (resolved with **`!Find`**). | + +Order matters for **`parents`**: every parent must already exist when the child row is applied — list parents **above** children in the merged list, or reference groups Authentik already created (for example **`noble-public-users`** before **`noble-public-admins`** with **`parents: [noble-public-users]`**). See [Group properties and attributes](https://docs.goauthentik.io/users-sources/groups/group_ref/). + +##### Audience groups vs per-service groups + +For Nikflix (and similar brands), prefer **one broad “users” group and a small “admins” group** (`nikflix-users` / `nikflix-admins`), then bind **OAuth providers**, **policies**, and **app access** to those groups. Add **per-service** groups (for example **`nikflix-media-readonly`**) only when a service truly needs a **different** membership set than the rest of the brand; every extra group is another object to keep in sync with enrollment and IdP claims. Optional pattern: make a service group a **child** of **`nikflix-users`** via **`parents`** so members inherit the parent for generic “logged in to Nikflix” checks. + **Confirming blueprints on the cluster:** the Ansible task **Install Authentik (Helm)** uses **`changed_when: true`**, so a **“changed”** line there does **not** prove Helm mutated the release. When **`noble_authentik_blueprints_enabled`** is true, the role asserts the **worker** Deployment has a volumeMount named **`blueprints-cm-`** (default **`blueprints-cm-authentik-noble-blueprints`**). You can also run: ```bash @@ -81,7 +102,7 @@ When **`noble_authentik_configure_idp`** is true, Ansible creates/updates OAuth2 ## Troubleshooting -- **Blueprints from Ansible fail to apply** (worker logs / **System → Blueprints**): confirm the ConfigMap exists (**`kubectl -n authentik get cm authentik-noble-blueprints`** unless you changed **`noble_authentik_blueprints_configmap_name`**), that Helm mounts it (**`blueprints.configMaps`** in the rendered extra values), and that every **`!Find`** in **`templates/blueprints/20-*.j2`** still matches your Authentik version’s default stage **names**. Re-run **`--tags authentik`** after editing templates. +- **Blueprints from Ansible fail to apply** (worker logs / **System → Blueprints**): confirm the ConfigMap exists (**`kubectl -n authentik get cm authentik-noble-blueprints`** unless you changed **`noble_authentik_blueprints_configmap_name`**), that Helm mounts it (**`blueprints.configMaps`** in the rendered extra values), and that every **`!Find`** in **`templates/blueprints/20-*.j2`** still matches your Authentik version’s default stage **names**. For **`10-noble-public-groups.yaml.j2`**, **`parents`** must reference groups that appear **earlier** in the merged list (or already exist in Authentik). Re-run **`--tags authentik`** after editing templates. - **oauth2-proxy shows 500** on **`oauth2.apps…/oauth2/callback`** (logs: `email in id_token (...) isn't verified`): Authentik’s id_token often lacks **`email_verified: true`** for bootstrap users. **`clusters/noble/bootstrap/oauth2-proxy/values.yaml`** sets **`insecure-oidc-allow-unverified-email`** for the lab; otherwise verify the user’s email in Authentik, then **`helm upgrade oauth2-proxy`** (or **`--tags authentik`**). - Re-run **`configure_authentik.py`** only by executing **`noble.yml`** with **`--tags authentik`** after fixing `.env`. - If Authentik API calls fail, check flows exist (slug **`default-provider-authorization-implicit-consent`**) and TLS reaches **`AUTHENTIK_API_BASE`**. diff --git a/ansible/roles/noble_authentik/defaults/main.yml b/ansible/roles/noble_authentik/defaults/main.yml index 8f2e36c..440086c 100644 --- a/ansible/roles/noble_authentik/defaults/main.yml +++ b/ansible/roles/noble_authentik/defaults/main.yml @@ -29,10 +29,35 @@ noble_authentik_ingress_extra_hosts: [] # Mounted **blueprints** (ConfigMap → worker `/blueprints/mounted/cm-*`). See README § split routing / two-Brand. noble_authentik_blueprints_enabled: false noble_authentik_blueprints_configmap_name: authentik-noble-blueprints -# Directory groups for the public Brand(s); adjust names to match your apps’ policies / OAuth claims. +# Directory groups for the public Brand(s), merged with **`noble_authentik_blueprint_extra_directory_groups`** +# and **`noble_authentik_blueprint_nikflix_groups`** into **`templates/blueprints/10-noble-public-groups.yaml.j2`**. Each item may be: +# - a **string** (group name only), or +# - a **dict** with **`name`** (required) and optional **`is_superuser`** (bool), **`attributes`** (dict → JSON in blueprint), +# **`parents`** (list of **existing** group names — list parents *before* children in these lists, or use built-in groups). noble_authentik_blueprint_public_groups: - - noble-public-users - - noble-public-admins + - name: noble-public-users + attributes: + "noble.ak/audience": public + - name: noble-public-admins + parents: + - noble-public-users + attributes: + "noble.ak/audience": public +# Additional directory groups (same entry shape as **`noble_authentik_blueprint_public_groups`**); merged into one blueprint. +noble_authentik_blueprint_extra_directory_groups: [] +# Nikflix (e.g. **auth.nikflix.ca**) directory groups — merged **after** public + extra so **`parents`** can reference those. +# Prefer **audience** groups (`nikflix-users` / `nikflix-admins`); add per-service groups only when an app needs a distinct binding. +noble_authentik_blueprint_nikflix_groups: + - name: nikflix-users + attributes: + "noble.ak/brand": nikflix + "noble.ak/audience": public + - name: nikflix-admins + parents: + - nikflix-users + attributes: + "noble.ak/brand": nikflix + "noble.ak/audience": public # Lab-only authentication flow slug (Brand for **`noble_authentik_host`** points here). noble_authentik_blueprint_lab_flow_slug: noble-lab-operator-authentication-flow noble_authentik_blueprint_operator_policy_name: noble-lab-operators-only diff --git a/ansible/roles/noble_authentik/tasks/main.yml b/ansible/roles/noble_authentik/tasks/main.yml index 854e7a3..1211dbf 100644 --- a/ansible/roles/noble_authentik/tasks/main.yml +++ b/ansible/roles/noble_authentik/tasks/main.yml @@ -84,15 +84,32 @@ - name: Assert noble Authentik blueprint variables (when blueprints enabled) ansible.builtin.assert: that: - - noble_authentik_blueprint_public_groups | default([]) | length > 0 + - >- + ((noble_authentik_blueprint_public_groups | default([])) | length + + (noble_authentik_blueprint_extra_directory_groups | default([])) | length + + (noble_authentik_blueprint_nikflix_groups | default([])) | length) > 0 - noble_authentik_blueprint_lab_operator_groups | default([]) | length > 0 - noble_authentik_blueprint_lab_flow_slug | default('') | trim | length > 0 fail_msg: >- - When noble_authentik_blueprints_enabled is true, set noble_authentik_blueprint_public_groups (non-empty), - noble_authentik_blueprint_lab_operator_groups (non-empty), and noble_authentik_blueprint_lab_flow_slug. + When noble_authentik_blueprints_enabled is true, set at least one entry across + noble_authentik_blueprint_public_groups, noble_authentik_blueprint_extra_directory_groups, + and/or noble_authentik_blueprint_nikflix_groups, + plus noble_authentik_blueprint_lab_operator_groups (non-empty) and noble_authentik_blueprint_lab_flow_slug. See ansible/roles/noble_authentik/defaults/main.yml and README. when: noble_authentik_blueprints_enabled | default(false) | bool + - name: Validate noble Authentik blueprint directory group entries (when blueprints enabled) + ansible.builtin.assert: + that: + - (item is string and (item | trim | length) > 0) or (item is mapping and (item.name | default('') | trim | length) > 0) + fail_msg: >- + Each noble_authentik_blueprint_*_groups entry must be a non-empty string or a dict with key **name** (string). + Invalid entry: {{ item }} + loop: "{{ (noble_authentik_blueprint_public_groups | default([])) + (noble_authentik_blueprint_extra_directory_groups | default([])) + (noble_authentik_blueprint_nikflix_groups | default([])) }}" + loop_control: + label: "{{ item if item is string else item.name }}" + when: noble_authentik_blueprints_enabled | default(false) | bool + - name: Render Authentik noble blueprint YAML files ansible.builtin.template: src: "blueprints/{{ item }}.j2" diff --git a/ansible/roles/noble_authentik/templates/blueprints/10-noble-public-groups.yaml.j2 b/ansible/roles/noble_authentik/templates/blueprints/10-noble-public-groups.yaml.j2 index 93b6c5c..e9e374a 100644 --- a/ansible/roles/noble_authentik/templates/blueprints/10-noble-public-groups.yaml.j2 +++ b/ansible/roles/noble_authentik/templates/blueprints/10-noble-public-groups.yaml.j2 @@ -1,13 +1,38 @@ -# Noble — directory groups for the **public** hostname Brand (see role README). -# Groups are global to the instance; use policies and OAuth scope mappings to scope claims per app. +# Noble — directory groups (blueprint). Merges (in order): **noble_authentik_blueprint_public_groups**, +# **noble_authentik_blueprint_extra_directory_groups**, **noble_authentik_blueprint_nikflix_groups** (see role README). +# Each entry: a string (**name** only), or a mapping with **name** and optional **is_superuser**, **attributes**, **parents**. +# **parents** must reference groups that already exist: list those entries *before* children in the merged list, or rely on built-in groups. version: 1 metadata: - name: noble-public-groups + name: noble-directory-groups labels: blueprints.goauthentik.io/instantiate: "true" entries: -{% for group in noble_authentik_blueprint_public_groups | default([]) %} +{% set _all = (noble_authentik_blueprint_public_groups | default([])) + + (noble_authentik_blueprint_extra_directory_groups | default([])) + + (noble_authentik_blueprint_nikflix_groups | default([])) %} +{% for g in _all %} +{% set gn = (g.name if (g is mapping) else g) | trim %} - model: authentik_core.group identifiers: - name: "{{ group | trim }}" + name: {{ gn | to_json }} +{% if g is mapping and ( + (g.get('is_superuser') | default(false) | bool) + or ((g.get('attributes') or {}) | length > 0) + or ((g.get('parents') or []) | length > 0) + ) %} + attrs: +{% if g.get('is_superuser') | default(false) | bool %} + is_superuser: true +{% endif %} +{% if (g.get('attributes') or {}) | length > 0 %} + attributes: {{ g.attributes | to_json }} +{% endif %} +{% if (g.get('parents') or []) | length > 0 %} + parents: +{% for p in g.parents %} + - !Find [authentik_core.group, [name, {{ p | trim | to_json }}]] +{% endfor %} +{% endif %} +{% endif %} {% endfor %}