authentik.enterprise.providers.microsoft_entra.clients.users

  1from deepmerge import always_merger
  2from django.db import transaction
  3from msgraph.generated.models.user import User as MSUser
  4from msgraph.generated.users.users_request_builder import UsersRequestBuilder
  5
  6from authentik.core.models import User
  7from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient
  8from authentik.enterprise.providers.microsoft_entra.models import (
  9    MicrosoftEntraProvider,
 10    MicrosoftEntraProviderMapping,
 11    MicrosoftEntraProviderUser,
 12)
 13from authentik.lib.sync.mapper import PropertyMappingManager
 14from authentik.lib.sync.outgoing.exceptions import (
 15    ObjectExistsSyncException,
 16    StopSync,
 17    TransientSyncException,
 18)
 19from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
 20from authentik.policies.utils import delete_none_values
 21
 22
 23class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProviderUser, MSUser]):
 24    """Sync authentik users into microsoft entra"""
 25
 26    connection_type = MicrosoftEntraProviderUser
 27    connection_type_query = "user"
 28    can_discover = True
 29
 30    def __init__(self, provider: MicrosoftEntraProvider) -> None:
 31        super().__init__(provider)
 32        self.mapper = PropertyMappingManager(
 33            self.provider.property_mappings.all().order_by("name").select_subclasses(),
 34            MicrosoftEntraProviderMapping,
 35            ["provider", "connection"],
 36        )
 37
 38    def to_schema(self, obj: User, connection: MicrosoftEntraProviderUser) -> MSUser:
 39        """Convert authentik user"""
 40        raw_microsoft_user = super().to_schema(obj, connection)
 41        try:
 42            return MSUser(**delete_none_values(raw_microsoft_user))
 43        except TypeError as exc:
 44            raise StopSync(exc, obj) from exc
 45
 46    def delete(self, identifier: str):
 47        """Delete user"""
 48        MicrosoftEntraProviderUser.objects.filter(
 49            provider=self.provider, microsoft_id=identifier
 50        ).delete()
 51        if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE:
 52            return self._request(self.client.users.by_user_id(identifier).delete())
 53        if self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND:
 54            return self._request(
 55                self.client.users.by_user_id(identifier).patch(MSUser(account_enabled=False))
 56            )
 57
 58    def get_select_fields(self) -> list[str]:
 59        """All fields that should be selected when we fetch user data."""
 60        # TODO: Make this customizable in the future
 61        return [
 62            # Default fields
 63            "businessPhones",
 64            "displayName",
 65            "givenName",
 66            "jobTitle",
 67            "mail",
 68            "mobilePhone",
 69            "officeLocation",
 70            "preferredLanguage",
 71            "surname",
 72            "userPrincipalName",
 73            "id",
 74            # Required for logging into M365 using authentik
 75            "onPremisesImmutableId",
 76        ]
 77
 78    def create(self, user: User):
 79        """Create user from scratch and create a connection object"""
 80        microsoft_user = self.to_schema(user, None)
 81        if microsoft_user.user_principal_name:
 82            self.check_email_valid(microsoft_user.user_principal_name)
 83        with transaction.atomic():
 84            try:
 85                response = self._request(self.client.users.post(microsoft_user))
 86            except ObjectExistsSyncException:
 87                # user already exists in microsoft entra, so we can connect them manually
 88                request_configuration = (
 89                    UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
 90                        query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
 91                            filter=f"mail eq '{microsoft_user.mail}'",
 92                            select=self.get_select_fields(),
 93                        ),
 94                    )
 95                )
 96                user_data = self._request(self.client.users.get(request_configuration))
 97                if user_data.odata_count < 1 or len(user_data.value) < 1:
 98                    self.logger.warning(
 99                        "User which could not be created also does not exist", user=user
100                    )
101                    return
102                ms_user = user_data.value[0]
103                return MicrosoftEntraProviderUser.objects.create(
104                    provider=self.provider,
105                    user=user,
106                    microsoft_id=ms_user.id,
107                    attributes=self.entity_as_dict(ms_user),
108                )
109            except TransientSyncException as exc:
110                raise exc
111            else:
112                return MicrosoftEntraProviderUser.objects.create(
113                    provider=self.provider,
114                    user=user,
115                    microsoft_id=response.id,
116                    attributes=self.entity_as_dict(response),
117                )
118
119    def update(self, user: User, connection: MicrosoftEntraProviderUser):
120        """Update existing user"""
121        microsoft_user = self.to_schema(user, connection)
122        if microsoft_user.user_principal_name:
123            self.check_email_valid(microsoft_user.user_principal_name)
124        response = self._request(
125            self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user)
126        )
127        if response:
128            always_merger.merge(connection.attributes, self.entity_as_dict(response))
129            connection.save()
130
131    def discover(self):
132        """Iterate through all users and connect them with authentik users if possible"""
133        request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
134            query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
135                select=self.get_select_fields(),
136            ),
137        )
138        users = self._request(self.client.users.get(request_configuration))
139        next_link = True
140        while next_link:
141            for user in users.value:
142                self._discover_single_user(user)
143            next_link = users.odata_next_link
144            if not next_link:
145                break
146            users = self._request(self.client.users.with_url(next_link).get())
147
148    def _discover_single_user(self, user: MSUser):
149        """handle discovery of a single user"""
150        matching_authentik_user = self.provider.get_object_qs(User).filter(email=user.mail).first()
151        if not matching_authentik_user:
152            return
153        MicrosoftEntraProviderUser.objects.update_or_create(
154            provider=self.provider,
155            user=matching_authentik_user,
156            microsoft_id=user.id,
157            defaults={"attributes": self.entity_as_dict(user)},
158        )
159
160    def update_single_attribute(self, connection: MicrosoftEntraProviderUser):
161        request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
162            query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
163                select=self.get_select_fields(),
164            ),
165        )
166        data = self._request(
167            self.client.users.by_user_id(connection.microsoft_id).get(request_configuration)
168        )
169        connection.attributes = self.entity_as_dict(data)
 24class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProviderUser, MSUser]):
 25    """Sync authentik users into microsoft entra"""
 26
 27    connection_type = MicrosoftEntraProviderUser
 28    connection_type_query = "user"
 29    can_discover = True
 30
 31    def __init__(self, provider: MicrosoftEntraProvider) -> None:
 32        super().__init__(provider)
 33        self.mapper = PropertyMappingManager(
 34            self.provider.property_mappings.all().order_by("name").select_subclasses(),
 35            MicrosoftEntraProviderMapping,
 36            ["provider", "connection"],
 37        )
 38
 39    def to_schema(self, obj: User, connection: MicrosoftEntraProviderUser) -> MSUser:
 40        """Convert authentik user"""
 41        raw_microsoft_user = super().to_schema(obj, connection)
 42        try:
 43            return MSUser(**delete_none_values(raw_microsoft_user))
 44        except TypeError as exc:
 45            raise StopSync(exc, obj) from exc
 46
 47    def delete(self, identifier: str):
 48        """Delete user"""
 49        MicrosoftEntraProviderUser.objects.filter(
 50            provider=self.provider, microsoft_id=identifier
 51        ).delete()
 52        if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE:
 53            return self._request(self.client.users.by_user_id(identifier).delete())
 54        if self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND:
 55            return self._request(
 56                self.client.users.by_user_id(identifier).patch(MSUser(account_enabled=False))
 57            )
 58
 59    def get_select_fields(self) -> list[str]:
 60        """All fields that should be selected when we fetch user data."""
 61        # TODO: Make this customizable in the future
 62        return [
 63            # Default fields
 64            "businessPhones",
 65            "displayName",
 66            "givenName",
 67            "jobTitle",
 68            "mail",
 69            "mobilePhone",
 70            "officeLocation",
 71            "preferredLanguage",
 72            "surname",
 73            "userPrincipalName",
 74            "id",
 75            # Required for logging into M365 using authentik
 76            "onPremisesImmutableId",
 77        ]
 78
 79    def create(self, user: User):
 80        """Create user from scratch and create a connection object"""
 81        microsoft_user = self.to_schema(user, None)
 82        if microsoft_user.user_principal_name:
 83            self.check_email_valid(microsoft_user.user_principal_name)
 84        with transaction.atomic():
 85            try:
 86                response = self._request(self.client.users.post(microsoft_user))
 87            except ObjectExistsSyncException:
 88                # user already exists in microsoft entra, so we can connect them manually
 89                request_configuration = (
 90                    UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
 91                        query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
 92                            filter=f"mail eq '{microsoft_user.mail}'",
 93                            select=self.get_select_fields(),
 94                        ),
 95                    )
 96                )
 97                user_data = self._request(self.client.users.get(request_configuration))
 98                if user_data.odata_count < 1 or len(user_data.value) < 1:
 99                    self.logger.warning(
100                        "User which could not be created also does not exist", user=user
101                    )
102                    return
103                ms_user = user_data.value[0]
104                return MicrosoftEntraProviderUser.objects.create(
105                    provider=self.provider,
106                    user=user,
107                    microsoft_id=ms_user.id,
108                    attributes=self.entity_as_dict(ms_user),
109                )
110            except TransientSyncException as exc:
111                raise exc
112            else:
113                return MicrosoftEntraProviderUser.objects.create(
114                    provider=self.provider,
115                    user=user,
116                    microsoft_id=response.id,
117                    attributes=self.entity_as_dict(response),
118                )
119
120    def update(self, user: User, connection: MicrosoftEntraProviderUser):
121        """Update existing user"""
122        microsoft_user = self.to_schema(user, connection)
123        if microsoft_user.user_principal_name:
124            self.check_email_valid(microsoft_user.user_principal_name)
125        response = self._request(
126            self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user)
127        )
128        if response:
129            always_merger.merge(connection.attributes, self.entity_as_dict(response))
130            connection.save()
131
132    def discover(self):
133        """Iterate through all users and connect them with authentik users if possible"""
134        request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
135            query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
136                select=self.get_select_fields(),
137            ),
138        )
139        users = self._request(self.client.users.get(request_configuration))
140        next_link = True
141        while next_link:
142            for user in users.value:
143                self._discover_single_user(user)
144            next_link = users.odata_next_link
145            if not next_link:
146                break
147            users = self._request(self.client.users.with_url(next_link).get())
148
149    def _discover_single_user(self, user: MSUser):
150        """handle discovery of a single user"""
151        matching_authentik_user = self.provider.get_object_qs(User).filter(email=user.mail).first()
152        if not matching_authentik_user:
153            return
154        MicrosoftEntraProviderUser.objects.update_or_create(
155            provider=self.provider,
156            user=matching_authentik_user,
157            microsoft_id=user.id,
158            defaults={"attributes": self.entity_as_dict(user)},
159        )
160
161    def update_single_attribute(self, connection: MicrosoftEntraProviderUser):
162        request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
163            query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
164                select=self.get_select_fields(),
165            ),
166        )
167        data = self._request(
168            self.client.users.by_user_id(connection.microsoft_id).get(request_configuration)
169        )
170        connection.attributes = self.entity_as_dict(data)

Sync authentik users into microsoft entra

MicrosoftEntraUserClient( provider: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider)
31    def __init__(self, provider: MicrosoftEntraProvider) -> None:
32        super().__init__(provider)
33        self.mapper = PropertyMappingManager(
34            self.provider.property_mappings.all().order_by("name").select_subclasses(),
35            MicrosoftEntraProviderMapping,
36            ["provider", "connection"],
37        )
connection_type_query = 'user'
can_discover = True
mapper
def to_schema( self, obj: authentik.core.models.User, connection: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderUser) -> msgraph.generated.models.user.User:
39    def to_schema(self, obj: User, connection: MicrosoftEntraProviderUser) -> MSUser:
40        """Convert authentik user"""
41        raw_microsoft_user = super().to_schema(obj, connection)
42        try:
43            return MSUser(**delete_none_values(raw_microsoft_user))
44        except TypeError as exc:
45            raise StopSync(exc, obj) from exc

Convert authentik user

def delete(self, identifier: str):
47    def delete(self, identifier: str):
48        """Delete user"""
49        MicrosoftEntraProviderUser.objects.filter(
50            provider=self.provider, microsoft_id=identifier
51        ).delete()
52        if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE:
53            return self._request(self.client.users.by_user_id(identifier).delete())
54        if self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND:
55            return self._request(
56                self.client.users.by_user_id(identifier).patch(MSUser(account_enabled=False))
57            )

Delete user

def get_select_fields(self) -> list[str]:
59    def get_select_fields(self) -> list[str]:
60        """All fields that should be selected when we fetch user data."""
61        # TODO: Make this customizable in the future
62        return [
63            # Default fields
64            "businessPhones",
65            "displayName",
66            "givenName",
67            "jobTitle",
68            "mail",
69            "mobilePhone",
70            "officeLocation",
71            "preferredLanguage",
72            "surname",
73            "userPrincipalName",
74            "id",
75            # Required for logging into M365 using authentik
76            "onPremisesImmutableId",
77        ]

All fields that should be selected when we fetch user data.

def create(self, user: authentik.core.models.User):
 79    def create(self, user: User):
 80        """Create user from scratch and create a connection object"""
 81        microsoft_user = self.to_schema(user, None)
 82        if microsoft_user.user_principal_name:
 83            self.check_email_valid(microsoft_user.user_principal_name)
 84        with transaction.atomic():
 85            try:
 86                response = self._request(self.client.users.post(microsoft_user))
 87            except ObjectExistsSyncException:
 88                # user already exists in microsoft entra, so we can connect them manually
 89                request_configuration = (
 90                    UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
 91                        query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
 92                            filter=f"mail eq '{microsoft_user.mail}'",
 93                            select=self.get_select_fields(),
 94                        ),
 95                    )
 96                )
 97                user_data = self._request(self.client.users.get(request_configuration))
 98                if user_data.odata_count < 1 or len(user_data.value) < 1:
 99                    self.logger.warning(
100                        "User which could not be created also does not exist", user=user
101                    )
102                    return
103                ms_user = user_data.value[0]
104                return MicrosoftEntraProviderUser.objects.create(
105                    provider=self.provider,
106                    user=user,
107                    microsoft_id=ms_user.id,
108                    attributes=self.entity_as_dict(ms_user),
109                )
110            except TransientSyncException as exc:
111                raise exc
112            else:
113                return MicrosoftEntraProviderUser.objects.create(
114                    provider=self.provider,
115                    user=user,
116                    microsoft_id=response.id,
117                    attributes=self.entity_as_dict(response),
118                )

Create user from scratch and create a connection object

120    def update(self, user: User, connection: MicrosoftEntraProviderUser):
121        """Update existing user"""
122        microsoft_user = self.to_schema(user, connection)
123        if microsoft_user.user_principal_name:
124            self.check_email_valid(microsoft_user.user_principal_name)
125        response = self._request(
126            self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user)
127        )
128        if response:
129            always_merger.merge(connection.attributes, self.entity_as_dict(response))
130            connection.save()

Update existing user

def discover(self):
132    def discover(self):
133        """Iterate through all users and connect them with authentik users if possible"""
134        request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
135            query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
136                select=self.get_select_fields(),
137            ),
138        )
139        users = self._request(self.client.users.get(request_configuration))
140        next_link = True
141        while next_link:
142            for user in users.value:
143                self._discover_single_user(user)
144            next_link = users.odata_next_link
145            if not next_link:
146                break
147            users = self._request(self.client.users.with_url(next_link).get())

Iterate through all users and connect them with authentik users if possible

def update_single_attribute( self, connection: authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProviderUser):
161    def update_single_attribute(self, connection: MicrosoftEntraProviderUser):
162        request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
163            query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
164                select=self.get_select_fields(),
165            ),
166        )
167        data = self._request(
168            self.client.users.by_user_id(connection.microsoft_id).get(request_configuration)
169        )
170        connection.attributes = self.entity_as_dict(data)

Update connection attributes on a connection object, when the connection is manually created