From f5e8c2891da40468717b862d1708cc02a1dc7140 Mon Sep 17 00:00:00 2001 From: Keyinator Date: Thu, 18 Jul 2024 21:15:48 +0200 Subject: [PATCH] website/integrations: Improve discord role sync policies (#10219) Co-authored-by: Tana M Berry Co-authored-by: Marc 'risson' Schmitt --- website/docs/sources/discord/index.md | 157 +++++++++++++++----------- 1 file changed, 89 insertions(+), 68 deletions(-) diff --git a/website/docs/sources/discord/index.md b/website/docs/sources/discord/index.md index da79ea8507..9970a12da2 100644 --- a/website/docs/sources/discord/index.md +++ b/website/docs/sources/discord/index.md @@ -56,10 +56,10 @@ For more details on how-to have the new source display on the Login Page see [he ### Checking for membership of a Discord Guild :::info -Ensure that the Discord OAuth source in 'Federation & Social login' has the additional `guilds` scope added under the 'Protocol settings'. +Ensure that the Discord OAuth source in **Federation & Social login** has the additional `guilds guilds.members.read` scopes added under **Protocol settings**. ::: -Create a new 'Expression Policy' with the content below, adjusting the variables where required: +Create a new **Expression Policy** with the content below, adjusting the variables where required: ```python # To get the guild ID number for the parameters, open Discord, go to Settings > Advanced and enable developer mode. @@ -98,10 +98,10 @@ Now bind this policy to the chosen enrollment and authentication flows for the D ### Checking for membership of a Discord Guild role :::info -Ensure that the Discord OAuth source in 'Federation & Social login' has the additional `guilds guilds.members.read` scopes added under the 'Protocol settings'. +Ensure that the Discord OAuth source in **Federation & Social login** has the additional `guilds guilds.members.read` scopes added under **Protocol settings**. ::: -Create a new 'Expression Policy' with the content below, adjusting the variables where required: +Create a new **Expression Policy** with the content below, adjusting the variables where required: ```python # To get the role and guild ID numbers for the parameters, open Discord, go to Settings > Advanced and @@ -158,32 +158,33 @@ Now bind this policy to the chosen enrollment and authentication flows for the D ### Syncing Discord roles to authentik groups :::info -Ensure that the Discord OAuth source in 'Federation & Social login' has the additional `guilds.members.read` scopes added under the 'Protocol settings'. +Ensure that the Discord OAuth source in **Federation & Social login** has the additional `guilds.members.read` scopes added under **Protocol settings**. ::: -Create a new 'Expression Policy' with the content below, adjusting the variables where required. +:::info +Any authentik role that you want to sync with a Discord role needs to have the **attribute** `discord_role_id` with a value of the Discord role's ID set. +This setting can be found under `Authentik > Admin Interface > Directory > Groups > YOUR_GROUP > Attributes` +Example: `discord_role_id: ""` +::: + +The following two policies allow you to synchronize roles in a Discord guild with roles in authentik. +Whenever a user enrolls or signs in to authentik via a Discord source, these policies will check the user's Discord roles and apply the user's authentik roles accordingly. +All roles with the attribute `discord_role_id` defined will be added or removed depending on whether the user is a member of the defined Discord role. + +Create a new **Expression Policy** with the content below, adjusting the variables where required. #### Sync on enrollment +The following policy will apply the above behaviour when a user enrolls. + ```python -# To get the role and guild ID numbers for the parameters, open Discord, go to Settings > Advanced and -# enable developer mode. -# Right-click on the server/guild title and select "Copy ID" to get the guild ID. -# Right-click on the server/guild title and select server settings > roles, right click on the role and click -# "Copy ID" to get the role ID. - from authentik.core.models import Group - -GUILD_ID = "" -MAPPED_ROLES = { - "": Group.objects.get_or_create(name="")[0], - "": Group.objects.get_or_create(name="")[0], - # You can add mapped roles by copying the above line and adjusting to your needs -} - -# Only change below here if you know what you are doing. GUILD_API_URL = "https://discord.com/api/users/@me/guilds/{guild_id}/member" +### CONFIG ### +guild_id = "" +############## + # Ensure flow is only run during OAuth logins via Discord if context["source"].provider_type != "discord": return True @@ -194,29 +195,43 @@ if not connection: return False access_token = connection.access_token -guild_member_info = requests.get( - GUILD_API_URL.format(guild_id=GUILD_ID), - headers={"Authorization": "Bearer " + access_token}, -).json() +guild_member_request = requests.get( + GUILD_API_URL.format(guild_id=guild_id), + headers={ + "Authorization": f"Bearer {access_token}", + }, +) +guild_member_info = guild_member_request.json() + +# Ensure we are not being ratelimited +if guild_member_request.status_code == 429: + ak_message(f"Discord is throttling this connection. Retry in {int(guild_member_info['retry_after'])}s") + return False # Ensure user is a member of the guild if "code" in guild_member_info: if guild_member_info["code"] == 10004: ak_message("User is not a member of the guild") else: - ak_create_event( - "discord_error", source=context["source"], code=guild_member_info["code"] - ) + ak_create_event("discord_error", source=context["source"], code=guild_member_info["code"]) ak_message("Discord API error, try again later.") return False -# Add all mapped roles the user has in the guild -groups_to_add = [] -for role_id in MAPPED_ROLES: - if role_id in guild_member_info["roles"]: - groups_to_add.append(MAPPED_ROLES[role_id]) +# Get all discord_groups +discord_groups = Group.objects.filter(attributes__discord_role_id__isnull=False) + +# Filter matching roles based on guild_member_info['roles'] +user_groups_discord_updated = discord_groups.filter(attributes__discord_role_id__in=guild_member_info["roles"]) + +# Set matchin_roles in flow context +request.context["flow_plan"].context["groups"] = user_groups_discord_updated + +# Create event with roles added +ak_create_event( + "discord_role_sync", + user_discord_roles_added=", ".join(str(group) for group in user_groups_discord_updated), +) -request.context["flow_plan"].context["groups"] = groups_to_add return True ``` @@ -225,25 +240,16 @@ Now bind this policy to the chosen enrollment flows for the Discord OAuth source #### Sync on authentication +The following policy will apply the above behaviour when a user logs in. + ```python -# To get the role and guild ID numbers for the parameters, open Discord, go to Settings > Advanced and -# enable developer mode. -# Right-click on the server/guild title and select "Copy ID" to get the guild ID. -# Right-click on the server/guild title and select server settings > roles, right click on the role and click -# "Copy ID" to get the role ID. - from authentik.core.models import Group - -GUILD_ID = "" -MAPPED_ROLES = { - "": Group.objects.get_or_create(name="")[0], - "": Group.objects.get_or_create(name="")[0], - # You can add mapped roles by copying the above line and adjusting to your needs -} - -# Only change below here if you know what you are doing. GUILD_API_URL = "https://discord.com/api/users/@me/guilds/{guild_id}/member" +### CONFIG ### +guild_id = "" +############## + # Ensure flow is only run during OAuth logins via Discord if context["source"].provider_type != "discord": return True @@ -254,35 +260,50 @@ if not connection: return False access_token = connection.access_token -guild_member_info = requests.get( - GUILD_API_URL.format(guild_id=GUILD_ID), - headers={"Authorization": "Bearer " + access_token}, -).json() +guild_member_request = requests.get( + GUILD_API_URL.format(guild_id=guild_id), + headers={ + "Authorization": f"Bearer {access_token}" + }, +) +guild_member_info = guild_member_request.json() + +# Ensure we are not being ratelimited +if guild_member_request.status_code == 429: + ak_message(f"Discord is throttling this connection. Retry in {int(guild_member_info['retry_after'])}s") + return False # Ensure user is a member of the guild if "code" in guild_member_info: if guild_member_info["code"] == 10004: ak_message("User is not a member of the guild") else: - ak_create_event( - "discord_error", source=context["source"], code=guild_member_info["code"] - ) + ak_create_event("discord_error", source=context["source"], code=guild_member_info["code"]) ak_message("Discord API error, try again later.") return False -# Get the user's current roles and remove all roles we want to remap -new_groups = [ - role for role in request.user.ak_groups.all() if role not in MAPPED_ROLES.values() -] +# Get all discord_groups +discord_groups = Group.objects.filter(attributes__discord_role_id__isnull=False) -# Add back mapped roles which the user has in the guild -for role_id in MAPPED_ROLES: - if role_id in guild_member_info["roles"]: - new_groups.append(MAPPED_ROLES[role_id]) +# Split user groups into discord groups and non discord groups +user_groups_non_discord = request.user.ak_groups.exclude(pk__in=discord_groups.values_list("pk", flat=True)) +user_groups_discord = list(request.user.ak_groups.filter(pk__in=discord_groups.values_list("pk", flat=True))) + +# Filter matching roles based on guild_member_info['roles'] +user_groups_discord_updated = discord_groups.filter(attributes__discord_role_id__in=guild_member_info["roles"]) + +# Combine user_groups_non_discord and matching_roles +user_groups_updated = user_groups_non_discord.union(user_groups_discord_updated) # Update user's groups -request.user.ak_groups.set(new_groups) -request.user.save() +request.user.ak_groups.set(user_groups_updated) + +# Create event with roles changed +ak_create_event( + "discord_role_sync", + user_discord_roles_before=", ".join(str(group) for group in user_groups_discord), + user_discord_roles_after=", ".join(str(group) for group in user_groups_discord_updated), +) return True @@ -293,14 +314,14 @@ Now bind this policy to the chosen authentication flows for the Discord OAuth so ### Store OAuth info in attribute and create avatar attribute from Discord avatar :::info -Ensure that the Discord OAuth source in 'Federation & Social login' has the additional `guilds.members.read` scopes added under the 'Protocol settings'. +Ensure that the Discord OAuth source in **Federation & Social login** has the additional `guilds.members.read` scopes added under **Protocol settings**. ::: :::info In order to use the created attribute in authentik you will have to set authentik configuration arguments found at: https://docs.goauthentik.io/docs/core/settings#avatars ::: -Create a new 'Expression Policy' with the content below, adjusting the variables where required: +Create a new **Expression Policy** with the content below, adjusting the variables where required: ```python import base64