feat(manage): per-user access: roles, access, grant, revoke #248

Merged
jfraeys merged 2 commits from jfraeys/feat/manage-user-access into main 2026-10-16 19:36:34 -04:00
Owner

Requested: access management for users. 'manage user' gets four subcommands built on the LLDAP groups the Authelia admin-panel rules and the Headscale login policy (#247) already use.

  • manage user roles: roles that exist and who holds them (admin; per site client-admin = client--admin, client = client--employee).
  • manage user access : LLDAP groups, derived roles, admin panels, whether they may sign in to Headscale, their tailnet user and devices, and whether the LOADED Headscale ACL treats them as an admin (and notes when that disagrees with the LLDAP role).
  • manage user grant|revoke --role admin|client-admin|client [--site ]: add/remove the group. Admin needs confirmation; the last admin cannot be revoked; --dry-run works; granting twice or revoking a non-member is a no-op.
  • Tailnet admin devices are NOT changed at runtime: they come from headscale_admin_users (#246), which is reviewed and deployed; a runtime ACL edit would be overwritten by the next deploy. 'access' says when the two disagree. Revoking a role does not remove enrolled devices: manage vpn revoke .

Files: scripts/lib/manage/user_access.sh (new), user.sh (usage + dispatch), roles/manage_cli (file list), runbook (onboard-client-user.md), lint.yml.

Checked: test-user-access.sh (22 cases against stubbed LLDAP/Headscale; disabling the last-admin guard and mis-mapping the client role are both caught); the report run against the real LLDAP user, Headscale users/nodes and loaded policy from my machine (output correct; it reports that the live ACL has no admin group yet, which #246 fixes); shellcheck -S error clean via the official image (and non-zero on a seeded error); ansible-lint on roles/manage_cli passes.
Not checked: grant/revoke against the real LLDAP (needs the new manage installed on services). After merge, promotion, then ./infra deploy --tags manage_cli --limit services, try 'manage user roles' and 'manage user access jfraeys', then a grant/revoke on a throwaway test user.

Depends on: nothing hard; the access report is most useful after #246/#247 are deployed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ

Requested: access management for users. 'manage user' gets four subcommands built on the LLDAP groups the Authelia admin-panel rules and the Headscale login policy (#247) already use. - manage user roles: roles that exist and who holds them (admin; per site client-admin = client-<site>-admin, client = client-<site>-employee). - manage user access <user>: LLDAP groups, derived roles, admin panels, whether they may sign in to Headscale, their tailnet user and devices, and whether the LOADED Headscale ACL treats them as an admin (and notes when that disagrees with the LLDAP role). - manage user grant|revoke <user> --role admin|client-admin|client [--site <slug>]: add/remove the group. Admin needs confirmation; the last admin cannot be revoked; --dry-run works; granting twice or revoking a non-member is a no-op. - Tailnet admin devices are NOT changed at runtime: they come from headscale_admin_users (#246), which is reviewed and deployed; a runtime ACL edit would be overwritten by the next deploy. 'access' says when the two disagree. Revoking a role does not remove enrolled devices: manage vpn revoke <node>. Files: scripts/lib/manage/user_access.sh (new), user.sh (usage + dispatch), roles/manage_cli (file list), runbook (onboard-client-user.md), lint.yml. Checked: test-user-access.sh (22 cases against stubbed LLDAP/Headscale; disabling the last-admin guard and mis-mapping the client role are both caught); the report run against the real LLDAP user, Headscale users/nodes and loaded policy from my machine (output correct; it reports that the live ACL has no admin group yet, which #246 fixes); shellcheck -S error clean via the official image (and non-zero on a seeded error); ansible-lint on roles/manage_cli passes. Not checked: grant/revoke against the real LLDAP (needs the new manage installed on services). After merge, promotion, then ./infra deploy --tags manage_cli --limit services, try 'manage user roles' and 'manage user access jfraeys', then a grant/revoke on a throwaway test user. Depends on: nothing hard; the access report is most useful after #246/#247 are deployed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
feat(manage): user roles/access/grant/revoke (LLDAP roles tied to the Authelia and Headscale access)
All checks were successful
Lint / lint (pull_request) Successful in 2m32s
Secret scan / scan (pull_request) Successful in 17s
2fd56cdb8d
Roles are LLDAP groups: admin; client-admin = client-<site>-admin; client = client-<site>-employee (the groups the sites' /admin/ rules and the
Headscale login policy already use). 'manage user roles' lists them and their holders; 'access <user>' shows groups, roles, admin panels, whether
they may sign in to Headscale, their tailnet user and devices, and whether the loaded ACL treats them as an admin; 'grant'/'revoke' change the group
(confirmation for admin, last admin cannot be revoked, dry run supported). Tailnet admin devices stay declarative (headscale_admin_users) and 'access'
reports when they disagree. user_access.sh is added to the manage_cli role's file list.
Tests: test-user-access.sh (stubbed LLDAP/Headscale; mutations of the last-admin guard and the role mapping are caught). Report run on the real data
(correctly says the live ACL has no admin group yet). shellcheck -S error clean.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Merge main into jfraeys/feat/manage-user-access (lint.yml: keep both new test steps)
All checks were successful
Lint / lint (pull_request) Successful in 3m14s
Secret scan / scan (pull_request) Successful in 19s
ec3dcee5b7
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
jfraeys/infra!248
No description provided.