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
 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_query = 'group'
can_discover = True
mapper
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

def create(self, group: authentik.core.models.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

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

def write(self, obj: authentik.core.models.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

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