lib/providers/sync: improve outgoing sync (#9835)

* make connection objects not updatable but allow creating with provider

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* save data returned from google/entra and show it in UI

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* pass connection object

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set immutable id on user automatically

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* better define transient error codes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix entra

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2024-05-30 10:40:10 +09:00
committed by GitHub
parent dae4bf0d6b
commit 50fffa72cc
25 changed files with 292 additions and 408 deletions

View File

@ -1,14 +1,15 @@
"""GoogleWorkspaceProviderGroup API Views""" """GoogleWorkspaceProviderGroup API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer from authentik.core.api.users import UserGroupSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup
class GoogleWorkspaceProviderGroupSerializer(SourceSerializer): class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
"""GoogleWorkspaceProviderGroup Serializer""" """GoogleWorkspaceProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True) group_obj = UserGroupSerializer(source="group", read_only=True)
@ -20,10 +21,20 @@ class GoogleWorkspaceProviderGroupSerializer(SourceSerializer):
"id", "id",
"group", "group",
"group_obj", "group_obj",
"provider",
"attributes",
] ]
extra_kwargs = {"attributes": {"read_only": True}}
class GoogleWorkspaceProviderGroupViewSet(UsedByMixin, ModelViewSet): class GoogleWorkspaceProviderGroupViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""GoogleWorkspaceProviderGroup Viewset""" """GoogleWorkspaceProviderGroup Viewset"""
queryset = GoogleWorkspaceProviderGroup.objects.all().select_related("group") queryset = GoogleWorkspaceProviderGroup.objects.all().select_related("group")

View File

@ -1,14 +1,15 @@
"""GoogleWorkspaceProviderUser API Views""" """GoogleWorkspaceProviderUser API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser
class GoogleWorkspaceProviderUserSerializer(SourceSerializer): class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
"""GoogleWorkspaceProviderUser Serializer""" """GoogleWorkspaceProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True) user_obj = GroupMemberSerializer(source="user", read_only=True)
@ -20,10 +21,20 @@ class GoogleWorkspaceProviderUserSerializer(SourceSerializer):
"id", "id",
"user", "user",
"user_obj", "user_obj",
"provider",
"attributes",
] ]
extra_kwargs = {"attributes": {"read_only": True}}
class GoogleWorkspaceProviderUserViewSet(UsedByMixin, ModelViewSet): class GoogleWorkspaceProviderUserViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""GoogleWorkspaceProviderUser Viewset""" """GoogleWorkspaceProviderUser Viewset"""
queryset = GoogleWorkspaceProviderUser.objects.all().select_related("user") queryset = GoogleWorkspaceProviderUser.objects.all().select_related("user")

View File

@ -33,14 +33,14 @@ class GoogleWorkspaceGroupClient(
self.mapper = PropertyMappingManager( self.mapper = PropertyMappingManager(
self.provider.property_mappings_group.all().order_by("name").select_subclasses(), self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
GoogleWorkspaceProviderMapping, GoogleWorkspaceProviderMapping,
["group", "provider", "creating"], ["group", "provider", "connection"],
) )
def to_schema(self, obj: Group, creating: bool) -> dict: def to_schema(self, obj: Group, connection: GoogleWorkspaceProviderGroup) -> dict:
"""Convert authentik group""" """Convert authentik group"""
return super().to_schema( return super().to_schema(
obj, obj,
creating, connection=connection,
email=f"{slugify(obj.name)}@{self.provider.default_group_email_domain}", email=f"{slugify(obj.name)}@{self.provider.default_group_email_domain}",
) )
@ -61,7 +61,7 @@ class GoogleWorkspaceGroupClient(
def create(self, group: Group): def create(self, group: Group):
"""Create group from scratch and create a connection object""" """Create group from scratch and create a connection object"""
google_group = self.to_schema(group, True) google_group = self.to_schema(group, None)
self.check_email_valid(google_group["email"]) self.check_email_valid(google_group["email"])
with transaction.atomic(): with transaction.atomic():
try: try:
@ -74,16 +74,22 @@ class GoogleWorkspaceGroupClient(
self.directory_service.groups().get(groupKey=google_group["email"]) self.directory_service.groups().get(groupKey=google_group["email"])
) )
return GoogleWorkspaceProviderGroup.objects.create( return GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=group_data["id"] provider=self.provider,
group=group,
google_id=group_data["id"],
attributes=group_data,
) )
else: else:
return GoogleWorkspaceProviderGroup.objects.create( return GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=response["id"] provider=self.provider,
group=group,
google_id=response["id"],
attributes=response,
) )
def update(self, group: Group, connection: GoogleWorkspaceProviderGroup): def update(self, group: Group, connection: GoogleWorkspaceProviderGroup):
"""Update existing group""" """Update existing group"""
google_group = self.to_schema(group, False) google_group = self.to_schema(group, connection)
self.check_email_valid(google_group["email"]) self.check_email_valid(google_group["email"])
try: try:
return self._request( return self._request(
@ -204,4 +210,5 @@ class GoogleWorkspaceGroupClient(
provider=self.provider, provider=self.provider,
group=matching_authentik_group, group=matching_authentik_group,
google_id=google_id, google_id=google_id,
attributes=group,
) )

View File

@ -28,15 +28,12 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
self.mapper = PropertyMappingManager( self.mapper = PropertyMappingManager(
self.provider.property_mappings.all().order_by("name").select_subclasses(), self.provider.property_mappings.all().order_by("name").select_subclasses(),
GoogleWorkspaceProviderMapping, GoogleWorkspaceProviderMapping,
["provider", "creating"], ["provider", "connection"],
) )
def to_schema(self, obj: User, creating: bool) -> dict: def to_schema(self, obj: User, connection: GoogleWorkspaceProviderUser) -> dict:
"""Convert authentik user""" """Convert authentik user"""
raw_google_user = super().to_schema(obj, creating) return delete_none_values(super().to_schema(obj, connection, primaryEmail=obj.email))
if "primaryEmail" not in raw_google_user:
raw_google_user["primaryEmail"] = str(obj.email)
return delete_none_values(raw_google_user)
def delete(self, obj: User): def delete(self, obj: User):
"""Delete user""" """Delete user"""
@ -63,7 +60,7 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
def create(self, user: User): def create(self, user: User):
"""Create user from scratch and create a connection object""" """Create user from scratch and create a connection object"""
google_user = self.to_schema(user, True) google_user = self.to_schema(user, None)
self.check_email_valid( self.check_email_valid(
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])] google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
) )
@ -73,18 +70,21 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
except ObjectExistsSyncException: except ObjectExistsSyncException:
# user already exists in google workspace, so we can connect them manually # user already exists in google workspace, so we can connect them manually
return GoogleWorkspaceProviderUser.objects.create( return GoogleWorkspaceProviderUser.objects.create(
provider=self.provider, user=user, google_id=user.email provider=self.provider, user=user, google_id=user.email, attributes={}
) )
except TransientSyncException as exc: except TransientSyncException as exc:
raise exc raise exc
else: else:
return GoogleWorkspaceProviderUser.objects.create( return GoogleWorkspaceProviderUser.objects.create(
provider=self.provider, user=user, google_id=response["primaryEmail"] provider=self.provider,
user=user,
google_id=response["primaryEmail"],
attributes=response,
) )
def update(self, user: User, connection: GoogleWorkspaceProviderUser): def update(self, user: User, connection: GoogleWorkspaceProviderUser):
"""Update existing user""" """Update existing user"""
google_user = self.to_schema(user, False) google_user = self.to_schema(user, connection)
self.check_email_valid( self.check_email_valid(
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])] google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
) )
@ -115,4 +115,5 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
provider=self.provider, provider=self.provider,
user=matching_authentik_user, user=matching_authentik_user,
google_id=email, google_id=email,
attributes=user,
) )

View File

@ -0,0 +1,26 @@
# Generated by Django 5.0.6 on 2024-05-23 20:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_google_workspace",
"0001_squashed_0002_alter_googleworkspaceprovidergroup_options_and_more",
),
]
operations = [
migrations.AddField(
model_name="googleworkspaceprovidergroup",
name="attributes",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="googleworkspaceprovideruser",
name="attributes",
field=models.JSONField(default=dict),
),
]

View File

@ -153,6 +153,7 @@ class GoogleWorkspaceProviderUser(SerializerModel):
google_id = models.TextField() google_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE) provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:
@ -178,6 +179,7 @@ class GoogleWorkspaceProviderGroup(SerializerModel):
google_id = models.TextField() google_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE) provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:

View File

@ -1,14 +1,15 @@
"""MicrosoftEntraProviderGroup API Views""" """MicrosoftEntraProviderGroup API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer from authentik.core.api.users import UserGroupSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup
class MicrosoftEntraProviderGroupSerializer(SourceSerializer): class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
"""MicrosoftEntraProviderGroup Serializer""" """MicrosoftEntraProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True) group_obj = UserGroupSerializer(source="group", read_only=True)
@ -20,10 +21,20 @@ class MicrosoftEntraProviderGroupSerializer(SourceSerializer):
"id", "id",
"group", "group",
"group_obj", "group_obj",
"provider",
"attributes",
] ]
extra_kwargs = {"attributes": {"read_only": True}}
class MicrosoftEntraProviderGroupViewSet(UsedByMixin, ModelViewSet): class MicrosoftEntraProviderGroupViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""MicrosoftEntraProviderGroup Viewset""" """MicrosoftEntraProviderGroup Viewset"""
queryset = MicrosoftEntraProviderGroup.objects.all().select_related("group") queryset = MicrosoftEntraProviderGroup.objects.all().select_related("group")

View File

@ -1,14 +1,15 @@
"""MicrosoftEntraProviderUser API Views""" """MicrosoftEntraProviderUser API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser
class MicrosoftEntraProviderUserSerializer(SourceSerializer): class MicrosoftEntraProviderUserSerializer(ModelSerializer):
"""MicrosoftEntraProviderUser Serializer""" """MicrosoftEntraProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True) user_obj = GroupMemberSerializer(source="user", read_only=True)
@ -20,10 +21,20 @@ class MicrosoftEntraProviderUserSerializer(SourceSerializer):
"id", "id",
"user", "user",
"user_obj", "user_obj",
"provider",
"attributes",
] ]
extra_kwargs = {"attributes": {"read_only": True}}
class MicrosoftEntraProviderUserViewSet(UsedByMixin, ModelViewSet): class MicrosoftEntraProviderUserViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""MicrosoftEntraProviderUser Viewset""" """MicrosoftEntraProviderUser Viewset"""
queryset = MicrosoftEntraProviderUser.objects.all().select_related("user") queryset = MicrosoftEntraProviderUser.objects.all().select_related("user")

View File

@ -1,5 +1,6 @@
from asyncio import run from asyncio import run
from collections.abc import Coroutine from collections.abc import Coroutine
from dataclasses import asdict
from typing import Any from typing import Any
from azure.core.exceptions import ( from azure.core.exceptions import (
@ -15,6 +16,7 @@ from kiota_authentication_azure.azure_identity_authentication_provider import (
AzureIdentityAuthenticationProvider, AzureIdentityAuthenticationProvider,
) )
from kiota_http.kiota_client_factory import KiotaClientFactory from kiota_http.kiota_client_factory import KiotaClientFactory
from msgraph.generated.models.entity import Entity
from msgraph.generated.models.o_data_errors.o_data_error import ODataError from msgraph.generated.models.o_data_errors.o_data_error import ODataError
from msgraph.graph_request_adapter import GraphRequestAdapter, options from msgraph.graph_request_adapter import GraphRequestAdapter, options
from msgraph.graph_service_client import GraphServiceClient from msgraph.graph_service_client import GraphServiceClient
@ -98,3 +100,10 @@ class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict]
for email in emails: for email in emails:
if not any(email.endswith(f"@{domain_name}") for domain_name in self.domains): if not any(email.endswith(f"@{domain_name}") for domain_name in self.domains):
raise BadRequestSyncException(f"Invalid email domain: {email}") raise BadRequestSyncException(f"Invalid email domain: {email}")
def entity_as_dict(self, entity: Entity) -> dict:
"""Create a dictionary of a model instance, making sure to remove (known) things
we can't JSON serialize"""
raw_data = asdict(entity)
raw_data.pop("backing_store", None)
return raw_data

View File

@ -36,12 +36,12 @@ class MicrosoftEntraGroupClient(
self.mapper = PropertyMappingManager( self.mapper = PropertyMappingManager(
self.provider.property_mappings_group.all().order_by("name").select_subclasses(), self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
MicrosoftEntraProviderMapping, MicrosoftEntraProviderMapping,
["group", "provider", "creating"], ["group", "provider", "connection"],
) )
def to_schema(self, obj: Group, creating: bool) -> MSGroup: def to_schema(self, obj: Group, connection: MicrosoftEntraProviderGroup) -> MSGroup:
"""Convert authentik group""" """Convert authentik group"""
raw_microsoft_group = super().to_schema(obj, creating) raw_microsoft_group = super().to_schema(obj, connection)
try: try:
return MSGroup(**raw_microsoft_group) return MSGroup(**raw_microsoft_group)
except TypeError as exc: except TypeError as exc:
@ -62,7 +62,7 @@ class MicrosoftEntraGroupClient(
def create(self, group: Group): def create(self, group: Group):
"""Create group from scratch and create a connection object""" """Create group from scratch and create a connection object"""
microsoft_group = self.to_schema(group, True) microsoft_group = self.to_schema(group, None)
with transaction.atomic(): with transaction.atomic():
try: try:
response = self._request(self.client.groups.post(microsoft_group)) response = self._request(self.client.groups.post(microsoft_group))
@ -79,22 +79,29 @@ class MicrosoftEntraGroupClient(
) )
) )
group_data = self._request(self.client.groups.get(request_configuration)) group_data = self._request(self.client.groups.get(request_configuration))
if group_data.odata_count < 1: if group_data.odata_count < 1 or len(group_data.value) < 1:
self.logger.warning( self.logger.warning(
"Group which could not be created also does not exist", group=group "Group which could not be created also does not exist", group=group
) )
return return
ms_group = group_data.value[0]
return MicrosoftEntraProviderGroup.objects.create( return MicrosoftEntraProviderGroup.objects.create(
provider=self.provider, group=group, microsoft_id=group_data.value[0].id provider=self.provider,
group=group,
microsoft_id=ms_group.id,
attributes=self.entity_as_dict(ms_group),
) )
else: else:
return MicrosoftEntraProviderGroup.objects.create( return MicrosoftEntraProviderGroup.objects.create(
provider=self.provider, group=group, microsoft_id=response.id provider=self.provider,
group=group,
microsoft_id=response.id,
attributes=self.entity_as_dict(response),
) )
def update(self, group: Group, connection: MicrosoftEntraProviderGroup): def update(self, group: Group, connection: MicrosoftEntraProviderGroup):
"""Update existing group""" """Update existing group"""
microsoft_group = self.to_schema(group, False) microsoft_group = self.to_schema(group, connection)
microsoft_group.id = connection.microsoft_id microsoft_group.id = connection.microsoft_id
try: try:
return self._request( return self._request(
@ -213,4 +220,5 @@ class MicrosoftEntraGroupClient(
provider=self.provider, provider=self.provider,
group=matching_authentik_group, group=matching_authentik_group,
microsoft_id=group.id, microsoft_id=group.id,
attributes=self.entity_as_dict(group),
) )

View File

@ -31,12 +31,12 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
self.mapper = PropertyMappingManager( self.mapper = PropertyMappingManager(
self.provider.property_mappings.all().order_by("name").select_subclasses(), self.provider.property_mappings.all().order_by("name").select_subclasses(),
MicrosoftEntraProviderMapping, MicrosoftEntraProviderMapping,
["provider", "creating"], ["provider", "connection"],
) )
def to_schema(self, obj: User, creating: bool) -> MSUser: def to_schema(self, obj: User, connection: MicrosoftEntraProviderUser) -> MSUser:
"""Convert authentik user""" """Convert authentik user"""
raw_microsoft_user = super().to_schema(obj, creating) raw_microsoft_user = super().to_schema(obj, connection)
try: try:
return MSUser(**delete_none_values(raw_microsoft_user)) return MSUser(**delete_none_values(raw_microsoft_user))
except TypeError as exc: except TypeError as exc:
@ -67,7 +67,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
def create(self, user: User): def create(self, user: User):
"""Create user from scratch and create a connection object""" """Create user from scratch and create a connection object"""
microsoft_user = self.to_schema(user, True) microsoft_user = self.to_schema(user, None)
self.check_email_valid(microsoft_user.user_principal_name) self.check_email_valid(microsoft_user.user_principal_name)
with transaction.atomic(): with transaction.atomic():
try: try:
@ -83,24 +83,32 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
) )
) )
user_data = self._request(self.client.users.get(request_configuration)) user_data = self._request(self.client.users.get(request_configuration))
if user_data.odata_count < 1: if user_data.odata_count < 1 or len(user_data.value) < 1:
self.logger.warning( self.logger.warning(
"User which could not be created also does not exist", user=user "User which could not be created also does not exist", user=user
) )
return return
ms_user = user_data.value[0]
return MicrosoftEntraProviderUser.objects.create( return MicrosoftEntraProviderUser.objects.create(
provider=self.provider, user=user, microsoft_id=user_data.value[0].id provider=self.provider,
user=user,
microsoft_id=ms_user.id,
attributes=self.entity_as_dict(ms_user),
) )
except TransientSyncException as exc: except TransientSyncException as exc:
raise exc raise exc
else: else:
print(self.entity_as_dict(response))
return MicrosoftEntraProviderUser.objects.create( return MicrosoftEntraProviderUser.objects.create(
provider=self.provider, user=user, microsoft_id=response.id provider=self.provider,
user=user,
microsoft_id=response.id,
attributes=self.entity_as_dict(response),
) )
def update(self, user: User, connection: MicrosoftEntraProviderUser): def update(self, user: User, connection: MicrosoftEntraProviderUser):
"""Update existing user""" """Update existing user"""
microsoft_user = self.to_schema(user, False) microsoft_user = self.to_schema(user, connection)
self.check_email_valid(microsoft_user.user_principal_name) self.check_email_valid(microsoft_user.user_principal_name)
self._request(self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user)) self._request(self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user))
@ -125,4 +133,5 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
provider=self.provider, provider=self.provider,
user=matching_authentik_user, user=matching_authentik_user,
microsoft_id=user.id, microsoft_id=user.id,
attributes=self.entity_as_dict(user),
) )

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-05-23 20:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_microsoft_entra", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="microsoftentraprovidergroup",
name="attributes",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="microsoftentraprovideruser",
name="attributes",
field=models.JSONField(default=dict),
),
]

View File

@ -142,6 +142,7 @@ class MicrosoftEntraProviderUser(SerializerModel):
microsoft_id = models.TextField() microsoft_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE) provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:
@ -167,6 +168,7 @@ class MicrosoftEntraProviderGroup(SerializerModel):
microsoft_id = models.TextField() microsoft_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE) provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property @property
def serializer(self) -> type[Serializer]: def serializer(self) -> type[Serializer]:

View File

@ -3,3 +3,6 @@
PAGE_SIZE = 100 PAGE_SIZE = 100
PAGE_TIMEOUT = 60 * 60 * 0.5 # Half an hour PAGE_TIMEOUT = 60 * 60 * 0.5 # Half an hour
HTTP_CONFLICT = 409 HTTP_CONFLICT = 409
HTTP_NO_CONTENT = 204
HTTP_SERVICE_UNAVAILABLE = 503
HTTP_TOO_MANY_REQUESTS = 429

View File

@ -79,14 +79,14 @@ class BaseOutgoingSyncClient[
"""Delete object from destination""" """Delete object from destination"""
raise NotImplementedError() raise NotImplementedError()
def to_schema(self, obj: TModel, creating: bool, **defaults) -> TSchema: def to_schema(self, obj: TModel, connection: TConnection | None, **defaults) -> TSchema:
"""Convert object to destination schema""" """Convert object to destination schema"""
raw_final_object = {} raw_final_object = {}
try: try:
eval_kwargs = { eval_kwargs = {
"request": None, "request": None,
"provider": self.provider, "provider": self.provider,
"creating": creating, "connection": connection,
obj._meta.model_name: obj, obj._meta.model_name: obj,
} }
eval_kwargs.setdefault("user", None) eval_kwargs.setdefault("user", None)

View File

@ -124,7 +124,6 @@ class KubernetesObjectReconciler(Generic[T]):
self.update(current, reference) self.update(current, reference)
self.logger.debug("Updating") self.logger.debug("Updating")
except (OpenApiException, HTTPError) as exc: except (OpenApiException, HTTPError) as exc:
if isinstance(exc, ApiException) and exc.status == 422: # noqa: PLR2004 if isinstance(exc, ApiException) and exc.status == 422: # noqa: PLR2004
self.logger.debug("Failed to update current, triggering re-create") self.logger.debug("Failed to update current, triggering re-create")
self._recreate(current=current, reference=reference) self._recreate(current=current, reference=reference)

View File

@ -6,9 +6,18 @@ from django.http import HttpResponseBadRequest, HttpResponseNotFound
from pydantic import ValidationError from pydantic import ValidationError
from requests import RequestException, Session from requests import RequestException, Session
from authentik.lib.sync.outgoing import HTTP_CONFLICT from authentik.lib.sync.outgoing import (
HTTP_CONFLICT,
HTTP_NO_CONTENT,
HTTP_SERVICE_UNAVAILABLE,
HTTP_TOO_MANY_REQUESTS,
)
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, ObjectExistsSyncException from authentik.lib.sync.outgoing.exceptions import (
NotFoundSyncException,
ObjectExistsSyncException,
TransientSyncException,
)
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.providers.scim.clients.exceptions import SCIMRequestException from authentik.providers.scim.clients.exceptions import SCIMRequestException
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
@ -61,13 +70,15 @@ class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
if response.status_code >= HttpResponseBadRequest.status_code: if response.status_code >= HttpResponseBadRequest.status_code:
if response.status_code == HttpResponseNotFound.status_code: if response.status_code == HttpResponseNotFound.status_code:
raise NotFoundSyncException(response) raise NotFoundSyncException(response)
if response.status_code in [HTTP_TOO_MANY_REQUESTS, HTTP_SERVICE_UNAVAILABLE]:
raise TransientSyncException()
if response.status_code == HTTP_CONFLICT: if response.status_code == HTTP_CONFLICT:
raise ObjectExistsSyncException(response) raise ObjectExistsSyncException(response)
self.logger.warning( self.logger.warning(
"Failed to send SCIM request", path=path, method=method, response=response.text "Failed to send SCIM request", path=path, method=method, response=response.text
) )
raise SCIMRequestException(response) raise SCIMRequestException(response)
if response.status_code == 204: # noqa: PLR2004 if response.status_code == HTTP_NO_CONTENT:
return {} return {}
return response.json() return response.json()

View File

@ -34,14 +34,14 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
self.mapper = PropertyMappingManager( self.mapper = PropertyMappingManager(
self.provider.property_mappings_group.all().order_by("name").select_subclasses(), self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
SCIMMapping, SCIMMapping,
["group", "provider", "creating"], ["group", "provider", "connection"],
) )
def to_schema(self, obj: Group, creating: bool) -> SCIMGroupSchema: def to_schema(self, obj: Group, connection: SCIMGroup) -> SCIMGroupSchema:
"""Convert authentik user into SCIM""" """Convert authentik user into SCIM"""
raw_scim_group = super().to_schema( raw_scim_group = super().to_schema(
obj, obj,
creating, connection,
schemas=(SCIM_GROUP_SCHEMA,), schemas=(SCIM_GROUP_SCHEMA,),
) )
try: try:
@ -76,7 +76,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
def create(self, group: Group): def create(self, group: Group):
"""Create group from scratch and create a connection object""" """Create group from scratch and create a connection object"""
scim_group = self.to_schema(group, True) scim_group = self.to_schema(group, None)
response = self._request( response = self._request(
"POST", "POST",
"/Groups", "/Groups",
@ -92,7 +92,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
def update(self, group: Group, connection: SCIMGroup): def update(self, group: Group, connection: SCIMGroup):
"""Update existing group""" """Update existing group"""
scim_group = self.to_schema(group, False) scim_group = self.to_schema(group, connection)
scim_group.id = connection.scim_id scim_group.id = connection.scim_id
try: try:
return self._request( return self._request(

View File

@ -24,14 +24,14 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
self.mapper = PropertyMappingManager( self.mapper = PropertyMappingManager(
self.provider.property_mappings.all().order_by("name").select_subclasses(), self.provider.property_mappings.all().order_by("name").select_subclasses(),
SCIMMapping, SCIMMapping,
["provider", "creating"], ["provider", "connection"],
) )
def to_schema(self, obj: User, creating: bool) -> SCIMUserSchema: def to_schema(self, obj: User, connection: SCIMUser) -> SCIMUserSchema:
"""Convert authentik user into SCIM""" """Convert authentik user into SCIM"""
raw_scim_user = super().to_schema( raw_scim_user = super().to_schema(
obj, obj,
creating, connection,
schemas=(SCIM_USER_SCHEMA,), schemas=(SCIM_USER_SCHEMA,),
) )
try: try:
@ -54,7 +54,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
def create(self, user: User): def create(self, user: User):
"""Create user from scratch and create a connection object""" """Create user from scratch and create a connection object"""
scim_user = self.to_schema(user, True) scim_user = self.to_schema(user, None)
response = self._request( response = self._request(
"POST", "POST",
"/Users", "/Users",
@ -70,7 +70,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
def update(self, user: User, connection: SCIMUser): def update(self, user: User, connection: SCIMUser):
"""Update existing user""" """Update existing user"""
scim_user = self.to_schema(user, False) scim_user = self.to_schema(user, connection)
scim_user.id = connection.scim_id scim_user.id = connection.scim_id
self._request( self._request(
"PUT", "PUT",

View File

@ -19,10 +19,16 @@ entries:
"mail_nickname": request.user.username, "mail_nickname": request.user.username,
"user_principal_name": request.user.email, "user_principal_name": request.user.email,
} }
if creating: if connection:
# If there is a connection already made (discover or update), we can use
# that connection's immutable_id...
user["on_premises_immutable_id"] = connection.attributes.get("on_premises_immutable_id")
else:
user["password_profile"] = PasswordProfile( user["password_profile"] = PasswordProfile(
password=request.user.password password=request.user.password
) )
# ...otherwise we set an immutable ID based on the user's UID
user["on_premises_immutable_id"] = request.user.uid,
return user return user
- identifiers: - identifiers:
managed: goauthentik.io/providers/microsoft_entra/group managed: goauthentik.io/providers/microsoft_entra/group

View File

@ -16369,85 +16369,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
put:
operationId: providers_google_workspace_groups_update
description: GoogleWorkspaceProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Google Workspace Provider Group.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderGroupRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: providers_google_workspace_groups_partial_update
description: GoogleWorkspaceProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Google Workspace Provider Group.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGoogleWorkspaceProviderGroupRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete: delete:
operationId: providers_google_workspace_groups_destroy operationId: providers_google_workspace_groups_destroy
description: GoogleWorkspaceProviderGroup Viewset description: GoogleWorkspaceProviderGroup Viewset
@ -16646,85 +16567,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
put:
operationId: providers_google_workspace_users_update
description: GoogleWorkspaceProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Google Workspace Provider User.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderUserRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: providers_google_workspace_users_partial_update
description: GoogleWorkspaceProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Google Workspace Provider User.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGoogleWorkspaceProviderUserRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/GoogleWorkspaceProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete: delete:
operationId: providers_google_workspace_users_destroy operationId: providers_google_workspace_users_destroy
description: GoogleWorkspaceProviderUser Viewset description: GoogleWorkspaceProviderUser Viewset
@ -17539,85 +17381,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
put:
operationId: providers_microsoft_entra_groups_update
description: MicrosoftEntraProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Microsoft Entra Provider Group.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderGroupRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: providers_microsoft_entra_groups_partial_update
description: MicrosoftEntraProviderGroup Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Microsoft Entra Provider Group.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedMicrosoftEntraProviderGroupRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderGroup'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete: delete:
operationId: providers_microsoft_entra_groups_destroy operationId: providers_microsoft_entra_groups_destroy
description: MicrosoftEntraProviderGroup Viewset description: MicrosoftEntraProviderGroup Viewset
@ -17816,85 +17579,6 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
put:
operationId: providers_microsoft_entra_users_update
description: MicrosoftEntraProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Microsoft Entra Provider User.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderUserRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: providers_microsoft_entra_users_partial_update
description: MicrosoftEntraProviderUser Viewset
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Microsoft Entra Provider User.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedMicrosoftEntraProviderUserRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/MicrosoftEntraProviderUser'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete: delete:
operationId: providers_microsoft_entra_users_destroy operationId: providers_microsoft_entra_users_destroy
description: MicrosoftEntraProviderUser Viewset description: MicrosoftEntraProviderUser Viewset
@ -36634,10 +36318,16 @@ components:
allOf: allOf:
- $ref: '#/components/schemas/UserGroup' - $ref: '#/components/schemas/UserGroup'
readOnly: true readOnly: true
provider:
type: integer
attributes:
readOnly: true
required: required:
- attributes
- group - group
- group_obj - group_obj
- id - id
- provider
GoogleWorkspaceProviderGroupRequest: GoogleWorkspaceProviderGroupRequest:
type: object type: object
description: GoogleWorkspaceProviderGroup Serializer description: GoogleWorkspaceProviderGroup Serializer
@ -36645,8 +36335,11 @@ components:
group: group:
type: string type: string
format: uuid format: uuid
provider:
type: integer
required: required:
- group - group
- provider
GoogleWorkspaceProviderMapping: GoogleWorkspaceProviderMapping:
type: object type: object
description: GoogleWorkspaceProviderMapping Serializer description: GoogleWorkspaceProviderMapping Serializer
@ -36773,8 +36466,14 @@ components:
allOf: allOf:
- $ref: '#/components/schemas/GroupMember' - $ref: '#/components/schemas/GroupMember'
readOnly: true readOnly: true
provider:
type: integer
attributes:
readOnly: true
required: required:
- attributes
- id - id
- provider
- user - user
- user_obj - user_obj
GoogleWorkspaceProviderUserRequest: GoogleWorkspaceProviderUserRequest:
@ -36783,7 +36482,10 @@ components:
properties: properties:
user: user:
type: integer type: integer
provider:
type: integer
required: required:
- provider
- user - user
Group: Group:
type: object type: object
@ -38284,10 +37986,16 @@ components:
allOf: allOf:
- $ref: '#/components/schemas/UserGroup' - $ref: '#/components/schemas/UserGroup'
readOnly: true readOnly: true
provider:
type: integer
attributes:
readOnly: true
required: required:
- attributes
- group - group
- group_obj - group_obj
- id - id
- provider
MicrosoftEntraProviderGroupRequest: MicrosoftEntraProviderGroupRequest:
type: object type: object
description: MicrosoftEntraProviderGroup Serializer description: MicrosoftEntraProviderGroup Serializer
@ -38295,8 +38003,11 @@ components:
group: group:
type: string type: string
format: uuid format: uuid
provider:
type: integer
required: required:
- group - group
- provider
MicrosoftEntraProviderMapping: MicrosoftEntraProviderMapping:
type: object type: object
description: MicrosoftEntraProviderMapping Serializer description: MicrosoftEntraProviderMapping Serializer
@ -38420,8 +38131,14 @@ components:
allOf: allOf:
- $ref: '#/components/schemas/GroupMember' - $ref: '#/components/schemas/GroupMember'
readOnly: true readOnly: true
provider:
type: integer
attributes:
readOnly: true
required: required:
- attributes
- id - id
- provider
- user - user
- user_obj - user_obj
MicrosoftEntraProviderUserRequest: MicrosoftEntraProviderUserRequest:
@ -38430,7 +38147,10 @@ components:
properties: properties:
user: user:
type: integer type: integer
provider:
type: integer
required: required:
- provider
- user - user
ModelEnum: ModelEnum:
enum: enum:
@ -41826,13 +41546,6 @@ components:
to a challenge. RETRY returns the error message and a similar challenge to a challenge. RETRY returns the error message and a similar challenge
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
restarts the flow while keeping the current context. restarts the flow while keeping the current context.
PatchedGoogleWorkspaceProviderGroupRequest:
type: object
description: GoogleWorkspaceProviderGroup Serializer
properties:
group:
type: string
format: uuid
PatchedGoogleWorkspaceProviderMappingRequest: PatchedGoogleWorkspaceProviderMappingRequest:
type: object type: object
description: GoogleWorkspaceProviderMapping Serializer description: GoogleWorkspaceProviderMapping Serializer
@ -41892,12 +41605,6 @@ components:
default_group_email_domain: default_group_email_domain:
type: string type: string
minLength: 1 minLength: 1
PatchedGoogleWorkspaceProviderUserRequest:
type: object
description: GoogleWorkspaceProviderUser Serializer
properties:
user:
type: integer
PatchedGroupRequest: PatchedGroupRequest:
type: object type: object
description: Group Serializer description: Group Serializer
@ -42253,13 +41960,6 @@ components:
key: key:
type: string type: string
minLength: 1 minLength: 1
PatchedMicrosoftEntraProviderGroupRequest:
type: object
description: MicrosoftEntraProviderGroup Serializer
properties:
group:
type: string
format: uuid
PatchedMicrosoftEntraProviderMappingRequest: PatchedMicrosoftEntraProviderMappingRequest:
type: object type: object
description: MicrosoftEntraProviderMapping Serializer description: MicrosoftEntraProviderMapping Serializer
@ -42316,12 +42016,6 @@ components:
$ref: '#/components/schemas/OutgoingSyncDeleteAction' $ref: '#/components/schemas/OutgoingSyncDeleteAction'
group_delete_action: group_delete_action:
$ref: '#/components/schemas/OutgoingSyncDeleteAction' $ref: '#/components/schemas/OutgoingSyncDeleteAction'
PatchedMicrosoftEntraProviderUserRequest:
type: object
description: MicrosoftEntraProviderUser Serializer
properties:
user:
type: integer
PatchedNotificationRequest: PatchedNotificationRequest:
type: object type: object
description: Notification Serializer description: Notification Serializer

View File

@ -13,6 +13,8 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
@property({ type: Number }) @property({ type: Number })
providerId?: number; providerId?: number;
expandable = true;
searchEnabled(): boolean { searchEnabled(): boolean {
return true; return true;
} }
@ -39,4 +41,12 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
html`${item.id}`, html`${item.id}`,
]; ];
} }
renderExpanded(item: GoogleWorkspaceProviderGroup): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
} }

View File

@ -17,6 +17,8 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
return true; return true;
} }
expandable = true;
async apiEndpoint(page: number): Promise<PaginatedResponse<GoogleWorkspaceProviderUser>> { async apiEndpoint(page: number): Promise<PaginatedResponse<GoogleWorkspaceProviderUser>> {
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUsersList({ return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUsersList({
page: page, page: page,
@ -40,4 +42,12 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
html`${item.id}`, html`${item.id}`,
]; ];
} }
renderExpanded(item: GoogleWorkspaceProviderUser): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
} }

View File

@ -13,6 +13,8 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
@property({ type: Number }) @property({ type: Number })
providerId?: number; providerId?: number;
expandable = true;
searchEnabled(): boolean { searchEnabled(): boolean {
return true; return true;
} }
@ -39,4 +41,12 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
html`${item.id}`, html`${item.id}`,
]; ];
} }
renderExpanded(item: MicrosoftEntraProviderGroup): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
} }

View File

@ -13,6 +13,8 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
@property({ type: Number }) @property({ type: Number })
providerId?: number; providerId?: number;
expandable = true;
searchEnabled(): boolean { searchEnabled(): boolean {
return true; return true;
} }
@ -40,4 +42,12 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
html`${item.id}`, html`${item.id}`,
]; ];
} }
renderExpanded(item: MicrosoftEntraProviderUser): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<pre>${JSON.stringify(item.attributes, null, 4)}</pre>
</div>
</td>`;
}
} }