roles policy: only revoke memberships it granted itself (manual adds survive); track in user attributes

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 17:34:43 -05:00
parent 2ab0a4e837
commit 52c065b753
2 changed files with 18 additions and 4 deletions

View File

@@ -264,7 +264,8 @@ Design: **Authentik authenticates once for the whole domain; Caddy authorizes pe
`discord-roles-to-groups` from `authentik/discord-roles-to-groups.py` (fill in the server ID and the
role-id → group table). Bind it to `default-source-enrollment` **and** `default-source-authentication`
(engine mode `all`, after `discord-guild-member`). From then on giving someone a Discord role gives them
the matching access on next login; removing it removes it.
the matching access on next login; removing it removes it. Memberships added by hand in Authentik are left
alone: the policy only revokes what it granted itself (tracked in the user attribute `discord_role_groups`).
copyparty additionally requires a header with a secret name that only Caddy adds (`idp-h-key`,
`COPYPARTY_IDP_KEY` in `.env`), so identity headers cannot be forged even if copyparty were reached directly.
Logout: copyparty's logout link, the live portal's "Sign out" and the dock's "sign out" go through

View File

@@ -5,6 +5,7 @@
# guilds guilds.members.read ). Existing users re-consent once at Discord.
# The Authentik groups must already exist (Directory -> Groups). Role IDs: Discord Developer Mode ->
# Server Settings -> Roles -> right-click a role -> Copy Role ID.
# Manual memberships are safe: the policy only removes groups it granted itself (user attribute discord_role_groups).
GUILD_ID = "1387203044084551690" # <- your Discord server ID
ROLE_TO_GROUP = { # <- Discord role id : Authentik group name
@@ -40,17 +41,29 @@ wanted = {g for r, g in ROLE_TO_GROUP.items() if r in roles}
managed = set(ROLE_TO_GROUP.values())
# Authentication flow: the user exists -> edit memberships directly.
# Only memberships THIS policy granted earlier are ever removed (tracked in user.attributes["discord_role_groups"]),
# so a group someone was added to by hand in Authentik survives role changes and re-logins.
user = context.get("pending_user") or (request.user if request.user.is_authenticated else None)
if user and user.pk:
attrs = user.attributes or {}
granted = set(attrs.get("discord_role_groups", []))
for group in Group.objects.filter(name__in=managed):
if group.name in wanted:
group.users.add(user)
else:
group.users.remove(user)
granted.add(group.name)
elif group.name in granted:
group.users.remove(user) # role gone -> take back what the role had given
granted.discard(group.name)
attrs["discord_role_groups"] = sorted(granted)
user.attributes = attrs
user.save()
return True
# Enrollment flow: the user is created later by the user-write stage; hand it the groups to attach.
# Enrollment flow: the user is created later by the user-write stage; hand it the groups to attach and
# record them as role-granted so a later role removal can revoke exactly these.
plan = context.get("flow_plan")
if plan is not None:
plan.context["groups"] = list(Group.objects.filter(name__in=wanted))
prompt = plan.context.setdefault("prompt_data", {})
prompt.setdefault("attributes", {})["discord_role_groups"] = sorted(wanted)
return True