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] | ||||
| current_version = 2024.6.0-rc1 | ||||
| current_version = 2024.4.2 | ||||
| tag = 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*))? | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|  | ||||
| from os import environ | ||||
|  | ||||
| __version__ = "2024.6.0" | ||||
| __version__ = "2024.4.2" | ||||
| 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.core.api.utils import PassiveSerializer | ||||
| from authentik.enterprise.license import LicenseKey | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.utils.reflection import get_env | ||||
| from authentik.outposts.apps import MANAGED_OUTPOST | ||||
| @ -33,7 +32,7 @@ class RuntimeDict(TypedDict): | ||||
|     platform: str | ||||
|     uname: str | ||||
|     openssl_version: str | ||||
|     openssl_fips_enabled: bool | None | ||||
|     openssl_fips_mode: bool | ||||
|     authentik_version: str | ||||
|  | ||||
|  | ||||
| @ -72,9 +71,7 @@ class SystemInfoSerializer(PassiveSerializer): | ||||
|             "architecture": platform.machine(), | ||||
|             "authentik_version": get_full_version(), | ||||
|             "environment": get_env(), | ||||
|             "openssl_fips_enabled": ( | ||||
|                 backend._fips_enabled if LicenseKey.get_total().is_valid() else None | ||||
|             ), | ||||
|             "openssl_fips_enabled": backend._fips_enabled, | ||||
|             "openssl_version": OPENSSL_VERSION, | ||||
|             "platform": platform.platform(), | ||||
|             "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.users import UserGroupSerializer | ||||
| from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup | ||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin | ||||
|  | ||||
|  | ||||
| class GoogleWorkspaceProviderGroupSerializer(ModelSerializer): | ||||
| @ -31,7 +30,6 @@ class GoogleWorkspaceProviderGroupSerializer(ModelSerializer): | ||||
|  | ||||
| class GoogleWorkspaceProviderGroupViewSet( | ||||
|     mixins.CreateModelMixin, | ||||
|     OutgoingSyncConnectionCreateMixin, | ||||
|     mixins.RetrieveModelMixin, | ||||
|     mixins.DestroyModelMixin, | ||||
|     UsedByMixin, | ||||
|  | ||||
| @ -7,7 +7,6 @@ from rest_framework.viewsets import GenericViewSet | ||||
| from authentik.core.api.groups import GroupMemberSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser | ||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin | ||||
|  | ||||
|  | ||||
| class GoogleWorkspaceProviderUserSerializer(ModelSerializer): | ||||
| @ -31,7 +30,6 @@ class GoogleWorkspaceProviderUserSerializer(ModelSerializer): | ||||
|  | ||||
| class GoogleWorkspaceProviderUserViewSet( | ||||
|     mixins.CreateModelMixin, | ||||
|     OutgoingSyncConnectionCreateMixin, | ||||
|     mixins.RetrieveModelMixin, | ||||
|     mixins.DestroyModelMixin, | ||||
|     UsedByMixin, | ||||
|  | ||||
| @ -214,7 +214,3 @@ class GoogleWorkspaceGroupClient( | ||||
|             google_id=google_id, | ||||
|             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, | ||||
|             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): | ||||
|     """Sync users from authentik into Google Workspace.""" | ||||
|  | ||||
| @ -111,16 +59,15 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider): | ||||
|     ) | ||||
|  | ||||
|     def client_for_model( | ||||
|         self, | ||||
|         model: type[User | Group | GoogleWorkspaceProviderUser | GoogleWorkspaceProviderGroup], | ||||
|         self, model: type[User | Group] | ||||
|     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: | ||||
|         if issubclass(model, User | GoogleWorkspaceProviderUser): | ||||
|         if issubclass(model, User): | ||||
|             from authentik.enterprise.providers.google_workspace.clients.users import ( | ||||
|                 GoogleWorkspaceUserClient, | ||||
|             ) | ||||
|  | ||||
|             return GoogleWorkspaceUserClient(self) | ||||
|         if issubclass(model, Group | GoogleWorkspaceProviderGroup): | ||||
|         if issubclass(model, Group): | ||||
|             from authentik.enterprise.providers.google_workspace.clients.groups import ( | ||||
|                 GoogleWorkspaceGroupClient, | ||||
|             ) | ||||
| @ -197,3 +144,55 @@ class GoogleWorkspaceProviderMapping(PropertyMapping): | ||||
|     class Meta: | ||||
|         verbose_name = _("Google Workspace Provider Mapping") | ||||
|         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.users import UserGroupSerializer | ||||
| from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup | ||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin | ||||
|  | ||||
|  | ||||
| class MicrosoftEntraProviderGroupSerializer(ModelSerializer): | ||||
| @ -31,7 +30,6 @@ class MicrosoftEntraProviderGroupSerializer(ModelSerializer): | ||||
|  | ||||
| class MicrosoftEntraProviderGroupViewSet( | ||||
|     mixins.CreateModelMixin, | ||||
|     OutgoingSyncConnectionCreateMixin, | ||||
|     mixins.RetrieveModelMixin, | ||||
|     mixins.DestroyModelMixin, | ||||
|     UsedByMixin, | ||||
|  | ||||
| @ -7,7 +7,6 @@ from rest_framework.viewsets import GenericViewSet | ||||
| from authentik.core.api.groups import GroupMemberSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser | ||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin | ||||
|  | ||||
|  | ||||
| class MicrosoftEntraProviderUserSerializer(ModelSerializer): | ||||
| @ -30,7 +29,6 @@ class MicrosoftEntraProviderUserSerializer(ModelSerializer): | ||||
|  | ||||
|  | ||||
| class MicrosoftEntraProviderUserViewSet( | ||||
|     OutgoingSyncConnectionCreateMixin, | ||||
|     mixins.CreateModelMixin, | ||||
|     mixins.RetrieveModelMixin, | ||||
|     mixins.DestroyModelMixin, | ||||
|  | ||||
| @ -226,7 +226,3 @@ class MicrosoftEntraGroupClient( | ||||
|             microsoft_id=group.id, | ||||
|             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() | ||||
|         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): | ||||
|         """Create user from scratch and create a connection object""" | ||||
|         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)) | ||||
|             except ObjectExistsSyncException: | ||||
|                 # 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 = ( | ||||
|                     UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( | ||||
|                         query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( | ||||
|                             filter=f"mail eq '{microsoft_user.mail}'", | ||||
|                             select=self.get_select_fields(), | ||||
|                         ), | ||||
|                         query_parameters=query_params, | ||||
|                     ) | ||||
|                 ) | ||||
|                 user_data = self._request(self.client.users.get(request_configuration)) | ||||
| @ -119,6 +99,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv | ||||
|             except TransientSyncException as exc: | ||||
|                 raise exc | ||||
|             else: | ||||
|                 print(self.entity_as_dict(response)) | ||||
|                 return MicrosoftEntraProviderUser.objects.create( | ||||
|                     provider=self.provider, | ||||
|                     user=user, | ||||
| @ -139,12 +120,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv | ||||
|  | ||||
|     def discover(self): | ||||
|         """Iterate through all users and connect them with authentik users if possible""" | ||||
|         request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration( | ||||
|             query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters( | ||||
|                 select=self.get_select_fields(), | ||||
|             ), | ||||
|         ) | ||||
|         users = self._request(self.client.users.get(request_configuration)) | ||||
|         users = self._request(self.client.users.get()) | ||||
|         next_link = True | ||||
|         while next_link: | ||||
|             for user in users.value: | ||||
| @ -165,14 +141,3 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv | ||||
|             microsoft_id=user.id, | ||||
|             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 | ||||
|  | ||||
|  | ||||
| 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): | ||||
|     """Sync users from authentik into Microsoft Entra.""" | ||||
|  | ||||
| @ -100,16 +48,15 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider): | ||||
|     ) | ||||
|  | ||||
|     def client_for_model( | ||||
|         self, | ||||
|         model: type[User | Group | MicrosoftEntraProviderUser | MicrosoftEntraProviderGroup], | ||||
|         self, model: type[User | Group] | ||||
|     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: | ||||
|         if issubclass(model, User | MicrosoftEntraProviderUser): | ||||
|         if issubclass(model, User): | ||||
|             from authentik.enterprise.providers.microsoft_entra.clients.users import ( | ||||
|                 MicrosoftEntraUserClient, | ||||
|             ) | ||||
|  | ||||
|             return MicrosoftEntraUserClient(self) | ||||
|         if issubclass(model, Group | MicrosoftEntraProviderGroup): | ||||
|         if issubclass(model, Group): | ||||
|             from authentik.enterprise.providers.microsoft_entra.clients.groups import ( | ||||
|                 MicrosoftEntraGroupClient, | ||||
|             ) | ||||
| @ -186,3 +133,55 @@ class MicrosoftEntraProviderMapping(PropertyMapping): | ||||
|     class Meta: | ||||
|         verbose_name = _("Microsoft Entra Provider Mapping") | ||||
|         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 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.organization import Organization | ||||
| from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse | ||||
| from msgraph.generated.models.user import User as MSUser | ||||
| from msgraph.generated.models.user_collection_response import UserCollectionResponse | ||||
| from msgraph.generated.models.verified_domain import VerifiedDomain | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.blueprints.tests import apply_blueprint | ||||
| 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 ( | ||||
|     MicrosoftEntraProvider, | ||||
|     MicrosoftEntraProviderMapping, | ||||
| @ -27,12 +25,11 @@ from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction | ||||
| from authentik.tenants.models import Tenant | ||||
|  | ||||
|  | ||||
| class MicrosoftEntraUserTests(APITestCase): | ||||
| class MicrosoftEntraUserTests(TestCase): | ||||
|     """Microsoft Entra User tests""" | ||||
|  | ||||
|     @apply_blueprint("system/providers-microsoft-entra.yaml") | ||||
|     def setUp(self) -> None: | ||||
|  | ||||
|         # Delete all users and groups as the mocked HTTP responses only return one ID | ||||
|         # which will cause errors with multiple users | ||||
|         Tenant.objects.update(avatars="none") | ||||
| @ -374,45 +371,3 @@ class MicrosoftEntraUserTests(APITestCase): | ||||
|             ) | ||||
|             self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists()) | ||||
|             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_flows: 300 | ||||
|   timeout_policies: 300 | ||||
|   timeout_reputation: 300 | ||||
|  | ||||
| # channel: | ||||
| #   url: "" | ||||
| @ -115,9 +116,6 @@ events: | ||||
|   context_processors: | ||||
|     geoip: "/geoip/GeoLite2-City.mmdb" | ||||
|     asn: "/geoip/GeoLite2-ASN.mmdb" | ||||
| compliance: | ||||
|   fips: | ||||
|     enabled: false | ||||
|  | ||||
| cert_discovery_dir: /certs | ||||
|  | ||||
|  | ||||
| @ -7,7 +7,6 @@ from rest_framework.decorators import action | ||||
| from rest_framework.fields import BooleanField | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
|  | ||||
| from authentik.core.api.utils import PassiveSerializer | ||||
| from authentik.events.api.tasks import SystemTaskSerializer | ||||
| @ -55,17 +54,3 @@ class OutgoingSyncProviderStatusMixin: | ||||
|                 "is_running": not lock_acquired, | ||||
|             } | ||||
|         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 | ||||
|         object in authentik based on a common identifier""" | ||||
|         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 drf_spectacular.utils import extend_schema | ||||
| 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.request import Request | ||||
| 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.utils import JSONDictField, PassiveSerializer | ||||
| from authentik.core.models import Provider | ||||
| from authentik.enterprise.license import LicenseKey | ||||
| from authentik.enterprise.providers.rac.models import RACProvider | ||||
| from authentik.outposts.api.service_connections import ServiceConnectionSerializer | ||||
| from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME | ||||
| @ -121,7 +120,7 @@ class OutpostHealthSerializer(PassiveSerializer): | ||||
|     golang_version = CharField(read_only=True) | ||||
|     openssl_enabled = BooleanField(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_outdated = BooleanField(read_only=True) | ||||
| @ -131,12 +130,6 @@ class OutpostHealthSerializer(PassiveSerializer): | ||||
|  | ||||
|     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): | ||||
|     """Filter for Outposts""" | ||||
|  | ||||
| @ -2,6 +2,8 @@ | ||||
|  | ||||
| from authentik.blueprints.apps import ManagedAppConfig | ||||
|  | ||||
| CACHE_KEY_PREFIX = "goauthentik.io/policies/reputation/scores/" | ||||
|  | ||||
|  | ||||
| class AuthentikPolicyReputationConfig(ManagedAppConfig): | ||||
|     """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_plural = _("Reputation Scores") | ||||
|         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""" | ||||
|  | ||||
| from django.contrib.auth.signals import user_logged_in | ||||
| from django.core.cache import cache | ||||
| from django.dispatch import receiver | ||||
| from django.http import HttpRequest | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.signals import login_failed | ||||
| from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR | ||||
| from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR | ||||
| from authentik.policies.reputation.models import Reputation, reputation_expiry | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.policies.reputation.apps import CACHE_KEY_PREFIX | ||||
| from authentik.policies.reputation.tasks import save_reputation | ||||
| from authentik.root.middleware import ClientIPMiddleware | ||||
| from authentik.stages.identification.signals import identification_failed | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| CACHE_TIMEOUT = CONFIG.get_int("cache.timeout_reputation") | ||||
|  | ||||
|  | ||||
| def update_score(request: HttpRequest, identifier: str, amount: int): | ||||
|     """Update score for IP and User""" | ||||
|     remote_ip = ClientIPMiddleware.get_client_ip(request) | ||||
|  | ||||
|     Reputation.objects.update_or_create( | ||||
|         ip=remote_ip, | ||||
|         identifier=identifier, | ||||
|         defaults={ | ||||
|             "score": amount, | ||||
|             "ip_geo_data": GEOIP_CONTEXT_PROCESSOR.city_dict(remote_ip) or {}, | ||||
|             "ip_asn_data": ASN_CONTEXT_PROCESSOR.asn_dict(remote_ip) or {}, | ||||
|             "expires": reputation_expiry(), | ||||
|         }, | ||||
|     try: | ||||
|         # We only update the cache here, as its faster than writing to the DB | ||||
|         score = cache.get_or_set( | ||||
|             CACHE_KEY_PREFIX + remote_ip + "/" + identifier, | ||||
|             {"ip": remote_ip, "identifier": identifier, "score": 0}, | ||||
|             CACHE_TIMEOUT, | ||||
|         ) | ||||
|         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) | ||||
|     save_reputation.delay() | ||||
|  | ||||
|  | ||||
| @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""" | ||||
|  | ||||
| from django.core.cache import cache | ||||
| from django.test import RequestFactory, TestCase | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.lib.generators import generate_id | ||||
| 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.tasks import save_reputation | ||||
| from authentik.policies.types import PolicyRequest | ||||
| from authentik.stages.password import BACKEND_INBUILT | ||||
| from authentik.stages.password.stage import authenticate | ||||
| @ -19,6 +22,8 @@ class TestReputationPolicy(TestCase): | ||||
|         self.request = self.request_factory.get("/") | ||||
|         self.test_ip = "127.0.0.1" | ||||
|         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 | ||||
|         self.user = User.objects.create(username=self.test_username) | ||||
|         self.backends = [BACKEND_INBUILT] | ||||
| @ -29,6 +34,13 @@ class TestReputationPolicy(TestCase): | ||||
|         authenticate( | ||||
|             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) | ||||
|  | ||||
|     def test_user_reputation(self): | ||||
| @ -37,6 +49,13 @@ class TestReputationPolicy(TestCase): | ||||
|         authenticate( | ||||
|             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) | ||||
|  | ||||
|     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.users import UserGroupSerializer | ||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin | ||||
| from authentik.providers.scim.models import SCIMProviderGroup | ||||
|  | ||||
|  | ||||
| @ -29,7 +28,6 @@ class SCIMProviderGroupSerializer(ModelSerializer): | ||||
|  | ||||
| class SCIMProviderGroupViewSet( | ||||
|     mixins.CreateModelMixin, | ||||
|     OutgoingSyncConnectionCreateMixin, | ||||
|     mixins.RetrieveModelMixin, | ||||
|     mixins.DestroyModelMixin, | ||||
|     UsedByMixin, | ||||
|  | ||||
| @ -6,7 +6,6 @@ from rest_framework.viewsets import GenericViewSet | ||||
|  | ||||
| from authentik.core.api.groups import GroupMemberSerializer | ||||
| from authentik.core.api.used_by import UsedByMixin | ||||
| from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin | ||||
| from authentik.providers.scim.models import SCIMProviderUser | ||||
|  | ||||
|  | ||||
| @ -29,7 +28,6 @@ class SCIMProviderUserSerializer(ModelSerializer): | ||||
|  | ||||
| class SCIMProviderUserViewSet( | ||||
|     mixins.CreateModelMixin, | ||||
|     OutgoingSyncConnectionCreateMixin, | ||||
|     mixins.RetrieveModelMixin, | ||||
|     mixins.DestroyModelMixin, | ||||
|     UsedByMixin, | ||||
|  | ||||
| @ -15,48 +15,6 @@ from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient | ||||
| 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): | ||||
|     """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") | ||||
|  | ||||
|     def client_for_model( | ||||
|         self, model: type[User | Group | SCIMProviderUser | SCIMProviderGroup] | ||||
|         self, model: type[User | Group] | ||||
|     ) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]: | ||||
|         if issubclass(model, User | SCIMProviderUser): | ||||
|         if issubclass(model, User): | ||||
|             from authentik.providers.scim.clients.users import SCIMUserClient | ||||
|  | ||||
|             return SCIMUserClient(self) | ||||
|         if issubclass(model, Group | SCIMProviderGroup): | ||||
|         if issubclass(model, Group): | ||||
|             from authentik.providers.scim.clients.groups import SCIMGroupClient | ||||
|  | ||||
|             return SCIMGroupClient(self) | ||||
| @ -147,3 +105,45 @@ class SCIMMapping(PropertyMapping): | ||||
|     class Meta: | ||||
|         verbose_name = _("SCIM Mapping") | ||||
|         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", | ||||
|     "$id": "https://goauthentik.io/blueprints/schema.json", | ||||
|     "type": "object", | ||||
|     "title": "authentik 2024.6.0 Blueprint schema", | ||||
|     "title": "authentik 2024.4.2 Blueprint schema", | ||||
|     "required": [ | ||||
|         "version", | ||||
|         "entries" | ||||
|  | ||||
| @ -31,7 +31,7 @@ services: | ||||
|     volumes: | ||||
|       - redis:/data | ||||
|   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 | ||||
|     command: server | ||||
|     environment: | ||||
| @ -52,7 +52,7 @@ services: | ||||
|       - postgresql | ||||
|       - redis | ||||
|   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 | ||||
|     command: worker | ||||
|     environment: | ||||
|  | ||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @ -5,7 +5,7 @@ go 1.22.2 | ||||
| require ( | ||||
| 	beryju.io/ldap v0.1.0 | ||||
| 	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-ldap/ldap/v3 v3.4.8 | ||||
| 	github.com/go-openapi/runtime v0.28.0 | ||||
| @ -16,7 +16,7 @@ require ( | ||||
| 	github.com/gorilla/mux v1.8.1 | ||||
| 	github.com/gorilla/securecookie v1.1.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/mitchellh/mapstructure v1.5.0 | ||||
| 	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/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= | ||||
| 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.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= | ||||
| github.com/getsentry/sentry-go v0.28.0 h1:7Rqx9M3ythTKy2J6uZLHmc8Sz9OGgIlseuO1iBX/s0M= | ||||
| 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/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | ||||
| 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/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.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= | ||||
| github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= | ||||
| 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.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= | ||||
| 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()) | ||||
| } | ||||
|  | ||||
| const VERSION = "2024.6.0" | ||||
| const VERSION = "2024.4.2" | ||||
|  | ||||
| @ -7,6 +7,7 @@ from pathlib import Path | ||||
| from tempfile import gettempdir | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from cryptography.exceptions import InternalError | ||||
| from cryptography.hazmat.backends.openssl.backend import backend | ||||
| from defusedxml import defuse_stdlib | ||||
| from prometheus_client.values import MultiProcessValue | ||||
| @ -29,8 +30,10 @@ if TYPE_CHECKING: | ||||
|  | ||||
| defuse_stdlib() | ||||
|  | ||||
| if CONFIG.get_bool("compliance.fips.enabled", False): | ||||
| try: | ||||
|     backend._enable_fips() | ||||
| except InternalError: | ||||
|     pass | ||||
|  | ||||
| wait_for_db() | ||||
|  | ||||
|  | ||||
| @ -4,7 +4,7 @@ import os | ||||
| import sys | ||||
| import warnings | ||||
|  | ||||
| from authentik.lib.config import CONFIG | ||||
| from cryptography.exceptions import InternalError | ||||
| from cryptography.hazmat.backends.openssl.backend import backend | ||||
| from defusedxml import defuse_stdlib | ||||
| from django.utils.autoreload import DJANGO_AUTORELOAD_ENV | ||||
| @ -24,8 +24,10 @@ warnings.filterwarnings( | ||||
|  | ||||
| defuse_stdlib() | ||||
|  | ||||
| if CONFIG.get_bool("compliance.fips.enabled", False): | ||||
| try: | ||||
|     backend._enable_fips() | ||||
| except InternalError: | ||||
|     pass | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|  | ||||
							
								
								
									
										12
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -353,13 +353,13 @@ msal-extensions = ">=0.3.0" | ||||
|  | ||||
| [[package]] | ||||
| name = "bandit" | ||||
| version = "1.7.9" | ||||
| version = "1.7.8" | ||||
| description = "Security oriented static analyser for python code." | ||||
| optional = false | ||||
| python-versions = ">=3.8" | ||||
| files = [ | ||||
|     {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, | ||||
|     {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, | ||||
|     {file = "bandit-1.7.8-py3-none-any.whl", hash = "sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381"}, | ||||
|     {file = "bandit-1.7.8.tar.gz", hash = "sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| @ -3376,13 +3376,13 @@ files = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "pydantic" | ||||
| version = "2.7.4" | ||||
| version = "2.7.3" | ||||
| description = "Data validation using Python type hints" | ||||
| optional = false | ||||
| python-versions = ">=3.8" | ||||
| files = [ | ||||
|     {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"}, | ||||
|     {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"}, | ||||
|     {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, | ||||
|     {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [tool.poetry] | ||||
| name = "authentik" | ||||
| version = "2024.6.0" | ||||
| version = "2024.4.2" | ||||
| description = "" | ||||
| authors = ["authentik Team <hello@goauthentik.io>"] | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| openapi: 3.0.3 | ||||
| info: | ||||
|   title: authentik | ||||
|   version: 2024.6.0 | ||||
|   version: 2024.4.2 | ||||
|   description: Making authentication simple. | ||||
|   contact: | ||||
|     email: hello@goauthentik.io | ||||
| @ -39547,8 +39547,6 @@ components: | ||||
|           readOnly: true | ||||
|         fips_enabled: | ||||
|           type: boolean | ||||
|           nullable: true | ||||
|           description: Get FIPS enabled | ||||
|           readOnly: true | ||||
|         version_should: | ||||
|           type: string | ||||
| @ -47406,16 +47404,15 @@ components: | ||||
|               type: string | ||||
|             openssl_version: | ||||
|               type: string | ||||
|             openssl_fips_enabled: | ||||
|             openssl_fips_mode: | ||||
|               type: boolean | ||||
|               nullable: true | ||||
|             authentik_version: | ||||
|               type: string | ||||
|           required: | ||||
|           - architecture | ||||
|           - authentik_version | ||||
|           - environment | ||||
|           - openssl_fips_enabled | ||||
|           - openssl_fips_mode | ||||
|           - openssl_version | ||||
|           - platform | ||||
|           - python_version | ||||
|  | ||||
| @ -3,6 +3,15 @@ | ||||
| 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. | ||||
|  | ||||
| # 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 | ||||
|  | ||||
| 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 | ||||
|         does not support generics well when parsing a subtype of `HTMLElement`. As a result, this threw | ||||
|         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", | ||||
|         "@formatjs/intl-listformat": "^7.5.7", | ||||
|         "@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/context": "^1.1.2", | ||||
|         "@lit/localize": "^0.12.1", | ||||
| @ -46,7 +46,7 @@ | ||||
|         "@open-wc/lit-helpers": "^0.7.0", | ||||
|         "@patternfly/elements": "^3.0.1", | ||||
|         "@patternfly/patternfly": "^4.224.2", | ||||
|         "@sentry/browser": "^8.9.2", | ||||
|         "@sentry/browser": "^8.9.1", | ||||
|         "@webcomponents/webcomponentsjs": "^2.8.0", | ||||
|         "base64-js": "^1.5.1", | ||||
|         "chart.js": "^4.4.3", | ||||
| @ -63,9 +63,10 @@ | ||||
|         "rapidoc": "^9.3.4", | ||||
|         "showdown": "^2.1.0", | ||||
|         "style-mod": "^4.1.2", | ||||
|         "ts-pattern": "^5.2.0", | ||||
|         "ts-pattern": "^5.1.2", | ||||
|         "webcomponent-qr-code": "^1.2.0", | ||||
|         "yaml": "^2.4.5" | ||||
|         "yaml": "^2.4.5", | ||||
|         "zxcvbn": "^4.4.2" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@babel/core": "^7.24.7", | ||||
| @ -80,20 +81,21 @@ | ||||
|         "@jeysal/storybook-addon-css-user-preferences": "^0.2.0", | ||||
|         "@lit/localize-tools": "^0.7.2", | ||||
|         "@rollup/plugin-replace": "^5.0.7", | ||||
|         "@spotlightjs/spotlight": "^2.0.0", | ||||
|         "@storybook/addon-essentials": "^8.1.9", | ||||
|         "@storybook/addon-links": "^8.1.9", | ||||
|         "@spotlightjs/spotlight": "^1.2.17", | ||||
|         "@storybook/addon-essentials": "^8.1.6", | ||||
|         "@storybook/addon-links": "^8.1.6", | ||||
|         "@storybook/api": "^7.6.17", | ||||
|         "@storybook/blocks": "^8.0.8", | ||||
|         "@storybook/manager-api": "^8.1.9", | ||||
|         "@storybook/web-components": "^8.1.9", | ||||
|         "@storybook/web-components-vite": "^8.1.9", | ||||
|         "@storybook/manager-api": "^8.1.6", | ||||
|         "@storybook/web-components": "^8.1.6", | ||||
|         "@storybook/web-components-vite": "^8.1.6", | ||||
|         "@trivago/prettier-plugin-sort-imports": "^4.3.0", | ||||
|         "@types/chart.js": "^2.9.41", | ||||
|         "@types/codemirror": "5.60.15", | ||||
|         "@types/grecaptcha": "^3.0.9", | ||||
|         "@types/guacamole-common-js": "1.5.2", | ||||
|         "@types/showdown": "^2.0.6", | ||||
|         "@types/zxcvbn": "^4.4.4", | ||||
|         "@typescript-eslint/eslint-plugin": "^7.5.0", | ||||
|         "@typescript-eslint/parser": "^7.5.0", | ||||
|         "babel-plugin-macros": "^3.1.0", | ||||
| @ -117,7 +119,7 @@ | ||||
|         "react-dom": "^18.3.1", | ||||
|         "rollup-plugin-modify": "^3.0.0", | ||||
|         "rollup-plugin-postcss-lit": "^2.1.0", | ||||
|         "storybook": "^8.1.9", | ||||
|         "storybook": "^8.1.6", | ||||
|         "storybook-addon-mock": "^5.0.0", | ||||
|         "ts-lit-plugin": "^2.0.2", | ||||
|         "tslib": "^2.6.3", | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| import "@goauthentik/admin/admin-overview/TopApplicationsTable"; | ||||
| 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/SystemStatusCard"; | ||||
| 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 { me } from "@goauthentik/common/users"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
| import "@goauthentik/elements/cards/AggregatePromiseCard"; | ||||
| import { paramURL } from "@goauthentik/elements/router/RouterOutlet"; | ||||
|  | ||||
| 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 { 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 PFDivider from "@patternfly/patternfly/components/Divider/divider.css"; | ||||
| @ -38,12 +33,8 @@ export function versionFamily(): string { | ||||
|     return parts.join("."); | ||||
| } | ||||
|  | ||||
| const AdminOverviewBase = WithLicenseSummary(AKElement); | ||||
|  | ||||
| type Renderer = () => TemplateResult | typeof nothing; | ||||
|  | ||||
| @customElement("ak-admin-overview") | ||||
| export class AdminOverviewPage extends AdminOverviewBase { | ||||
| export class AdminOverviewPage extends AKElement { | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
|             PFBase, | ||||
| @ -82,7 +73,6 @@ export class AdminOverviewPage extends AdminOverviewBase { | ||||
|  | ||||
|     render(): TemplateResult { | ||||
|         const name = this.user?.user.name ?? this.user?.user.username; | ||||
|  | ||||
|         return html`<ak-page-header icon="" header="" description=${msg("General system status")}> | ||||
|                 <span slot="header"> ${msg(str`Welcome, ${name}.`)} </span> | ||||
|             </ak-page-header> | ||||
| @ -99,7 +89,48 @@ export class AdminOverviewPage extends AdminOverviewBase { | ||||
|                                 .isCenter=${false} | ||||
|                             > | ||||
|                                 <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> | ||||
|                             </ak-aggregate-card> | ||||
|                         </div> | ||||
| @ -122,7 +153,21 @@ export class AdminOverviewPage extends AdminOverviewBase { | ||||
|                         <div class="pf-l-grid__item pf-m-12-col"> | ||||
|                             <hr class="pf-c-divider" /> | ||||
|                         </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 class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl"> | ||||
|                         <ak-recent-events pageSize="6"></ak-recent-events> | ||||
| @ -156,70 +201,4 @@ export class AdminOverviewPage extends AdminOverviewBase { | ||||
|                 </div> | ||||
|             </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 PROGRESS_CLASS = "pf-m-in-progress"; | ||||
| 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 ROUTE_SEPARATOR = ";"; | ||||
|  | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| 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 { property } from "lit/decorators.js"; | ||||
|  | ||||
| @ -67,7 +67,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { | ||||
|         this.onError = this.onError.bind(this); | ||||
|         this.onClick = this.onClick.bind(this); | ||||
|         this.actionTask = new Task(this, { | ||||
|             task: () => this.callAction(), | ||||
|             task: () => this.runCallAction(), | ||||
|             args: () => [], | ||||
|             autoRun: false, | ||||
|             onComplete: (r: unknown) => this.onSuccess(r), | ||||
| @ -77,7 +77,6 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { | ||||
|  | ||||
|     onComplete() { | ||||
|         setTimeout(() => { | ||||
|             this.actionTask.status = TaskStatus.INITIAL; | ||||
|             this.dispatchCustomEvent(`${this.eventPrefix}-reset`); | ||||
|             this.requestUpdate(); | ||||
|         }, SPINNER_TIMEOUT); | ||||
| @ -97,10 +96,12 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { | ||||
|         this.onComplete(); | ||||
|     } | ||||
|  | ||||
|     onClick() { | ||||
|         if (this.actionTask.status !== TaskStatus.INITIAL) { | ||||
|             return; | ||||
|     async runCallAction() { | ||||
|         await this.callAction(); | ||||
|         return initialState; | ||||
|     } | ||||
|  | ||||
|     onClick() { | ||||
|         this.dispatchCustomEvent(`${this.eventPrefix}-click`); | ||||
|         this.actionTask.run(); | ||||
|     } | ||||
| @ -113,7 +114,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) { | ||||
|         return [ | ||||
|             ...this.classList, | ||||
|             StatusMap.get(this.actionTask.status), | ||||
|             this.actionTask.status === TaskStatus.INITIAL ? "" : "working", | ||||
|             this.actionTask.status === TaskStatus.PENDING ? "working" : "", | ||||
|         ] | ||||
|             .join(" ") | ||||
|             .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/EmptyState"; | ||||
| import { | ||||
|     CapabilitiesEnum, | ||||
|     WithCapabilitiesConfig, | ||||
| } from "@goauthentik/elements/Interface/capabilitiesProvider"; | ||||
| import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions"; | ||||
| import { WithCapabilitiesConfig } from "@goauthentik/elements/Interface/capabilitiesProvider"; | ||||
| import "@goauthentik/elements/forms/FormElement"; | ||||
| import { BaseStage } from "@goauthentik/flow/stages/base"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { unsafeHTML } from "lit/directives/unsafe-html.js"; | ||||
|  | ||||
| import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; | ||||
| import PFButton from "@patternfly/patternfly/components/Button/button.css"; | ||||
| @ -29,6 +24,14 @@ import { | ||||
|     StagePrompt, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| import { renderCheckbox } from "./FieldRenderers"; | ||||
| import { | ||||
|     renderContinue, | ||||
|     renderPromptHelpText, | ||||
|     renderPromptInner, | ||||
|     shouldRenderInWrapper, | ||||
| } from "./helpers"; | ||||
|  | ||||
| @customElement("ak-stage-prompt") | ||||
| export class PromptStage extends WithCapabilitiesConfig( | ||||
|     BaseStage<PromptChallenge, PromptChallengeResponseRequest>, | ||||
| @ -53,232 +56,35 @@ export class PromptStage extends WithCapabilitiesConfig( | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     renderPromptInner(prompt: StagePrompt): TemplateResult { | ||||
|         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> `, | ||||
|                 ); | ||||
|     /* TODO: Legacy: None of these refer to the `this` field. Static fields are a code smell. */ | ||||
|  | ||||
|                 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>`; | ||||
|     renderPromptInner(prompt: StagePrompt) { | ||||
|         return renderPromptInner(prompt); | ||||
|     } | ||||
|             default: | ||||
|                 return html`<p>invalid type '${prompt.type}'</p>`; | ||||
|     renderPromptHelpText(prompt: StagePrompt) { | ||||
|         return renderPromptHelpText(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; | ||||
|     shouldRenderInWrapper(prompt: StagePrompt) { | ||||
|         return shouldRenderInWrapper(prompt); | ||||
|     } | ||||
|  | ||||
|     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) { | ||||
|             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>`; | ||||
|             return renderCheckbox(prompt); | ||||
|         } | ||||
|         if (this.shouldRenderInWrapper(prompt)) { | ||||
|  | ||||
|         if (shouldRenderInWrapper(prompt)) { | ||||
|             return html`<ak-form-element | ||||
|                 label="${prompt.label}" | ||||
|                 ?required="${prompt.required}" | ||||
|                 class="pf-c-form__group" | ||||
|                 .errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]} | ||||
|             > | ||||
|                 ${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)} | ||||
|                 ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)} | ||||
|             </ak-form-element>`; | ||||
|         } | ||||
|         return html` ${this.renderPromptInner(prompt)} ${this.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>`; | ||||
|         return html` ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}`; | ||||
|     } | ||||
|  | ||||
|     render(): TemplateResult { | ||||
| @ -286,6 +92,7 @@ ${prompt.initialValue}</textarea | ||||
|             return html`<ak-empty-state ?loading="${true}" header=${msg("Loading")}> | ||||
|             </ak-empty-state>`; | ||||
|         } | ||||
|  | ||||
|         return html`<header class="pf-c-login__main-header"> | ||||
|                 <h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1> | ||||
|             </header> | ||||
| @ -304,7 +111,7 @@ ${prompt.initialValue}</textarea | ||||
|                               this.challenge?.responseErrors?.non_field_errors || [], | ||||
|                           ) | ||||
|                         : html``} | ||||
|                     ${this.renderContinue()} | ||||
|                     ${renderContinue()} | ||||
|                 </form> | ||||
|             </div> | ||||
|             <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 id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -6943,18 +6943,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -6594,18 +6594,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -8789,18 +8789,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <source>SCIM User(s)</source> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -8523,18 +8523,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -8367,18 +8367,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de | ||||
| </trans-unit> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -8793,18 +8793,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <source>SCIM User(s)</source> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -8638,16 +8638,4 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
| </body></file></xliff> | ||||
|  | ||||
| @ -6587,18 +6587,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -5509,18 +5509,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
| </body> | ||||
| </file> | ||||
| </xliff> | ||||
|  | ||||
| @ -8791,18 +8791,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <source>SCIM User(s)</source> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -6635,18 +6635,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
|     </body> | ||||
|   </file> | ||||
|  | ||||
| @ -8484,18 +8484,6 @@ Bindings to groups/users are checked against the user of the event.</source> | ||||
| </trans-unit> | ||||
| <trans-unit id="s146769fb55f1ee50"> | ||||
|   <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> | ||||
|     </body> | ||||
|   </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", | ||||
|                 title: "Releases", | ||||
|                 slug: "releases", | ||||
|                 description: "Release Notes for recent authentik versions", | ||||
|                 description: "Release notes for recent authentik versions", | ||||
|             }, | ||||
|             items: [ | ||||
|                 "releases/2024/v2024.6", | ||||
|                 "releases/2024/v2024.4", | ||||
|                 "releases/2024/v2024.2", | ||||
|                 "releases/2023/v2023.10", | ||||
|                 { | ||||
|                     type: "category", | ||||
|                     label: "Previous versions", | ||||
|                     items: [ | ||||
|                         "releases/2023/v2023.10", | ||||
|                         "releases/2023/v2023.8", | ||||
|                         "releases/2023/v2023.6", | ||||
|                         "releases/2023/v2023.5", | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	