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:
@@ -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
|
`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`
|
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
|
(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 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.
|
`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
|
Logout: copyparty's logout link, the live portal's "Sign out" and the dock's "sign out" go through
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
# guilds guilds.members.read ). Existing users re-consent once at Discord.
|
# guilds guilds.members.read ). Existing users re-consent once at Discord.
|
||||||
# The Authentik groups must already exist (Directory -> Groups). Role IDs: Discord Developer Mode ->
|
# The Authentik groups must already exist (Directory -> Groups). Role IDs: Discord Developer Mode ->
|
||||||
# Server Settings -> Roles -> right-click a role -> Copy Role ID.
|
# 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
|
GUILD_ID = "1387203044084551690" # <- your Discord server ID
|
||||||
ROLE_TO_GROUP = { # <- Discord role id : Authentik group name
|
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())
|
managed = set(ROLE_TO_GROUP.values())
|
||||||
|
|
||||||
# Authentication flow: the user exists -> edit memberships directly.
|
# 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)
|
user = context.get("pending_user") or (request.user if request.user.is_authenticated else None)
|
||||||
if user and user.pk:
|
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):
|
for group in Group.objects.filter(name__in=managed):
|
||||||
if group.name in wanted:
|
if group.name in wanted:
|
||||||
group.users.add(user)
|
group.users.add(user)
|
||||||
else:
|
granted.add(group.name)
|
||||||
group.users.remove(user)
|
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
|
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")
|
plan = context.get("flow_plan")
|
||||||
if plan is not None:
|
if plan is not None:
|
||||||
plan.context["groups"] = list(Group.objects.filter(name__in=wanted))
|
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
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user