Compare commits

..

19 Commits

Author SHA1 Message Date
35cd126406 release: 2024.6.0-rc1 2024-06-14 18:42:26 +02:00
f89a4fc276 website/docs: update 2024.6 release notes with latest changes (cherry-pick #10109) (#10115)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-14 18:33:26 +02:00
4d7f380b2d web: bump API Client version (cherry-pick #10113) (#10114) 2024-06-15 00:33:28 +09:00
cb8379031a admin: system api: fix FIPS status schema (cherry-pick #10110) (#10112)
admin: system api: fix FIPS status schema (#10110)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-15 00:27:33 +09:00
0c604ceba4 website/docs: release notes for 2024.6 (#9812)
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-14 15:38:21 +02:00
30e39c75ff policies/reputation: save to database directly (#10059)
* policies/reputation: save to database directly

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* makemigrations

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix settings

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* also update expiry

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint?

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-14 22:34:43 +09:00
6d7bebbcc3 providers/enterprise: import user/group data when manually linking objects (#10089)
* providers/enterprise: import user/group data when manually linking objects

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

* select immutable ID

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

* generalize and implement for all

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

* fix

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

* fix more

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-06-14 22:34:33 +09:00
dc332ec7b0 core, web: update translations (#10108)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-06-14 12:11:55 +00:00
31e94a2814 web: Add enterprise / FIPS notification to the AdminOverviewPage (#10090)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-14 11:38:48 +00:00
eb08214f0e core: bump github.com/getsentry/sentry-go from 0.28.0 to 0.28.1 (#10095)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-14 13:21:57 +02:00
a5ab8a618e web: bump API Client version (#10107)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-06-14 11:15:08 +00:00
b8cbdcae22 admin: system api: do not show FIPS status if no valid license (#10091)
* admin: system api: do not show FIPS status if no valid license

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* also for outposts

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* black

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-14 12:52:24 +02:00
ae86184511 root: add configuration option to enable fips (#10088) 2024-06-14 10:04:00 +00:00
b704388c2f web: bump the sentry group across 1 directory with 2 updates (#10101) 2024-06-14 15:52:07 +09:00
a35f9fdd7b web: bump ts-pattern from 5.1.2 to 5.2.0 in /web (#10098)
Bumps [ts-pattern](https://github.com/gvergnaud/ts-pattern) from 5.1.2 to 5.2.0.
- [Release notes](https://github.com/gvergnaud/ts-pattern/releases)
- [Commits](https://github.com/gvergnaud/ts-pattern/compare/v5.1.2...v5.2.0)

---
updated-dependencies:
- dependency-name: ts-pattern
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-14 15:31:30 +09:00
d95220be0e web: bump the storybook group across 1 directory with 7 updates (#10102)
Bumps the storybook group with 6 updates in the /web directory:

| Package | From | To |
| --- | --- | --- |
| [@storybook/addon-essentials](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/essentials) | `8.1.6` | `8.1.9` |
| [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links) | `8.1.6` | `8.1.9` |
| [@storybook/manager-api](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/manager-api) | `8.1.6` | `8.1.9` |
| [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) | `8.1.6` | `8.1.9` |
| [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite) | `8.1.6` | `8.1.9` |
| [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/cli) | `8.1.6` | `8.1.9` |



Updates `@storybook/addon-essentials` from 8.1.6 to 8.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.1.9/code/addons/essentials)

Updates `@storybook/addon-links` from 8.1.6 to 8.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.1.9/code/addons/links)

Updates `@storybook/blocks` from 8.1.6 to 8.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.1.9/code/ui/blocks)

Updates `@storybook/manager-api` from 8.1.6 to 8.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.1.9/code/lib/manager-api)

Updates `@storybook/web-components` from 8.1.6 to 8.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.1.9/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 8.1.6 to 8.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.1.9/code/frameworks/web-components-vite)

Updates `storybook` from 8.1.6 to 8.1.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.1.9/code/lib/cli)

---
updated-dependencies:
- dependency-name: "@storybook/addon-essentials"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/blocks"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/manager-api"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-14 15:31:16 +09:00
ba1b86efa1 core: bump github.com/gorilla/websocket from 1.5.2 to 1.5.3 (#10103)
Bumps [github.com/gorilla/websocket](https://github.com/gorilla/websocket) from 1.5.2 to 1.5.3.
- [Release notes](https://github.com/gorilla/websocket/releases)
- [Commits](https://github.com/gorilla/websocket/compare/v1.5.2...v1.5.3)

---
updated-dependencies:
- dependency-name: github.com/gorilla/websocket
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-14 15:31:05 +09:00
cd93de1141 core: bump pydantic from 2.7.3 to 2.7.4 (#10093)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-13 15:47:13 +00:00
cc148bd552 core: bump bandit from 1.7.8 to 1.7.9 (#10094)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-13 17:27:09 +02:00
72 changed files with 6337 additions and 3548 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.4.2
current_version = 2024.6.0-rc1
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*))?

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.4.2"
__version__ = "2024.6.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -16,6 +16,7 @@ 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
@ -32,7 +33,7 @@ class RuntimeDict(TypedDict):
platform: str
uname: str
openssl_version: str
openssl_fips_mode: bool
openssl_fips_enabled: bool | None
authentik_version: str
@ -71,7 +72,9 @@ class SystemInfoSerializer(PassiveSerializer):
"architecture": platform.machine(),
"authentik_version": get_full_version(),
"environment": get_env(),
"openssl_fips_enabled": backend._fips_enabled,
"openssl_fips_enabled": (
backend._fips_enabled if LicenseKey.get_total().is_valid() else None
),
"openssl_version": OPENSSL_VERSION,
"platform": platform.platform(),
"python_version": python_version,

View File

@ -7,6 +7,7 @@ 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):
@ -30,6 +31,7 @@ class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
class GoogleWorkspaceProviderGroupViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

View File

@ -7,6 +7,7 @@ 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):
@ -30,6 +31,7 @@ class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
class GoogleWorkspaceProviderUserViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

View File

@ -214,3 +214,7 @@ 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

View File

@ -119,3 +119,7 @@ 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

View File

@ -31,6 +31,58 @@ 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."""
@ -59,15 +111,16 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
)
def client_for_model(
self, model: type[User | Group]
self,
model: type[User | Group | GoogleWorkspaceProviderUser | GoogleWorkspaceProviderGroup],
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User):
if issubclass(model, User | GoogleWorkspaceProviderUser):
from authentik.enterprise.providers.google_workspace.clients.users import (
GoogleWorkspaceUserClient,
)
return GoogleWorkspaceUserClient(self)
if issubclass(model, Group):
if issubclass(model, Group | GoogleWorkspaceProviderGroup):
from authentik.enterprise.providers.google_workspace.clients.groups import (
GoogleWorkspaceGroupClient,
)
@ -144,55 +197,3 @@ 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}"

View File

@ -7,6 +7,7 @@ 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):
@ -30,6 +31,7 @@ class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
class MicrosoftEntraProviderGroupViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

View File

@ -7,6 +7,7 @@ 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):
@ -29,6 +30,7 @@ class MicrosoftEntraProviderUserSerializer(ModelSerializer):
class MicrosoftEntraProviderUserViewSet(
OutgoingSyncConnectionCreateMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,

View File

@ -226,3 +226,7 @@ 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)

View File

@ -66,6 +66,26 @@ 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)
@ -75,12 +95,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=query_params,
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
filter=f"mail eq '{microsoft_user.mail}'",
select=self.get_select_fields(),
),
)
)
user_data = self._request(self.client.users.get(request_configuration))
@ -99,7 +119,6 @@ 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,
@ -120,7 +139,12 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
def discover(self):
"""Iterate through all users and connect them with authentik users if possible"""
users = self._request(self.client.users.get())
request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=self.get_select_fields(),
),
)
users = self._request(self.client.users.get(request_configuration))
next_link = True
while next_link:
for user in users.value:
@ -141,3 +165,14 @@ 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)

View File

@ -22,6 +22,58 @@ 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."""
@ -48,15 +100,16 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
)
def client_for_model(
self, model: type[User | Group]
self,
model: type[User | Group | MicrosoftEntraProviderUser | MicrosoftEntraProviderGroup],
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User):
if issubclass(model, User | MicrosoftEntraProviderUser):
from authentik.enterprise.providers.microsoft_entra.clients.users import (
MicrosoftEntraUserClient,
)
return MicrosoftEntraUserClient(self)
if issubclass(model, Group):
if issubclass(model, Group | MicrosoftEntraProviderGroup):
from authentik.enterprise.providers.microsoft_entra.clients.groups import (
MicrosoftEntraGroupClient,
)
@ -133,55 +186,3 @@ 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}"

View File

@ -3,16 +3,18 @@
from unittest.mock import AsyncMock, MagicMock, patch
from azure.identity.aio import ClientSecretCredential
from django.test import TestCase
from django.urls import reverse
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,
@ -25,11 +27,12 @@ from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
from authentik.tenants.models import Tenant
class MicrosoftEntraUserTests(TestCase):
class MicrosoftEntraUserTests(APITestCase):
"""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")
@ -371,3 +374,45 @@ class MicrosoftEntraUserTests(TestCase):
)
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()

View File

@ -50,7 +50,6 @@ cache:
timeout: 300
timeout_flows: 300
timeout_policies: 300
timeout_reputation: 300
# channel:
# url: ""
@ -116,6 +115,9 @@ events:
context_processors:
geoip: "/geoip/GeoLite2-City.mmdb"
asn: "/geoip/GeoLite2-ASN.mmdb"
compliance:
fips:
enabled: false
cert_discovery_dir: /certs

View File

@ -7,6 +7,7 @@ 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
@ -54,3 +55,17 @@ 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

View File

@ -114,3 +114,8 @@ 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

View File

@ -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
from rest_framework.fields import BooleanField, CharField, DateTimeField, SerializerMethodField
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
@ -18,6 +18,7 @@ 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
@ -120,7 +121,7 @@ class OutpostHealthSerializer(PassiveSerializer):
golang_version = CharField(read_only=True)
openssl_enabled = BooleanField(read_only=True)
openssl_version = CharField(read_only=True)
fips_enabled = BooleanField(read_only=True)
fips_enabled = SerializerMethodField()
version_should = CharField(read_only=True)
version_outdated = BooleanField(read_only=True)
@ -130,6 +131,12 @@ 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"""

View File

@ -2,8 +2,6 @@
from authentik.blueprints.apps import ManagedAppConfig
CACHE_KEY_PREFIX = "goauthentik.io/policies/reputation/scores/"
class AuthentikPolicyReputationConfig(ManagedAppConfig):
"""Authentik reputation app config"""

View File

@ -0,0 +1,25 @@
# 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"),
),
]

View File

@ -96,3 +96,8 @@ 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"]),
]

View File

@ -1,11 +0,0 @@
"""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"},
},
}

View File

@ -1,40 +1,35 @@
"""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.lib.config import CONFIG
from authentik.policies.reputation.apps import CACHE_KEY_PREFIX
from authentik.policies.reputation.tasks import save_reputation
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.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)
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)
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(),
},
)
LOGGER.debug("Updated score", amount=amount, for_user=identifier, for_ip=remote_ip)
save_reputation.delay()
@receiver(login_failed)

View File

@ -1,32 +0,0 @@
"""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")

View File

@ -1,14 +1,11 @@
"""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
@ -22,8 +19,6 @@ 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]
@ -34,13 +29,6 @@ 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):
@ -49,13 +37,6 @@ 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):

View File

@ -6,6 +6,7 @@ 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
@ -28,6 +29,7 @@ class SCIMProviderGroupSerializer(ModelSerializer):
class SCIMProviderGroupViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

View File

@ -6,6 +6,7 @@ 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
@ -28,6 +29,7 @@ class SCIMProviderUserSerializer(ModelSerializer):
class SCIMProviderUserViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

View File

@ -15,6 +15,48 @@ 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"""
@ -39,13 +81,13 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
return static("authentik/sources/scim.png")
def client_for_model(
self, model: type[User | Group]
self, model: type[User | Group | SCIMProviderUser | SCIMProviderGroup]
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User):
if issubclass(model, User | SCIMProviderUser):
from authentik.providers.scim.clients.users import SCIMUserClient
return SCIMUserClient(self)
if issubclass(model, Group):
if issubclass(model, Group | SCIMProviderGroup):
from authentik.providers.scim.clients.groups import SCIMGroupClient
return SCIMGroupClient(self)
@ -105,45 +147,3 @@ 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}"

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.4.2 Blueprint schema",
"title": "authentik 2024.6.0 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
restart: unless-stopped
command: server
environment:
@ -52,7 +52,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.6.0}
restart: unless-stopped
command: worker
environment:

4
go.mod
View File

@ -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.0
github.com/getsentry/sentry-go v0.28.1
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.2
github.com/gorilla/websocket v1.5.3
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
View File

@ -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.0 h1:7Rqx9M3ythTKy2J6uZLHmc8Sz9OGgIlseuO1iBX/s0M=
github.com/getsentry/sentry-go v0.28.0/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
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/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.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw=
github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30=
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/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=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2024.4.2"
const VERSION = "2024.6.0"

View File

@ -7,7 +7,6 @@ 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
@ -30,10 +29,8 @@ if TYPE_CHECKING:
defuse_stdlib()
try:
if CONFIG.get_bool("compliance.fips.enabled", False):
backend._enable_fips()
except InternalError:
pass
wait_for_db()

View File

@ -4,7 +4,7 @@ import os
import sys
import warnings
from cryptography.exceptions import InternalError
from authentik.lib.config import CONFIG
from cryptography.hazmat.backends.openssl.backend import backend
from defusedxml import defuse_stdlib
from django.utils.autoreload import DJANGO_AUTORELOAD_ENV
@ -24,10 +24,8 @@ warnings.filterwarnings(
defuse_stdlib()
try:
if CONFIG.get_bool("compliance.fips.enabled", False):
backend._enable_fips()
except InternalError:
pass
if __name__ == "__main__":

12
poetry.lock generated
View File

@ -353,13 +353,13 @@ msal-extensions = ">=0.3.0"
[[package]]
name = "bandit"
version = "1.7.8"
version = "1.7.9"
description = "Security oriented static analyser for python code."
optional = false
python-versions = ">=3.8"
files = [
{file = "bandit-1.7.8-py3-none-any.whl", hash = "sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381"},
{file = "bandit-1.7.8.tar.gz", hash = "sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b"},
{file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"},
{file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"},
]
[package.dependencies]
@ -3376,13 +3376,13 @@ files = [
[[package]]
name = "pydantic"
version = "2.7.3"
version = "2.7.4"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"},
{file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"},
{file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"},
{file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"},
]
[package.dependencies]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.4.2"
version = "2024.6.0"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.4.2
version: 2024.6.0
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -39547,6 +39547,8 @@ components:
readOnly: true
fips_enabled:
type: boolean
nullable: true
description: Get FIPS enabled
readOnly: true
version_should:
type: string
@ -47404,15 +47406,16 @@ components:
type: string
openssl_version:
type: string
openssl_fips_mode:
openssl_fips_enabled:
type: boolean
nullable: true
authentik_version:
type: string
required:
- architecture
- authentik_version
- environment
- openssl_fips_mode
- openssl_fips_enabled
- openssl_version
- platform
- python_version

View File

@ -3,15 +3,6 @@
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
@ -116,7 +107,3 @@ 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`.

6611
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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-1717645682",
"@goauthentik/api": "^2024.4.2-1718378698",
"@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.1",
"@sentry/browser": "^8.9.2",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.4.3",
@ -63,10 +63,9 @@
"rapidoc": "^9.3.4",
"showdown": "^2.1.0",
"style-mod": "^4.1.2",
"ts-pattern": "^5.1.2",
"ts-pattern": "^5.2.0",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.4.5",
"zxcvbn": "^4.4.2"
"yaml": "^2.4.5"
},
"devDependencies": {
"@babel/core": "^7.24.7",
@ -81,21 +80,20 @@
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.7",
"@spotlightjs/spotlight": "^1.2.17",
"@storybook/addon-essentials": "^8.1.6",
"@storybook/addon-links": "^8.1.6",
"@spotlightjs/spotlight": "^2.0.0",
"@storybook/addon-essentials": "^8.1.9",
"@storybook/addon-links": "^8.1.9",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.0.8",
"@storybook/manager-api": "^8.1.6",
"@storybook/web-components": "^8.1.6",
"@storybook/web-components-vite": "^8.1.6",
"@storybook/manager-api": "^8.1.9",
"@storybook/web-components": "^8.1.9",
"@storybook/web-components-vite": "^8.1.9",
"@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",
@ -119,7 +117,7 @@
"react-dom": "^18.3.1",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.1.6",
"storybook": "^8.1.9",
"storybook-addon-mock": "^5.0.0",
"ts-lit-plugin": "^2.0.2",
"tslib": "^2.6.3",

View File

@ -1,5 +1,6 @@
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";
@ -10,13 +11,17 @@ 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 } from "lit";
import { CSSResult, TemplateResult, css, html, nothing } 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";
@ -33,8 +38,12 @@ export function versionFamily(): string {
return parts.join(".");
}
const AdminOverviewBase = WithLicenseSummary(AKElement);
type Renderer = () => TemplateResult | typeof nothing;
@customElement("ak-admin-overview")
export class AdminOverviewPage extends AKElement {
export class AdminOverviewPage extends AdminOverviewBase {
static get styles(): CSSResult[] {
return [
PFBase,
@ -73,6 +82,7 @@ export class AdminOverviewPage extends AKElement {
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>
@ -89,48 +99,7 @@ export class AdminOverviewPage extends AKElement {
.isCenter=${false}
>
<ul class="pf-c-list">
<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>
${this.renderActions()}
</ul>
</ak-aggregate-card>
</div>
@ -153,21 +122,7 @@ export class AdminOverviewPage extends AKElement {
<div class="pf-l-grid__item pf-m-12-col">
<hr class="pf-c-divider" />
</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-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>
${this.renderCards()}
</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>
@ -201,4 +156,70 @@ export class AdminOverviewPage extends AKElement {
</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;
}
}

View File

@ -0,0 +1,56 @@
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;
}
}

View File

@ -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.4.2";
export const VERSION = "2024.6.0";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -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, initialState } from "@lit/task";
import { Task, TaskStatus } 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.runCallAction(),
task: () => this.callAction(),
args: () => [],
autoRun: false,
onComplete: (r: unknown) => this.onSuccess(r),
@ -77,6 +77,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
onComplete() {
setTimeout(() => {
this.actionTask.status = TaskStatus.INITIAL;
this.dispatchCustomEvent(`${this.eventPrefix}-reset`);
this.requestUpdate();
}, SPINNER_TIMEOUT);
@ -96,12 +97,10 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
this.onComplete();
}
async runCallAction() {
await this.callAction();
return initialState;
}
onClick() {
if (this.actionTask.status !== TaskStatus.INITIAL) {
return;
}
this.dispatchCustomEvent(`${this.eventPrefix}-click`);
this.actionTask.run();
}
@ -114,7 +113,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
return [
...this.classList,
StatusMap.get(this.actionTask.status),
this.actionTask.status === TaskStatus.PENDING ? "working" : "",
this.actionTask.status === TaskStatus.INITIAL ? "" : "working",
]
.join(" ")
.trim();

View File

@ -1,5 +0,0 @@
import PasswordMatchIndicator from "./password-match-indicator.js";
export { PasswordMatchIndicator };
export default PasswordMatchIndicator;

View File

@ -1,19 +0,0 @@
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>`;

View File

@ -1,94 +0,0 @@
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;

View File

@ -1,18 +0,0 @@
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;

View File

@ -1,5 +0,0 @@
import PasswordStrengthIndicator from "./password-strength-indicator.js";
export { PasswordStrengthIndicator };
export default PasswordStrengthIndicator;

View File

@ -1,13 +0,0 @@
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>`;

View File

@ -1,91 +0,0 @@
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;

View File

@ -1,108 +0,0 @@
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",
};

View File

@ -1,271 +0,0 @@
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;

View File

@ -1,12 +1,17 @@
import "@goauthentik/elements/Divider";
import "@goauthentik/elements/EmptyState";
import { WithCapabilitiesConfig } from "@goauthentik/elements/Interface/capabilitiesProvider";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions";
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";
@ -24,14 +29,6 @@ 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>,
@ -56,35 +53,232 @@ export class PromptStage extends WithCapabilitiesConfig(
];
}
/* TODO: Legacy: None of these refer to the `this` field. Static fields are a code smell. */
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> `,
);
renderPromptInner(prompt: StagePrompt) {
return renderPromptInner(prompt);
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>`;
}
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) {
return shouldRenderInWrapper(prompt);
shouldRenderInWrapper(prompt: StagePrompt): boolean {
// Special types that aren't rendered in a wrapper
if (
prompt.type === PromptTypeEnum.Static ||
prompt.type === PromptTypeEnum.Hidden ||
prompt.type === PromptTypeEnum.Separator
) {
return false;
}
return true;
}
renderField(prompt: StagePrompt): TemplateResult {
// Checkbox has a slightly different layout, so it must be intercepted early.
// Checkbox is rendered differently
if (prompt.type === PromptTypeEnum.Checkbox) {
return renderCheckbox(prompt);
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>`;
}
if (shouldRenderInWrapper(prompt)) {
if (this.shouldRenderInWrapper(prompt)) {
return html`<ak-form-element
label="${prompt.label}"
?required="${prompt.required}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
>
${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}
${this.renderPromptInner(prompt)} ${this.renderPromptHelpText(prompt)}
</ak-form-element>`;
}
return html` ${renderPromptInner(prompt)} ${renderPromptHelpText(prompt)}`;
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>`;
}
render(): TemplateResult {
@ -92,7 +286,6 @@ export class PromptStage extends WithCapabilitiesConfig(
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>
@ -111,7 +304,7 @@ export class PromptStage extends WithCapabilitiesConfig(
this.challenge?.responseErrors?.non_field_errors || [],
)
: html``}
${renderContinue()}
${this.renderContinue()}
</form>
</div>
<footer class="pf-c-login__main-footer">

View File

@ -1,37 +0,0 @@
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>`;
}

View File

@ -6677,6 +6677,18 @@ 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>

View File

@ -6943,6 +6943,18 @@ 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>

View File

@ -6594,6 +6594,18 @@ 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>

View File

@ -8789,6 +8789,18 @@ 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>

View File

@ -8523,6 +8523,18 @@ 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>

View File

@ -8367,6 +8367,18 @@ 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>

View File

@ -8793,6 +8793,18 @@ 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>

View File

@ -8638,4 +8638,16 @@ 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>

View File

@ -6587,6 +6587,18 @@ 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>

View File

@ -5509,6 +5509,18 @@ 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>

View File

@ -8791,6 +8791,18 @@ 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>

View File

@ -6635,6 +6635,18 @@ 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>

View File

@ -8484,6 +8484,18 @@ 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

View File

@ -1,54 +0,0 @@
---
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_ -->

View File

@ -409,16 +409,17 @@ 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",