From 7b337f71281d4f395f265a1c799db9455135890b Mon Sep 17 00:00:00 2001 From: Nikholas Pcenicni <82239765+nikpcenicni@users.noreply.github.com> Date: Thu, 14 May 2026 22:39:53 -0400 Subject: [PATCH] Refactor Authentik blueprint configuration to merge public, extra, and Nikflix directory groups into a single YAML template. Update README to clarify group entry requirements and enhance validation in Ansible tasks for blueprint entries. This improves the structure and usability of directory groups in Authentik deployments. --- ansible/roles/noble_authentik/README.md | 27 ++++++++++++-- .../roles/noble_authentik/defaults/main.yml | 31 ++++++++++++++-- ansible/roles/noble_authentik/tasks/main.yml | 23 ++++++++++-- .../blueprints/10-noble-public-groups.yaml.j2 | 35 ++++++++++++++++--- 4 files changed, 102 insertions(+), 14 deletions(-) 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 %}