diff --git a/README.md b/README.md index d1cb732..aa55f94 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/authentik/discord-roles-to-groups.py b/authentik/discord-roles-to-groups.py index 9ce6410..9030aaa 100644 --- a/authentik/discord-roles-to-groups.py +++ b/authentik/discord-roles-to-groups.py @@ -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