authentik.enterprise.providers.google_workspace.clients.groups
1from django.db import transaction 2from django.utils.text import slugify 3 4from authentik.core.models import Group 5from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient 6from authentik.enterprise.providers.google_workspace.models import ( 7 GoogleWorkspaceProvider, 8 GoogleWorkspaceProviderGroup, 9 GoogleWorkspaceProviderMapping, 10 GoogleWorkspaceProviderUser, 11) 12from authentik.lib.sync.mapper import PropertyMappingManager 13from authentik.lib.sync.outgoing.base import Direction 14from authentik.lib.sync.outgoing.exceptions import ( 15 NotFoundSyncException, 16 ObjectExistsSyncException, 17 TransientSyncException, 18) 19from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction 20 21 22class GoogleWorkspaceGroupClient( 23 GoogleWorkspaceSyncClient[Group, GoogleWorkspaceProviderGroup, dict] 24): 25 """Google client for groups""" 26 27 connection_type = GoogleWorkspaceProviderGroup 28 connection_type_query = "group" 29 can_discover = True 30 31 def __init__(self, provider: GoogleWorkspaceProvider) -> None: 32 super().__init__(provider) 33 self.mapper = PropertyMappingManager( 34 self.provider.property_mappings_group.all().order_by("name").select_subclasses(), 35 GoogleWorkspaceProviderMapping, 36 ["group", "provider", "connection"], 37 ) 38 39 def to_schema(self, obj: Group, connection: GoogleWorkspaceProviderGroup) -> dict: 40 """Convert authentik group""" 41 return super().to_schema( 42 obj, 43 connection=connection, 44 email=f"{slugify(obj.name)}@{self.provider.default_group_email_domain}", 45 ) 46 47 def delete(self, identifier: str): 48 """Delete group""" 49 GoogleWorkspaceProviderGroup.objects.filter( 50 provider=self.provider, google_id=identifier 51 ).delete() 52 if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE: 53 return self._request(self.directory_service.groups().delete(groupKey=identifier)) 54 55 def create(self, group: Group): 56 """Create group from scratch and create a connection object""" 57 google_group = self.to_schema(group, None) 58 self.check_email_valid(google_group["email"]) 59 with transaction.atomic(): 60 try: 61 response = self._request(self.directory_service.groups().insert(body=google_group)) 62 except ObjectExistsSyncException: 63 # group already exists in google workspace, so we can connect them manually 64 # for groups we need to fetch the group from google as we connect on 65 # ID and not group email 66 group_data = self._request( 67 self.directory_service.groups().get(groupKey=google_group["email"]) 68 ) 69 return GoogleWorkspaceProviderGroup.objects.create( 70 provider=self.provider, 71 group=group, 72 google_id=group_data["id"], 73 attributes=group_data, 74 ) 75 else: 76 return GoogleWorkspaceProviderGroup.objects.create( 77 provider=self.provider, 78 group=group, 79 google_id=response["id"], 80 attributes=response, 81 ) 82 83 def update(self, group: Group, connection: GoogleWorkspaceProviderGroup): 84 """Update existing group""" 85 google_group = self.to_schema(group, connection) 86 self.check_email_valid(google_group["email"]) 87 try: 88 response = self._request( 89 self.directory_service.groups().update( 90 groupKey=connection.google_id, 91 body=google_group, 92 ) 93 ) 94 connection.attributes = response 95 connection.save() 96 except NotFoundSyncException: 97 # Resource missing is handled by self.write, which will re-create the group 98 raise 99 100 def write(self, obj: Group): 101 google_group, created = super().write(obj) 102 self.create_sync_members(obj, google_group) 103 return google_group, created 104 105 def create_sync_members(self, obj: Group, google_group: GoogleWorkspaceProviderGroup): 106 """Sync all members after a group was created""" 107 users = list(obj.users.order_by("id").values_list("id", flat=True)) 108 connections = GoogleWorkspaceProviderUser.objects.filter( 109 provider=self.provider, user__pk__in=users 110 ).values_list("google_id", flat=True) 111 self._patch(google_group.google_id, Direction.add, connections) 112 113 def update_group(self, group: Group, action: Direction, users_set: set[int]): 114 """Update a groups members""" 115 if action == Direction.add: 116 return self._patch_add_users(group, users_set) 117 if action == Direction.remove: 118 return self._patch_remove_users(group, users_set) 119 120 def _patch(self, google_group_id: str, direction: Direction, members: list[str]): 121 for user in members: 122 try: 123 if direction == Direction.add: 124 self._request( 125 self.directory_service.members().insert( 126 groupKey=google_group_id, body={"email": user} 127 ) 128 ) 129 if direction == Direction.remove: 130 self._request( 131 self.directory_service.members().delete( 132 groupKey=google_group_id, memberKey=user 133 ) 134 ) 135 except ObjectExistsSyncException: 136 pass 137 except TransientSyncException: 138 raise 139 140 def _patch_add_users(self, group: Group, users_set: set[int]): 141 """Add users in users_set to group""" 142 if len(users_set) < 1: 143 return 144 google_group = GoogleWorkspaceProviderGroup.objects.filter( 145 provider=self.provider, group=group 146 ).first() 147 if not google_group: 148 self.logger.warning( 149 "could not sync group membership, group does not exist", group=group 150 ) 151 return 152 user_ids = list( 153 GoogleWorkspaceProviderUser.objects.filter( 154 user__pk__in=users_set, provider=self.provider 155 ).values_list("google_id", flat=True) 156 ) 157 if len(user_ids) < 1: 158 return 159 self._patch(google_group.google_id, Direction.add, user_ids) 160 161 def _patch_remove_users(self, group: Group, users_set: set[int]): 162 """Remove users in users_set from group""" 163 if len(users_set) < 1: 164 return 165 google_group = GoogleWorkspaceProviderGroup.objects.filter( 166 provider=self.provider, group=group 167 ).first() 168 if not google_group: 169 self.logger.warning( 170 "could not sync group membership, group does not exist", group=group 171 ) 172 return 173 user_ids = list( 174 GoogleWorkspaceProviderUser.objects.filter( 175 user__pk__in=users_set, provider=self.provider 176 ).values_list("google_id", flat=True) 177 ) 178 if len(user_ids) < 1: 179 return 180 self._patch(google_group.google_id, Direction.remove, user_ids) 181 182 def discover(self): 183 """Iterate through all groups and connect them with authentik groups if possible""" 184 request = self.directory_service.groups().list( 185 customer="my_customer", maxResults=500, orderBy="email" 186 ) 187 while request: 188 response = request.execute() 189 for group in response.get("groups", []): 190 self._discover_single_group(group) 191 request = self.directory_service.groups().list_next( 192 previous_request=request, previous_response=response 193 ) 194 195 def _discover_single_group(self, group: dict): 196 """handle discovery of a single group""" 197 google_name = group["name"] 198 google_id = group["id"] 199 matching_authentik_group = ( 200 self.provider.get_object_qs(Group).filter(name=google_name).first() 201 ) 202 if not matching_authentik_group: 203 return 204 GoogleWorkspaceProviderGroup.objects.update_or_create( 205 provider=self.provider, 206 group=matching_authentik_group, 207 google_id=google_id, 208 defaults={"attributes": group}, 209 ) 210 211 def update_single_attribute(self, connection: GoogleWorkspaceProviderUser): 212 group = self.directory_service.groups().get(connection.google_id) 213 connection.attributes = group
class
GoogleWorkspaceGroupClient(authentik.enterprise.providers.google_workspace.clients.base.GoogleWorkspaceSyncClient[authentik.core.models.Group, authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProviderGroup, dict]):
23class GoogleWorkspaceGroupClient( 24 GoogleWorkspaceSyncClient[Group, GoogleWorkspaceProviderGroup, dict] 25): 26 """Google client for groups""" 27 28 connection_type = GoogleWorkspaceProviderGroup 29 connection_type_query = "group" 30 can_discover = True 31 32 def __init__(self, provider: GoogleWorkspaceProvider) -> None: 33 super().__init__(provider) 34 self.mapper = PropertyMappingManager( 35 self.provider.property_mappings_group.all().order_by("name").select_subclasses(), 36 GoogleWorkspaceProviderMapping, 37 ["group", "provider", "connection"], 38 ) 39 40 def to_schema(self, obj: Group, connection: GoogleWorkspaceProviderGroup) -> dict: 41 """Convert authentik group""" 42 return super().to_schema( 43 obj, 44 connection=connection, 45 email=f"{slugify(obj.name)}@{self.provider.default_group_email_domain}", 46 ) 47 48 def delete(self, identifier: str): 49 """Delete group""" 50 GoogleWorkspaceProviderGroup.objects.filter( 51 provider=self.provider, google_id=identifier 52 ).delete() 53 if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE: 54 return self._request(self.directory_service.groups().delete(groupKey=identifier)) 55 56 def create(self, group: Group): 57 """Create group from scratch and create a connection object""" 58 google_group = self.to_schema(group, None) 59 self.check_email_valid(google_group["email"]) 60 with transaction.atomic(): 61 try: 62 response = self._request(self.directory_service.groups().insert(body=google_group)) 63 except ObjectExistsSyncException: 64 # group already exists in google workspace, so we can connect them manually 65 # for groups we need to fetch the group from google as we connect on 66 # ID and not group email 67 group_data = self._request( 68 self.directory_service.groups().get(groupKey=google_group["email"]) 69 ) 70 return GoogleWorkspaceProviderGroup.objects.create( 71 provider=self.provider, 72 group=group, 73 google_id=group_data["id"], 74 attributes=group_data, 75 ) 76 else: 77 return GoogleWorkspaceProviderGroup.objects.create( 78 provider=self.provider, 79 group=group, 80 google_id=response["id"], 81 attributes=response, 82 ) 83 84 def update(self, group: Group, connection: GoogleWorkspaceProviderGroup): 85 """Update existing group""" 86 google_group = self.to_schema(group, connection) 87 self.check_email_valid(google_group["email"]) 88 try: 89 response = self._request( 90 self.directory_service.groups().update( 91 groupKey=connection.google_id, 92 body=google_group, 93 ) 94 ) 95 connection.attributes = response 96 connection.save() 97 except NotFoundSyncException: 98 # Resource missing is handled by self.write, which will re-create the group 99 raise 100 101 def write(self, obj: Group): 102 google_group, created = super().write(obj) 103 self.create_sync_members(obj, google_group) 104 return google_group, created 105 106 def create_sync_members(self, obj: Group, google_group: GoogleWorkspaceProviderGroup): 107 """Sync all members after a group was created""" 108 users = list(obj.users.order_by("id").values_list("id", flat=True)) 109 connections = GoogleWorkspaceProviderUser.objects.filter( 110 provider=self.provider, user__pk__in=users 111 ).values_list("google_id", flat=True) 112 self._patch(google_group.google_id, Direction.add, connections) 113 114 def update_group(self, group: Group, action: Direction, users_set: set[int]): 115 """Update a groups members""" 116 if action == Direction.add: 117 return self._patch_add_users(group, users_set) 118 if action == Direction.remove: 119 return self._patch_remove_users(group, users_set) 120 121 def _patch(self, google_group_id: str, direction: Direction, members: list[str]): 122 for user in members: 123 try: 124 if direction == Direction.add: 125 self._request( 126 self.directory_service.members().insert( 127 groupKey=google_group_id, body={"email": user} 128 ) 129 ) 130 if direction == Direction.remove: 131 self._request( 132 self.directory_service.members().delete( 133 groupKey=google_group_id, memberKey=user 134 ) 135 ) 136 except ObjectExistsSyncException: 137 pass 138 except TransientSyncException: 139 raise 140 141 def _patch_add_users(self, group: Group, users_set: set[int]): 142 """Add users in users_set to group""" 143 if len(users_set) < 1: 144 return 145 google_group = GoogleWorkspaceProviderGroup.objects.filter( 146 provider=self.provider, group=group 147 ).first() 148 if not google_group: 149 self.logger.warning( 150 "could not sync group membership, group does not exist", group=group 151 ) 152 return 153 user_ids = list( 154 GoogleWorkspaceProviderUser.objects.filter( 155 user__pk__in=users_set, provider=self.provider 156 ).values_list("google_id", flat=True) 157 ) 158 if len(user_ids) < 1: 159 return 160 self._patch(google_group.google_id, Direction.add, user_ids) 161 162 def _patch_remove_users(self, group: Group, users_set: set[int]): 163 """Remove users in users_set from group""" 164 if len(users_set) < 1: 165 return 166 google_group = GoogleWorkspaceProviderGroup.objects.filter( 167 provider=self.provider, group=group 168 ).first() 169 if not google_group: 170 self.logger.warning( 171 "could not sync group membership, group does not exist", group=group 172 ) 173 return 174 user_ids = list( 175 GoogleWorkspaceProviderUser.objects.filter( 176 user__pk__in=users_set, provider=self.provider 177 ).values_list("google_id", flat=True) 178 ) 179 if len(user_ids) < 1: 180 return 181 self._patch(google_group.google_id, Direction.remove, user_ids) 182 183 def discover(self): 184 """Iterate through all groups and connect them with authentik groups if possible""" 185 request = self.directory_service.groups().list( 186 customer="my_customer", maxResults=500, orderBy="email" 187 ) 188 while request: 189 response = request.execute() 190 for group in response.get("groups", []): 191 self._discover_single_group(group) 192 request = self.directory_service.groups().list_next( 193 previous_request=request, previous_response=response 194 ) 195 196 def _discover_single_group(self, group: dict): 197 """handle discovery of a single group""" 198 google_name = group["name"] 199 google_id = group["id"] 200 matching_authentik_group = ( 201 self.provider.get_object_qs(Group).filter(name=google_name).first() 202 ) 203 if not matching_authentik_group: 204 return 205 GoogleWorkspaceProviderGroup.objects.update_or_create( 206 provider=self.provider, 207 group=matching_authentik_group, 208 google_id=google_id, 209 defaults={"attributes": group}, 210 ) 211 212 def update_single_attribute(self, connection: GoogleWorkspaceProviderUser): 213 group = self.directory_service.groups().get(connection.google_id) 214 connection.attributes = group
Google client for groups
GoogleWorkspaceGroupClient( provider: authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider)
32 def __init__(self, provider: GoogleWorkspaceProvider) -> None: 33 super().__init__(provider) 34 self.mapper = PropertyMappingManager( 35 self.provider.property_mappings_group.all().order_by("name").select_subclasses(), 36 GoogleWorkspaceProviderMapping, 37 ["group", "provider", "connection"], 38 )
connection_type =
<class 'authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProviderGroup'>
def
to_schema( self, obj: authentik.core.models.Group, connection: authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProviderGroup) -> dict:
40 def to_schema(self, obj: Group, connection: GoogleWorkspaceProviderGroup) -> dict: 41 """Convert authentik group""" 42 return super().to_schema( 43 obj, 44 connection=connection, 45 email=f"{slugify(obj.name)}@{self.provider.default_group_email_domain}", 46 )
Convert authentik group
def
delete(self, identifier: str):
48 def delete(self, identifier: str): 49 """Delete group""" 50 GoogleWorkspaceProviderGroup.objects.filter( 51 provider=self.provider, google_id=identifier 52 ).delete() 53 if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE: 54 return self._request(self.directory_service.groups().delete(groupKey=identifier))
Delete group
56 def create(self, group: Group): 57 """Create group from scratch and create a connection object""" 58 google_group = self.to_schema(group, None) 59 self.check_email_valid(google_group["email"]) 60 with transaction.atomic(): 61 try: 62 response = self._request(self.directory_service.groups().insert(body=google_group)) 63 except ObjectExistsSyncException: 64 # group already exists in google workspace, so we can connect them manually 65 # for groups we need to fetch the group from google as we connect on 66 # ID and not group email 67 group_data = self._request( 68 self.directory_service.groups().get(groupKey=google_group["email"]) 69 ) 70 return GoogleWorkspaceProviderGroup.objects.create( 71 provider=self.provider, 72 group=group, 73 google_id=group_data["id"], 74 attributes=group_data, 75 ) 76 else: 77 return GoogleWorkspaceProviderGroup.objects.create( 78 provider=self.provider, 79 group=group, 80 google_id=response["id"], 81 attributes=response, 82 )
Create group from scratch and create a connection object
def
update( self, group: authentik.core.models.Group, connection: authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProviderGroup):
84 def update(self, group: Group, connection: GoogleWorkspaceProviderGroup): 85 """Update existing group""" 86 google_group = self.to_schema(group, connection) 87 self.check_email_valid(google_group["email"]) 88 try: 89 response = self._request( 90 self.directory_service.groups().update( 91 groupKey=connection.google_id, 92 body=google_group, 93 ) 94 ) 95 connection.attributes = response 96 connection.save() 97 except NotFoundSyncException: 98 # Resource missing is handled by self.write, which will re-create the group 99 raise
Update existing group
101 def write(self, obj: Group): 102 google_group, created = super().write(obj) 103 self.create_sync_members(obj, google_group) 104 return google_group, created
Write object to destination. Uses self.create and self.update, but can be overwritten for further logic
def
create_sync_members( self, obj: authentik.core.models.Group, google_group: authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProviderGroup):
106 def create_sync_members(self, obj: Group, google_group: GoogleWorkspaceProviderGroup): 107 """Sync all members after a group was created""" 108 users = list(obj.users.order_by("id").values_list("id", flat=True)) 109 connections = GoogleWorkspaceProviderUser.objects.filter( 110 provider=self.provider, user__pk__in=users 111 ).values_list("google_id", flat=True) 112 self._patch(google_group.google_id, Direction.add, connections)
Sync all members after a group was created
def
update_group( self, group: authentik.core.models.Group, action: authentik.lib.sync.outgoing.base.Direction, users_set: set[int]):
114 def update_group(self, group: Group, action: Direction, users_set: set[int]): 115 """Update a groups members""" 116 if action == Direction.add: 117 return self._patch_add_users(group, users_set) 118 if action == Direction.remove: 119 return self._patch_remove_users(group, users_set)
Update a groups members
def
discover(self):
183 def discover(self): 184 """Iterate through all groups and connect them with authentik groups if possible""" 185 request = self.directory_service.groups().list( 186 customer="my_customer", maxResults=500, orderBy="email" 187 ) 188 while request: 189 response = request.execute() 190 for group in response.get("groups", []): 191 self._discover_single_group(group) 192 request = self.directory_service.groups().list_next( 193 previous_request=request, previous_response=response 194 )
Iterate through all groups and connect them with authentik groups if possible
def
update_single_attribute( self, connection: authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProviderUser):
212 def update_single_attribute(self, connection: GoogleWorkspaceProviderUser): 213 group = self.directory_service.groups().get(connection.google_id) 214 connection.attributes = group
Update connection attributes on a connection object, when the connection is manually created