feat(ess-pro): generate AIM spaces from authentik groups #20

Open
Simon wants to merge 2 commits from feat/aim-spaces-generator into main
Owner

Summary

  • Space generator (ess_aim_spaces_generator_enabled, default false): every authentik group with matrix_space: true becomes an AIM space without a playbook run. A systemd timer runs run.sh → docker compose run --rm aim-spaces-generator (stock python:3.12-alpine, stdlib-only script mounted like ldap-paging-shim, no image build).
  • One writer per file: with the generator on, Ansible renders aim-config.base.yaml only; the generator writes aim-config.yaml = base + spaces: <json> atomically (temp file + rename) and restarts AIM only on change. flock serialises timer and play; the play's final generator run always picks up the new base. Switching it off stops the timer before Ansible writes aim-config.yaml again.
  • Safety: LDAP error/timeout, 0 groups, duplicate ids, a spaces: key in the base (assert on ess_aim_extra_config), or more than max_removals dropped spaces → file untouched, non-zero exit (journal / failed task). Space id = group uid (rename-stable), externalId = group DN (verified in group-sync v0.21.0: groups.get(rule.externalId) keyed by group.dn).
  • Shim memberOf gate (separate commit): brings the SHIM_REQUIRE_MEMBER_OF gate already running on a deployed host back into the role as ess_aim_ldap_paging_shim_require_member_of (default empty = off, compose unchanged). Rendered shim is byte-identical to the host's local copy.

Before deploying to a host that uses AIM spaces today

  • Set ess_aim_ldap_paging_shim_require_member_of in the inventory if the host runs the gate — otherwise the deploy turns it off and every user gets provisioned.
  • Move any fixed space from ess_aim_extra_config (spaces:) to ess_aim_spaces_generator_static_spaces, keeping its id, before enabling the generator (the play fails fast otherwise).

Test plan

  • Generator off: rendered compose.yml and aim-config.yaml byte-identical to main
  • Against a live authentik outpost via the shim (read-only): 31 groups, all with uid; DN format matches group-sync's log; boolean custom attributes render as true
  • Local E2E (OpenLDAP behind the real shim, run.sh): changed + restart, then unchanged; rename keeps id; removal over limit blocked, --allow-removals override; wrong password (49), wrong base (32), shim down → rc=1, file untouched
  • generate.py --self-test, shellcheck, argument_specs validation; ansible-lint: only new var-naming[no-role-prefix] (role-wide ess_* convention)
  • On the host: --check --diff, second run without changed
  • Dry-run (ess_aim_dry_run: true): flag a group → space in log; remove flag → abandon logged; rename; shim stopped → timer unit fails
## Summary - **Space generator** (`ess_aim_spaces_generator_enabled`, default `false`): every authentik group with `matrix_space: true` becomes an AIM space without a playbook run. A systemd timer runs `run.sh` → `docker compose run --rm aim-spaces-generator` (stock `python:3.12-alpine`, stdlib-only script mounted like `ldap-paging-shim`, no image build). - **One writer per file**: with the generator on, Ansible renders `aim-config.base.yaml` only; the generator writes `aim-config.yaml` = base + `spaces: <json>` atomically (temp file + rename) and restarts AIM only on change. `flock` serialises timer and play; the play's final generator run always picks up the new base. Switching it off stops the timer before Ansible writes `aim-config.yaml` again. - **Safety**: LDAP error/timeout, 0 groups, duplicate ids, a `spaces:` key in the base (assert on `ess_aim_extra_config`), or more than `max_removals` dropped spaces → file untouched, non-zero exit (journal / failed task). Space id = group `uid` (rename-stable), `externalId` = group DN (verified in group-sync v0.21.0: `groups.get(rule.externalId)` keyed by `group.dn`). - **Shim memberOf gate** (separate commit): brings the `SHIM_REQUIRE_MEMBER_OF` gate already running on a deployed host back into the role as `ess_aim_ldap_paging_shim_require_member_of` (default empty = off, compose unchanged). Rendered shim is byte-identical to the host's local copy. ## Before deploying to a host that uses AIM spaces today - Set `ess_aim_ldap_paging_shim_require_member_of` in the inventory if the host runs the gate — otherwise the deploy turns it off and every user gets provisioned. - Move any fixed space from `ess_aim_extra_config` (`spaces:`) to `ess_aim_spaces_generator_static_spaces`, keeping its `id`, before enabling the generator (the play fails fast otherwise). ## Test plan - [x] Generator off: rendered `compose.yml` and `aim-config.yaml` byte-identical to `main` - [x] Against a live authentik outpost via the shim (read-only): 31 groups, all with `uid`; DN format matches group-sync's log; boolean custom attributes render as `true` - [x] Local E2E (OpenLDAP behind the real shim, `run.sh`): changed + restart, then unchanged; rename keeps id; removal over limit blocked, `--allow-removals` override; wrong password (49), wrong base (32), shim down → rc=1, file untouched - [x] `generate.py --self-test`, shellcheck, argument_specs validation; ansible-lint: only new `var-naming[no-role-prefix]` (role-wide `ess_*` convention) - [ ] On the host: `--check --diff`, second run without `changed` - [ ] Dry-run (`ess_aim_dry_run: true`): flag a group → space in log; remove flag → abandon logged; rename; shim stopped → timer unit fails
Every authentik group with matrix_space: true becomes a group-sync space
without a playbook run. A systemd timer runs a one-shot compose service that
reads ou=groups via LDAP and writes aim-config.yaml = aim-config.base.yaml +
spaces: <json>; Ansible then only renders the base file, so the config has a
single writer. LDAP errors, empty results and removals above max_removals
leave the file untouched and exit non-zero. Like ldap-paging-shim, the
generator is a stdlib-only script on the stock python image (no image
build). Off by default; rendered files are byte-identical when disabled.

Signed-off-by: Simon Bärlocher <simon@whatwedo.ch>
Brings the gate that already runs on a deployed host back into the role:
with ess_aim_ldap_paging_shim_require_member_of set to a group DN, the shim
drops user entries not memberOf that group from group-sync's rewritten
search, so only its members are provisioned. Empty by default (gate off,
compose unchanged); without it, the next deploy would overwrite the host's
local shim and silently provision every user.

Signed-off-by: Simon Bärlocher <simon@whatwedo.ch>
Owner

Review feat/aim-spaces-generator (2b05e85, 0a8b1cc)

Sauber gebaut, aus meiner Sicht merge-fähig. Ein paar Punkte zum Klären vorher, einer davon zeitkritisch.

Geprüft

  • Kompletter Diff gelesen (13 Dateien), generate.py --self-test läuft durch.
  • Merge in main konfliktfrei (main steht noch auf dem Merge-Base 63ff6a5).
  • ansible-lint: keine neuen Fehlerarten, nur die bestehenden var-naming[no-role-prefix].
  • Im authentik-Code gegengeprüft (internal/outpost/ldap/group/group.go, utils.go): Gruppen liefern uid = Gruppen-PK (bleibt bei Umbenennung stabil). Attributnamen werden nur bei / → - und . angepasst, matrix_space kommt also unverändert an. Ein boolesches true wird zu "true".
  • Task-Reihenfolge passt: deploy.yml (Compose gerendert und gestartet) läuft vor aim.yml, also vor dem ersten run.sh.

Gefällt mir

  • Genau ein Schreiber pro Datei (Base durch Ansible, aim-config.yaml durch den Generator) plus assert gegen spaces: in ess_aim_extra_config.
  • Sicherungen: atomares Schreiben, Abbruch bei LDAP-Fehler oder 0 Gruppen, max_removals, flock, Restart nur bei RESULT: changed, Bind-Passwort als 0400-Datei.

Punkte

  1. Drift auf Prod (zeitkritisch): Auf .148 läuft ldap-paging-shim.py bereits im Stand dieses Branches, mit SHIM_REQUIRE_MEMBER_OF=cn=matrix-users,…. Ein Playbook-Lauf aus main würde den Filter wieder entfernen. Bitte mergen, bevor das nächste Mal aus main deployt wird.
  2. Root-Space beim Umstieg: sek2-root steht heute als mehrzeilige spaces:-Liste in ess_aim_extra_config. Er muss nach ess_aim_spaces_generator_static_spaces, sonst verschwindet er. parse_spaces() erkennt nur die eigene einzeilige JSON-Form, also sieht der Entfernungsschutz beim ersten Lauf die alten Spaces nicht. Vorschlag: im README erwähnen oder die alte Form beim Vergleich mitlesen.
  3. Filter offen bei Lesefehlern: drop_for_membership() lässt einen Eintrag durch, wenn entry_attributes() ihn nicht lesen kann. Für einen Filter würde ich eher geschlossen erwarten: im Zweifel weglassen und loggen. Mindestens ein Log-Eintrag wäre hilfreich.
  4. Filter nur auf direkte Mitgliedschaft: memberOf vom Outpost enthält nur direkte Gruppen. Unterguppen von matrix-users greifen also nur mit flattenInheritance: true (Flatten-Timer). Gehört ins README.
  5. Abgrenzung: Der Filter steuert nur die AIM-Provisionierung, nicht den Login über MAS. Wäre gut, das im README klarzustellen.
  6. Klein:
    • aim-config.base.yaml und aim-config.yaml enthalten beide Admin-Token und DB-Passwort mit 0644. Das Muster gab es vorher schon, jetzt liegt es doppelt da. Eventuell 0640 mit Gruppe 65532?
    • „Run the space generator once“: Ist LDAP nicht erreichbar, bricht jeder ESS-Playbook-Lauf ab, auch bei fremden Änderungen. Bewusst so? Sonst ignore_errors mit Warnung.
    • gated_ids_dropped wird pro Verbindung gezählt und vermischt sich bei parallelen Suchen auf derselben Verbindung. Betrifft nur das Log.

Für später (nicht blockierend)

Rollen fehlen noch: Der Generator setzt pro Space nur externalId, kein powerLevel. Für Moderatoren und Admins aus authentik bräuchte es z. B. ein zweites Attribut (matrix_space_moderators: <Gruppe>) oder Einträge in static_spaces.

Vorschlag zum Vorgehen

Punkte 2 und 3 klären, dann mergen. Danach im sek2-Inventar ess_aim_spaces_generator_* setzen, den Root-Space nach static_spaces verschieben und zuerst mit ess_aim_dry_run: true deployen. Im Log prüfen, ob die externalId (DN) passt, dann live schalten.

## Review `feat/aim-spaces-generator` (2b05e85, 0a8b1cc) Sauber gebaut, aus meiner Sicht **merge-fähig**. Ein paar Punkte zum Klären vorher, einer davon zeitkritisch. ### Geprüft - Kompletter Diff gelesen (13 Dateien), `generate.py --self-test` läuft durch. - Merge in `main` konfliktfrei (`main` steht noch auf dem Merge-Base 63ff6a5). - ansible-lint: keine neuen Fehlerarten, nur die bestehenden `var-naming[no-role-prefix]`. - Im authentik-Code gegengeprüft (`internal/outpost/ldap/group/group.go`, `utils.go`): Gruppen liefern `uid` = Gruppen-PK (bleibt bei Umbenennung stabil). Attributnamen werden nur bei `/` → `-` und `.` angepasst, `matrix_space` kommt also unverändert an. Ein boolesches `true` wird zu `"true"`. - Task-Reihenfolge passt: `deploy.yml` (Compose gerendert und gestartet) läuft vor `aim.yml`, also vor dem ersten `run.sh`. ### Gefällt mir - Genau ein Schreiber pro Datei (Base durch Ansible, `aim-config.yaml` durch den Generator) plus `assert` gegen `spaces:` in `ess_aim_extra_config`. - Sicherungen: atomares Schreiben, Abbruch bei LDAP-Fehler oder 0 Gruppen, `max_removals`, `flock`, Restart nur bei `RESULT: changed`, Bind-Passwort als 0400-Datei. ### Punkte 1. **Drift auf Prod (zeitkritisch):** Auf .148 läuft `ldap-paging-shim.py` bereits im Stand dieses Branches, mit `SHIM_REQUIRE_MEMBER_OF=cn=matrix-users,…`. Ein Playbook-Lauf aus `main` würde den Filter wieder entfernen. Bitte mergen, bevor das nächste Mal aus `main` deployt wird. 2. **Root-Space beim Umstieg:** `sek2-root` steht heute als mehrzeilige `spaces:`-Liste in `ess_aim_extra_config`. Er muss nach `ess_aim_spaces_generator_static_spaces`, sonst verschwindet er. `parse_spaces()` erkennt nur die eigene einzeilige JSON-Form, also sieht der Entfernungsschutz beim ersten Lauf die alten Spaces nicht. Vorschlag: im README erwähnen oder die alte Form beim Vergleich mitlesen. 3. **Filter offen bei Lesefehlern:** `drop_for_membership()` lässt einen Eintrag durch, wenn `entry_attributes()` ihn nicht lesen kann. Für einen Filter würde ich eher geschlossen erwarten: im Zweifel weglassen und loggen. Mindestens ein Log-Eintrag wäre hilfreich. 4. **Filter nur auf direkte Mitgliedschaft:** `memberOf` vom Outpost enthält nur direkte Gruppen. Unterguppen von `matrix-users` greifen also nur mit `flattenInheritance: true` (Flatten-Timer). Gehört ins README. 5. **Abgrenzung:** Der Filter steuert nur die AIM-Provisionierung, nicht den Login über MAS. Wäre gut, das im README klarzustellen. 6. **Klein:** - `aim-config.base.yaml` und `aim-config.yaml` enthalten beide Admin-Token und DB-Passwort mit 0644. Das Muster gab es vorher schon, jetzt liegt es doppelt da. Eventuell 0640 mit Gruppe 65532? - „Run the space generator once“: Ist LDAP nicht erreichbar, bricht jeder ESS-Playbook-Lauf ab, auch bei fremden Änderungen. Bewusst so? Sonst `ignore_errors` mit Warnung. - `gated_ids_dropped` wird pro Verbindung gezählt und vermischt sich bei parallelen Suchen auf derselben Verbindung. Betrifft nur das Log. ### Für später (nicht blockierend) Rollen fehlen noch: Der Generator setzt pro Space nur `externalId`, kein `powerLevel`. Für Moderatoren und Admins aus authentik bräuchte es z. B. ein zweites Attribut (`matrix_space_moderators: <Gruppe>`) oder Einträge in `static_spaces`. ### Vorschlag zum Vorgehen Punkte 2 und 3 klären, dann mergen. Danach im sek2-Inventar `ess_aim_spaces_generator_*` setzen, den Root-Space nach `static_spaces` verschieben und zuerst mit `ess_aim_dry_run: true` deployen. Im Log prüfen, ob die `externalId` (DN) passt, dann live schalten.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/aim-spaces-generator:feat/aim-spaces-generator
git switch feat/aim-spaces-generator

Merge

Merge the changes and update on Forgejo.
git switch main
git merge --no-ff feat/aim-spaces-generator
git switch feat/aim-spaces-generator
git rebase main
git switch main
git merge --ff-only feat/aim-spaces-generator
git switch feat/aim-spaces-generator
git rebase main
git switch main
git merge --no-ff feat/aim-spaces-generator
git switch main
git merge --squash feat/aim-spaces-generator
git switch main
git merge --ff-only feat/aim-spaces-generator
git switch main
git merge feat/aim-spaces-generator
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
souveredu/digitalboard.core!20
No description provided.