Skip to content

Commit

Permalink
enterprise/providers/microsoft_entra: initial account sync to microso…
Browse files Browse the repository at this point in the history
…ft entra (#9632)

* initial

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

* add entra mappings

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

* fix some stuff

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

* make API endpoints more consistent

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

* implement more things

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

* add user tests

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

* fix most group tests + fix bugs

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

* more group tests, fix bugs

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

* fix missing __init__

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

* add ui for provisioned users

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

* fix a bunch of bugs

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

* add `creating` to property mapping env

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

* always sync group members

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

* fix stuff

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

* fix group membership

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

* fix some types

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

* fix tests

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

* add group member add test

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

* create sync status component to dedupe

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

* fix discovery tests

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

* get rid of more code and fix more issues

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

* add error handling for auth and transient

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

* make sure autoretry is on

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

* format web

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

* wait for task in signal

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

* fix tests

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

* add squashed google migration

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
  • Loading branch information
BeryJu authored May 9, 2024
1 parent ff4ec6f commit 99ad492
Show file tree
Hide file tree
Showing 85 changed files with 6,930 additions and 1,061 deletions.
13 changes: 7 additions & 6 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
"asgi",
"authentik",
"authn",

This comment has been minimized.

Copy link
@lumhtawng2002

lumhtawng2002 May 25, 2024

sftgs_gfvhsggvgb bvv bb b h bh gbvf bsbb

"entra",
"goauthentik",
"jwks",
"kubernetes",
"oidc",
"openid",
"passwordless",
"plex",
"saml",
"scim",
"slo",
"sso",
"totp",
"webauthn",
"traefik",
"passwordless",
"kubernetes",
"sso",
"slo",
"scim",
"webauthn",
],
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
Expand Down
13 changes: 13 additions & 0 deletions authentik/blueprints/v1/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@
)
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import LicenseUsage
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceProviderGroup,
GoogleWorkspaceProviderUser,
)
from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProviderGroup,
MicrosoftEntraProviderUser,
)
from authentik.enterprise.providers.rac.models import ConnectionToken
from authentik.events.logs import LogEvent, capture_logs
from authentik.events.models import SystemTask
Expand Down Expand Up @@ -86,6 +94,7 @@ def excluded_models() -> list[type[Model]]:
# Classes that have other dependencies
AuthenticatedSession,

This comment has been minimized.

Copy link
@lumhtawng2002
# Classes which are only internally managed
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
FlowToken,
LicenseUsage,
SCIMGroup,
Expand All @@ -100,6 +109,10 @@ def excluded_models() -> list[type[Model]]:
WebAuthnDeviceType,
SCIMSourceUser,
SCIMSourceGroup,
GoogleWorkspaceProviderUser,
GoogleWorkspaceProviderGroup,
MicrosoftEntraProviderUser,
MicrosoftEntraProviderGroup,
)


Expand Down
33 changes: 33 additions & 0 deletions authentik/enterprise/providers/google_workspace/api/groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""GoogleWorkspaceProviderGroup API Views"""

from rest_framework.viewsets import ModelViewSet

from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup


class GoogleWorkspaceProviderGroupSerializer(SourceSerializer):
"""GoogleWorkspaceProviderGroup Serializer"""

group_obj = UserGroupSerializer(source="group", read_only=True)

class Meta:

model = GoogleWorkspaceProviderGroup
fields = [
"id",
"group",
"group_obj",
]


class GoogleWorkspaceProviderGroupViewSet(UsedByMixin, ModelViewSet):
"""GoogleWorkspaceProviderGroup Viewset"""

queryset = GoogleWorkspaceProviderGroup.objects.all().select_related("group")
serializer_class = GoogleWorkspaceProviderGroupSerializer
filterset_fields = ["provider__id", "group__name", "group__group_uuid"]
search_fields = ["provider__name", "group__name"]
ordering = ["group__name"]
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderMapping


class GoogleProviderMappingSerializer(PropertyMappingSerializer):
"""GoogleProviderMapping Serializer"""
class GoogleWorkspaceProviderMappingSerializer(PropertyMappingSerializer):
"""GoogleWorkspaceProviderMapping Serializer"""

class Meta:
model = GoogleWorkspaceProviderMapping
fields = PropertyMappingSerializer.Meta.fields


class GoogleProviderMappingFilter(FilterSet):
"""Filter for GoogleProviderMapping"""
class GoogleWorkspaceProviderMappingFilter(FilterSet):
"""Filter for GoogleWorkspaceProviderMapping"""

managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))

Expand All @@ -29,11 +29,11 @@ class Meta:
fields = "__all__"


class GoogleProviderMappingViewSet(UsedByMixin, ModelViewSet):
"""GoogleProviderMapping Viewset"""
class GoogleWorkspaceProviderMappingViewSet(UsedByMixin, ModelViewSet):
"""GoogleWorkspaceProviderMapping Viewset"""

queryset = GoogleWorkspaceProviderMapping.objects.all()
serializer_class = GoogleProviderMappingSerializer
filterset_class = GoogleProviderMappingFilter
serializer_class = GoogleWorkspaceProviderMappingSerializer
filterset_class = GoogleWorkspaceProviderMappingFilter
search_fields = ["name"]
ordering = ["name"]
10 changes: 5 additions & 5 deletions authentik/enterprise/providers/google_workspace/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin


class GoogleProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
"""GoogleProvider Serializer"""
class GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
"""GoogleWorkspaceProvider Serializer"""

class Meta:
model = GoogleWorkspaceProvider
Expand All @@ -38,11 +38,11 @@ class Meta:
extra_kwargs = {}


class GoogleProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet):
"""GoogleProvider Viewset"""
class GoogleWorkspaceProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet):
"""GoogleWorkspaceProvider Viewset"""

queryset = GoogleWorkspaceProvider.objects.all()
serializer_class = GoogleProviderSerializer
serializer_class = GoogleWorkspaceProviderSerializer
filterset_fields = [
"name",
"exclude_users_service_account",
Expand Down
33 changes: 33 additions & 0 deletions authentik/enterprise/providers/google_workspace/api/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""GoogleWorkspaceProviderUser API Views"""

from rest_framework.viewsets import ModelViewSet

from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser


class GoogleWorkspaceProviderUserSerializer(SourceSerializer):
"""GoogleWorkspaceProviderUser Serializer"""

user_obj = GroupMemberSerializer(source="user", read_only=True)

class Meta:

model = GoogleWorkspaceProviderUser
fields = [
"id",
"user",
"user_obj",
]


class GoogleWorkspaceProviderUserViewSet(UsedByMixin, ModelViewSet):
"""GoogleWorkspaceProviderUser Viewset"""

queryset = GoogleWorkspaceProviderUser.objects.all().select_related("user")
serializer_class = GoogleWorkspaceProviderUserSerializer
filterset_fields = ["provider__id", "user__username", "user__id"]
search_fields = ["provider__name", "user__username"]
ordering = ["user__username"]
13 changes: 8 additions & 5 deletions authentik/enterprise/providers/google_workspace/clients/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.db.models import Model
from django.http import HttpResponseNotFound
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from google.auth.exceptions import GoogleAuthError, TransportError
from googleapiclient.discovery import build
from googleapiclient.errors import Error, HttpError
Expand All @@ -10,6 +10,7 @@
from authentik.lib.sync.outgoing import HTTP_CONFLICT
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import (
BadRequestSyncException,
NotFoundSyncException,
ObjectExistsSyncException,
StopSync,
Expand Down Expand Up @@ -50,22 +51,24 @@ def _request(self, request: HttpRequest):
raise StopSync(exc) from exc
except HttpLib2Error as exc:
if isinstance(exc, HttpLib2ErrorWithResponse):
self._response_handle_status_code(exc.response.status, exc)
self._response_handle_status_code(request.body, exc.response.status, exc)
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
except HttpError as exc:
self._response_handle_status_code(exc.status_code, exc)
self._response_handle_status_code(request.body, exc.status_code, exc)
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
except Error as exc:
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
return response

def _response_handle_status_code(self, status_code: int, root_exc: Exception):
def _response_handle_status_code(self, request: dict, status_code: int, root_exc: Exception):
if status_code == HttpResponseNotFound.status_code:
raise NotFoundSyncException("Object not found") from root_exc
if status_code == HTTP_CONFLICT:
raise ObjectExistsSyncException("Object exists") from root_exc
if status_code == HttpResponseBadRequest.status_code:
raise BadRequestSyncException("Bad request", request) from root_exc

def check_email_valid(self, *emails: str):
for email in emails:
if not any(email.endswith(f"@{domain_name}") for domain_name in self.domains):
raise TransientSyncException(f"Invalid email domain: {email}")
raise BadRequestSyncException(f"Invalid email domain: {email}")
38 changes: 13 additions & 25 deletions authentik/enterprise/providers/google_workspace/clients/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from authentik.core.models import Group
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceDeleteAction,
GoogleWorkspaceProviderGroup,
GoogleWorkspaceProviderMapping,
GoogleWorkspaceProviderUser,
Expand All @@ -22,6 +21,7 @@
StopSync,
TransientSyncException,
)
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
from authentik.lib.utils.errors import exception_to_string


Expand All @@ -34,7 +34,7 @@ class GoogleWorkspaceGroupClient(
connection_type_query = "group"
can_discover = True

def to_schema(self, obj: Group) -> dict:
def to_schema(self, obj: Group, creating: bool) -> dict:
"""Convert authentik group"""
raw_google_group = {
"email": f"{slugify(obj.name)}@{self.provider.default_group_email_domain}"
Expand All @@ -45,12 +45,12 @@ def to_schema(self, obj: Group) -> dict:
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
continue
try:
mapping: GoogleWorkspaceProviderMapping
value = mapping.evaluate(
user=None,
request=None,
group=obj,
provider=self.provider,
creating=creating,
)
if value is None:
continue
Expand Down Expand Up @@ -79,15 +79,15 @@ def delete(self, obj: Group):
self.logger.debug("Group does not exist in Google, skipping")
return None
with transaction.atomic():
if self.provider.group_delete_action == GoogleWorkspaceDeleteAction.DELETE:
if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE:
self._request(
self.directory_service.groups().delete(groupKey=google_group.google_id)
)
google_group.delete()

def create(self, group: Group):
"""Create group from scratch and create a connection object"""
google_group = self.to_schema(group)
google_group = self.to_schema(group, True)
self.check_email_valid(google_group["email"])
with transaction.atomic():
try:
Expand All @@ -99,17 +99,17 @@ def create(self, group: Group):
group_data = self._request(
self.directory_service.groups().get(groupKey=google_group["email"])
)
GoogleWorkspaceProviderGroup.objects.create(
return GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=group_data["id"]
)
else:
GoogleWorkspaceProviderGroup.objects.create(
return GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=response["id"]
)

def update(self, group: Group, connection: GoogleWorkspaceProviderGroup):
"""Update existing group"""
google_group = self.to_schema(group)
google_group = self.to_schema(group, False)
self.check_email_valid(google_group["email"])
try:
return self._request(
Expand All @@ -124,28 +124,16 @@ def update(self, group: Group, connection: GoogleWorkspaceProviderGroup):

def write(self, obj: Group):
google_group, created = super().write(obj)
if created:
self.create_sync_members(obj, google_group)
return google_group
self.create_sync_members(obj, google_group)
return google_group, created

def create_sync_members(self, obj: Group, google_group: dict):
def create_sync_members(self, obj: Group, google_group: GoogleWorkspaceProviderGroup):
"""Sync all members after a group was created"""
users = list(obj.users.order_by("id").values_list("id", flat=True))
connections = GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user__pk__in=users
)
for user in connections:
try:
self._request(
self.directory_service.members().insert(
groupKey=google_group["id"],
body={
"email": user.google_id,
},
)
)
except TransientSyncException:
continue
).values_list("google_id", flat=True)
self._patch(google_group.google_id, Direction.add, connections)

def update_group(self, group: Group, action: Direction, users_set: set[int]):
"""Update a groups members"""
Expand Down
Loading

0 comments on commit 99ad492

Please sign in to comment.