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

@@ -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