website/integrations: Improve discord role sync policies (#10219)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
		| @ -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 | ### Checking for membership of a Discord Guild | ||||||
|  |  | ||||||
| :::info | :::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 | ```python | ||||||
| # To get the guild ID number for the parameters, open Discord, go to Settings > Advanced and enable developer mode. | # 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 | ### Checking for membership of a Discord Guild role | ||||||
|  |  | ||||||
| :::info | :::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 | ```python | ||||||
| # To get the role and guild ID numbers for the parameters, open Discord, go to Settings > Advanced and | # 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 | ### Syncing Discord roles to authentik groups | ||||||
|  |  | ||||||
| :::info | :::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: "<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 | #### Sync on enrollment | ||||||
|  |  | ||||||
|  | The following policy will apply the above behaviour when a user enrolls. | ||||||
|  |  | ||||||
| ```python | ```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 | from authentik.core.models import Group | ||||||
|  |  | ||||||
| GUILD_ID = "<YOUR GUILD ID>" |  | ||||||
| MAPPED_ROLES = { |  | ||||||
|     "<Discord Role Id 1>": Group.objects.get_or_create(name="<Authentik Role Name 1>")[0], |  | ||||||
|     "<Discord Role Id 2>": Group.objects.get_or_create(name="<Authentik Role Name 2>")[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" | GUILD_API_URL = "https://discord.com/api/users/@me/guilds/{guild_id}/member" | ||||||
|  |  | ||||||
|  | ### CONFIG ### | ||||||
|  | guild_id = "<YOUR GUILD ID>" | ||||||
|  | ############## | ||||||
|  |  | ||||||
| # Ensure flow is only run during OAuth logins via Discord | # Ensure flow is only run during OAuth logins via Discord | ||||||
| if context["source"].provider_type != "discord": | if context["source"].provider_type != "discord": | ||||||
|     return True |     return True | ||||||
| @ -194,29 +195,43 @@ if not connection: | |||||||
|     return False |     return False | ||||||
| access_token = connection.access_token | access_token = connection.access_token | ||||||
|  |  | ||||||
| guild_member_info = requests.get( | guild_member_request = requests.get( | ||||||
|     GUILD_API_URL.format(guild_id=GUILD_ID), |     GUILD_API_URL.format(guild_id=guild_id), | ||||||
|     headers={"Authorization": "Bearer " + access_token}, |     headers={ | ||||||
| ).json() |         "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 | # Ensure user is a member of the guild | ||||||
| if "code" in guild_member_info: | if "code" in guild_member_info: | ||||||
|     if guild_member_info["code"] == 10004: |     if guild_member_info["code"] == 10004: | ||||||
|         ak_message("User is not a member of the guild") |         ak_message("User is not a member of the guild") | ||||||
|     else: |     else: | ||||||
|         ak_create_event( |         ak_create_event("discord_error", source=context["source"], code=guild_member_info["code"]) | ||||||
|             "discord_error", source=context["source"], code=guild_member_info["code"] |  | ||||||
|         ) |  | ||||||
|         ak_message("Discord API error, try again later.") |         ak_message("Discord API error, try again later.") | ||||||
|     return False |     return False | ||||||
|  |  | ||||||
| # Add all mapped roles the user has in the guild | # Get all discord_groups | ||||||
| groups_to_add = [] | discord_groups = Group.objects.filter(attributes__discord_role_id__isnull=False) | ||||||
| for role_id in MAPPED_ROLES: |  | ||||||
|     if role_id in guild_member_info["roles"]: | # Filter matching roles based on guild_member_info['roles'] | ||||||
|         groups_to_add.append(MAPPED_ROLES[role_id]) | 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 | return True | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| @ -225,25 +240,16 @@ Now bind this policy to the chosen enrollment flows for the Discord OAuth source | |||||||
|  |  | ||||||
| #### Sync on authentication | #### Sync on authentication | ||||||
|  |  | ||||||
|  | The following policy will apply the above behaviour when a user logs in. | ||||||
|  |  | ||||||
| ```python | ```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 | from authentik.core.models import Group | ||||||
|  |  | ||||||
| GUILD_ID = "<YOUR GUILD ID>" |  | ||||||
| MAPPED_ROLES = { |  | ||||||
|     "<Discord Role Id 1>": Group.objects.get_or_create(name="<Authentik Role Name 1>")[0], |  | ||||||
|     "<Discord Role Id 2>": Group.objects.get_or_create(name="<Authentik Role Name 2>")[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" | GUILD_API_URL = "https://discord.com/api/users/@me/guilds/{guild_id}/member" | ||||||
|  |  | ||||||
|  | ### CONFIG ### | ||||||
|  | guild_id = "<YOUR GUILD ID>" | ||||||
|  | ############## | ||||||
|  |  | ||||||
| # Ensure flow is only run during OAuth logins via Discord | # Ensure flow is only run during OAuth logins via Discord | ||||||
| if context["source"].provider_type != "discord": | if context["source"].provider_type != "discord": | ||||||
|     return True |     return True | ||||||
| @ -254,35 +260,50 @@ if not connection: | |||||||
|     return False |     return False | ||||||
| access_token = connection.access_token | access_token = connection.access_token | ||||||
|  |  | ||||||
| guild_member_info = requests.get( | guild_member_request = requests.get( | ||||||
|     GUILD_API_URL.format(guild_id=GUILD_ID), |     GUILD_API_URL.format(guild_id=guild_id), | ||||||
|     headers={"Authorization": "Bearer " + access_token}, |     headers={ | ||||||
| ).json() |         "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 | # Ensure user is a member of the guild | ||||||
| if "code" in guild_member_info: | if "code" in guild_member_info: | ||||||
|     if guild_member_info["code"] == 10004: |     if guild_member_info["code"] == 10004: | ||||||
|         ak_message("User is not a member of the guild") |         ak_message("User is not a member of the guild") | ||||||
|     else: |     else: | ||||||
|         ak_create_event( |         ak_create_event("discord_error", source=context["source"], code=guild_member_info["code"]) | ||||||
|             "discord_error", source=context["source"], code=guild_member_info["code"] |  | ||||||
|         ) |  | ||||||
|         ak_message("Discord API error, try again later.") |         ak_message("Discord API error, try again later.") | ||||||
|     return False |     return False | ||||||
|  |  | ||||||
| # Get the user's current roles and remove all roles we want to remap | # Get all discord_groups | ||||||
| new_groups = [ | discord_groups = Group.objects.filter(attributes__discord_role_id__isnull=False) | ||||||
|     role for role in request.user.ak_groups.all() if role not in MAPPED_ROLES.values() |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| # Add back mapped roles which the user has in the guild | # Split user groups into discord groups and non discord groups | ||||||
| for role_id in MAPPED_ROLES: | user_groups_non_discord = request.user.ak_groups.exclude(pk__in=discord_groups.values_list("pk", flat=True)) | ||||||
|     if role_id in guild_member_info["roles"]: | user_groups_discord = list(request.user.ak_groups.filter(pk__in=discord_groups.values_list("pk", flat=True))) | ||||||
|         new_groups.append(MAPPED_ROLES[role_id]) |  | ||||||
|  | # 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 | # Update user's groups | ||||||
| request.user.ak_groups.set(new_groups) | request.user.ak_groups.set(user_groups_updated) | ||||||
| request.user.save() |  | ||||||
|  | # 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 | 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 | ### Store OAuth info in attribute and create avatar attribute from Discord avatar | ||||||
|  |  | ||||||
| :::info | :::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 | :::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 | 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 | ```python | ||||||
| import base64 | import base64 | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Keyinator
					Keyinator