Compare commits
	
		
			9 Commits
		
	
	
		
			version/20
			...
			5165-passw
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5b9bb12822 | |||
| 8f09955d58 | |||
| 465820b002 | |||
| a75c9434d9 | |||
| 4ea9b69ab5 | |||
| c48eee0ebf | |||
| 0d94373f10 | |||
| 1c85dc512f | |||
| a71778651f | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2024.6.0-rc1 | current_version = 2024.4.2 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))? | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| from os import environ | from os import environ | ||||||
|  |  | ||||||
| __version__ = "2024.6.0" | __version__ = "2024.4.2" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,7 +16,6 @@ from rest_framework.views import APIView | |||||||
|  |  | ||||||
| from authentik import get_full_version | from authentik import get_full_version | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.enterprise.license import LicenseKey |  | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.utils.reflection import get_env | from authentik.lib.utils.reflection import get_env | ||||||
| from authentik.outposts.apps import MANAGED_OUTPOST | from authentik.outposts.apps import MANAGED_OUTPOST | ||||||
| @ -33,7 +32,7 @@ class RuntimeDict(TypedDict): | |||||||
|     platform: str |     platform: str | ||||||
|     uname: str |     uname: str | ||||||
|     openssl_version: str |     openssl_version: str | ||||||
|     openssl_fips_enabled: bool | None |     openssl_fips_mode: bool | ||||||
|     authentik_version: str |     authentik_version: str | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -72,9 +71,7 @@ class SystemInfoSerializer(PassiveSerializer): | |||||||
|             "architecture": platform.machine(), |             "architecture": platform.machine(), | ||||||
|             "authentik_version": get_full_version(), |             "authentik_version": get_full_version(), | ||||||
|             "environment": get_env(), |             "environment": get_env(), | ||||||
|             "openssl_fips_enabled": ( |             "openssl_fips_enabled": backend._fips_enabled, | ||||||
|                 backend._fips_enabled if LicenseKey.get_total().is_valid() else None |  | ||||||
|             ), |  | ||||||
|             "openssl_version": OPENSSL_VERSION, |             "openssl_version": OPENSSL_VERSION, | ||||||
|             "platform": platform.platform(), |             "platform": platform.platform(), | ||||||
|             "python_version": python_version, |             "python_version": python_version, | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ from rest_framework.viewsets import GenericViewSet | |||||||
| 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 | ||||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class GoogleWorkspaceProviderGroupSerializer(ModelSerializer): | class GoogleWorkspaceProviderGroupSerializer(ModelSerializer): | ||||||
| @ -31,7 +30,6 @@ class GoogleWorkspaceProviderGroupSerializer(ModelSerializer): | |||||||
|  |  | ||||||
| class GoogleWorkspaceProviderGroupViewSet( | class GoogleWorkspaceProviderGroupViewSet( | ||||||
|     mixins.CreateModelMixin, |     mixins.CreateModelMixin, | ||||||
|     OutgoingSyncConnectionCreateMixin, |  | ||||||
|     mixins.RetrieveModelMixin, |     mixins.RetrieveModelMixin, | ||||||
|     mixins.DestroyModelMixin, |     mixins.DestroyModelMixin, | ||||||
|     UsedByMixin, |     UsedByMixin, | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ from rest_framework.viewsets import GenericViewSet | |||||||
| from authentik.core.api.groups import GroupMemberSerializer | from authentik.core.api.groups import GroupMemberSerializer | ||||||
| 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 | ||||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class GoogleWorkspaceProviderUserSerializer(ModelSerializer): | class GoogleWorkspaceProviderUserSerializer(ModelSerializer): | ||||||
| @ -31,7 +30,6 @@ class GoogleWorkspaceProviderUserSerializer(ModelSerializer): | |||||||
|  |  | ||||||
| class GoogleWorkspaceProviderUserViewSet( | class GoogleWorkspaceProviderUserViewSet( | ||||||
|     mixins.CreateModelMixin, |     mixins.CreateModelMixin, | ||||||
|     OutgoingSyncConnectionCreateMixin, |  | ||||||
|     mixins.RetrieveModelMixin, |     mixins.RetrieveModelMixin, | ||||||
|     mixins.DestroyModelMixin, |     mixins.DestroyModelMixin, | ||||||
|     UsedByMixin, |     UsedByMixin, | ||||||
|  | |||||||
| @ -214,7 +214,3 @@ class GoogleWorkspaceGroupClient( | |||||||
|             google_id=google_id, |             google_id=google_id, | ||||||
|             attributes=group, |             attributes=group, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def update_single_attribute(self, connection: GoogleWorkspaceProviderUser): |  | ||||||
|         group = self.directory_service.groups().get(connection.google_id) |  | ||||||
|         connection.attributes = group |  | ||||||
|  | |||||||
| @ -119,7 +119,3 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP | |||||||
|             google_id=email, |             google_id=email, | ||||||
|             attributes=user, |             attributes=user, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def update_single_attribute(self, connection: GoogleWorkspaceProviderUser): |  | ||||||
|         user = self.directory_service.users().get(connection.google_id) |  | ||||||
|         connection.attributes = user |  | ||||||
|  | |||||||
| @ -31,58 +31,6 @@ def default_scopes() -> list[str]: | |||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class GoogleWorkspaceProviderUser(SerializerModel): |  | ||||||
|     """Mapping of a user and provider to a Google user ID""" |  | ||||||
|  |  | ||||||
|     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) |  | ||||||
|     google_id = models.TextField() |  | ||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |  | ||||||
|     provider = models.ForeignKey("GoogleWorkspaceProvider", on_delete=models.CASCADE) |  | ||||||
|     attributes = models.JSONField(default=dict) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[Serializer]: |  | ||||||
|         from authentik.enterprise.providers.google_workspace.api.users import ( |  | ||||||
|             GoogleWorkspaceProviderUserSerializer, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         return GoogleWorkspaceProviderUserSerializer |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         verbose_name = _("Google Workspace Provider User") |  | ||||||
|         verbose_name_plural = _("Google Workspace Provider Users") |  | ||||||
|         unique_together = (("google_id", "user", "provider"),) |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         return f"Google Workspace Provider User {self.user_id} to {self.provider_id}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class GoogleWorkspaceProviderGroup(SerializerModel): |  | ||||||
|     """Mapping of a group and provider to a Google group ID""" |  | ||||||
|  |  | ||||||
|     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) |  | ||||||
|     google_id = models.TextField() |  | ||||||
|     group = models.ForeignKey(Group, on_delete=models.CASCADE) |  | ||||||
|     provider = models.ForeignKey("GoogleWorkspaceProvider", on_delete=models.CASCADE) |  | ||||||
|     attributes = models.JSONField(default=dict) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[Serializer]: |  | ||||||
|         from authentik.enterprise.providers.google_workspace.api.groups import ( |  | ||||||
|             GoogleWorkspaceProviderGroupSerializer, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         return GoogleWorkspaceProviderGroupSerializer |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         verbose_name = _("Google Workspace Provider Group") |  | ||||||
|         verbose_name_plural = _("Google Workspace Provider Groups") |  | ||||||
|         unique_together = (("google_id", "group", "provider"),) |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         return f"Google Workspace Provider Group {self.group_id} to {self.provider_id}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider): | class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider): | ||||||
|     """Sync users from authentik into Google Workspace.""" |     """Sync users from authentik into Google Workspace.""" | ||||||
|  |  | ||||||
| @ -111,16 +59,15 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     def client_for_model( |     def client_for_model( | ||||||
|         self, |         self, model: type[User | Group] | ||||||
|         model: type[User | Group | GoogleWorkspaceProviderUser | GoogleWorkspaceProviderGroup], |  | ||||||
|     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: |     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: | ||||||
|         if issubclass(model, User | GoogleWorkspaceProviderUser): |         if issubclass(model, User): | ||||||
|             from authentik.enterprise.providers.google_workspace.clients.users import ( |             from authentik.enterprise.providers.google_workspace.clients.users import ( | ||||||
|                 GoogleWorkspaceUserClient, |                 GoogleWorkspaceUserClient, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             return GoogleWorkspaceUserClient(self) |             return GoogleWorkspaceUserClient(self) | ||||||
|         if issubclass(model, Group | GoogleWorkspaceProviderGroup): |         if issubclass(model, Group): | ||||||
|             from authentik.enterprise.providers.google_workspace.clients.groups import ( |             from authentik.enterprise.providers.google_workspace.clients.groups import ( | ||||||
|                 GoogleWorkspaceGroupClient, |                 GoogleWorkspaceGroupClient, | ||||||
|             ) |             ) | ||||||
| @ -197,3 +144,55 @@ class GoogleWorkspaceProviderMapping(PropertyMapping): | |||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("Google Workspace Provider Mapping") |         verbose_name = _("Google Workspace Provider Mapping") | ||||||
|         verbose_name_plural = _("Google Workspace Provider Mappings") |         verbose_name_plural = _("Google Workspace Provider Mappings") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class GoogleWorkspaceProviderUser(SerializerModel): | ||||||
|  |     """Mapping of a user and provider to a Google user ID""" | ||||||
|  |  | ||||||
|  |     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |     google_id = models.TextField() | ||||||
|  |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
|  |     provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE) | ||||||
|  |     attributes = models.JSONField(default=dict) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def serializer(self) -> type[Serializer]: | ||||||
|  |         from authentik.enterprise.providers.google_workspace.api.users import ( | ||||||
|  |             GoogleWorkspaceProviderUserSerializer, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return GoogleWorkspaceProviderUserSerializer | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("Google Workspace Provider User") | ||||||
|  |         verbose_name_plural = _("Google Workspace Provider Users") | ||||||
|  |         unique_together = (("google_id", "user", "provider"),) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"Google Workspace Provider User {self.user_id} to {self.provider_id}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class GoogleWorkspaceProviderGroup(SerializerModel): | ||||||
|  |     """Mapping of a group and provider to a Google group ID""" | ||||||
|  |  | ||||||
|  |     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |     google_id = models.TextField() | ||||||
|  |     group = models.ForeignKey(Group, on_delete=models.CASCADE) | ||||||
|  |     provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE) | ||||||
|  |     attributes = models.JSONField(default=dict) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def serializer(self) -> type[Serializer]: | ||||||
|  |         from authentik.enterprise.providers.google_workspace.api.groups import ( | ||||||
|  |             GoogleWorkspaceProviderGroupSerializer, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return GoogleWorkspaceProviderGroupSerializer | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("Google Workspace Provider Group") | ||||||
|  |         verbose_name_plural = _("Google Workspace Provider Groups") | ||||||
|  |         unique_together = (("google_id", "group", "provider"),) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"Google Workspace Provider Group {self.group_id} to {self.provider_id}" | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ from rest_framework.viewsets import GenericViewSet | |||||||
| 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 | ||||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class MicrosoftEntraProviderGroupSerializer(ModelSerializer): | class MicrosoftEntraProviderGroupSerializer(ModelSerializer): | ||||||
| @ -31,7 +30,6 @@ class MicrosoftEntraProviderGroupSerializer(ModelSerializer): | |||||||
|  |  | ||||||
| class MicrosoftEntraProviderGroupViewSet( | class MicrosoftEntraProviderGroupViewSet( | ||||||
|     mixins.CreateModelMixin, |     mixins.CreateModelMixin, | ||||||
|     OutgoingSyncConnectionCreateMixin, |  | ||||||
|     mixins.RetrieveModelMixin, |     mixins.RetrieveModelMixin, | ||||||
|     mixins.DestroyModelMixin, |     mixins.DestroyModelMixin, | ||||||
|     UsedByMixin, |     UsedByMixin, | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ from rest_framework.viewsets import GenericViewSet | |||||||
| from authentik.core.api.groups import GroupMemberSerializer | from authentik.core.api.groups import GroupMemberSerializer | ||||||
| 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 | ||||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class MicrosoftEntraProviderUserSerializer(ModelSerializer): | class MicrosoftEntraProviderUserSerializer(ModelSerializer): | ||||||
| @ -30,7 +29,6 @@ class MicrosoftEntraProviderUserSerializer(ModelSerializer): | |||||||
|  |  | ||||||
|  |  | ||||||
| class MicrosoftEntraProviderUserViewSet( | class MicrosoftEntraProviderUserViewSet( | ||||||
|     OutgoingSyncConnectionCreateMixin, |  | ||||||
|     mixins.CreateModelMixin, |     mixins.CreateModelMixin, | ||||||
|     mixins.RetrieveModelMixin, |     mixins.RetrieveModelMixin, | ||||||
|     mixins.DestroyModelMixin, |     mixins.DestroyModelMixin, | ||||||
|  | |||||||
| @ -226,7 +226,3 @@ class MicrosoftEntraGroupClient( | |||||||
|             microsoft_id=group.id, |             microsoft_id=group.id, | ||||||
|             attributes=self.entity_as_dict(group), |             attributes=self.entity_as_dict(group), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def update_single_attribute(self, connection: MicrosoftEntraProviderGroup): |  | ||||||
|         data = self._request(self.client.groups.by_group_id(connection.microsoft_id).get()) |  | ||||||
|         connection.attributes = self.entity_as_dict(data) |  | ||||||
|  | |||||||
| @ -66,26 +66,6 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv | |||||||
|             microsoft_user.delete() |             microsoft_user.delete() | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|     def get_select_fields(self) -> list[str]: |  | ||||||
|         """All fields that should be selected when we fetch user data.""" |  | ||||||
|         # TODO: Make this customizable in the future |  | ||||||
|         return [ |  | ||||||
|             # Default fields |  | ||||||
|             "businessPhones", |  | ||||||
|             "displayName", |  | ||||||
|             "givenName", |  | ||||||
|             "jobTitle", |  | ||||||
|             "mail", |  | ||||||
|             "mobilePhone", |  | ||||||
|             "officeLocation", |  | ||||||
|             "preferredLanguage", |  | ||||||
|             "surname", |  | ||||||
|             "userPrincipalName", |  | ||||||
|             "id", |  | ||||||
|             # Required for logging into M365 using authentik |  | ||||||
|             "onPremisesImmutableId", |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|     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, None) |         microsoft_user = self.to_schema(user, None) | ||||||
| @ -95,12 +75,12 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv | |||||||
|                 response = self._request(self.client.users.post(microsoft_user)) |                 response = self._request(self.client.users.post(microsoft_user)) | ||||||
|             except ObjectExistsSyncException: |             except ObjectExistsSyncException: | ||||||
|                 # user already exists in microsoft entra, so we can connect them manually |                 # user already exists in microsoft entra, so we can connect them manually | ||||||
|  |                 query_params = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters()( | ||||||
|  |                     filter=f"mail eq '{microsoft_user.mail}'", | ||||||
|  |                 ) | ||||||
|                 request_configuration = ( |                 request_configuration = ( | ||||||
|                     UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( |                     UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( | ||||||
|                         query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( |                         query_parameters=query_params, | ||||||
|                             filter=f"mail eq '{microsoft_user.mail}'", |  | ||||||
|                             select=self.get_select_fields(), |  | ||||||
|                         ), |  | ||||||
|                     ) |                     ) | ||||||
|                 ) |                 ) | ||||||
|                 user_data = self._request(self.client.users.get(request_configuration)) |                 user_data = self._request(self.client.users.get(request_configuration)) | ||||||
| @ -119,6 +99,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv | |||||||
|             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, |                     provider=self.provider, | ||||||
|                     user=user, |                     user=user, | ||||||
| @ -139,12 +120,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv | |||||||
|  |  | ||||||
|     def discover(self): |     def discover(self): | ||||||
|         """Iterate through all users and connect them with authentik users if possible""" |         """Iterate through all users and connect them with authentik users if possible""" | ||||||
|         request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( |         users = self._request(self.client.users.get()) | ||||||
|             query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( |  | ||||||
|                 select=self.get_select_fields(), |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|         users = self._request(self.client.users.get(request_configuration)) |  | ||||||
|         next_link = True |         next_link = True | ||||||
|         while next_link: |         while next_link: | ||||||
|             for user in users.value: |             for user in users.value: | ||||||
| @ -165,14 +141,3 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv | |||||||
|             microsoft_id=user.id, |             microsoft_id=user.id, | ||||||
|             attributes=self.entity_as_dict(user), |             attributes=self.entity_as_dict(user), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def update_single_attribute(self, connection: MicrosoftEntraProviderUser): |  | ||||||
|         request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( |  | ||||||
|             query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( |  | ||||||
|                 select=self.get_select_fields(), |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
|         data = self._request( |  | ||||||
|             self.client.users.by_user_id(connection.microsoft_id).get(request_configuration) |  | ||||||
|         ) |  | ||||||
|         connection.attributes = self.entity_as_dict(data) |  | ||||||
|  | |||||||
| @ -22,58 +22,6 @@ from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient | |||||||
| from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider | from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider | ||||||
|  |  | ||||||
|  |  | ||||||
| class MicrosoftEntraProviderUser(SerializerModel): |  | ||||||
|     """Mapping of a user and provider to a Microsoft user ID""" |  | ||||||
|  |  | ||||||
|     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) |  | ||||||
|     microsoft_id = models.TextField() |  | ||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |  | ||||||
|     provider = models.ForeignKey("MicrosoftEntraProvider", on_delete=models.CASCADE) |  | ||||||
|     attributes = models.JSONField(default=dict) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[Serializer]: |  | ||||||
|         from authentik.enterprise.providers.microsoft_entra.api.users import ( |  | ||||||
|             MicrosoftEntraProviderUserSerializer, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         return MicrosoftEntraProviderUserSerializer |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         verbose_name = _("Microsoft Entra Provider User") |  | ||||||
|         verbose_name_plural = _("Microsoft Entra Provider User") |  | ||||||
|         unique_together = (("microsoft_id", "user", "provider"),) |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class MicrosoftEntraProviderGroup(SerializerModel): |  | ||||||
|     """Mapping of a group and provider to a Microsoft group ID""" |  | ||||||
|  |  | ||||||
|     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) |  | ||||||
|     microsoft_id = models.TextField() |  | ||||||
|     group = models.ForeignKey(Group, on_delete=models.CASCADE) |  | ||||||
|     provider = models.ForeignKey("MicrosoftEntraProvider", on_delete=models.CASCADE) |  | ||||||
|     attributes = models.JSONField(default=dict) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[Serializer]: |  | ||||||
|         from authentik.enterprise.providers.microsoft_entra.api.groups import ( |  | ||||||
|             MicrosoftEntraProviderGroupSerializer, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         return MicrosoftEntraProviderGroupSerializer |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         verbose_name = _("Microsoft Entra Provider Group") |  | ||||||
|         verbose_name_plural = _("Microsoft Entra Provider Groups") |  | ||||||
|         unique_together = (("microsoft_id", "group", "provider"),) |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         return f"Microsoft Entra Provider Group {self.group_id} to {self.provider_id}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider): | class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider): | ||||||
|     """Sync users from authentik into Microsoft Entra.""" |     """Sync users from authentik into Microsoft Entra.""" | ||||||
|  |  | ||||||
| @ -100,16 +48,15 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     def client_for_model( |     def client_for_model( | ||||||
|         self, |         self, model: type[User | Group] | ||||||
|         model: type[User | Group | MicrosoftEntraProviderUser | MicrosoftEntraProviderGroup], |  | ||||||
|     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: |     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: | ||||||
|         if issubclass(model, User | MicrosoftEntraProviderUser): |         if issubclass(model, User): | ||||||
|             from authentik.enterprise.providers.microsoft_entra.clients.users import ( |             from authentik.enterprise.providers.microsoft_entra.clients.users import ( | ||||||
|                 MicrosoftEntraUserClient, |                 MicrosoftEntraUserClient, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             return MicrosoftEntraUserClient(self) |             return MicrosoftEntraUserClient(self) | ||||||
|         if issubclass(model, Group | MicrosoftEntraProviderGroup): |         if issubclass(model, Group): | ||||||
|             from authentik.enterprise.providers.microsoft_entra.clients.groups import ( |             from authentik.enterprise.providers.microsoft_entra.clients.groups import ( | ||||||
|                 MicrosoftEntraGroupClient, |                 MicrosoftEntraGroupClient, | ||||||
|             ) |             ) | ||||||
| @ -186,3 +133,55 @@ class MicrosoftEntraProviderMapping(PropertyMapping): | |||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("Microsoft Entra Provider Mapping") |         verbose_name = _("Microsoft Entra Provider Mapping") | ||||||
|         verbose_name_plural = _("Microsoft Entra Provider Mappings") |         verbose_name_plural = _("Microsoft Entra Provider Mappings") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MicrosoftEntraProviderUser(SerializerModel): | ||||||
|  |     """Mapping of a user and provider to a Microsoft user ID""" | ||||||
|  |  | ||||||
|  |     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |     microsoft_id = models.TextField() | ||||||
|  |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
|  |     provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE) | ||||||
|  |     attributes = models.JSONField(default=dict) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def serializer(self) -> type[Serializer]: | ||||||
|  |         from authentik.enterprise.providers.microsoft_entra.api.users import ( | ||||||
|  |             MicrosoftEntraProviderUserSerializer, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return MicrosoftEntraProviderUserSerializer | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("Microsoft Entra Provider User") | ||||||
|  |         verbose_name_plural = _("Microsoft Entra Provider User") | ||||||
|  |         unique_together = (("microsoft_id", "user", "provider"),) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MicrosoftEntraProviderGroup(SerializerModel): | ||||||
|  |     """Mapping of a group and provider to a Microsoft group ID""" | ||||||
|  |  | ||||||
|  |     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |     microsoft_id = models.TextField() | ||||||
|  |     group = models.ForeignKey(Group, on_delete=models.CASCADE) | ||||||
|  |     provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE) | ||||||
|  |     attributes = models.JSONField(default=dict) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def serializer(self) -> type[Serializer]: | ||||||
|  |         from authentik.enterprise.providers.microsoft_entra.api.groups import ( | ||||||
|  |             MicrosoftEntraProviderGroupSerializer, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return MicrosoftEntraProviderGroupSerializer | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         verbose_name = _("Microsoft Entra Provider Group") | ||||||
|  |         verbose_name_plural = _("Microsoft Entra Provider Groups") | ||||||
|  |         unique_together = (("microsoft_id", "group", "provider"),) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"Microsoft Entra Provider Group {self.group_id} to {self.provider_id}" | ||||||
|  | |||||||
| @ -3,18 +3,16 @@ | |||||||
| from unittest.mock import AsyncMock, MagicMock, patch | from unittest.mock import AsyncMock, MagicMock, patch | ||||||
|  |  | ||||||
| from azure.identity.aio import ClientSecretCredential | from azure.identity.aio import ClientSecretCredential | ||||||
| from django.urls import reverse | from django.test import TestCase | ||||||
| from msgraph.generated.models.group_collection_response import GroupCollectionResponse | from msgraph.generated.models.group_collection_response import GroupCollectionResponse | ||||||
| from msgraph.generated.models.organization import Organization | from msgraph.generated.models.organization import Organization | ||||||
| from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse | from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse | ||||||
| from msgraph.generated.models.user import User as MSUser | from msgraph.generated.models.user import User as MSUser | ||||||
| from msgraph.generated.models.user_collection_response import UserCollectionResponse | from msgraph.generated.models.user_collection_response import UserCollectionResponse | ||||||
| from msgraph.generated.models.verified_domain import VerifiedDomain | from msgraph.generated.models.verified_domain import VerifiedDomain | ||||||
| from rest_framework.test import APITestCase |  | ||||||
|  |  | ||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import Application, Group, User | from authentik.core.models import Application, Group, User | ||||||
| from authentik.core.tests.utils import create_test_admin_user |  | ||||||
| from authentik.enterprise.providers.microsoft_entra.models import ( | from authentik.enterprise.providers.microsoft_entra.models import ( | ||||||
|     MicrosoftEntraProvider, |     MicrosoftEntraProvider, | ||||||
|     MicrosoftEntraProviderMapping, |     MicrosoftEntraProviderMapping, | ||||||
| @ -27,12 +25,11 @@ from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction | |||||||
| from authentik.tenants.models import Tenant | from authentik.tenants.models import Tenant | ||||||
|  |  | ||||||
|  |  | ||||||
| class MicrosoftEntraUserTests(APITestCase): | class MicrosoftEntraUserTests(TestCase): | ||||||
|     """Microsoft Entra User tests""" |     """Microsoft Entra User tests""" | ||||||
|  |  | ||||||
|     @apply_blueprint("system/providers-microsoft-entra.yaml") |     @apply_blueprint("system/providers-microsoft-entra.yaml") | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|  |  | ||||||
|         # Delete all users and groups as the mocked HTTP responses only return one ID |         # Delete all users and groups as the mocked HTTP responses only return one ID | ||||||
|         # which will cause errors with multiple users |         # which will cause errors with multiple users | ||||||
|         Tenant.objects.update(avatars="none") |         Tenant.objects.update(avatars="none") | ||||||
| @ -374,45 +371,3 @@ class MicrosoftEntraUserTests(APITestCase): | |||||||
|             ) |             ) | ||||||
|             self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) |             self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) | ||||||
|             user_list.assert_called_once() |             user_list.assert_called_once() | ||||||
|  |  | ||||||
|     def test_connect_manual(self): |  | ||||||
|         """test manual user connection""" |  | ||||||
|         uid = generate_id() |  | ||||||
|         self.app.backchannel_providers.remove(self.provider) |  | ||||||
|         admin = create_test_admin_user() |  | ||||||
|         different_user = User.objects.create( |  | ||||||
|             username=uid, |  | ||||||
|             email=f"{uid}@goauthentik.io", |  | ||||||
|         ) |  | ||||||
|         self.app.backchannel_providers.add(self.provider) |  | ||||||
|         with ( |  | ||||||
|             patch( |  | ||||||
|                 "authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials", |  | ||||||
|                 MagicMock(return_value={"credentials": self.creds}), |  | ||||||
|             ), |  | ||||||
|             patch( |  | ||||||
|                 "msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get", |  | ||||||
|                 AsyncMock( |  | ||||||
|                     return_value=OrganizationCollectionResponse( |  | ||||||
|                         value=[ |  | ||||||
|                             Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")]) |  | ||||||
|                         ] |  | ||||||
|                     ) |  | ||||||
|                 ), |  | ||||||
|             ), |  | ||||||
|             patch( |  | ||||||
|                 "authentik.enterprise.providers.microsoft_entra.clients.users.MicrosoftEntraUserClient.update_single_attribute", |  | ||||||
|                 MagicMock(), |  | ||||||
|             ) as user_get, |  | ||||||
|         ): |  | ||||||
|             self.client.force_login(admin) |  | ||||||
|             response = self.client.post( |  | ||||||
|                 reverse("authentik_api:microsoftentraprovideruser-list"), |  | ||||||
|                 data={ |  | ||||||
|                     "microsoft_id": generate_id(), |  | ||||||
|                     "user": different_user.pk, |  | ||||||
|                     "provider": self.provider.pk, |  | ||||||
|                 }, |  | ||||||
|             ) |  | ||||||
|             self.assertEqual(response.status_code, 201) |  | ||||||
|             user_get.assert_called_once() |  | ||||||
|  | |||||||
| @ -50,6 +50,7 @@ cache: | |||||||
|   timeout: 300 |   timeout: 300 | ||||||
|   timeout_flows: 300 |   timeout_flows: 300 | ||||||
|   timeout_policies: 300 |   timeout_policies: 300 | ||||||
|  |   timeout_reputation: 300 | ||||||
|  |  | ||||||
| # channel: | # channel: | ||||||
| #   url: "" | #   url: "" | ||||||
| @ -115,9 +116,6 @@ events: | |||||||
|   context_processors: |   context_processors: | ||||||
|     geoip: "/geoip/GeoLite2-City.mmdb" |     geoip: "/geoip/GeoLite2-City.mmdb" | ||||||
|     asn: "/geoip/GeoLite2-ASN.mmdb" |     asn: "/geoip/GeoLite2-ASN.mmdb" | ||||||
| compliance: |  | ||||||
|   fips: |  | ||||||
|     enabled: false |  | ||||||
|  |  | ||||||
| cert_discovery_dir: /certs | cert_discovery_dir: /certs | ||||||
|  |  | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ from rest_framework.decorators import action | |||||||
| from rest_framework.fields import BooleanField | from rest_framework.fields import BooleanField | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.serializers import ModelSerializer |  | ||||||
|  |  | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.events.api.tasks import SystemTaskSerializer | from authentik.events.api.tasks import SystemTaskSerializer | ||||||
| @ -55,17 +54,3 @@ class OutgoingSyncProviderStatusMixin: | |||||||
|                 "is_running": not lock_acquired, |                 "is_running": not lock_acquired, | ||||||
|             } |             } | ||||||
|         return Response(SyncStatusSerializer(status).data) |         return Response(SyncStatusSerializer(status).data) | ||||||
|  |  | ||||||
|  |  | ||||||
| class OutgoingSyncConnectionCreateMixin: |  | ||||||
|     """Mixin for connection objects that fetches remote data upon creation""" |  | ||||||
|  |  | ||||||
|     def perform_create(self, serializer: ModelSerializer): |  | ||||||
|         super().perform_create(serializer) |  | ||||||
|         try: |  | ||||||
|             instance = serializer.instance |  | ||||||
|             client = instance.provider.client_for_model(instance.__class__) |  | ||||||
|             client.update_single_attribute(instance) |  | ||||||
|             instance.save() |  | ||||||
|         except NotImplementedError: |  | ||||||
|             pass |  | ||||||
|  | |||||||
| @ -114,8 +114,3 @@ class BaseOutgoingSyncClient[ | |||||||
|         pre-link any users/groups in the remote system with the respective |         pre-link any users/groups in the remote system with the respective | ||||||
|         object in authentik based on a common identifier""" |         object in authentik based on a common identifier""" | ||||||
|         raise NotImplementedError() |         raise NotImplementedError() | ||||||
|  |  | ||||||
|     def update_single_attribute(self, connection: TConnection): |  | ||||||
|         """Update connection attributes on a connection object, when the connection |  | ||||||
|         is manually created""" |  | ||||||
|         raise NotImplementedError |  | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from django_filters.filters import ModelMultipleChoiceFilter | |||||||
| from django_filters.filterset import FilterSet | from django_filters.filterset import FilterSet | ||||||
| from drf_spectacular.utils import extend_schema | from drf_spectacular.utils import extend_schema | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.fields import BooleanField, CharField, DateTimeField, SerializerMethodField | from rest_framework.fields import BooleanField, CharField, DateTimeField | ||||||
| from rest_framework.relations import PrimaryKeyRelatedField | from rest_framework.relations import PrimaryKeyRelatedField | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| @ -18,7 +18,6 @@ from authentik.core.api.providers import ProviderSerializer | |||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import JSONDictField, PassiveSerializer | from authentik.core.api.utils import JSONDictField, PassiveSerializer | ||||||
| from authentik.core.models import Provider | from authentik.core.models import Provider | ||||||
| from authentik.enterprise.license import LicenseKey |  | ||||||
| from authentik.enterprise.providers.rac.models import RACProvider | from authentik.enterprise.providers.rac.models import RACProvider | ||||||
| from authentik.outposts.api.service_connections import ServiceConnectionSerializer | from authentik.outposts.api.service_connections import ServiceConnectionSerializer | ||||||
| from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME | from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME | ||||||
| @ -121,7 +120,7 @@ class OutpostHealthSerializer(PassiveSerializer): | |||||||
|     golang_version = CharField(read_only=True) |     golang_version = CharField(read_only=True) | ||||||
|     openssl_enabled = BooleanField(read_only=True) |     openssl_enabled = BooleanField(read_only=True) | ||||||
|     openssl_version = CharField(read_only=True) |     openssl_version = CharField(read_only=True) | ||||||
|     fips_enabled = SerializerMethodField() |     fips_enabled = BooleanField(read_only=True) | ||||||
|  |  | ||||||
|     version_should = CharField(read_only=True) |     version_should = CharField(read_only=True) | ||||||
|     version_outdated = BooleanField(read_only=True) |     version_outdated = BooleanField(read_only=True) | ||||||
| @ -131,12 +130,6 @@ class OutpostHealthSerializer(PassiveSerializer): | |||||||
|  |  | ||||||
|     hostname = CharField(read_only=True, required=False) |     hostname = CharField(read_only=True, required=False) | ||||||
|  |  | ||||||
|     def get_fips_enabled(self, obj: dict) -> bool | None: |  | ||||||
|         """Get FIPS enabled""" |  | ||||||
|         if not LicenseKey.get_total().is_valid(): |  | ||||||
|             return None |  | ||||||
|         return obj["fips_enabled"] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class OutpostFilter(FilterSet): | class OutpostFilter(FilterSet): | ||||||
|     """Filter for Outposts""" |     """Filter for Outposts""" | ||||||
|  | |||||||
| @ -2,6 +2,8 @@ | |||||||
|  |  | ||||||
| from authentik.blueprints.apps import ManagedAppConfig | from authentik.blueprints.apps import ManagedAppConfig | ||||||
|  |  | ||||||
|  | CACHE_KEY_PREFIX = "goauthentik.io/policies/reputation/scores/" | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikPolicyReputationConfig(ManagedAppConfig): | class AuthentikPolicyReputationConfig(ManagedAppConfig): | ||||||
|     """Authentik reputation app config""" |     """Authentik reputation app config""" | ||||||
|  | |||||||
| @ -1,25 +0,0 @@ | |||||||
| # Generated by Django 5.0.6 on 2024-06-11 08:50 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ("authentik_policies_reputation", "0006_reputation_ip_asn_data"), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddIndex( |  | ||||||
|             model_name="reputation", |  | ||||||
|             index=models.Index(fields=["identifier"], name="authentik_p_identif_9434d7_idx"), |  | ||||||
|         ), |  | ||||||
|         migrations.AddIndex( |  | ||||||
|             model_name="reputation", |  | ||||||
|             index=models.Index(fields=["ip"], name="authentik_p_ip_7ad0df_idx"), |  | ||||||
|         ), |  | ||||||
|         migrations.AddIndex( |  | ||||||
|             model_name="reputation", |  | ||||||
|             index=models.Index(fields=["ip", "identifier"], name="authentik_p_ip_d779aa_idx"), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -96,8 +96,3 @@ class Reputation(ExpiringModel, SerializerModel): | |||||||
|         verbose_name = _("Reputation Score") |         verbose_name = _("Reputation Score") | ||||||
|         verbose_name_plural = _("Reputation Scores") |         verbose_name_plural = _("Reputation Scores") | ||||||
|         unique_together = ("identifier", "ip") |         unique_together = ("identifier", "ip") | ||||||
|         indexes = [ |  | ||||||
|             models.Index(fields=["identifier"]), |  | ||||||
|             models.Index(fields=["ip"]), |  | ||||||
|             models.Index(fields=["ip", "identifier"]), |  | ||||||
|         ] |  | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								authentik/policies/reputation/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								authentik/policies/reputation/settings.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | """Reputation Settings""" | ||||||
|  |  | ||||||
|  | from celery.schedules import crontab | ||||||
|  |  | ||||||
|  | CELERY_BEAT_SCHEDULE = { | ||||||
|  |     "policies_reputation_save": { | ||||||
|  |         "task": "authentik.policies.reputation.tasks.save_reputation", | ||||||
|  |         "schedule": crontab(minute="1-59/5"), | ||||||
|  |         "options": {"queue": "authentik_scheduled"}, | ||||||
|  |     }, | ||||||
|  | } | ||||||
| @ -1,35 +1,40 @@ | |||||||
| """authentik reputation request signals""" | """authentik reputation request signals""" | ||||||
|  |  | ||||||
| from django.contrib.auth.signals import user_logged_in | from django.contrib.auth.signals import user_logged_in | ||||||
|  | from django.core.cache import cache | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.signals import login_failed | from authentik.core.signals import login_failed | ||||||
| from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR | from authentik.lib.config import CONFIG | ||||||
| from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR | from authentik.policies.reputation.apps import CACHE_KEY_PREFIX | ||||||
| from authentik.policies.reputation.models import Reputation, reputation_expiry | from authentik.policies.reputation.tasks import save_reputation | ||||||
| from authentik.root.middleware import ClientIPMiddleware | from authentik.root.middleware import ClientIPMiddleware | ||||||
| from authentik.stages.identification.signals import identification_failed | from authentik.stages.identification.signals import identification_failed | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | CACHE_TIMEOUT = CONFIG.get_int("cache.timeout_reputation") | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_score(request: HttpRequest, identifier: str, amount: int): | def update_score(request: HttpRequest, identifier: str, amount: int): | ||||||
|     """Update score for IP and User""" |     """Update score for IP and User""" | ||||||
|     remote_ip = ClientIPMiddleware.get_client_ip(request) |     remote_ip = ClientIPMiddleware.get_client_ip(request) | ||||||
|  |  | ||||||
|     Reputation.objects.update_or_create( |     try: | ||||||
|         ip=remote_ip, |         # We only update the cache here, as its faster than writing to the DB | ||||||
|         identifier=identifier, |         score = cache.get_or_set( | ||||||
|         defaults={ |             CACHE_KEY_PREFIX + remote_ip + "/" + identifier, | ||||||
|             "score": amount, |             {"ip": remote_ip, "identifier": identifier, "score": 0}, | ||||||
|             "ip_geo_data": GEOIP_CONTEXT_PROCESSOR.city_dict(remote_ip) or {}, |             CACHE_TIMEOUT, | ||||||
|             "ip_asn_data": ASN_CONTEXT_PROCESSOR.asn_dict(remote_ip) or {}, |  | ||||||
|             "expires": reputation_expiry(), |  | ||||||
|         }, |  | ||||||
|         ) |         ) | ||||||
|  |         score["score"] += amount | ||||||
|  |         cache.set(CACHE_KEY_PREFIX + remote_ip + "/" + identifier, score) | ||||||
|  |     except ValueError as exc: | ||||||
|  |         LOGGER.warning("failed to set reputation", exc=exc) | ||||||
|  |  | ||||||
|     LOGGER.debug("Updated score", amount=amount, for_user=identifier, for_ip=remote_ip) |     LOGGER.debug("Updated score", amount=amount, for_user=identifier, for_ip=remote_ip) | ||||||
|  |     save_reputation.delay() | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(login_failed) | @receiver(login_failed) | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								authentik/policies/reputation/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								authentik/policies/reputation/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | """Reputation tasks""" | ||||||
|  |  | ||||||
|  | from django.core.cache import cache | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR | ||||||
|  | from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR | ||||||
|  | from authentik.events.models import TaskStatus | ||||||
|  | from authentik.events.system_tasks import SystemTask, prefill_task | ||||||
|  | from authentik.policies.reputation.apps import CACHE_KEY_PREFIX | ||||||
|  | from authentik.policies.reputation.models import Reputation | ||||||
|  | from authentik.root.celery import CELERY_APP | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task(bind=True, base=SystemTask) | ||||||
|  | @prefill_task | ||||||
|  | def save_reputation(self: SystemTask): | ||||||
|  |     """Save currently cached reputation to database""" | ||||||
|  |     objects_to_update = [] | ||||||
|  |     for _, score in cache.get_many(cache.keys(CACHE_KEY_PREFIX + "*")).items(): | ||||||
|  |         rep, _ = Reputation.objects.get_or_create( | ||||||
|  |             ip=score["ip"], | ||||||
|  |             identifier=score["identifier"], | ||||||
|  |         ) | ||||||
|  |         rep.ip_geo_data = GEOIP_CONTEXT_PROCESSOR.city_dict(score["ip"]) or {} | ||||||
|  |         rep.ip_asn_data = ASN_CONTEXT_PROCESSOR.asn_dict(score["ip"]) or {} | ||||||
|  |         rep.score = score["score"] | ||||||
|  |         objects_to_update.append(rep) | ||||||
|  |     Reputation.objects.bulk_update(objects_to_update, ["score", "ip_geo_data"]) | ||||||
|  |     self.set_status(TaskStatus.SUCCESSFUL, "Successfully updated Reputation") | ||||||
| @ -1,11 +1,14 @@ | |||||||
| """test reputation signals and policy""" | """test reputation signals and policy""" | ||||||
|  |  | ||||||
|  | from django.core.cache import cache | ||||||
| from django.test import RequestFactory, TestCase | from django.test import RequestFactory, TestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.policies.reputation.api import ReputationPolicySerializer | from authentik.policies.reputation.api import ReputationPolicySerializer | ||||||
|  | from authentik.policies.reputation.apps import CACHE_KEY_PREFIX | ||||||
| from authentik.policies.reputation.models import Reputation, ReputationPolicy | from authentik.policies.reputation.models import Reputation, ReputationPolicy | ||||||
|  | from authentik.policies.reputation.tasks import save_reputation | ||||||
| from authentik.policies.types import PolicyRequest | from authentik.policies.types import PolicyRequest | ||||||
| from authentik.stages.password import BACKEND_INBUILT | from authentik.stages.password import BACKEND_INBUILT | ||||||
| from authentik.stages.password.stage import authenticate | from authentik.stages.password.stage import authenticate | ||||||
| @ -19,6 +22,8 @@ class TestReputationPolicy(TestCase): | |||||||
|         self.request = self.request_factory.get("/") |         self.request = self.request_factory.get("/") | ||||||
|         self.test_ip = "127.0.0.1" |         self.test_ip = "127.0.0.1" | ||||||
|         self.test_username = "test" |         self.test_username = "test" | ||||||
|  |         keys = cache.keys(CACHE_KEY_PREFIX + "*") | ||||||
|  |         cache.delete_many(keys) | ||||||
|         # We need a user for the one-to-one in userreputation |         # We need a user for the one-to-one in userreputation | ||||||
|         self.user = User.objects.create(username=self.test_username) |         self.user = User.objects.create(username=self.test_username) | ||||||
|         self.backends = [BACKEND_INBUILT] |         self.backends = [BACKEND_INBUILT] | ||||||
| @ -29,6 +34,13 @@ class TestReputationPolicy(TestCase): | |||||||
|         authenticate( |         authenticate( | ||||||
|             self.request, self.backends, username=self.test_username, password=self.test_username |             self.request, self.backends, username=self.test_username, password=self.test_username | ||||||
|         ) |         ) | ||||||
|  |         # Test value in cache | ||||||
|  |         self.assertEqual( | ||||||
|  |             cache.get(CACHE_KEY_PREFIX + self.test_ip + "/" + self.test_username), | ||||||
|  |             {"ip": "127.0.0.1", "identifier": "test", "score": -1}, | ||||||
|  |         ) | ||||||
|  |         # Save cache and check db values | ||||||
|  |         save_reputation.delay().get() | ||||||
|         self.assertEqual(Reputation.objects.get(ip=self.test_ip).score, -1) |         self.assertEqual(Reputation.objects.get(ip=self.test_ip).score, -1) | ||||||
|  |  | ||||||
|     def test_user_reputation(self): |     def test_user_reputation(self): | ||||||
| @ -37,6 +49,13 @@ class TestReputationPolicy(TestCase): | |||||||
|         authenticate( |         authenticate( | ||||||
|             self.request, self.backends, username=self.test_username, password=self.test_username |             self.request, self.backends, username=self.test_username, password=self.test_username | ||||||
|         ) |         ) | ||||||
|  |         # Test value in cache | ||||||
|  |         self.assertEqual( | ||||||
|  |             cache.get(CACHE_KEY_PREFIX + self.test_ip + "/" + self.test_username), | ||||||
|  |             {"ip": "127.0.0.1", "identifier": "test", "score": -1}, | ||||||
|  |         ) | ||||||
|  |         # Save cache and check db values | ||||||
|  |         save_reputation.delay().get() | ||||||
|         self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, -1) |         self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, -1) | ||||||
|  |  | ||||||
|     def test_policy(self): |     def test_policy(self): | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ from rest_framework.viewsets import GenericViewSet | |||||||
|  |  | ||||||
| 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.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin |  | ||||||
| from authentik.providers.scim.models import SCIMProviderGroup | from authentik.providers.scim.models import SCIMProviderGroup | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -29,7 +28,6 @@ class SCIMProviderGroupSerializer(ModelSerializer): | |||||||
|  |  | ||||||
| class SCIMProviderGroupViewSet( | class SCIMProviderGroupViewSet( | ||||||
|     mixins.CreateModelMixin, |     mixins.CreateModelMixin, | ||||||
|     OutgoingSyncConnectionCreateMixin, |  | ||||||
|     mixins.RetrieveModelMixin, |     mixins.RetrieveModelMixin, | ||||||
|     mixins.DestroyModelMixin, |     mixins.DestroyModelMixin, | ||||||
|     UsedByMixin, |     UsedByMixin, | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ from rest_framework.viewsets import GenericViewSet | |||||||
|  |  | ||||||
| from authentik.core.api.groups import GroupMemberSerializer | from authentik.core.api.groups import GroupMemberSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin |  | ||||||
| from authentik.providers.scim.models import SCIMProviderUser | from authentik.providers.scim.models import SCIMProviderUser | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -29,7 +28,6 @@ class SCIMProviderUserSerializer(ModelSerializer): | |||||||
|  |  | ||||||
| class SCIMProviderUserViewSet( | class SCIMProviderUserViewSet( | ||||||
|     mixins.CreateModelMixin, |     mixins.CreateModelMixin, | ||||||
|     OutgoingSyncConnectionCreateMixin, |  | ||||||
|     mixins.RetrieveModelMixin, |     mixins.RetrieveModelMixin, | ||||||
|     mixins.DestroyModelMixin, |     mixins.DestroyModelMixin, | ||||||
|     UsedByMixin, |     UsedByMixin, | ||||||
|  | |||||||
| @ -15,48 +15,6 @@ from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient | |||||||
| from authentik.lib.sync.outgoing.models import OutgoingSyncProvider | from authentik.lib.sync.outgoing.models import OutgoingSyncProvider | ||||||
|  |  | ||||||
|  |  | ||||||
| class SCIMProviderUser(SerializerModel): |  | ||||||
|     """Mapping of a user and provider to a SCIM user ID""" |  | ||||||
|  |  | ||||||
|     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) |  | ||||||
|     scim_id = models.TextField() |  | ||||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) |  | ||||||
|     provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[Serializer]: |  | ||||||
|         from authentik.providers.scim.api.users import SCIMProviderUserSerializer |  | ||||||
|  |  | ||||||
|         return SCIMProviderUserSerializer |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         unique_together = (("scim_id", "user", "provider"),) |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         return f"SCIM Provider User {self.user_id} to {self.provider_id}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SCIMProviderGroup(SerializerModel): |  | ||||||
|     """Mapping of a group and provider to a SCIM user ID""" |  | ||||||
|  |  | ||||||
|     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) |  | ||||||
|     scim_id = models.TextField() |  | ||||||
|     group = models.ForeignKey(Group, on_delete=models.CASCADE) |  | ||||||
|     provider = models.ForeignKey("SCIMProvider", on_delete=models.CASCADE) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[Serializer]: |  | ||||||
|         from authentik.providers.scim.api.groups import SCIMProviderGroupSerializer |  | ||||||
|  |  | ||||||
|         return SCIMProviderGroupSerializer |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         unique_together = (("scim_id", "group", "provider"),) |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         return f"SCIM Provider Group {self.group_id} to {self.provider_id}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SCIMProvider(OutgoingSyncProvider, BackchannelProvider): | class SCIMProvider(OutgoingSyncProvider, BackchannelProvider): | ||||||
|     """SCIM 2.0 provider to create users and groups in external applications""" |     """SCIM 2.0 provider to create users and groups in external applications""" | ||||||
|  |  | ||||||
| @ -81,13 +39,13 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider): | |||||||
|         return static("authentik/sources/scim.png") |         return static("authentik/sources/scim.png") | ||||||
|  |  | ||||||
|     def client_for_model( |     def client_for_model( | ||||||
|         self, model: type[User | Group | SCIMProviderUser | SCIMProviderGroup] |         self, model: type[User | Group] | ||||||
|     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: |     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: | ||||||
|         if issubclass(model, User | SCIMProviderUser): |         if issubclass(model, User): | ||||||
|             from authentik.providers.scim.clients.users import SCIMUserClient |             from authentik.providers.scim.clients.users import SCIMUserClient | ||||||
|  |  | ||||||
|             return SCIMUserClient(self) |             return SCIMUserClient(self) | ||||||
|         if issubclass(model, Group | SCIMProviderGroup): |         if issubclass(model, Group): | ||||||
|             from authentik.providers.scim.clients.groups import SCIMGroupClient |             from authentik.providers.scim.clients.groups import SCIMGroupClient | ||||||
|  |  | ||||||
|             return SCIMGroupClient(self) |             return SCIMGroupClient(self) | ||||||
| @ -147,3 +105,45 @@ class SCIMMapping(PropertyMapping): | |||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("SCIM Mapping") |         verbose_name = _("SCIM Mapping") | ||||||
|         verbose_name_plural = _("SCIM Mappings") |         verbose_name_plural = _("SCIM Mappings") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SCIMProviderUser(SerializerModel): | ||||||
|  |     """Mapping of a user and provider to a SCIM user ID""" | ||||||
|  |  | ||||||
|  |     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |     scim_id = models.TextField() | ||||||
|  |     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
|  |     provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def serializer(self) -> type[Serializer]: | ||||||
|  |         from authentik.providers.scim.api.users import SCIMProviderUserSerializer | ||||||
|  |  | ||||||
|  |         return SCIMProviderUserSerializer | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         unique_together = (("scim_id", "user", "provider"),) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"SCIM Provider User {self.user_id} to {self.provider_id}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SCIMProviderGroup(SerializerModel): | ||||||
|  |     """Mapping of a group and provider to a SCIM user ID""" | ||||||
|  |  | ||||||
|  |     id = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
|  |     scim_id = models.TextField() | ||||||
|  |     group = models.ForeignKey(Group, on_delete=models.CASCADE) | ||||||
|  |     provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def serializer(self) -> type[Serializer]: | ||||||
|  |         from authentik.providers.scim.api.groups import SCIMProviderGroupSerializer | ||||||
|  |  | ||||||
|  |         return SCIMProviderGroupSerializer | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         unique_together = (("scim_id", "group", "provider"),) | ||||||
|  |  | ||||||
|  |     def __str__(self) -> str: | ||||||
|  |         return f"SCIM Provider Group {self.group_id} to {self.provider_id}" | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
|     "$schema": "http://json-schema.org/draft-07/schema", |     "$schema": "http://json-schema.org/draft-07/schema", | ||||||
|     "$id": "https://goauthentik.io/blueprints/schema.json", |     "$id": "https://goauthentik.io/blueprints/schema.json", | ||||||
|     "type": "object", |     "type": "object", | ||||||
|     "title": "authentik 2024.6.0 Blueprint schema", |     "title": "authentik 2024.4.2 Blueprint schema", | ||||||
|     "required": [ |     "required": [ | ||||||
|         "version", |         "version", | ||||||
|         "entries" |         "entries" | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redis:/data |       - redis:/data | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -52,7 +52,7 @@ services: | |||||||
|       - postgresql |       - postgresql | ||||||
|       - redis |       - redis | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @ -5,7 +5,7 @@ go 1.22.2 | |||||||
| require ( | require ( | ||||||
| 	beryju.io/ldap v0.1.0 | 	beryju.io/ldap v0.1.0 | ||||||
| 	github.com/coreos/go-oidc v2.2.1+incompatible | 	github.com/coreos/go-oidc v2.2.1+incompatible | ||||||
| 	github.com/getsentry/sentry-go v0.28.1 | 	github.com/getsentry/sentry-go v0.28.0 | ||||||
| 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | ||||||
| 	github.com/go-ldap/ldap/v3 v3.4.8 | 	github.com/go-ldap/ldap/v3 v3.4.8 | ||||||
| 	github.com/go-openapi/runtime v0.28.0 | 	github.com/go-openapi/runtime v0.28.0 | ||||||
| @ -16,7 +16,7 @@ require ( | |||||||
| 	github.com/gorilla/mux v1.8.1 | 	github.com/gorilla/mux v1.8.1 | ||||||
| 	github.com/gorilla/securecookie v1.1.2 | 	github.com/gorilla/securecookie v1.1.2 | ||||||
| 	github.com/gorilla/sessions v1.2.2 | 	github.com/gorilla/sessions v1.2.2 | ||||||
| 	github.com/gorilla/websocket v1.5.3 | 	github.com/gorilla/websocket v1.5.2 | ||||||
| 	github.com/jellydator/ttlcache/v3 v3.2.0 | 	github.com/jellydator/ttlcache/v3 v3.2.0 | ||||||
| 	github.com/mitchellh/mapstructure v1.5.0 | 	github.com/mitchellh/mapstructure v1.5.0 | ||||||
| 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							| @ -69,8 +69,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m | |||||||
| github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||||
| github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= | ||||||
| github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | ||||||
| github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= | github.com/getsentry/sentry-go v0.28.0 h1:7Rqx9M3ythTKy2J6uZLHmc8Sz9OGgIlseuO1iBX/s0M= | ||||||
| github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= | github.com/getsentry/sentry-go v0.28.0/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= | ||||||
| github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= | github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= | ||||||
| github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | ||||||
| github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= | ||||||
| @ -176,8 +176,8 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z | |||||||
| github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= | github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= | ||||||
| github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= | github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= | ||||||
| github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
| github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= | github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= | ||||||
| github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= | ||||||
| github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||||
| github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= | ||||||
| github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||||
|  | |||||||
| @ -29,4 +29,4 @@ func UserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2024.6.0" | const VERSION = "2024.4.2" | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ from pathlib import Path | |||||||
| from tempfile import gettempdir | from tempfile import gettempdir | ||||||
| from typing import TYPE_CHECKING | from typing import TYPE_CHECKING | ||||||
|  |  | ||||||
|  | from cryptography.exceptions import InternalError | ||||||
| from cryptography.hazmat.backends.openssl.backend import backend | from cryptography.hazmat.backends.openssl.backend import backend | ||||||
| from defusedxml import defuse_stdlib | from defusedxml import defuse_stdlib | ||||||
| from prometheus_client.values import MultiProcessValue | from prometheus_client.values import MultiProcessValue | ||||||
| @ -29,8 +30,10 @@ if TYPE_CHECKING: | |||||||
|  |  | ||||||
| defuse_stdlib() | defuse_stdlib() | ||||||
|  |  | ||||||
| if CONFIG.get_bool("compliance.fips.enabled", False): | try: | ||||||
|     backend._enable_fips() |     backend._enable_fips() | ||||||
|  | except InternalError: | ||||||
|  |     pass | ||||||
|  |  | ||||||
| wait_for_db() | wait_for_db() | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import os | |||||||
| import sys | import sys | ||||||
| import warnings | import warnings | ||||||
|  |  | ||||||
| from authentik.lib.config import CONFIG | from cryptography.exceptions import InternalError | ||||||
| from cryptography.hazmat.backends.openssl.backend import backend | from cryptography.hazmat.backends.openssl.backend import backend | ||||||
| from defusedxml import defuse_stdlib | from defusedxml import defuse_stdlib | ||||||
| from django.utils.autoreload import DJANGO_AUTORELOAD_ENV | from django.utils.autoreload import DJANGO_AUTORELOAD_ENV | ||||||
| @ -24,8 +24,10 @@ warnings.filterwarnings( | |||||||
|  |  | ||||||
| defuse_stdlib() | defuse_stdlib() | ||||||
|  |  | ||||||
| if CONFIG.get_bool("compliance.fips.enabled", False): | try: | ||||||
|     backend._enable_fips() |     backend._enable_fips() | ||||||
|  | except InternalError: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -353,13 +353,13 @@ msal-extensions = ">=0.3.0" | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "bandit" | name = "bandit" | ||||||
| version = "1.7.9" | version = "1.7.8" | ||||||
| description = "Security oriented static analyser for python code." | description = "Security oriented static analyser for python code." | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, |     {file = "bandit-1.7.8-py3-none-any.whl", hash = "sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381"}, | ||||||
|     {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, |     {file = "bandit-1.7.8.tar.gz", hash = "sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
| @ -3376,13 +3376,13 @@ files = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pydantic" | name = "pydantic" | ||||||
| version = "2.7.4" | version = "2.7.3" | ||||||
| description = "Data validation using Python type hints" | description = "Data validation using Python type hints" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.8" | python-versions = ">=3.8" | ||||||
| files = [ | files = [ | ||||||
|     {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"}, |     {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, | ||||||
|     {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"}, |     {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2024.6.0" | version = "2024.4.2" | ||||||
| description = "" | description = "" | ||||||
| authors = ["authentik Team <hello@goauthentik.io>"] | authors = ["authentik Team <hello@goauthentik.io>"] | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2024.6.0 |   version: 2024.4.2 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@goauthentik.io |     email: hello@goauthentik.io | ||||||
| @ -39547,8 +39547,6 @@ components: | |||||||
|           readOnly: true |           readOnly: true | ||||||
|         fips_enabled: |         fips_enabled: | ||||||
|           type: boolean |           type: boolean | ||||||
|           nullable: true |  | ||||||
|           description: Get FIPS enabled |  | ||||||
|           readOnly: true |           readOnly: true | ||||||
|         version_should: |         version_should: | ||||||
|           type: string |           type: string | ||||||
| @ -47406,16 +47404,15 @@ components: | |||||||
|               type: string |               type: string | ||||||
|             openssl_version: |             openssl_version: | ||||||
|               type: string |               type: string | ||||||
|             openssl_fips_enabled: |             openssl_fips_mode: | ||||||
|               type: boolean |               type: boolean | ||||||
|               nullable: true |  | ||||||
|             authentik_version: |             authentik_version: | ||||||
|               type: string |               type: string | ||||||
|           required: |           required: | ||||||
|           - architecture |           - architecture | ||||||
|           - authentik_version |           - authentik_version | ||||||
|           - environment |           - environment | ||||||
|           - openssl_fips_enabled |           - openssl_fips_mode | ||||||
|           - openssl_version |           - openssl_version | ||||||
|           - platform |           - platform | ||||||
|           - python_version |           - python_version | ||||||
|  | |||||||
| @ -3,6 +3,15 @@ | |||||||
| This is the default UI for the authentik server. The documentation is going to be a little sparse | This is the default UI for the authentik server. The documentation is going to be a little sparse | ||||||
| for awhile, but at least let's get started. | for awhile, but at least let's get started. | ||||||
|  |  | ||||||
|  | # Standards | ||||||
|  |  | ||||||
|  | -   Be flexible in what you accept as input, be precise in what you produce as output. | ||||||
|  | -   Mis-use is always a crash. A component that takes the ID of an HTMLInputElement as an argument | ||||||
|  |     should throw an exception if the element is anything but an HTMLInputElement ("anything" includes | ||||||
|  |     non-existent, null, undefined, etc.). | ||||||
|  | -   Single Responsibility is ideal, but not always practical. To the best of your obility, every | ||||||
|  |     object in the system should do one thing and do it well. | ||||||
|  |  | ||||||
| # The Theory of the authentik UI | # The Theory of the authentik UI | ||||||
|  |  | ||||||
| In Peter Naur's 1985 essay [Programming as Theory | In Peter Naur's 1985 essay [Programming as Theory | ||||||
| @ -107,3 +116,7 @@ settings in JSON files, which do not support comments. | |||||||
|     -   `compilerOptions.plugins.ts-lit-plugin.rules.no-incompatible-type-binding: "warn"`: lit-analyzer |     -   `compilerOptions.plugins.ts-lit-plugin.rules.no-incompatible-type-binding: "warn"`: lit-analyzer | ||||||
|         does not support generics well when parsing a subtype of `HTMLElement`. As a result, this threw |         does not support generics well when parsing a subtype of `HTMLElement`. As a result, this threw | ||||||
|         too many errors to be supportable. |         too many errors to be supportable. | ||||||
|  | -   `package.json` | ||||||
|  |     -   `prettier` should always be the last thing run in any pre-commit pass. The `precommit` script | ||||||
|  |         does this, but if you don't use `precommit`, make sure `prettier` is the _last_ thing you do | ||||||
|  |         before a `git commit`. | ||||||
|  | |||||||
							
								
								
									
										6609
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6609
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -38,7 +38,7 @@ | |||||||
|         "@codemirror/theme-one-dark": "^6.1.2", |         "@codemirror/theme-one-dark": "^6.1.2", | ||||||
|         "@formatjs/intl-listformat": "^7.5.7", |         "@formatjs/intl-listformat": "^7.5.7", | ||||||
|         "@fortawesome/fontawesome-free": "^6.5.2", |         "@fortawesome/fontawesome-free": "^6.5.2", | ||||||
|         "@goauthentik/api": "^2024.4.2-1718378698", |         "@goauthentik/api": "^2024.4.2-1717645682", | ||||||
|         "@lit-labs/task": "^3.1.0", |         "@lit-labs/task": "^3.1.0", | ||||||
|         "@lit/context": "^1.1.2", |         "@lit/context": "^1.1.2", | ||||||
|         "@lit/localize": "^0.12.1", |         "@lit/localize": "^0.12.1", | ||||||
| @ -46,7 +46,7 @@ | |||||||
|         "@open-wc/lit-helpers": "^0.7.0", |         "@open-wc/lit-helpers": "^0.7.0", | ||||||
|         "@patternfly/elements": "^3.0.1", |         "@patternfly/elements": "^3.0.1", | ||||||
|         "@patternfly/patternfly": "^4.224.2", |         "@patternfly/patternfly": "^4.224.2", | ||||||
|         "@sentry/browser": "^8.9.2", |         "@sentry/browser": "^8.9.1", | ||||||
|         "@webcomponents/webcomponentsjs": "^2.8.0", |         "@webcomponents/webcomponentsjs": "^2.8.0", | ||||||
|         "base64-js": "^1.5.1", |         "base64-js": "^1.5.1", | ||||||
|         "chart.js": "^4.4.3", |         "chart.js": "^4.4.3", | ||||||
| @ -63,9 +63,10 @@ | |||||||
|         "rapidoc": "^9.3.4", |         "rapidoc": "^9.3.4", | ||||||
|         "showdown": "^2.1.0", |         "showdown": "^2.1.0", | ||||||
|         "style-mod": "^4.1.2", |         "style-mod": "^4.1.2", | ||||||
|         "ts-pattern": "^5.2.0", |         "ts-pattern": "^5.1.2", | ||||||
|         "webcomponent-qr-code": "^1.2.0", |         "webcomponent-qr-code": "^1.2.0", | ||||||
|         "yaml": "^2.4.5" |         "yaml": "^2.4.5", | ||||||
|  |         "zxcvbn": "^4.4.2" | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@babel/core": "^7.24.7", |         "@babel/core": "^7.24.7", | ||||||
| @ -80,20 +81,21 @@ | |||||||
|         "@jeysal/storybook-addon-css-user-preferences": "^0.2.0", |         "@jeysal/storybook-addon-css-user-preferences": "^0.2.0", | ||||||
|         "@lit/localize-tools": "^0.7.2", |         "@lit/localize-tools": "^0.7.2", | ||||||
|         "@rollup/plugin-replace": "^5.0.7", |         "@rollup/plugin-replace": "^5.0.7", | ||||||
|         "@spotlightjs/spotlight": "^2.0.0", |         "@spotlightjs/spotlight": "^1.2.17", | ||||||
|         "@storybook/addon-essentials": "^8.1.9", |         "@storybook/addon-essentials": "^8.1.6", | ||||||
|         "@storybook/addon-links": "^8.1.9", |         "@storybook/addon-links": "^8.1.6", | ||||||
|         "@storybook/api": "^7.6.17", |         "@storybook/api": "^7.6.17", | ||||||
|         "@storybook/blocks": "^8.0.8", |         "@storybook/blocks": "^8.0.8", | ||||||
|         "@storybook/manager-api": "^8.1.9", |         "@storybook/manager-api": "^8.1.6", | ||||||
|         "@storybook/web-components": "^8.1.9", |         "@storybook/web-components": "^8.1.6", | ||||||
|         "@storybook/web-components-vite": "^8.1.9", |         "@storybook/web-components-vite": "^8.1.6", | ||||||
|         "@trivago/prettier-plugin-sort-imports": "^4.3.0", |         "@trivago/prettier-plugin-sort-imports": "^4.3.0", | ||||||
|         "@types/chart.js": "^2.9.41", |         "@types/chart.js": "^2.9.41", | ||||||
|         "@types/codemirror": "5.60.15", |         "@types/codemirror": "5.60.15", | ||||||
|         "@types/grecaptcha": "^3.0.9", |         "@types/grecaptcha": "^3.0.9", | ||||||
|         "@types/guacamole-common-js": "1.5.2", |         "@types/guacamole-common-js": "1.5.2", | ||||||
|         "@types/showdown": "^2.0.6", |         "@types/showdown": "^2.0.6", | ||||||
|  |         "@types/zxcvbn": "^4.4.4", | ||||||
|         "@typescript-eslint/eslint-plugin": "^7.5.0", |         "@typescript-eslint/eslint-plugin": "^7.5.0", | ||||||
|         "@typescript-eslint/parser": "^7.5.0", |         "@typescript-eslint/parser": "^7.5.0", | ||||||
|         "babel-plugin-macros": "^3.1.0", |         "babel-plugin-macros": "^3.1.0", | ||||||
| @ -117,7 +119,7 @@ | |||||||
|         "react-dom": "^18.3.1", |         "react-dom": "^18.3.1", | ||||||
|         "rollup-plugin-modify": "^3.0.0", |         "rollup-plugin-modify": "^3.0.0", | ||||||
|         "rollup-plugin-postcss-lit": "^2.1.0", |         "rollup-plugin-postcss-lit": "^2.1.0", | ||||||
|         "storybook": "^8.1.9", |         "storybook": "^8.1.6", | ||||||
|         "storybook-addon-mock": "^5.0.0", |         "storybook-addon-mock": "^5.0.0", | ||||||
|         "ts-lit-plugin": "^2.0.2", |         "ts-lit-plugin": "^2.0.2", | ||||||
|         "tslib": "^2.6.3", |         "tslib": "^2.6.3", | ||||||
|  | |||||||
| @ -1,6 +1,5 @@ | |||||||
| import "@goauthentik/admin/admin-overview/TopApplicationsTable"; | import "@goauthentik/admin/admin-overview/TopApplicationsTable"; | ||||||
| import "@goauthentik/admin/admin-overview/cards/AdminStatusCard"; | import "@goauthentik/admin/admin-overview/cards/AdminStatusCard"; | ||||||
| import "@goauthentik/admin/admin-overview/cards/FipsStatusCard"; |  | ||||||
| import "@goauthentik/admin/admin-overview/cards/RecentEventsCard"; | import "@goauthentik/admin/admin-overview/cards/RecentEventsCard"; | ||||||
| import "@goauthentik/admin/admin-overview/cards/SystemStatusCard"; | import "@goauthentik/admin/admin-overview/cards/SystemStatusCard"; | ||||||
| import "@goauthentik/admin/admin-overview/cards/VersionStatusCard"; | import "@goauthentik/admin/admin-overview/cards/VersionStatusCard"; | ||||||
| @ -11,17 +10,13 @@ import "@goauthentik/admin/admin-overview/charts/SyncStatusChart"; | |||||||
| import { VERSION } from "@goauthentik/common/constants"; | import { VERSION } from "@goauthentik/common/constants"; | ||||||
| import { me } from "@goauthentik/common/users"; | import { me } from "@goauthentik/common/users"; | ||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js"; |  | ||||||
| import "@goauthentik/elements/PageHeader"; | import "@goauthentik/elements/PageHeader"; | ||||||
| import "@goauthentik/elements/cards/AggregatePromiseCard"; | import "@goauthentik/elements/cards/AggregatePromiseCard"; | ||||||
| import { paramURL } from "@goauthentik/elements/router/RouterOutlet"; | import { paramURL } from "@goauthentik/elements/router/RouterOutlet"; | ||||||
|  |  | ||||||
| import { msg, str } from "@lit/localize"; | import { msg, str } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, css, html, nothing } from "lit"; | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
| import { customElement, state } from "lit/decorators.js"; | import { customElement, state } from "lit/decorators.js"; | ||||||
| import { classMap } from "lit/directives/class-map.js"; |  | ||||||
| import { map } from "lit/directives/map.js"; |  | ||||||
| import { when } from "lit/directives/when.js"; |  | ||||||
|  |  | ||||||
| import PFContent from "@patternfly/patternfly/components/Content/content.css"; | import PFContent from "@patternfly/patternfly/components/Content/content.css"; | ||||||
| import PFDivider from "@patternfly/patternfly/components/Divider/divider.css"; | import PFDivider from "@patternfly/patternfly/components/Divider/divider.css"; | ||||||
| @ -38,12 +33,8 @@ export function versionFamily(): string { | |||||||
|     return parts.join("."); |     return parts.join("."); | ||||||
| } | } | ||||||
|  |  | ||||||
| const AdminOverviewBase = WithLicenseSummary(AKElement); |  | ||||||
|  |  | ||||||
| type Renderer = () => TemplateResult | typeof nothing; |  | ||||||
|  |  | ||||||
| @customElement("ak-admin-overview") | @customElement("ak-admin-overview") | ||||||
| export class AdminOverviewPage extends AdminOverviewBase { | export class AdminOverviewPage extends AKElement { | ||||||
|     static get styles(): CSSResult[] { |     static get styles(): CSSResult[] { | ||||||
|         return [ |         return [ | ||||||
|             PFBase, |             PFBase, | ||||||
| @ -82,7 +73,6 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
|         const name = this.user?.user.name ?? this.user?.user.username; |         const name = this.user?.user.name ?? this.user?.user.username; | ||||||
|  |  | ||||||
|         return html`<ak-page-header icon="" header="" description=${msg("General system status")}> |         return html`<ak-page-header icon="" header="" description=${msg("General system status")}> | ||||||
|                 <span slot="header"> ${msg(str`Welcome, ${name}.`)} </span> |                 <span slot="header"> ${msg(str`Welcome, ${name}.`)} </span> | ||||||
|             </ak-page-header> |             </ak-page-header> | ||||||
| @ -99,7 +89,48 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|                                 .isCenter=${false} |                                 .isCenter=${false} | ||||||
|                             > |                             > | ||||||
|                                 <ul class="pf-c-list"> |                                 <ul class="pf-c-list"> | ||||||
|                                     ${this.renderActions()} |                                     <li> | ||||||
|  |                                         <a | ||||||
|  |                                             class="pf-u-mb-xl" | ||||||
|  |                                             href=${paramURL("/core/applications", { | ||||||
|  |                                                 createForm: true, | ||||||
|  |                                             })} | ||||||
|  |                                             >${msg("Create a new application")}</a | ||||||
|  |                                         > | ||||||
|  |                                     </li> | ||||||
|  |                                     <li> | ||||||
|  |                                         <a class="pf-u-mb-xl" href=${paramURL("/events/log")} | ||||||
|  |                                             >${msg("Check the logs")}</a | ||||||
|  |                                         > | ||||||
|  |                                     </li> | ||||||
|  |                                     <li> | ||||||
|  |                                         <a | ||||||
|  |                                             class="pf-u-mb-xl" | ||||||
|  |                                             target="_blank" | ||||||
|  |                                             href="https://goauthentik.io/integrations/" | ||||||
|  |                                             >${msg("Explore integrations")}<i | ||||||
|  |                                                 class="fas fa-external-link-alt ak-external-link" | ||||||
|  |                                             ></i | ||||||
|  |                                         ></a> | ||||||
|  |                                     </li> | ||||||
|  |                                     <li> | ||||||
|  |                                         <a class="pf-u-mb-xl" href=${paramURL("/identity/users")} | ||||||
|  |                                             >${msg("Manage users")}</a | ||||||
|  |                                         > | ||||||
|  |                                     </li> | ||||||
|  |                                     <li> | ||||||
|  |                                         <a | ||||||
|  |                                             class="pf-u-mb-xl" | ||||||
|  |                                             target="_blank" | ||||||
|  |                                             href="https://goauthentik.io/docs/releases/${versionFamily()}#fixed-in-${VERSION.replaceAll( | ||||||
|  |                                                 ".", | ||||||
|  |                                                 "", | ||||||
|  |                                             )}" | ||||||
|  |                                             >${msg("Check the release notes")}<i | ||||||
|  |                                                 class="fas fa-external-link-alt ak-external-link" | ||||||
|  |                                             ></i | ||||||
|  |                                         ></a> | ||||||
|  |                                     </li> | ||||||
|                                 </ul> |                                 </ul> | ||||||
|                             </ak-aggregate-card> |                             </ak-aggregate-card> | ||||||
|                         </div> |                         </div> | ||||||
| @ -122,7 +153,21 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|                         <div class="pf-l-grid__item pf-m-12-col"> |                         <div class="pf-l-grid__item pf-m-12-col"> | ||||||
|                             <hr class="pf-c-divider" /> |                             <hr class="pf-c-divider" /> | ||||||
|                         </div> |                         </div> | ||||||
|                         ${this.renderCards()} |                         <div | ||||||
|  |                             class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container" | ||||||
|  |                         > | ||||||
|  |                             <ak-admin-status-system> </ak-admin-status-system> | ||||||
|  |                         </div> | ||||||
|  |                         <div | ||||||
|  |                             class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container" | ||||||
|  |                         > | ||||||
|  |                             <ak-admin-status-version> </ak-admin-status-version> | ||||||
|  |                         </div> | ||||||
|  |                         <div | ||||||
|  |                             class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container" | ||||||
|  |                         > | ||||||
|  |                             <ak-admin-status-card-workers> </ak-admin-status-card-workers> | ||||||
|  |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                     <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl"> |                     <div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl"> | ||||||
|                         <ak-recent-events pageSize="6"></ak-recent-events> |                         <ak-recent-events pageSize="6"></ak-recent-events> | ||||||
| @ -156,70 +201,4 @@ export class AdminOverviewPage extends AdminOverviewBase { | |||||||
|                 </div> |                 </div> | ||||||
|             </section>`; |             </section>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderCards() { |  | ||||||
|         const isEnterprise = this.hasEnterpriseLicense; |  | ||||||
|         const classes = { |  | ||||||
|             "card-container": true, |  | ||||||
|             "pf-l-grid__item": true, |  | ||||||
|             "pf-m-6-col": true, |  | ||||||
|             "pf-m-4-col-on-md": !isEnterprise, |  | ||||||
|             "pf-m-4-col-on-xl": !isEnterprise, |  | ||||||
|             "pf-m-3-col-on-md": isEnterprise, |  | ||||||
|             "pf-m-3-col-on-xl": isEnterprise, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         return html`<div class=${classMap(classes)}> |  | ||||||
|                 <ak-admin-status-system> </ak-admin-status-system> |  | ||||||
|             </div> |  | ||||||
|             <div class=${classMap(classes)}> |  | ||||||
|                 <ak-admin-status-version> </ak-admin-status-version> |  | ||||||
|             </div> |  | ||||||
|             <div class=${classMap(classes)}> |  | ||||||
|                 <ak-admin-status-card-workers> </ak-admin-status-card-workers> |  | ||||||
|             </div> |  | ||||||
|             ${isEnterprise |  | ||||||
|                 ? html` <div class=${classMap(classes)}> |  | ||||||
|                       <ak-admin-fips-status-system> </ak-admin-fips-status-system> |  | ||||||
|                   </div>` |  | ||||||
|                 : nothing} `; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     renderActions() { |  | ||||||
|         const release = `${versionFamily()}#fixed-in-${VERSION.replaceAll(".", "")}`; |  | ||||||
|  |  | ||||||
|         const quickActions: [string, string][] = [ |  | ||||||
|             [msg("Create a new application"), paramURL("/core/applications", { createForm: true })], |  | ||||||
|             [msg("Check the logs"), paramURL("/events/log")], |  | ||||||
|             [msg("Explore integrations"), "https://goauthentik.io/integrations/"], |  | ||||||
|             [msg("Manage users"), paramURL("/identity/users")], |  | ||||||
|             [msg("Check the release notes"), `https://goauthentik.io/docs/releases/${release}`], |  | ||||||
|         ]; |  | ||||||
|  |  | ||||||
|         const action = ([label, url]: [string, string]) => { |  | ||||||
|             const isExternal = url.startsWith("https://"); |  | ||||||
|             const ex = (truecase: Renderer, falsecase: Renderer) => |  | ||||||
|                 when(isExternal, truecase, falsecase); |  | ||||||
|  |  | ||||||
|             const content = html`${label}${ex( |  | ||||||
|                 () => html`<i class="fas fa-external-link-alt ak-external-link"></i>`, |  | ||||||
|                 () => nothing, |  | ||||||
|             )}`; |  | ||||||
|  |  | ||||||
|             return html`<li> |  | ||||||
|                 ${ex( |  | ||||||
|                     () => html`<a href="${url}" class="pf-u-mb-xl" target="_blank">${content}</a>`, |  | ||||||
|                     () => html`<a href="${url}" class="pf-u-mb-xl" )>${content}</a>`, |  | ||||||
|                 )} |  | ||||||
|             </li>`; |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         return html`${map(quickActions, action)}`; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|     interface HTMLElementTagNameMap { |  | ||||||
|         "ak-admin-overview": AdminOverviewPage; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,56 +0,0 @@ | |||||||
| import { |  | ||||||
|     AdminStatus, |  | ||||||
|     AdminStatusCard, |  | ||||||
| } from "@goauthentik/admin/admin-overview/cards/AdminStatusCard"; |  | ||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; |  | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; |  | ||||||
| import { TemplateResult, html } from "lit"; |  | ||||||
| import { customElement, state } from "lit/decorators.js"; |  | ||||||
|  |  | ||||||
| import { AdminApi, SystemInfo } from "@goauthentik/api"; |  | ||||||
|  |  | ||||||
| type StatusContent = { icon: string; message: TemplateResult }; |  | ||||||
|  |  | ||||||
| @customElement("ak-admin-fips-status-system") |  | ||||||
| export class FipsStatusCard extends AdminStatusCard<SystemInfo> { |  | ||||||
|     icon = "pf-icon pf-icon-server"; |  | ||||||
|  |  | ||||||
|     @state() |  | ||||||
|     statusSummary?: string; |  | ||||||
|  |  | ||||||
|     async getPrimaryValue(): Promise<SystemInfo> { |  | ||||||
|         return await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     setStatus(summary: string, content: StatusContent): Promise<AdminStatus> { |  | ||||||
|         this.statusSummary = summary; |  | ||||||
|         return Promise.resolve<AdminStatus>(content); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     getStatus(value: SystemInfo): Promise<AdminStatus> { |  | ||||||
|         return value.runtime.opensslFipsEnabled |  | ||||||
|             ? this.setStatus(msg("OK"), { |  | ||||||
|                   icon: "fa fa-check-circle pf-m-success", |  | ||||||
|                   message: html`${msg("FIPS compliance: passing")}`, |  | ||||||
|               }) |  | ||||||
|             : this.setStatus(msg("Unverified"), { |  | ||||||
|                   icon: "fa fa-info-circle pf-m-warning", |  | ||||||
|                   message: html`${msg("FIPS compliance: unverified")}`, |  | ||||||
|               }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     renderHeader(): TemplateResult { |  | ||||||
|         return html`${msg("FIPS Status")}`; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     renderValue(): TemplateResult { |  | ||||||
|         return html`${this.statusSummary}`; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|     interface HTMLElementTagNameMap { |  | ||||||
|         "ak-admin-fips-status-system": FipsStatusCard; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | |||||||
| export const ERROR_CLASS = "pf-m-danger"; | export const ERROR_CLASS = "pf-m-danger"; | ||||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||||
| export const CURRENT_CLASS = "pf-m-current"; | export const CURRENT_CLASS = "pf-m-current"; | ||||||
| export const VERSION = "2024.6.0"; | export const VERSION = "2024.4.2"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ import { PFSize } from "@goauthentik/common/enums.js"; | |||||||
| import { AKElement } from "@goauthentik/elements/Base"; | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
| import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; | ||||||
|  |  | ||||||
| import { Task, TaskStatus } from "@lit/task"; | import { Task, TaskStatus, initialState } from "@lit/task"; | ||||||
| import { css, html } from "lit"; | import { css, html } from "lit"; | ||||||
| import { property } from "lit/decorators.js"; | import { property } from "lit/decorators.js"; | ||||||
|  |  | ||||||
| @ -67,7 +67,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { | |||||||
|         this.onError = this.onError.bind(this); |         this.onError = this.onError.bind(this); | ||||||
|         this.onClick = this.onClick.bind(this); |         this.onClick = this.onClick.bind(this); | ||||||
|         this.actionTask = new Task(this, { |         this.actionTask = new Task(this, { | ||||||
|             task: () => this.callAction(), |             task: () => this.runCallAction(), | ||||||
|             args: () => [], |             args: () => [], | ||||||
|             autoRun: false, |             autoRun: false, | ||||||
|             onComplete: (r: unknown) => this.onSuccess(r), |             onComplete: (r: unknown) => this.onSuccess(r), | ||||||
| @ -77,7 +77,6 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { | |||||||
|  |  | ||||||
|     onComplete() { |     onComplete() { | ||||||
|         setTimeout(() => { |         setTimeout(() => { | ||||||
|             this.actionTask.status = TaskStatus.INITIAL; |  | ||||||
|             this.dispatchCustomEvent(`${this.eventPrefix}-reset`); |             this.dispatchCustomEvent(`${this.eventPrefix}-reset`); | ||||||
|             this.requestUpdate(); |             this.requestUpdate(); | ||||||
|         }, SPINNER_TIMEOUT); |         }, SPINNER_TIMEOUT); | ||||||
| @ -97,10 +96,12 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { | |||||||
|         this.onComplete(); |         this.onComplete(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     onClick() { |     async runCallAction() { | ||||||
|         if (this.actionTask.status !== TaskStatus.INITIAL) { |         await this.callAction(); | ||||||
|             return; |         return initialState; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     onClick() { | ||||||
|         this.dispatchCustomEvent(`${this.eventPrefix}-click`); |         this.dispatchCustomEvent(`${this.eventPrefix}-click`); | ||||||
|         this.actionTask.run(); |         this.actionTask.run(); | ||||||
|     } |     } | ||||||
| @ -113,7 +114,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { | |||||||
|         return [ |         return [ | ||||||
|             ...this.classList, |             ...this.classList, | ||||||
|             StatusMap.get(this.actionTask.status), |             StatusMap.get(this.actionTask.status), | ||||||
|             this.actionTask.status === TaskStatus.INITIAL ? "" : "working", |             this.actionTask.status === TaskStatus.PENDING ? "working" : "", | ||||||
|         ] |         ] | ||||||
|             .join(" ") |             .join(" ") | ||||||
|             .trim(); |             .trim(); | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								web/src/elements/password-match-indicator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web/src/elements/password-match-indicator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | import PasswordMatchIndicator from "./password-match-indicator.js"; | ||||||
|  |  | ||||||
|  | export { PasswordMatchIndicator }; | ||||||
|  |  | ||||||
|  | export default PasswordMatchIndicator; | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | import { html } from "lit"; | ||||||
|  |  | ||||||
|  | import "."; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     title: "Elements/Password Match Indicator", | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const Primary = () => | ||||||
|  |     html`<div style="background: #fff; padding: 4em"> | ||||||
|  |         <p>Type some text: <input id="primary-example" style="color:#000" /></p> | ||||||
|  |         <p style="margin-top:0.5em"> | ||||||
|  |             Type some other text: <input id="primary-example_repeat" style="color:#000" /> | ||||||
|  |             <ak-password-match-indicator | ||||||
|  |                 first="#primary-example" | ||||||
|  |                 second="#primary-example_repeat" | ||||||
|  |             ></ak-password-match-indicator> | ||||||
|  |         </p> | ||||||
|  |     </div>`; | ||||||
| @ -0,0 +1,94 @@ | |||||||
|  | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  |  | ||||||
|  | import { css, html } from "lit"; | ||||||
|  | import { customElement, property, state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
|  | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
|  | import findInput from "../password-strength-indicator/findInput.js"; | ||||||
|  |  | ||||||
|  | const ELEMENT = "ak-password-match-indicator"; | ||||||
|  |  | ||||||
|  | @customElement(ELEMENT) | ||||||
|  | export class PasswordMatchIndicator extends AKElement { | ||||||
|  |     static styles = [ | ||||||
|  |         PFBase, | ||||||
|  |         css` | ||||||
|  |             :host { | ||||||
|  |                 display: grid; | ||||||
|  |                 place-items: center center; | ||||||
|  |             } | ||||||
|  |         `, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A valid selector for the first input element to observe. Attaching this to anything other | ||||||
|  |      * than an HTMLInputElement will throw an exception. | ||||||
|  |      */ | ||||||
|  |     @property({ attribute: true }) | ||||||
|  |     first = ""; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A valid selector for the second input element to observe. Attaching this to anything other | ||||||
|  |      * than an HTMLInputElement will throw an exception. | ||||||
|  |      */ | ||||||
|  |     @property({ attribute: true }) | ||||||
|  |     second = ""; | ||||||
|  |  | ||||||
|  |     firstElement?: HTMLInputElement; | ||||||
|  |  | ||||||
|  |     secondElement?: HTMLInputElement; | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|  |     match = false; | ||||||
|  |  | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         this.checkPasswordMatch = this.checkPasswordMatch.bind(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     connectedCallback() { | ||||||
|  |         super.connectedCallback(); | ||||||
|  |         this.firstInput.addEventListener("keyup", this.checkPasswordMatch); | ||||||
|  |         this.secondInput.addEventListener("keyup", this.checkPasswordMatch); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     disconnectedCallback() { | ||||||
|  |         this.secondInput.removeEventListener("keyup", this.checkPasswordMatch); | ||||||
|  |         this.firstInput.removeEventListener("keyup", this.checkPasswordMatch); | ||||||
|  |         super.disconnectedCallback(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     checkPasswordMatch() { | ||||||
|  |         this.match = | ||||||
|  |             this.firstInput.value.length > 0 && | ||||||
|  |             this.secondInput.value.length > 0 && | ||||||
|  |             this.firstInput.value === this.secondInput.value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get firstInput() { | ||||||
|  |         if (this.firstElement) { | ||||||
|  |             return this.firstElement; | ||||||
|  |         } | ||||||
|  |         return (this.firstElement = findInput(this.getRootNode() as Element, ELEMENT, this.first)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get secondInput() { | ||||||
|  |         if (this.secondElement) { | ||||||
|  |             return this.secondElement; | ||||||
|  |         } | ||||||
|  |         return (this.secondElement = findInput( | ||||||
|  |             this.getRootNode() as Element, | ||||||
|  |             ELEMENT, | ||||||
|  |             this.second, | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render() { | ||||||
|  |         return this.match | ||||||
|  |             ? html`<i class="pf-icon pf-icon-ok pf-m-success"></i>` | ||||||
|  |             : html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i>`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default PasswordMatchIndicator; | ||||||
							
								
								
									
										18
									
								
								web/src/elements/password-strength-indicator/findInput.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/src/elements/password-strength-indicator/findInput.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | export function findInput(root: Element, tag: string, src: string) { | ||||||
|  |     const inputs = Array.from(root.querySelectorAll(src)); | ||||||
|  |     if (inputs.length === 0) { | ||||||
|  |         throw new Error(`${tag}: no element found for 'src' ${src}`); | ||||||
|  |     } | ||||||
|  |     if (inputs.length > 1) { | ||||||
|  |         throw new Error(`${tag}: more than one element found for 'src' ${src}`); | ||||||
|  |     } | ||||||
|  |     const input = inputs[0]; | ||||||
|  |     if (!(input instanceof HTMLInputElement)) { | ||||||
|  |         throw new Error( | ||||||
|  |             `${tag}: the 'src' element must be an <input> tag, found ${input.localName}`, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     return input; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default findInput; | ||||||
							
								
								
									
										5
									
								
								web/src/elements/password-strength-indicator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web/src/elements/password-strength-indicator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | import PasswordStrengthIndicator from "./password-strength-indicator.js"; | ||||||
|  |  | ||||||
|  | export { PasswordStrengthIndicator }; | ||||||
|  |  | ||||||
|  | export default PasswordStrengthIndicator; | ||||||
| @ -0,0 +1,13 @@ | |||||||
|  | import { html } from "lit"; | ||||||
|  |  | ||||||
|  | import "."; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     title: "Elements/Password Strength Indicator", | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const Primary = () => | ||||||
|  |     html`<div style="background: #fff; padding: 4em"> | ||||||
|  |         <p>Type some text: <input id="primary-example" style="color:#000" /></p> | ||||||
|  |         <ak-password-strength-indicator src="#primary-example"></ak-password-strength-indicator> | ||||||
|  |     </div>`; | ||||||
| @ -0,0 +1,91 @@ | |||||||
|  | import { AKElement } from "@goauthentik/elements/Base"; | ||||||
|  | import zxcvbn from "zxcvbn"; | ||||||
|  |  | ||||||
|  | import { css, html } from "lit"; | ||||||
|  | import { styleMap } from "lit-html/directives/style-map.js"; | ||||||
|  | import { customElement, property, state } from "lit/decorators.js"; | ||||||
|  |  | ||||||
|  | import findInput from "./findInput"; | ||||||
|  |  | ||||||
|  | const styles = css` | ||||||
|  |     .password-meter-wrap { | ||||||
|  |         margin-top: 5px; | ||||||
|  |         height: 0.5em; | ||||||
|  |         background-color: #ddd; | ||||||
|  |         border-radius: 0.25em; | ||||||
|  |  | ||||||
|  |         overflow: hidden; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .password-meter-bar { | ||||||
|  |         width: 0; | ||||||
|  |         height: 100%; | ||||||
|  |         transition: width 400ms ease-in; | ||||||
|  |     } | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | const LEVELS = [ | ||||||
|  |     ["20%", "#dd0000"], | ||||||
|  |     ["40%", "#ff5500"], | ||||||
|  |     ["60%", "#ffff00"], | ||||||
|  |     ["80%", "#a1a841"], | ||||||
|  |     ["100%", "#339933"], | ||||||
|  | ].map(([width, backgroundColor]) => ({ width, backgroundColor })); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A simple display of the password strength. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | const ELEMENT = "ak-password-strength-indicator"; | ||||||
|  |  | ||||||
|  | @customElement(ELEMENT) | ||||||
|  | export class PasswordStrengthIndicator extends AKElement { | ||||||
|  |     static styles = styles; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The input element to observe. Attaching this to anything other than an HTMLInputElement will | ||||||
|  |      * throw an exception. | ||||||
|  |      */ | ||||||
|  |     @property({ attribute: true }) | ||||||
|  |     src = ""; | ||||||
|  |  | ||||||
|  |     sourceInput?: HTMLInputElement; | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|  |     strength = LEVELS[0]; | ||||||
|  |  | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         this.checkPasswordStrength = this.checkPasswordStrength.bind(this); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     connectedCallback() { | ||||||
|  |         super.connectedCallback(); | ||||||
|  |         this.input.addEventListener("keyup", this.checkPasswordStrength); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     disconnectedCallback() { | ||||||
|  |         this.input.removeEventListener("keyup", this.checkPasswordStrength); | ||||||
|  |         super.disconnectedCallback(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     checkPasswordStrength() { | ||||||
|  |         const { score } = zxcvbn(this.input.value); | ||||||
|  |         this.strength = LEVELS[score]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     get input(): HTMLInputElement { | ||||||
|  |         if (this.sourceInput) { | ||||||
|  |             return this.sourceInput; | ||||||
|  |         } | ||||||
|  |         return (this.sourceInput = findInput(this.getRootNode() as Element, ELEMENT, this.src)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render() { | ||||||
|  |         return html` <div class="password-meter-wrap"> | ||||||
|  |             <div class="password-meter-bar" style=${styleMap(this.strength)}></div> | ||||||
|  |         </div>`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default PasswordStrengthIndicator; | ||||||
							
								
								
									
										108
									
								
								web/src/flow/stages/prompt/FieldRenderers.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								web/src/flow/stages/prompt/FieldRenderers.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | |||||||
|  | import { TemplateResult, html } from "lit"; | ||||||
|  |  | ||||||
|  | import "@patternfly/patternfly/components/Alert/alert.css"; | ||||||
|  | import "@patternfly/patternfly/components/Button/button.css"; | ||||||
|  | import "@patternfly/patternfly/components/Check/check.css"; | ||||||
|  | import "@patternfly/patternfly/components/Form/form.css"; | ||||||
|  | import "@patternfly/patternfly/components/FormControl/form-control.css"; | ||||||
|  | import "@patternfly/patternfly/components/Login/login.css"; | ||||||
|  | import "@patternfly/patternfly/components/Title/title.css"; | ||||||
|  | import "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
|  | import { PromptTypeEnum } from "@goauthentik/api"; | ||||||
|  | import type { StagePrompt } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | import promptRenderers from "./FieldRenderers"; | ||||||
|  | import { renderContinue, renderPromptHelpText, renderPromptInner } from "./helpers"; | ||||||
|  |  | ||||||
|  | // Storybook stories are meant to show not just that the objects work, but to document good | ||||||
|  | // practices around using them.  Because of their uniform signature, the renderers can easily | ||||||
|  | // be encapsulated into containers that show them at their most functional, even without | ||||||
|  | // building Shadow DOMs with which to do it.  This is 100% Light DOM work, and they still | ||||||
|  | // work well. | ||||||
|  |  | ||||||
|  | const baseRenderer = (prompt: TemplateResult) => | ||||||
|  |     html`<div style="background: #fff; padding: 4em; max-width: 24em;"> | ||||||
|  |         <style> | ||||||
|  |             input, | ||||||
|  |             textarea, | ||||||
|  |             select, | ||||||
|  |             button, | ||||||
|  |             .pf-c-form__helper-text:not(.pf-m-error), | ||||||
|  |             input + label.pf-c-check__label { | ||||||
|  |                 color: #000; | ||||||
|  |             } | ||||||
|  |             input[readonly], | ||||||
|  |             textarea[readonly] { | ||||||
|  |                 color: #fff; | ||||||
|  |             } | ||||||
|  |         </style> | ||||||
|  |         ${prompt} | ||||||
|  |     </div>`; | ||||||
|  |  | ||||||
|  | function renderer(kind: PromptTypeEnum, prompt: Partial<StagePrompt>) { | ||||||
|  |     const renderer = promptRenderers.get(kind); | ||||||
|  |     if (!renderer) { | ||||||
|  |         throw new Error(`A renderer of type ${kind} does not exist.`); | ||||||
|  |     } | ||||||
|  |     return baseRenderer(html`${renderer(prompt as StagePrompt)}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const textPrompt = { | ||||||
|  |     fieldKey: "test_text_field", | ||||||
|  |     placeholder: "This is the placeholder", | ||||||
|  |     required: false, | ||||||
|  |     initialValue: "initial value", | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const Text = () => renderer(PromptTypeEnum.Text, textPrompt); | ||||||
|  | export const TextArea = () => renderer(PromptTypeEnum.TextArea, textPrompt); | ||||||
|  | export const TextReadOnly = () => renderer(PromptTypeEnum.TextReadOnly, textPrompt); | ||||||
|  | export const TextAreaReadOnly = () => renderer(PromptTypeEnum.TextAreaReadOnly, textPrompt); | ||||||
|  | export const Username = () => renderer(PromptTypeEnum.Username, textPrompt); | ||||||
|  | export const Password = () => renderer(PromptTypeEnum.Password, textPrompt); | ||||||
|  |  | ||||||
|  | const emailPrompt = { ...textPrompt, initialValue: "example@example.fun" }; | ||||||
|  | export const Email = () => renderer(PromptTypeEnum.Email, emailPrompt); | ||||||
|  |  | ||||||
|  | const numberPrompt = { ...textPrompt, initialValue: "10" }; | ||||||
|  | export const Number = () => renderer(PromptTypeEnum.Number, numberPrompt); | ||||||
|  |  | ||||||
|  | const datePrompt = { ...textPrompt, initialValue: "2018-06-12T19:30" }; | ||||||
|  | export const Date = () => renderer(PromptTypeEnum.Date, datePrompt); | ||||||
|  | export const DateTime = () => renderer(PromptTypeEnum.DateTime, datePrompt); | ||||||
|  |  | ||||||
|  | const separatorPrompt = { placeholder: "😊" }; | ||||||
|  | export const Separator = () => renderer(PromptTypeEnum.Separator, separatorPrompt); | ||||||
|  |  | ||||||
|  | const staticPrompt = { initialValue: "😊" }; | ||||||
|  | export const Static = () => renderer(PromptTypeEnum.Static, staticPrompt); | ||||||
|  |  | ||||||
|  | const choicePrompt = { | ||||||
|  |     fieldKey: "test_text_field", | ||||||
|  |     placeholder: "This is the placeholder", | ||||||
|  |     required: false, | ||||||
|  |     initialValue: "first", | ||||||
|  |     choices: ["first", "second", "third"], | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const Dropdown = () => renderer(PromptTypeEnum.Dropdown, choicePrompt); | ||||||
|  | export const RadioButtonGroup = () => renderer(PromptTypeEnum.RadioButtonGroup, choicePrompt); | ||||||
|  |  | ||||||
|  | const checkPrompt = { ...textPrompt, label: "Favorite Subtext?", subText: "(Xena & Gabrielle)" }; | ||||||
|  | export const Checkbox = () => renderer(PromptTypeEnum.Checkbox, checkPrompt); | ||||||
|  |  | ||||||
|  | const localePrompt = { ...textPrompt, initialValue: "en" }; | ||||||
|  | export const Locale = () => renderer(PromptTypeEnum.AkLocale, localePrompt); | ||||||
|  |  | ||||||
|  | export const PromptFailure = () => | ||||||
|  |     baseRenderer(renderPromptInner({ type: null } as unknown as StagePrompt)); | ||||||
|  |  | ||||||
|  | export const HelpText = () => | ||||||
|  |     baseRenderer(renderPromptHelpText({ subText: "There is no subtext here." } as StagePrompt)); | ||||||
|  |  | ||||||
|  | export const Continue = () => baseRenderer(renderContinue()); | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |     title: "Flow Components/Field Renderers", | ||||||
|  | }; | ||||||
							
								
								
									
										271
									
								
								web/src/flow/stages/prompt/FieldRenderers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										271
									
								
								web/src/flow/stages/prompt/FieldRenderers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,271 @@ | |||||||
|  | import { rootInterface } from "@goauthentik/elements/Base"; | ||||||
|  | import { LOCALES } from "@goauthentik/elements/ak-locale-context/helpers"; | ||||||
|  | import "@goauthentik/elements/password-match-indicator"; | ||||||
|  | import "@goauthentik/elements/password-strength-indicator"; | ||||||
|  |  | ||||||
|  | import { msg } from "@lit/localize"; | ||||||
|  | import { TemplateResult, html } from "lit"; | ||||||
|  | import { unsafeHTML } from "lit/directives/unsafe-html.js"; | ||||||
|  |  | ||||||
|  | import { CapabilitiesEnum, PromptTypeEnum, StagePrompt } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | export function password(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |             type="password" | ||||||
|  |             name="${prompt.fieldKey}" | ||||||
|  |             placeholder="${prompt.placeholder}" | ||||||
|  |             autocomplete="new-password" | ||||||
|  |             class="pf-c-form-control" | ||||||
|  |             ?required=${prompt.required} | ||||||
|  |         /><ak-password-strength-indicator | ||||||
|  |             src='input[name="${prompt.fieldKey}"]' | ||||||
|  |         ></ak-password-strength-indicator>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const REPEAT = /_repeat/; | ||||||
|  |  | ||||||
|  | export function repeatPassword(prompt: StagePrompt) { | ||||||
|  |     const first = `input[name="${prompt.fieldKey}"]`; | ||||||
|  |     const second = `input[name="${prompt.fieldKey.replace(REPEAT, "")}"]`; | ||||||
|  |  | ||||||
|  |     return html` <div style="display:flex; flex-direction:row; gap: 0.5em; align-content: center"> | ||||||
|  |         <input | ||||||
|  |             style="flex:1 0" | ||||||
|  |             type="password" | ||||||
|  |             name="${prompt.fieldKey}" | ||||||
|  |             placeholder="${prompt.placeholder}" | ||||||
|  |             autocomplete="new-password" | ||||||
|  |             class="pf-c-form-control" | ||||||
|  |             ?required=${prompt.required} | ||||||
|  |         /><ak-password-match-indicator | ||||||
|  |             first="${first}" | ||||||
|  |             second="${second}" | ||||||
|  |         ></ak-password-match-indicator> | ||||||
|  |     </div>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderPassword(prompt: StagePrompt) { | ||||||
|  |     return REPEAT.test(prompt.fieldKey) ? repeatPassword(prompt) : password(prompt); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderText(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="text" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         autocomplete="off" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderTextArea(prompt: StagePrompt) { | ||||||
|  |     return html`<textarea | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         autocomplete="off" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |     > | ||||||
|  | ${prompt.initialValue}</textarea | ||||||
|  |     >`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderTextReadOnly(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="text" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?readonly=${true} | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderTextAreaReadOnly(prompt: StagePrompt) { | ||||||
|  |     return html`<textarea | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         readonly | ||||||
|  |     > | ||||||
|  | ${prompt.initialValue}</textarea | ||||||
|  |     >`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderUsername(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="text" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         autocomplete="username" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderEmail(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="email" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderNumber(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="number" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderDate(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="date" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderDateTime(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="datetime" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderFile(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="file" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         placeholder="${prompt.placeholder}" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderSeparator(prompt: StagePrompt) { | ||||||
|  |     return html`<ak-divider>${prompt.placeholder}</ak-divider>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderHidden(prompt: StagePrompt) { | ||||||
|  |     return html`<input | ||||||
|  |         type="hidden" | ||||||
|  |         name="${prompt.fieldKey}" | ||||||
|  |         value="${prompt.initialValue}" | ||||||
|  |         class="pf-c-form-control" | ||||||
|  |         ?required=${prompt.required} | ||||||
|  |     />`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderStatic(prompt: StagePrompt) { | ||||||
|  |     return html`<p>${unsafeHTML(prompt.initialValue)}</p>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderDropdown(prompt: StagePrompt) { | ||||||
|  |     return html`<select class="pf-c-form-control" name="${prompt.fieldKey}"> | ||||||
|  |         ${prompt.choices?.map((choice) => { | ||||||
|  |             return html`<option value="${choice}" ?selected=${prompt.initialValue === choice}> | ||||||
|  |                 ${choice} | ||||||
|  |             </option>`; | ||||||
|  |         })} | ||||||
|  |     </select>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderRadioButtonGroup(prompt: StagePrompt) { | ||||||
|  |     return html`${(prompt.choices || []).map((choice) => { | ||||||
|  |         const id = `${prompt.fieldKey}-${choice}`; | ||||||
|  |         return html`<div class="pf-c-check"> | ||||||
|  |             <input | ||||||
|  |                 type="radio" | ||||||
|  |                 class="pf-c-check__input" | ||||||
|  |                 name="${prompt.fieldKey}" | ||||||
|  |                 id="${id}" | ||||||
|  |                 ?checked="${prompt.initialValue === choice}" | ||||||
|  |                 ?required="${prompt.required}" | ||||||
|  |                 value="${choice}" | ||||||
|  |             /> | ||||||
|  |             <label class="pf-c-check__label" for=${id}>${choice}</label> | ||||||
|  |         </div> `; | ||||||
|  |     })}`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderCheckbox(prompt: StagePrompt) { | ||||||
|  |     return html`<div class="pf-c-check"> | ||||||
|  |         <input | ||||||
|  |             type="checkbox" | ||||||
|  |             class="pf-c-check__input" | ||||||
|  |             id="${prompt.fieldKey}" | ||||||
|  |             name="${prompt.fieldKey}" | ||||||
|  |             ?checked=${prompt.initialValue !== ""} | ||||||
|  |             ?required=${prompt.required} | ||||||
|  |         /> | ||||||
|  |         <label class="pf-c-check__label" for="${prompt.fieldKey}">${prompt.label}</label> | ||||||
|  |         ${prompt.required | ||||||
|  |             ? html`<p class="pf-c-form__helper-text">${msg("Required.")}</p>` | ||||||
|  |             : html``} | ||||||
|  |         <p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p> | ||||||
|  |     </div>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderAkLocale(prompt: StagePrompt) { | ||||||
|  |     // TODO: External reference. | ||||||
|  |     const inDebug = rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanDebug); | ||||||
|  |     const locales = inDebug ? LOCALES : LOCALES.filter((locale) => locale.code !== "debug"); | ||||||
|  |  | ||||||
|  |     const options = locales.map( | ||||||
|  |         (locale) => | ||||||
|  |             html`<option value=${locale.code} ?selected=${locale.code === prompt.initialValue}> | ||||||
|  |                 ${locale.code.toUpperCase()} - ${locale.label()} | ||||||
|  |             </option> `, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return html`<select class="pf-c-form-control" name="${prompt.fieldKey}"> | ||||||
|  |         <option value="" ?selected=${prompt.initialValue === ""}> | ||||||
|  |             ${msg("Auto-detect (based on your browser)")} | ||||||
|  |         </option> | ||||||
|  |         ${options} | ||||||
|  |     </select>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Renderer = (prompt: StagePrompt) => TemplateResult; | ||||||
|  |  | ||||||
|  | export const promptRenderers = new Map<PromptTypeEnum, Renderer>([ | ||||||
|  |     [PromptTypeEnum.Text, renderText], | ||||||
|  |     [PromptTypeEnum.TextArea, renderTextArea], | ||||||
|  |     [PromptTypeEnum.TextReadOnly, renderTextReadOnly], | ||||||
|  |     [PromptTypeEnum.TextAreaReadOnly, renderTextAreaReadOnly], | ||||||
|  |     [PromptTypeEnum.Username, renderUsername], | ||||||
|  |     [PromptTypeEnum.Email, renderEmail], | ||||||
|  |     [PromptTypeEnum.Password, renderPassword], | ||||||
|  |     [PromptTypeEnum.Number, renderNumber], | ||||||
|  |     [PromptTypeEnum.Date, renderDate], | ||||||
|  |     [PromptTypeEnum.DateTime, renderDateTime], | ||||||
|  |     [PromptTypeEnum.File, renderFile], | ||||||
|  |     [PromptTypeEnum.Separator, renderSeparator], | ||||||
|  |     [PromptTypeEnum.Hidden, renderHidden], | ||||||
|  |     [PromptTypeEnum.Static, renderStatic], | ||||||
|  |     [PromptTypeEnum.Dropdown, renderDropdown], | ||||||
|  |     [PromptTypeEnum.RadioButtonGroup, renderRadioButtonGroup], | ||||||
|  |     [PromptTypeEnum.Checkbox, renderCheckbox], | ||||||
|  |     [PromptTypeEnum.AkLocale, renderAkLocale], | ||||||
|  | ]); | ||||||
|  |  | ||||||
|  | export default promptRenderers; | ||||||
| @ -1,17 +1,12 @@ | |||||||
| import "@goauthentik/elements/Divider"; | import "@goauthentik/elements/Divider"; | ||||||
| import "@goauthentik/elements/EmptyState"; | import "@goauthentik/elements/EmptyState"; | ||||||
| import { | import { WithCapabilitiesConfig } from "@goauthentik/elements/Interface/capabilitiesProvider"; | ||||||
|     CapabilitiesEnum, |  | ||||||
|     WithCapabilitiesConfig, |  | ||||||
| } from "@goauthentik/elements/Interface/capabilitiesProvider"; |  | ||||||
| import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions"; |  | ||||||
| import "@goauthentik/elements/forms/FormElement"; | import "@goauthentik/elements/forms/FormElement"; | ||||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||||
|  |  | ||||||
| import { msg } from "@lit/localize"; | import { msg } from "@lit/localize"; | ||||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | import { CSSResult, TemplateResult, css, html } from "lit"; | ||||||
| import { customElement } from "lit/decorators.js"; | import { customElement } from "lit/decorators.js"; | ||||||
| import { unsafeHTML } from "lit/directives/unsafe-html.js"; |  | ||||||
|  |  | ||||||
| import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; | import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; | ||||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||||
| @ -29,6 +24,14 @@ import { | |||||||
|     StagePrompt, |     StagePrompt, | ||||||
| } from "@goauthentik/api"; | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | import { renderCheckbox } from "./FieldRenderers"; | ||||||
|  | import { | ||||||
|  |     renderContinue, | ||||||
|  |     renderPromptHelpText, | ||||||
|  |     renderPromptInner, | ||||||
|  |     shouldRenderInWrapper, | ||||||
|  | } from "./helpers"; | ||||||
|  |  | ||||||
| @customElement("ak-stage-prompt") | @customElement("ak-stage-prompt") | ||||||
| export class PromptStage extends WithCapabilitiesConfig( | export class PromptStage extends WithCapabilitiesConfig( | ||||||
|     BaseStage<PromptChallenge, PromptChallengeResponseRequest>, |     BaseStage<PromptChallenge, PromptChallengeResponseRequest>, | ||||||
| @ -53,232 +56,35 @@ export class PromptStage extends WithCapabilitiesConfig( | |||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderPromptInner(prompt: StagePrompt): TemplateResult { |     /* TODO: Legacy: None of these refer to the `this` field. Static fields are a code smell. */ | ||||||
|         switch (prompt.type) { |  | ||||||
|             case PromptTypeEnum.Text: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="text" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     autocomplete="off" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.TextArea: |  | ||||||
|                 return html`<textarea |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     autocomplete="off" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                 > |  | ||||||
| ${prompt.initialValue}</textarea |  | ||||||
|                 >`; |  | ||||||
|             case PromptTypeEnum.TextReadOnly: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="text" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?readonly=${true} |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.TextAreaReadOnly: |  | ||||||
|                 return html`<textarea |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     readonly |  | ||||||
|                 > |  | ||||||
| ${prompt.initialValue}</textarea |  | ||||||
|                 >`; |  | ||||||
|             case PromptTypeEnum.Username: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="text" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     autocomplete="username" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.Email: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="email" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.Password: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="password" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     autocomplete="new-password" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.Number: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="number" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.Date: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="date" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.DateTime: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="datetime" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.File: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="file" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     placeholder="${prompt.placeholder}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.Separator: |  | ||||||
|                 return html`<ak-divider>${prompt.placeholder}</ak-divider>`; |  | ||||||
|             case PromptTypeEnum.Hidden: |  | ||||||
|                 return html`<input |  | ||||||
|                     type="hidden" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     value="${prompt.initialValue}" |  | ||||||
|                     class="pf-c-form-control" |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                 />`; |  | ||||||
|             case PromptTypeEnum.Static: |  | ||||||
|                 return html`<p>${unsafeHTML(prompt.initialValue)}</p>`; |  | ||||||
|             case PromptTypeEnum.Dropdown: |  | ||||||
|                 return html`<select class="pf-c-form-control" name="${prompt.fieldKey}"> |  | ||||||
|                     ${prompt.choices?.map((choice) => { |  | ||||||
|                         return html`<option |  | ||||||
|                             value="${choice}" |  | ||||||
|                             ?selected=${prompt.initialValue === choice} |  | ||||||
|                         > |  | ||||||
|                             ${choice} |  | ||||||
|                         </option>`; |  | ||||||
|                     })} |  | ||||||
|                 </select>`; |  | ||||||
|             case PromptTypeEnum.RadioButtonGroup: |  | ||||||
|                 return html`${(prompt.choices || []).map((choice) => { |  | ||||||
|                     const id = `${prompt.fieldKey}-${choice}`; |  | ||||||
|                     return html`<div class="pf-c-check"> |  | ||||||
|                         <input |  | ||||||
|                             type="radio" |  | ||||||
|                             class="pf-c-check__input" |  | ||||||
|                             name="${prompt.fieldKey}" |  | ||||||
|                             id="${id}" |  | ||||||
|                             ?checked="${prompt.initialValue === choice}" |  | ||||||
|                             ?required="${prompt.required}" |  | ||||||
|                             value="${choice}" |  | ||||||
|                         /> |  | ||||||
|                         <label class="pf-c-check__label" for=${id}>${choice}</label> |  | ||||||
|                     </div> `; |  | ||||||
|                 })}`; |  | ||||||
|             case PromptTypeEnum.AkLocale: { |  | ||||||
|                 const locales = this.can(CapabilitiesEnum.CanDebug) |  | ||||||
|                     ? LOCALES |  | ||||||
|                     : LOCALES.filter((locale) => locale.code !== "debug"); |  | ||||||
|                 const options = locales.map( |  | ||||||
|                     (locale) => |  | ||||||
|                         html`<option |  | ||||||
|                             value=${locale.code} |  | ||||||
|                             ?selected=${locale.code === prompt.initialValue} |  | ||||||
|                         > |  | ||||||
|                             ${locale.code.toUpperCase()} - ${locale.label()} |  | ||||||
|                         </option> `, |  | ||||||
|                 ); |  | ||||||
|  |  | ||||||
|                 return html`<select class="pf-c-form-control" name="${prompt.fieldKey}"> |     renderPromptInner(prompt: StagePrompt) { | ||||||
|                     <option value="" ?selected=${prompt.initialValue === ""}> |         return renderPromptInner(prompt); | ||||||
|                         ${msg("Auto-detect (based on your browser)")} |  | ||||||
|                     </option> |  | ||||||
|                     ${options} |  | ||||||
|                 </select>`; |  | ||||||
|     } |     } | ||||||
|             default: |     renderPromptHelpText(prompt: StagePrompt) { | ||||||
|                 return html`<p>invalid type '${prompt.type}'</p>`; |         return renderPromptHelpText(prompt); | ||||||
|     } |     } | ||||||
|     } |     shouldRenderInWrapper(prompt: StagePrompt) { | ||||||
|  |         return shouldRenderInWrapper(prompt); | ||||||
|     renderPromptHelpText(prompt: StagePrompt): TemplateResult { |  | ||||||
|         if (prompt.subText === "") { |  | ||||||
|             return html``; |  | ||||||
|         } |  | ||||||
|         return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     shouldRenderInWrapper(prompt: StagePrompt): boolean { |  | ||||||
|         // Special types that aren't rendered in a wrapper |  | ||||||
|         if ( |  | ||||||
|             prompt.type === PromptTypeEnum.Static || |  | ||||||
|             prompt.type === PromptTypeEnum.Hidden || |  | ||||||
|             prompt.type === PromptTypeEnum.Separator |  | ||||||
|         ) { |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
|         return true; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderField(prompt: StagePrompt): TemplateResult { |     renderField(prompt: StagePrompt): TemplateResult { | ||||||
|         // Checkbox is rendered differently |         // Checkbox has a slightly different layout, so it must be intercepted early. | ||||||
|         if (prompt.type === PromptTypeEnum.Checkbox) { |         if (prompt.type === PromptTypeEnum.Checkbox) { | ||||||
|             return html`<div class="pf-c-check"> |             return renderCheckbox(prompt); | ||||||
|                 <input |  | ||||||
|                     type="checkbox" |  | ||||||
|                     class="pf-c-check__input" |  | ||||||
|                     id="${prompt.fieldKey}" |  | ||||||
|                     name="${prompt.fieldKey}" |  | ||||||
|                     ?checked=${prompt.initialValue !== ""} |  | ||||||
|                     ?required=${prompt.required} |  | ||||||
|                 /> |  | ||||||
|                 <label class="pf-c-check__label" for="${prompt.fieldKey}">${prompt.label}</label> |  | ||||||
|                 ${prompt.required |  | ||||||
|                     ? html`<p class="pf-c-form__helper-text">${msg("Required.")}</p>` |  | ||||||
|                     : html``} |  | ||||||
|                 <p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p> |  | ||||||
|             </div>`; |  | ||||||
|         } |         } | ||||||
|         if (this.shouldRenderInWrapper(prompt)) { |  | ||||||
|  |         if (shouldRenderInWrapper(prompt)) { | ||||||
|             return html`<ak-form-element |             return html`<ak-form-element | ||||||
|                 label="${prompt.label}" |                 label="${prompt.label}" | ||||||
|                 ?required="${prompt.required}" |                 ?required="${prompt.required}" | ||||||
|                 class="pf-c-form__group" |                 class="pf-c-form__group" | ||||||
|                 .errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]} |                 .errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]} | ||||||
|             > |             > | ||||||
|                 ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)} |                 ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)} | ||||||
|             </ak-form-element>`; |             </ak-form-element>`; | ||||||
|         } |         } | ||||||
|         return html` ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}`; |         return html` ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}`; | ||||||
|     } |  | ||||||
|  |  | ||||||
|     renderContinue(): TemplateResult { |  | ||||||
|         return html` <div class="pf-c-form__group pf-m-action"> |  | ||||||
|             <button type="submit" class="pf-c-button pf-m-primary pf-m-block"> |  | ||||||
|                 ${msg("Continue")} |  | ||||||
|             </button> |  | ||||||
|         </div>`; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     render(): TemplateResult { | ||||||
| @ -286,6 +92,7 @@ ${prompt.initialValue}</textarea | |||||||
|             return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}> |             return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}> | ||||||
|             </ak-empty-state>`; |             </ak-empty-state>`; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return html`<header class="pf-c-login__main-header"> |         return html`<header class="pf-c-login__main-header"> | ||||||
|                 <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1> |                 <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1> | ||||||
|             </header> |             </header> | ||||||
| @ -304,7 +111,7 @@ ${prompt.initialValue}</textarea | |||||||
|                               this.challenge?.responseErrors?.non_field_errors || [], |                               this.challenge?.responseErrors?.non_field_errors || [], | ||||||
|                           ) |                           ) | ||||||
|                         : html``} |                         : html``} | ||||||
|                     ${this.renderContinue()} |                     ${renderContinue()} | ||||||
|                 </form> |                 </form> | ||||||
|             </div> |             </div> | ||||||
|             <footer class="pf-c-login__main-footer"> |             <footer class="pf-c-login__main-footer"> | ||||||
|  | |||||||
							
								
								
									
										37
									
								
								web/src/flow/stages/prompt/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/flow/stages/prompt/helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | import { msg } from "@lit/localize"; | ||||||
|  | import { html } from "lit"; | ||||||
|  | import { unsafeHTML } from "lit/directives/unsafe-html.js"; | ||||||
|  |  | ||||||
|  | import { PromptTypeEnum, StagePrompt } from "@goauthentik/api"; | ||||||
|  |  | ||||||
|  | import promptRenderers from "./FieldRenderers"; | ||||||
|  |  | ||||||
|  | export function renderPromptInner(prompt: StagePrompt) { | ||||||
|  |     const renderer = promptRenderers.get(prompt.type); | ||||||
|  |     if (!renderer) { | ||||||
|  |         return html`<p>invalid type '${JSON.stringify(prompt.type, null, 2)}'</p>`; | ||||||
|  |     } | ||||||
|  |     return renderer(prompt); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderPromptHelpText(prompt: StagePrompt) { | ||||||
|  |     if (prompt.subText === "") { | ||||||
|  |         return html``; | ||||||
|  |     } | ||||||
|  |     return html`<p class="pf-c-form__helper-text">${unsafeHTML(prompt.subText)}</p>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function shouldRenderInWrapper(prompt: StagePrompt) { | ||||||
|  |     // Special types that aren't rendered in a wrapper | ||||||
|  |     const specialTypes = [PromptTypeEnum.Static, PromptTypeEnum.Hidden, PromptTypeEnum.Separator]; | ||||||
|  |     const special = specialTypes.find((s) => s === prompt.type); | ||||||
|  |     return !special; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function renderContinue() { | ||||||
|  |     return html` <div class="pf-c-form__group pf-m-action"> | ||||||
|  |         <button type="submit" class="pf-c-button pf-m-primary pf-m-block"> | ||||||
|  |             ${msg("Continue")} | ||||||
|  |         </button> | ||||||
|  |     </div>`; | ||||||
|  | } | ||||||
| @ -6677,18 +6677,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -6943,18 +6943,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -6594,18 +6594,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -8789,18 +8789,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | |||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
|   <target>Utilisateur(s) du fournisseur SCIM</target> |   <target>Utilisateur(s) du fournisseur SCIM</target> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -8523,18 +8523,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -8367,18 +8367,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -8793,18 +8793,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz | |||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
|   <target>Użytkowni(k/cy) SCIM</target> |   <target>Użytkowni(k/cy) SCIM</target> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -8638,16 +8638,4 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> |  | ||||||
| </body></file></xliff> | </body></file></xliff> | ||||||
|  | |||||||
| @ -6587,18 +6587,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -5509,18 +5509,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> |  | ||||||
| </body> | </body> | ||||||
| </file> | </file> | ||||||
| </xliff> | </xliff> | ||||||
|  | |||||||
| @ -8791,18 +8791,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
|   <target>SCIM 用户</target> |   <target>SCIM 用户</target> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -6635,18 +6635,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
| @ -8484,18 +8484,6 @@ Bindings to groups/users are checked against the user of the event.</source> | |||||||
| </trans-unit> | </trans-unit> | ||||||
| <trans-unit id="s146769fb55f1ee50"> | <trans-unit id="s146769fb55f1ee50"> | ||||||
|   <source>SCIM User(s)</source> |   <source>SCIM User(s)</source> | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s35f47dbd321aaf15"> |  | ||||||
|   <source>FIPS compliance: passing</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="sc94578030c702562"> |  | ||||||
|   <source>Unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s16749cce7c4c1589"> |  | ||||||
|   <source>FIPS compliance: unverified</source> |  | ||||||
| </trans-unit> |  | ||||||
| <trans-unit id="s0b2ad58c3deaa8dd"> |  | ||||||
|   <source>FIPS Status</source> |  | ||||||
| </trans-unit> | </trans-unit> | ||||||
|     </body> |     </body> | ||||||
|   </file> |   </file> | ||||||
|  | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										54
									
								
								website/docs/releases/2024/v2024.next.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								website/docs/releases/2024/v2024.next.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | --- | ||||||
|  | title: Release 2024.next | ||||||
|  | slug: "/releases/2024.next" | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | :::::note | ||||||
|  | 2024.next has not been released yet! We're publishing these release notes as a preview of what's to come, and for our awesome beta testers trying out release candidates. | ||||||
|  |  | ||||||
|  | To try out the release candidate, replace your Docker image tag with the latest release candidate number, such as 2024.next.0-rc1. You can find the latest one in [the latest releases on GitHub](https://github.com/goauthentik/authentik/releases). If you don't find any, it means we haven't released one yet. | ||||||
|  | ::::: | ||||||
|  |  | ||||||
|  | ## Breaking changes | ||||||
|  |  | ||||||
|  | ### PostgreSQL minimum supported version upgrade | ||||||
|  |  | ||||||
|  | authentik now requires PostgreSQL version 14 or later. We recommend upgrading to the latest version if you are running an older version. | ||||||
|  |  | ||||||
|  | The provided Helm chart defaults to PostgreSQL 15. If you are using the Helm chart with the default values, no action is required. | ||||||
|  |  | ||||||
|  | The provided Compose file was updated with PostgreSQL 16. You can follow the procedure [here](../../troubleshooting/postgres/upgrade_docker.md) to upgrade. | ||||||
|  |  | ||||||
|  | ## New features | ||||||
|  |  | ||||||
|  | ## Upgrading | ||||||
|  |  | ||||||
|  | authentik now requires PostgreSQL version 14 or later. We recommend upgrading to the latest version if needed. Follow the instructions [here](../../troubleshooting/postgres/upgrade_docker.md) if you need to upgrade PostgreSQL with docker-compose. | ||||||
|  |  | ||||||
|  | ### Docker Compose | ||||||
|  |  | ||||||
|  | To upgrade, download the new Compose file and update the Docker stack with the new version, using these commands: | ||||||
|  |  | ||||||
|  | ```shell | ||||||
|  | wget -O docker-compose.yml https://goauthentik.io/version/2024.next/docker-compose.yml | ||||||
|  | docker compose up -d | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The `-O` flag retains the downloaded file's name, overwriting any existing local file with the same name. | ||||||
|  |  | ||||||
|  | ### Kubernetes | ||||||
|  |  | ||||||
|  | Upgrade the Helm Chart to the new version, using the following commands: | ||||||
|  |  | ||||||
|  | ```shell | ||||||
|  | helm repo update | ||||||
|  | helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.next | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Minor changes/fixes | ||||||
|  |  | ||||||
|  | <!-- _Insert the output of `make gen-changelog` here_ --> | ||||||
|  |  | ||||||
|  | ## API Changes | ||||||
|  |  | ||||||
|  | <!-- _Insert output of `make gen-diff` here_ --> | ||||||
| @ -409,17 +409,16 @@ const docsSidebar = { | |||||||
|                 type: "generated-index", |                 type: "generated-index", | ||||||
|                 title: "Releases", |                 title: "Releases", | ||||||
|                 slug: "releases", |                 slug: "releases", | ||||||
|                 description: "Release Notes for recent authentik versions", |                 description: "Release notes for recent authentik versions", | ||||||
|             }, |             }, | ||||||
|             items: [ |             items: [ | ||||||
|                 "releases/2024/v2024.6", |  | ||||||
|                 "releases/2024/v2024.4", |                 "releases/2024/v2024.4", | ||||||
|                 "releases/2024/v2024.2", |                 "releases/2024/v2024.2", | ||||||
|  |                 "releases/2023/v2023.10", | ||||||
|                 { |                 { | ||||||
|                     type: "category", |                     type: "category", | ||||||
|                     label: "Previous versions", |                     label: "Previous versions", | ||||||
|                     items: [ |                     items: [ | ||||||
|                         "releases/2023/v2023.10", |  | ||||||
|                         "releases/2023/v2023.8", |                         "releases/2023/v2023.8", | ||||||
|                         "releases/2023/v2023.6", |                         "releases/2023/v2023.6", | ||||||
|                         "releases/2023/v2023.5", |                         "releases/2023/v2023.5", | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	