Compare commits

..

15 Commits

Author SHA1 Message Date
Teffen Ellis
723846f232 web: Update WebDriver types. Fix issues surrounding async tests.
WIP;

WIP 2

web: Flesh out fixtures, test IDs.

web: Flesh out provider tests.

web: Flesh out LDAP test.

web: Fix typo.

web: Allow base URL to be updated.

web: Clean up.

web: Tidy types.

web: Update ARIA attributes for better test targeting.

web: Clean up message labeling.

web: Clean up ARIA labels.

web: Flesh out table ARIA labels.

web: Flesh out series.

web: Fix linter.

web: Clean up test reporting, timing issues. Add RADIUS test.
2025-07-18 22:09:05 +02:00
Dewi Roberts
8eedfe5c4e website/docs: add e2e testing steps (#15656)
* Add e2e testing steps.

* Apply suggestion
2025-07-18 13:07:45 -05:00
Dewi Roberts
33f83bec46 website/docs: fix user ref typos (#15653)
Fixed typos
2025-07-18 13:05:51 -05:00
Teffen Ellis
b93a450b38 web: Update license mixing types to anticipate load state. (#15634)
web: Update types to anticipate load state.
2025-07-18 11:15:10 -04:00
dependabot[bot]
6c169ce6a6 web: bump on-headers and compression in /packages/docusaurus-config (#15638)
---
updated-dependencies:
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
- dependency-name: compression
  dependency-version: 1.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 12:45:46 +02:00
dependabot[bot]
d53bb73c91 website: bump the build group in /website with 6 updates (#15640)
---
updated-dependencies:
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: "@swc/html-darwin-arm64"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: "@swc/html-linux-arm64-gnu"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
- dependency-name: "@swc/html-linux-x64-gnu"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 12:45:43 +02:00
dependabot[bot]
a182d7671e core: bump github.com/grafana/pyroscope-go from 1.2.3 to 1.2.4 (#15641)
Bumps [github.com/grafana/pyroscope-go](https://github.com/grafana/pyroscope-go) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/grafana/pyroscope-go/releases)
- [Commits](https://github.com/grafana/pyroscope-go/compare/v1.2.3...v1.2.4)

---
updated-dependencies:
- dependency-name: github.com/grafana/pyroscope-go
  dependency-version: 1.2.4
  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>
2025-07-18 12:45:40 +02:00
dependabot[bot]
9941cec71f web: bump @sentry/browser from 9.39.0 to 9.40.0 in /web in the sentry group across 1 directory (#15642)
web: bump @sentry/browser in /web in the sentry group across 1 directory

Bumps the sentry group with 1 update in the /web directory: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 9.39.0 to 9.40.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/9.39.0...9.40.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-version: 9.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 12:45:37 +02:00
dependabot[bot]
458344638f web: bump the swc group across 1 directory with 11 updates (#15643)
Bumps the swc group with 1 update in the /web directory: [@swc/core](https://github.com/swc-project/swc).


Updates `@swc/core` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-darwin-arm64` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-darwin-x64` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-linux-arm-gnueabihf` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-linux-arm64-gnu` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-linux-arm64-musl` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-linux-x64-gnu` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-linux-x64-musl` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-win32-arm64-msvc` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-win32-ia32-msvc` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

Updates `@swc/core-win32-x64-msvc` from 1.12.14 to 1.13.0
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.12.14...v1.13.0)

---
updated-dependencies:
- dependency-name: "@swc/core"
  dependency-version: 1.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-darwin-arm64"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-darwin-x64"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm-gnueabihf"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-linux-arm64-musl"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-linux-x64-musl"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-win32-arm64-msvc"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-win32-ia32-msvc"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
- dependency-name: "@swc/core-win32-x64-msvc"
  dependency-version: 1.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: swc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 12:45:34 +02:00
dependabot[bot]
11ffd672ca web: bump @lit/task from 1.0.2 to 1.0.3 in /web (#15644)
Bumps [@lit/task](https://github.com/lit/lit/tree/HEAD/packages/task) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/lit/lit/releases)
- [Changelog](https://github.com/lit/lit/blob/main/packages/task/CHANGELOG.md)
- [Commits](https://github.com/lit/lit/commits/@lit/task@1.0.3/packages/task)

---
updated-dependencies:
- dependency-name: "@lit/task"
  dependency-version: 1.0.3
  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>
2025-07-18 12:45:31 +02:00
dependabot[bot]
4ebbf5b097 web: bump core-js from 3.42.0 to 3.44.0 in /web (#15645)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.42.0 to 3.44.0.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.44.0/packages/core-js)

---
updated-dependencies:
- dependency-name: core-js
  dependency-version: 3.44.0
  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>
2025-07-18 12:45:28 +02:00
dependabot[bot]
1bf5688ec6 web: bump bootstrap from 4.6.2 to 5.3.7 in /web (#15646)
Bumps [bootstrap](https://github.com/twbs/bootstrap) from 4.6.2 to 5.3.7.
- [Release notes](https://github.com/twbs/bootstrap/releases)
- [Commits](https://github.com/twbs/bootstrap/compare/v4.6.2...v5.3.7)

---
updated-dependencies:
- dependency-name: bootstrap
  dependency-version: 5.3.7
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 12:45:25 +02:00
dependabot[bot]
9f1e6b3ba4 web: bump codemirror from 6.0.1 to 6.0.2 in /web (#15647)
Bumps [codemirror](https://github.com/codemirror/basic-setup) from 6.0.1 to 6.0.2.
- [Changelog](https://github.com/codemirror/basic-setup/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/basic-setup/compare/6.0.1...6.0.2)

---
updated-dependencies:
- dependency-name: codemirror
  dependency-version: 6.0.2
  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>
2025-07-18 12:45:22 +02:00
dependabot[bot]
93ec4b3c17 web: bump knip from 5.58.0 to 5.61.3 in /web (#15648)
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 5.58.0 to 5.61.3.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Changelog](https://github.com/webpro-nl/knip/blob/main/packages/knip/.release-it.json)
- [Commits](https://github.com/webpro-nl/knip/commits/5.61.3/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 5.61.3
  dependency-type: direct:development
  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>
2025-07-18 12:45:19 +02:00
dependabot[bot]
477fc11148 core: bump astral-sh/uv from 0.7.21 to 0.8.0 (#15649)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.7.21 to 0.8.0.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.7.21...0.8.0)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.8.0
  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>
2025-07-18 12:45:17 +02:00
245 changed files with 9511 additions and 10101 deletions

View File

@@ -75,7 +75,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 4: Download uv
FROM ghcr.io/astral-sh/uv:0.7.21 AS uv
FROM ghcr.io/astral-sh/uv:0.8.0 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base

View File

@@ -77,7 +77,7 @@ class GroupSerializer(ModelSerializer):
return None
return GroupMemberSerializer(instance.users, many=True).data
def validate_parent(self, parent: Group | None) -> None:
def validate_parent(self, parent: Group | None):
"""Validate group parent (if set), ensuring the parent isn't itself"""
if not self.instance or not parent:
return parent
@@ -85,7 +85,7 @@ class GroupSerializer(ModelSerializer):
raise ValidationError(_("Cannot set group as parent of itself."))
return parent
def validate_is_superuser(self, superuser: bool) -> bool:
def validate_is_superuser(self, superuser: bool):
"""Ensure that the user creating this group has permissions to set the superuser flag"""
request: Request = self.context.get("request", None)
if not request:
@@ -210,7 +210,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_users", bool, default=True),
]
)
def list(self, request: Request, *args, **kwargs) -> Response:
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
@extend_schema(
@@ -218,7 +218,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_users", bool, default=True),
]
)
def retrieve(self, request: Request, *args, **kwargs) -> Response:
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
@permission_required("authentik_core.add_user_to_group")

View File

@@ -5,7 +5,6 @@ from django.db.models.query import Q
from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from model_utils.managers import InheritanceQuerySet
from rest_framework import mixins
from rest_framework.fields import ReadOnlyField, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
@@ -100,5 +99,5 @@ class ProviderViewSet(
"application__name",
]
def get_queryset(self) -> InheritanceQuerySet: # pragma: no cover
def get_queryset(self): # pragma: no cover
return Provider.objects.select_subclasses()

View File

@@ -3,7 +3,6 @@
from collections.abc import Iterable
from drf_spectacular.utils import OpenApiResponse, extend_schema
from model_utils.managers import InheritanceQuerySet
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
@@ -89,7 +88,7 @@ class SourceViewSet(
search_fields = ["slug", "name"]
filterset_fields = ["slug", "name", "managed", "pbm_uuid"]
def get_queryset(self) -> InheritanceQuerySet: # pragma: no cover
def get_queryset(self): # pragma: no cover
return Source.objects.select_subclasses()
@permission_required("authentik_core.change_source")

View File

@@ -2,7 +2,6 @@
from typing import Any
from django.db.models.query import QuerySet
from django.utils.timezone import now
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user
@@ -42,7 +41,7 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)
def validate_user(self, user: User) -> User:
def validate_user(self, user: User):
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
@@ -139,13 +138,13 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
owner_field = "user"
rbac_allow_create_without_perm = True
def get_queryset(self) -> QuerySet:
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)
def perform_create(self, serializer: TokenSerializer) -> Token:
def perform_create(self, serializer: TokenSerializer):
if not self.request.user.is_superuser:
instance = serializer.save(
user=self.request.user,

View File

@@ -21,7 +21,6 @@ from django_filters.filters import (
UUIDFilter,
)
from django_filters.filterset import FilterSet
from djangoql.schema import BoolField, StrField
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiParameter,
@@ -73,10 +72,8 @@ from authentik.core.models import (
Token,
TokenIntents,
User,
UserQuerySet,
UserTypes,
)
from authentik.enterprise.search.fields import ChoiceSearchField, JSONSearchField
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
@@ -352,7 +349,7 @@ class UsersFilter(FilterSet):
queryset=Group.objects.all().order_by("name"),
)
def filter_is_superuser(self, queryset: UserQuerySet, name: str, value: bool) -> UserQuerySet:
def filter_is_superuser(self, queryset, name, value):
if value:
return queryset.filter(ak_groups__is_superuser=True).distinct()
return queryset.exclude(ak_groups__is_superuser=True).distinct()
@@ -398,7 +395,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
filterset_class = UsersFilter
search_fields = ["username", "name", "is_active", "email", "uuid", "attributes"]
def get_ql_fields(self) -> list[StrField | BoolField | ChoiceSearchField | JSONSearchField]:
def get_ql_fields(self):
from djangoql.schema import BoolField, StrField
from authentik.enterprise.search.fields import ChoiceSearchField, JSONSearchField
@@ -413,7 +410,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
JSONSearchField(User, "attributes", suggest_nested=False),
]
def get_queryset(self) -> UserQuerySet:
def get_queryset(self):
base_qs = User.objects.all().exclude_anonymous()
if self.serializer_class(context={"request": self.request})._should_include_groups:
base_qs = base_qs.prefetch_related("ak_groups")
@@ -424,10 +421,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
OpenApiParameter("include_groups", bool, default=True),
]
)
def list(self, request: Request, *args, **kwargs) -> Response:
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def _create_recovery_link(self, for_email: bool = False) -> tuple[str, Token]:
def _create_recovery_link(self, for_email=False) -> tuple[str, Token]:
"""Create a recovery link (when the current brand has a recovery flow set),
that can either be shown to an admin or sent to the user directly"""
brand: Brand = self.request._request.brand

View File

@@ -42,7 +42,7 @@ class JSONExtension(OpenApiSerializerFieldExtension):
target_class = "authentik.core.api.utils.JSONDictField"
def map_serializer_field(self, auto_schema, direction: str) -> dict[str, str]:
def map_serializer_field(self, auto_schema, direction):
return build_basic_type(OpenApiTypes.OBJECT)
@@ -52,7 +52,7 @@ class ModelSerializer(BaseModelSerializer):
serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy()
serializer_field_mapping[models.JSONField] = JSONDictField
def create(self, validated_data: dict[str, Any]):
def create(self, validated_data):
instance = super().create(validated_data)
request = self.context.get("request")
@@ -61,7 +61,7 @@ class ModelSerializer(BaseModelSerializer):
return instance
def update(self, instance: Model, validated_data: dict[str, Any]):
def update(self, instance: Model, validated_data):
raise_errors_on_nested_writes("update", self, validated_data)
info = model_meta.get_field_info(instance)

View File

@@ -7,7 +7,6 @@ from uuid import uuid4
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpRequest, HttpResponse
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject
@@ -15,8 +14,6 @@ from django.utils.translation import override
from sentry_sdk.api import set_tag
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
from authentik.core.models import User
SESSION_KEY_IMPERSONATE_USER = "authentik/impersonate/user"
SESSION_KEY_IMPERSONATE_ORIGINAL_USER = "authentik/impersonate/original_user"
RESPONSE_HEADER_ID = "X-authentik-id"
@@ -28,7 +25,7 @@ CTX_HOST = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + "host", default=None)
CTX_AUTH_VIA = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
def get_user(request: WSGIRequest) -> AnonymousUser | User:
def get_user(request):
if not hasattr(request, "_cached_user"):
user = None
if (authenticated_session := request.session.get("authenticatedsession", None)) is not None:
@@ -49,7 +46,7 @@ async def aget_user(request):
class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request: WSGIRequest):
def process_request(self, request):
if not hasattr(request, "session"):
raise ImproperlyConfigured(
"The Django authentication middleware requires session "

View File

@@ -11,7 +11,6 @@ from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.contrib.sessions.base_session import AbstractBaseSession
from django.core.handlers.wsgi import WSGIRequest
from django.db import models
from django.db.models import Q, QuerySet, options
from django.db.models.constants import LOOKUP_SEP
@@ -23,7 +22,6 @@ from django_cte import CTE, with_cte
from guardian.conf import settings
from guardian.mixins import GuardianUserMixin
from model_utils.managers import InheritanceManager
from rest_framework.request import Request
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
@@ -139,7 +137,7 @@ class AttributesMixin(models.Model):
class GroupQuerySet(QuerySet):
def with_children_recursive(self) -> "GroupQuerySet":
def with_children_recursive(self):
"""Recursively get all groups that have the current queryset as parents
or are indirectly related."""
@@ -212,7 +210,7 @@ class Group(SerializerModel, AttributesMixin):
("disable_group_superuser", _("Disable superuser status")),
]
def __str__(self) -> str:
def __str__(self):
return f"Group {self.name}"
@property
@@ -243,7 +241,7 @@ class Group(SerializerModel, AttributesMixin):
class UserQuerySet(models.QuerySet):
"""User queryset"""
def exclude_anonymous(self) -> "UserQuerySet":
def exclude_anonymous(self):
"""Exclude anonymous user"""
return self.exclude(**{User.USERNAME_FIELD: settings.ANONYMOUS_USER_NAME})
@@ -251,7 +249,7 @@ class UserQuerySet(models.QuerySet):
class UserManager(DjangoUserManager):
"""User manager that doesn't assign is_superuser and is_staff"""
def get_queryset(self) -> UserQuerySet:
def get_queryset(self):
"""Create special user queryset"""
return UserQuerySet(self.model, using=self._db)
@@ -297,7 +295,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
models.Index(fields=["type"]),
]
def __str__(self) -> str:
def __str__(self):
return self.username
@staticmethod
@@ -362,13 +360,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
"""superuser == staff user"""
return self.is_superuser # type: ignore
def set_password(
self,
raw_password: str,
signal: bool = True,
sender: None = None,
request: WSGIRequest | Request | None = None,
) -> None:
def set_password(self, raw_password, signal=True, sender=None, request=None):
if self.pk and signal:
from authentik.core.signals import password_changed
@@ -487,7 +479,7 @@ class Provider(SerializerModel):
"""Get serializer for this model"""
raise NotImplementedError
def __str__(self) -> str:
def __str__(self):
return str(self.name)
@@ -619,7 +611,7 @@ class Application(SerializerModel, PolicyBindingModel):
)
return getattr(providers.first(), provider_type._meta.model_name)
def __str__(self) -> str:
def __str__(self):
return str(self.name)
class Meta:
@@ -639,7 +631,7 @@ class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingMode
verbose_name_plural = _("Application Entitlements")
unique_together = (("app", "name"),)
def __str__(self) -> str:
def __str__(self):
return f"Application Entitlement {self.name} for app {self.app_id}"
@property
@@ -648,7 +640,7 @@ class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingMode
return ApplicationEntitlementSerializer
def supported_policy_binding_targets(self) -> list[str]:
def supported_policy_binding_targets(self):
return ["group", "user"]
@@ -820,7 +812,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
return {}
raise NotImplementedError
def __str__(self) -> str:
def __str__(self):
return str(self.name)
class Meta:
@@ -903,7 +895,7 @@ class ExpiringModel(models.Model):
models.Index(fields=["expiring", "expires"]),
]
def expire_action(self, *args, **kwargs) -> tuple[int, dict[str, int]]:
def expire_action(self, *args, **kwargs):
"""Handler which is called when this object is expired. By
default the object is deleted. This is less efficient compared
to bulk deleting objects, but classes like Token() need to change
@@ -966,7 +958,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
("set_token_key", _("Set a token's key")),
]
def __str__(self) -> str:
def __str__(self):
description = f"{self.identifier}"
if self.expiring:
description += f" (expires={self.expires})"
@@ -1031,7 +1023,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
except Exception as exc:
raise PropertyMappingExpressionException(exc, self) from exc
def __str__(self) -> str:
def __str__(self):
return f"Property Mapping {self.name}"
class Meta:
@@ -1059,7 +1051,7 @@ class Session(ExpiringModel, AbstractBaseSession):
]
default_permissions = []
def __str__(self) -> str:
def __str__(self):
return self.session_key
class Keys(StrEnum):

View File

@@ -1,7 +1,6 @@
"""authentik sessions engine"""
import pickle # nosec
from typing import Any
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
from django.contrib.sessions.backends.db import SessionStore as SessionBase
@@ -10,19 +9,13 @@ from django.utils import timezone
from django.utils.functional import cached_property
from structlog.stdlib import get_logger
from authentik.core.models import Session
from authentik.root.middleware import ClientIPMiddleware
LOGGER = get_logger()
class SessionStore(SessionBase):
def __init__(
self,
session_key: str | None = None,
last_ip: str | None = None,
last_user_agent: str = "",
):
def __init__(self, session_key=None, last_ip=None, last_user_agent=""):
super().__init__(session_key)
self._create_kwargs = {
"last_ip": last_ip or ClientIPMiddleware.default_ip,
@@ -30,16 +23,16 @@ class SessionStore(SessionBase):
}
@classmethod
def get_model_class(cls) -> type[Session]:
def get_model_class(cls):
from authentik.core.models import Session
return Session
@cached_property
def model_fields(self) -> list[str]:
def model_fields(self):
return [k.value for k in self.model.Keys]
def _get_session_from_db(self) -> Session:
def _get_session_from_db(self):
try:
return (
self.model.objects.select_related(
@@ -81,10 +74,10 @@ class SessionStore(SessionBase):
LOGGER.warning(str(exc))
self._session_key = None
def encode(self, session_dict: dict[str, Any]) -> bytes:
def encode(self, session_dict):
return pickle.dumps(session_dict, protocol=pickle.HIGHEST_PROTOCOL)
def decode(self, session_data: bytes) -> dict[str, Any]:
def decode(self, session_data):
try:
return pickle.loads(session_data) # nosec
except pickle.PickleError:
@@ -93,7 +86,7 @@ class SessionStore(SessionBase):
pass
return {}
def load(self) -> dict[str, Any]:
def load(self):
s = self._get_session_from_db()
if s:
return {
@@ -115,7 +108,7 @@ class SessionStore(SessionBase):
else:
return {}
def create_model_instance(self, data: dict[str, Any]) -> Session:
def create_model_instance(self, data):
args = {
"session_key": self._get_or_create_session_key(),
"expires": self.get_expiry_date(),

View File

@@ -3,7 +3,6 @@
from django.http.response import (
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponseNotAllowed,
HttpResponseNotFound,
HttpResponseServerError,
)
@@ -62,6 +61,6 @@ class ServerErrorView(TemplateView):
response_class = ServerErrorTemplateResponse
template_name = "if/error.html"
def dispatch(self, *args, **kwargs) -> HttpResponseNotAllowed: # pragma: no cover
def dispatch(self, *args, **kwargs): # pragma: no cover
"""Little wrapper so django accepts this function"""
return super().dispatch(*args, **kwargs)

2
go.mod
View File

@@ -17,7 +17,7 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/gorilla/websocket v1.5.3
github.com/grafana/pyroscope-go v1.2.3
github.com/grafana/pyroscope-go v1.2.4
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484

4
go.sum
View File

@@ -180,8 +180,8 @@ github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2e
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.2.3 h1:Rp8mjqqGqmRDvV6XYmuedUAv7wVnQJK/M1pBt6uNwxU=
github.com/grafana/pyroscope-go v1.2.3/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
github.com/grafana/pyroscope-go v1.2.4 h1:B22GMXz+O0nWLatxLuaP7o7L9dvP0clLvIpmeEQQM0Q=
github.com/grafana/pyroscope-go v1.2.4/go.mod h1:zzT9QXQAp2Iz2ZdS216UiV8y9uXJYQiGE1q8v1FyhqU=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=

View File

@@ -1,20 +0,0 @@
# Standard Library
import os
from collections.abc import Iterator
from contextlib import contextmanager
# 3rd-party
from monkeytype.config import DefaultConfig
class MonkeyConfig(DefaultConfig):
@contextmanager
def cli_context(self, command: str) -> Iterator[None]:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
import django
django.setup()
yield
CONFIG = MonkeyConfig()

View File

@@ -5954,9 +5954,9 @@
}
},
"node_modules/compression": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5964,7 +5964,7 @@
"compressible": "~2.0.18",
"debug": "2.6.9",
"negotiator": "~0.6.4",
"on-headers": "~1.0.2",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
"vary": "~1.1.2"
},
@@ -12708,9 +12708,9 @@
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -142,6 +142,7 @@ skip = [
"**/web/src/locales",
"**/web/xliff",
"**/web/out",
"**/web/playwright-report",
"./web/storybook-static",
"./web/custom-elements.json",
"./website/build",

2
web/.gitignore vendored
View File

@@ -25,6 +25,8 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
playwright-report
test-results
*.lcov
# nyc test coverage

View File

@@ -0,0 +1,29 @@
/**
* @file Vite plugin to inline CSS imports
* @import { Plugin as VitePlugin } from "vite";
*/
const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
export function inlineCSSPlugin() {
/**
* @satisfies {VitePlugin}
*/
const inlineCSSPlugin = {
name: "inline-css-plugin",
transform: (source, id) => {
if (!JavaScriptFilePattern.test(id)) return;
const code = source.replace(CSSImportPattern, (match) => {
return `${match}?inline`;
});
return {
code,
};
},
};
return inlineCSSPlugin;
}

View File

@@ -1,38 +0,0 @@
/// <reference types="@wdio/globals/types" />
/// <reference types="./types/webdriver.js" />
/**
*
* @param {WebdriverIO.Browser} browser
*/
export function addCommands(browser) {
/**
* @file Custom WDIO browser commands
*/
browser.addCommand(
"focus",
/**
* @this {HTMLElement}
*/
function () {
this.focus();
return this;
},
/* attachToElement */ true,
);
browser.addCommand(
"blur",
/**
* @this {HTMLElement}
*/
function () {
this.blur();
return this;
},
/* attachToElement */ true,
);
}

90
web/e2e/elements/proxy.ts Normal file
View File

@@ -0,0 +1,90 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { LocatorContext } from "#e2e/selectors/types";
import { ConsoleLogger } from "#logger/node";
import { expect, Locator } from "@playwright/test";
import { kebabCase } from "change-case";
export type LocatorMatchers = ReturnType<typeof expect<Locator>>;
export interface LocatorProxy extends Pick<Locator, keyof Locator> {
$: Locator;
expect: LocatorMatchers;
}
// Type helpers to extract the shape of the proxy
export type DeepLocatorProxy<T> =
Disposable & T extends Record<string, any>
? T extends HTMLElement
? LocatorProxy
: {
[K in keyof T]: DeepLocatorProxy<T[K]>;
}
: LocatorProxy;
export function createLocatorProxy<T extends Record<string, any>>(
ctx: LocatorContext,
initialPathPrefix: string[] = [],
dataAttribute: string = "test-id",
): DeepLocatorProxy<T> {
dataAttribute = kebabCase(dataAttribute);
function createProxy(path: string[] = initialPathPrefix): any {
const proxyCache = new Map<string, LocatorProxy>();
return new Proxy({} as any, {
get(_, property: string) {
// Build the current path
const currentPath = [...path, property];
// Convert the path to kebab-case and join with hyphens
const selectorValue = currentPath.map((segment) => kebabCase(segment)).join("-");
const selector = `[data-${dataAttribute}="${selectorValue}"]`;
// Create a locator for the current selector
const locator = ctx.locator(selector);
if (proxyCache.has(selector)) {
ConsoleLogger.debug(`Using cached locator for ${selector}`);
return proxyCache.get(selector)!;
}
// Return a new proxy that also behaves like a Locator
// This allows us to either continue chaining or use Locator methods
const nextProxy = new Proxy(locator, {
get(target, prop) {
if (typeof prop === "string") {
// The user is likely trying to access a property on the page.
if (prop === "$") {
return target as any;
}
if (prop === "expect") {
return expect(target);
}
}
// If the property exists on the Locator, use it
if (prop in target) {
const value = (target as any)[prop];
// Bind methods to the locator instance
if (typeof value === "function") {
return value.bind(target);
}
return value;
}
// Otherwise, continue building the path
return createProxy(currentPath)[prop];
},
});
proxyCache.set(selector, nextProxy as LocatorProxy);
return nextProxy;
},
});
}
return createProxy() as DeepLocatorProxy<T>;
}

View File

@@ -0,0 +1,175 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import type { LocatorContext } from "#e2e/selectors/types";
import { expect, Page } from "@playwright/test";
export class FormFixture extends PageFixture {
static fixtureName = "Form";
//#region Selector Methods
//#endregion
//#region Field Methods
/**
* Set the value of a text input.
*
* @param fieldName The name of the form element.
* @param value the value to set.
*/
public fill = async (
fieldName: string,
value: string,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent
.getByRole("textbox", {
name: fieldName,
})
.or(
parent.getByRole("spinbutton", {
name: fieldName,
}),
)
.first();
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
await control.fill(value);
};
/**
* Set the value of a radio or checkbox input.
*
* @param fieldName The name of the form element.
* @param value the value to set.
*/
public setInputCheck = async (
fieldName: string,
value: boolean = true,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent.locator("ak-switch-input", {
hasText: fieldName,
});
await control.scrollIntoViewIfNeeded();
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
const currentChecked = await control
.getAttribute("checked")
.then((value) => value !== null);
if (currentChecked === value) {
return;
}
await control.click();
};
/**
* Set the value of a radio or checkbox input.
*
* @param fieldName The name of the form element.
* @param pattern the value to set.
*/
public setRadio = async (
groupName: string,
fieldName: string,
parent: LocatorContext = this.page,
): Promise<void> => {
const group = parent.getByRole("group", { name: groupName });
await expect(group, `Field "${groupName}" should be visible`).toBeVisible();
const control = parent.getByRole("radio", { name: fieldName });
await control.setChecked(true, {
force: true,
});
};
/**
* Set the value of a search select input.
*
* @param fieldLabel The name of the search select element.
* @param pattern The text to match against the search select entry.
*/
public selectSearchValue = async (
fieldLabel: string,
pattern: string | RegExp,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent.getByRole("textbox", { name: fieldLabel });
await expect(
control,
`Search select control (${fieldLabel}) should be visible`,
).toBeVisible();
const fieldName = await control.getAttribute("name");
if (!fieldName) {
throw new Error(`Unable to find name attribute on search select (${fieldLabel})`);
}
// Find the search select input control and activate it.
await control.click();
const button = this.page
// ---
.locator(`div[data-managed-for*="${fieldName}"] button`, {
hasText: pattern,
});
if (!button) {
throw new Error(
`Unable to find an ak-search-select entry matching ${fieldLabel}:${pattern.toString()}`,
);
}
await button.click();
await this.page.keyboard.press("Tab");
await control.blur();
};
public setFormGroup = async (
pattern: string | RegExp,
value: boolean = true,
parent: LocatorContext = this.page,
) => {
const control = parent
.locator("ak-form-group", {
hasText: pattern,
})
.first();
const currentOpen = await control.getAttribute("open").then((value) => value !== null);
if (currentOpen === value) {
this.logger.debug(`Form group ${pattern} is already ${value ? "open" : "closed"}`);
return;
}
this.logger.debug(`Toggling form group ${pattern} to ${value ? "open" : "closed"}`);
await control.click();
if (value) {
await expect(control).toHaveAttribute("open");
} else {
await expect(control).not.toHaveAttribute("open");
}
};
//#endregion
//#region Lifecycle
constructor(page: Page, testName: string) {
super({ page, testName });
}
//#endregion
}

View File

@@ -0,0 +1,30 @@
import { ConsoleLogger, FixtureLogger } from "#logger/node";
import { Page } from "@playwright/test";
export interface PageFixtureOptions {
page: Page;
testName: string;
}
export abstract class PageFixture {
/**
* The name of the fixture.
*
* Used for logging.
*/
static fixtureName: string;
protected readonly logger: FixtureLogger;
protected readonly page: Page;
protected readonly testName: string;
constructor({ page, testName }: PageFixtureOptions) {
this.page = page;
this.testName = testName;
const Constructor = this.constructor as typeof PageFixture;
this.logger = ConsoleLogger.fixture(Constructor.fixtureName, this.testName);
}
}

View File

@@ -0,0 +1,42 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import type { LocatorContext } from "#e2e/selectors/types";
import { Page } from "@playwright/test";
export type GetByRoleParameters = Parameters<Page["getByRole"]>;
export type ARIARole = GetByRoleParameters[0];
export type ARIAOptions = GetByRoleParameters[1];
export type ClickByName = (name: string) => Promise<void>;
export type ClickByRole = (
role: ARIARole,
options?: ARIAOptions,
context?: LocatorContext,
) => Promise<void>;
export class PointerFixture extends PageFixture {
public static fixtureName = "Pointer";
public click = (
name: string,
optionsOrRole?: ARIAOptions | ARIARole,
context: LocatorContext = this.page,
): Promise<void> => {
if (typeof optionsOrRole === "string") {
return context.getByRole(optionsOrRole, { name }).click();
}
const options = {
...optionsOrRole,
name,
};
return (
context
// ---
.getByRole("button", options)
.or(context.getByRole("link", options))
.click()
);
};
}

View File

@@ -0,0 +1,119 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import { expect, Page } from "@playwright/test";
export const GOOD_USERNAME = "test-admin@goauthentik.io";
export const GOOD_PASSWORD = "test-runner";
export const BAD_USERNAME = "bad-username@bad-login.io";
export const BAD_PASSWORD = "-this-is-a-bad-password-";
export interface LoginInit {
username?: string;
password?: string;
to?: URL | string;
}
export class SessionFixture extends PageFixture {
static fixtureName = "Session";
public static readonly pathname = "/if/flow/default-authentication-flow/";
//#region Selectors
public $identificationStage = this.page.locator("ak-stage-identification");
/**
* The username field on the login page.
*/
public $usernameField = this.$identificationStage.locator('input[name="uidField"]');
/**
* The button to continue with the login process,
* typically to the password flow stage.
*/
public $submitUsernameStageButton = this.$identificationStage.locator('button[type="submit"]');
public $passwordStage = this.page.locator("ak-stage-password");
public $passwordField = this.$passwordStage.locator('input[name="password"]');
/**
* The button to submit the the login flow,
* typically redirecting to the authenticated interface.
*/
public $submitPasswordStageButton = this.$passwordStage.locator('button[type="submit"]');
/**
* A possible authentication failure message.
*/
public $authFailureMessage = this.page.locator(".pf-m-error");
//#endregion
constructor(page: Page, testName: string) {
super({ page, testName });
}
//#region Specific interactions
public async submitUsernameStage(username: string) {
this.logger.info("Submitting username stage", username);
await this.$usernameField.fill(username);
await expect(this.$submitUsernameStageButton).toBeEnabled();
await this.$submitUsernameStageButton.click();
}
public async submitPasswordStage(password: string) {
this.logger.info("Submitting password stage");
await this.$passwordField.fill(password);
await expect(this.$submitPasswordStageButton).toBeEnabled();
await this.$submitPasswordStageButton.click();
}
public checkAuthenticated = async (): Promise<boolean> => {
// TODO: Check if the user is authenticated via API
return true;
};
/**
* Log into the application.
*/
public async login({
username = GOOD_USERNAME,
password = GOOD_PASSWORD,
to = SessionFixture.pathname,
}: LoginInit = {}) {
this.logger.info("Logging in...");
const initialURL = new URL(this.page.url());
if (initialURL.pathname === SessionFixture.pathname) {
this.logger.info("Skipping navigation because we're already in a authentication flow");
} else {
await this.page.goto(to.toString());
}
await this.submitUsernameStage(username);
await this.$passwordField.waitFor({ state: "visible" });
await this.submitPasswordStage(password);
const expectedPathname = typeof to === "string" ? to : to.pathname;
await this.page.waitForURL(`**${expectedPathname}`);
}
//#endregion
//#region Navigation
public async toLoginPage() {
await this.page.goto(SessionFixture.pathname);
}
}

56
web/e2e/index.ts Normal file
View File

@@ -0,0 +1,56 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { createLocatorProxy, DeepLocatorProxy } from "#e2e/elements/proxy";
import { FormFixture } from "#e2e/fixtures/FormFixture";
import { PointerFixture } from "#e2e/fixtures/PointerFixture";
import { SessionFixture } from "#e2e/fixtures/SessionFixture";
import { createOUIDNameEngine } from "#e2e/selectors/ouid";
import { test as base } from "@playwright/test";
export { expect } from "@playwright/test";
type TestIDLocatorProxy = DeepLocatorProxy<TestIDSelectorMap>;
interface E2EFixturesTestScope {
/**
* A proxy to retrieve elements by test ID.
*
* ```ts
* const $button = $.button;
* ```
*/
$: TestIDLocatorProxy;
session: SessionFixture;
pointer: PointerFixture;
form: FormFixture;
}
interface E2EWorkerScope {
selectorRegistration: void;
}
export const test = base.extend<E2EFixturesTestScope, E2EWorkerScope>({
selectorRegistration: [
async ({ playwright }, use) => {
await playwright.selectors.register("ouid", createOUIDNameEngine);
await use();
},
{ auto: true, scope: "worker" },
],
$: async ({ page }, use) => {
await use(createLocatorProxy<TestIDSelectorMap>(page));
},
session: async ({ page }, use, { title }) => {
await use(new SessionFixture(page, title));
},
form: async ({ page }, use, { title }) => {
await use(new FormFixture(page, title));
},
pointer: async ({ page }, use, { title }) => {
await use(new PointerFixture({ page, testName: title }));
},
});

44
web/e2e/selectors/ouid.ts Normal file
View File

@@ -0,0 +1,44 @@
/* eslint-disable no-console */
type SelectorRoot = Document | ShadowRoot;
export function createOUIDNameEngine() {
const attributeName = "data-ouid-component-name";
console.log("Creating OUID selector engine!!");
return {
// Returns all elements matching given selector in the root's subtree.
queryAll(scope: SelectorRoot, componentName: string) {
const result: Element[] = [];
const match = (element: Element) => {
const name = element.getAttribute(attributeName);
if (name === componentName) {
result.push(element);
}
};
const query = (root: Element | ShadowRoot | Document) => {
const shadows: ShadowRoot[] = [];
if ((root as Element).shadowRoot) {
shadows.push((root as Element).shadowRoot!);
}
for (const element of root.querySelectorAll("*")) {
match(element);
if (element.shadowRoot) {
shadows.push(element.shadowRoot);
}
}
shadows.forEach(query);
};
query(scope);
return result;
},
};
}

View File

@@ -0,0 +1,13 @@
import type { Locator } from "@playwright/test";
export type LocatorContext = Pick<
Locator,
| "locator"
| "getByRole"
| "getByTestId"
| "getByText"
| "getByLabel"
| "getByAltText"
| "getByTitle"
| "getByPlaceholder"
>;

View File

@@ -0,0 +1,60 @@
import { IDGenerator } from "@goauthentik/core/id";
import {
adjectives,
colors,
Config as NameConfig,
uniqueNamesGenerator,
} from "unique-names-generator";
/**
* Given a dictionary of words, slice the dictionary to only include words that start with the given letter.
*/
export function alliterate(dictionary: string[], letter: string): string[] {
let firstIndex = 0;
for (let i = 0; i < dictionary.length; i++) {
if (dictionary[i][0] === letter) {
firstIndex = i;
break;
}
}
let lastIndex = firstIndex;
for (let i = firstIndex; i < dictionary.length; i++) {
if (dictionary[i][0] !== letter) {
lastIndex = i;
break;
}
}
return dictionary.slice(firstIndex, lastIndex);
}
export function createRandomName({
seed = IDGenerator.randomID(),
...config
}: Partial<NameConfig> = {}) {
const randomLetterIndex =
typeof seed === "number"
? seed
: Array.from(seed).reduce((acc, char) => acc + char.charCodeAt(0), 0);
const letter = adjectives[randomLetterIndex % adjectives.length][0];
const availableAdjectives = alliterate(adjectives, letter);
const availableColors = alliterate(colors, letter);
const name = uniqueNamesGenerator({
dictionaries: [availableAdjectives, availableAdjectives, availableColors],
style: "capital",
separator: " ",
length: 3,
seed,
...config,
});
return name;
}

102
web/logger/node.js Normal file
View File

@@ -0,0 +1,102 @@
/**
* Application logger.
*
* @import { LoggerOptions, Logger, Level, ChildLoggerOptions } from "pino"
* @import { PrettyOptions } from "pino-pretty"
*/
import { pino } from "pino";
//#region Constants
/**
* Default options for creating a Pino logger.
*
* @category Logger
* @satisfies {LoggerOptions<never, false>}
*/
export const DEFAULT_PINO_LOGGER_OPTIONS = {
enabled: true,
level: "info",
transport: {
target: "./transport.js",
options: /** @satisfies {PrettyOptions} */ ({
colorize: true,
}),
},
};
//#endregion
//#region Functions
/**
* Read the log level from the environment.
* @return {Level}
*/
export function readLogLevel() {
return process.env.AK_LOG_LEVEL || DEFAULT_PINO_LOGGER_OPTIONS.level;
}
/**
* @typedef {Logger} FixtureLogger
*/
/**
* @this {Logger}
* @param {string} fixtureName
* @param {string} [testName]
* @param {ChildLoggerOptions} [options]
* @returns {FixtureLogger}
*/
function createFixtureLogger(fixtureName, testName, options) {
return this.child(
{ name: fixtureName },
{
msgPrefix: `[${testName}] `,
...options,
},
);
}
/**
* @typedef {object} CustomLoggerMethods
* @property {typeof createFixtureLogger} fixture
*/
/**
* @typedef {Logger & CustomLoggerMethods} ConsoleLogger
*/
/**
* A singleton logger instance for Node.js.
*
* ```js
* import { ConsoleLogger } from "#logger/node";
*
* ConsoleLogger.info("Hello, world!");
* ```
*
* @runtime node
* @type {ConsoleLogger}
*/
export const ConsoleLogger = Object.assign(
pino({
...DEFAULT_PINO_LOGGER_OPTIONS,
level: readLogLevel(),
}),
{ fixture: createFixtureLogger },
);
/**
* @typedef {ReturnType<ConsoleLogger['child']>} ChildConsoleLogger
*/
//#region Aliases
export const info = ConsoleLogger.info.bind(ConsoleLogger);
export const debug = ConsoleLogger.debug.bind(ConsoleLogger);
export const warn = ConsoleLogger.warn.bind(ConsoleLogger);
export const error = ConsoleLogger.error.bind(ConsoleLogger);
//#endregion

22
web/logger/transport.js Normal file
View File

@@ -0,0 +1,22 @@
/**
* @file Pretty transport for Pino
*
* @import { PrettyOptions } from "pino-pretty"
*/
import PinoPretty from "pino-pretty";
/**
* @param {PrettyOptions} options
*/
function prettyTransporter(options) {
const pretty = PinoPretty({
...options,
ignore: "pid,hostname",
translateTime: "SYS:HH:MM:ss",
});
return pretty;
}
export default prettyTransporter;

11989
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,8 +24,8 @@
"pseudolocalize": "node ./scripts/pseudolocalize.mjs",
"storybook": "storybook dev -p 6006",
"storybook:build": "wireit",
"test": "wireit",
"test:e2e": "wireit",
"test": "vitest",
"test:e2e": "playwright test",
"test:e2e:watch": "wireit",
"test:watch": "wireit",
"tsc": "wireit",
@@ -69,6 +69,9 @@
"#flow/*": "./src/flow/*.js",
"#locales/*": "./src/locales/*.js",
"#stories/*": "./src/stories/*.js",
"#tests/*": "./tests/*.js",
"#e2e": "./e2e/index.ts",
"#e2e/*": "./e2e/*.ts",
"#*/browser": {
"types": "./out/*/browser.d.ts",
"import": "./*/browser.js"
@@ -97,7 +100,7 @@
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
"@lit/reactive-element": "^2.0.4",
"@lit/task": "^1.0.2",
"@lit/task": "^1.0.3",
"@mdx-js/mdx": "^3.1.0",
"@mrmarble/djangoql-completion": "^0.8.3",
"@open-wc/lit-helpers": "^0.7.0",
@@ -105,16 +108,21 @@
"@openlayers-elements/maps": "^0.4.0",
"@patternfly/elements": "^4.1.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^9.39.0",
"@sentry/browser": "^9.40.0",
"@spotlightjs/spotlight": "^3.0.1",
"@wdio/browser-runner": "9.15",
"@wdio/cli": "9.15",
"@wdio/spec-reporter": "^9.15.0",
"@web/test-runner": "^0.20.2",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"change-case": "^5.4.4",
"chart.js": "^4.4.9",
"chartjs-adapter-date-fns": "^3.0.0",
"codemirror": "^6.0.1",
"chromedriver": "^136.0.3",
"codemirror": "^6.0.2",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.42.0",
"core-js": "^3.44.0",
"country-flag-icons": "^1.5.19",
"date-fns": "^4.1.0",
"deepmerge-ts": "^7.1.5",
@@ -122,9 +130,11 @@
"fuse.js": "^7.1.0",
"guacamole-common-js": "^1.5.0",
"hastscript": "^9.0.1",
"lit": "^3.2.0",
"lit": "^3.3.1",
"md-front-matter": "^1.0.4",
"mermaid": "^11.9.0",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"rapidoc": "^9.3.8",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@@ -135,10 +145,11 @@
"remark-directive": "^4.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"remark-mdx-frontmatter": "^5.2.0",
"style-mod": "^4.1.2",
"trusted-types": "^2.0.0",
"ts-pattern": "^5.7.1",
"unique-names-generator": "^4.7.1",
"unist-util-visit": "^5.0.0",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.8.0"
@@ -146,19 +157,19 @@
"devDependencies": {
"@eslint/js": "^9.27.0",
"@goauthentik/core": "^1.0.0",
"@goauthentik/esbuild-plugin-live-reload": "^1.0.5",
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
"@goauthentik/eslint-config": "^1.0.5",
"@goauthentik/prettier-config": "^3.1.0",
"@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0",
"@playwright/test": "^1.54.1",
"@storybook/addon-docs": "^9.0.17",
"@storybook/addon-links": "^9.0.17",
"@storybook/web-components": "^9.0.17",
"@storybook/web-components-vite": "^9.0.17",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15",
"@types/dompurify": "^3.2.0",
"@types/grecaptcha": "^3.0.9",
"@types/guacamole-common-js": "^1.5.3",
"@types/mocha": "^10.0.10",
@@ -167,28 +178,28 @@
"@types/react-dom": "^19.1.6",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@wdio/browser-runner": "9.15",
"@wdio/cli": "9.15",
"@wdio/spec-reporter": "^9.15.0",
"@web/test-runner": "^0.20.2",
"chromedriver": "^136.0.3",
"@vitest/browser": "^3.2.4",
"@wdio/spec-reporter": "^9.18.0",
"esbuild": "^0.25.6",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.1",
"eslint": "^9.31.0",
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
"eslint": "^9.30.1",
"eslint-plugin-lit": "^2.1.1",
"eslint-plugin-wc": "^3.0.1",
"github-slugger": "^2.0.0",
"globals": "^15.10.0",
"knip": "^5.58.0",
"knip": "^5.61.3",
"lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5",
"p-iteration": "^1.1.8",
"playwright": "^1.54.1",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
"rollup-plugin-postcss-lit": "^2.2.0",
"storybook": "^9.0.16",
"turnstile-types": "^1.2.3",
"type-fest": "^4.41.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.37.0",
"vite-plugin-lit-css": "^2.0.0",
@@ -273,7 +284,7 @@
"command": "lit-analyzer src"
},
"lint:types:tests": {
"command": "tsc --noEmit -p ./tests"
"command": "tsc --noEmit -p tsconfig.test.json"
},
"lint:types": {
"command": "tsc -p .",
@@ -309,33 +320,33 @@
}
},
"test": {
"command": "wdio ./wdio.conf.ts --logLevel=warn",
"command": "wdio ./wdio.conf.js --logLevel=warn",
"env": {
"CI": "true",
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:e2e": {
"command": "wdio run ./tests/wdio.conf.ts",
"command": "wdio run ./tests/wdio.conf.js",
"dependencies": [
"build"
],
"env": {
"CI": "true",
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:e2e:watch": {
"command": "wdio run ./tests/wdio.conf.ts",
"command": "wdio run ./tests/wdio.conf.js",
"dependencies": [
"build"
],
"env": {
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:watch": {
"command": "wdio run ./wdio.conf.ts",
"command": "wdio run ./wdio.conf.js",
"dependencies": [
"build"
],

View File

@@ -0,0 +1,50 @@
/**
* @file Unique ID utilities.
*/
/**
* A global ID generator.
*
* @singleton
* @runtime common
*
* @category IDs
*/
export class IDGenerator {
static #sequenceIndex = 0;
static #elementIndex = 0;
/**
* Create a new ID for an HTML element.
*
* This ID will be unique for the lifetime of the page and will not be
* exposed on the `window` object.
*
* @param {string | number} [name] An optional name to use for the element.
*/
static elementID(name) {
name = name || ++this.#elementIndex;
return "«ak-" + name + "»";
}
/**
* Create a new ID.
*/
static next() {
this.#sequenceIndex += 1;
return this.#sequenceIndex;
}
/**
* Generate a random ID in hexadecimal format.
*
* @param {number} [characterLength]
*/
static randomID(characterLength = 6) {
const bytes = crypto.getRandomValues(new Uint8Array(characterLength / 2));
return Array.from(bytes, (a) => a.toString(16)).join("");
}
}

View File

@@ -49,7 +49,7 @@
"@goauthentik/tsconfig": "^1.0.4",
"@types/node": "^24.0.14",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20.11"

View File

@@ -0,0 +1,27 @@
/**
* @file Helpers for running tests.
*/
/**
* A function that returns a promise.
* @template {never[]} [A=never[]]
* @typedef {(...args: A) => Promise<unknown>} Thenable
*/
/**
* A tuple of a function and its arguments.
* @template {Thenable} [T=Thenable]
* @typedef {[T, Parameters<T>]} SerializedThenable
*/
/**
* Executes a sequence of promise-returning functions in series
* @template {Thenable[]} T
* @param {{ [K in keyof T]: [T[K], ...Parameters<T[K]>] }} sequence
* @returns {Promise<void>}
*/
export async function series(...sequence) {
for (const [thenable, ...args] of sequence) {
await thenable(...args);
}
}

View File

@@ -14,7 +14,6 @@ declare module "module" {
* const relativeDirname = dirname(fileURLToPath(import.meta.url));
* ```
*/
var __dirname: string;
}
}

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@goauthentik/api": "^2024.6.0-1719577139",
"base64-js": "^1.5.1",
"bootstrap": "^4.6.1",
"bootstrap": "^5.3.7",
"formdata-polyfill": "^4.0.10",
"jquery": "^3.7.1",
"weakmap-polyfill": "^2.0.4"
@@ -23,7 +23,7 @@
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-swc": "^0.4.0",
"@swc/cli": "^0.7.8",
"@swc/core": "^1.12.14",
"@swc/core": "^1.13.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"prettier": "^3.5.3",
"rollup": "^4.45.1",

94
web/playwright.config.js Normal file
View File

@@ -0,0 +1,94 @@
/**
* @file Playwright configuration.
*
* @see https://playwright.dev/docs/test-configuration
*
* @import { LogFn, Logger } from "pino"
*/
import { ConsoleLogger } from "#logger/node";
import { defineConfig, devices } from "@playwright/test";
const CI = !!process.env.CI;
/**
* @type {Map<string, Logger>}
*/
const LoggerCache = new Map();
const baseURL = process.env.AK_TEST_RUNNER_PAGE_URL ?? "http://localhost:9000";
export default defineConfig({
testDir: "./test/browser",
fullyParallel: true,
forbidOnly: CI,
retries: CI ? 2 : 0,
workers: CI ? 1 : undefined,
reporter: CI
? "github"
: [
// ---
["list", { printSteps: true }],
["html", { open: "never" }],
],
use: {
testIdAttribute: "data-test-id",
baseURL,
trace: "on-first-retry",
launchOptions: {
logger: {
isEnabled() {
return true;
},
log: (name, severity, message, args) => {
let logger = LoggerCache.get(name);
if (!logger) {
logger = ConsoleLogger.child({
name: `Playwright ${name.toUpperCase()}`,
});
LoggerCache.set(name, logger);
}
/**
* @type {LogFn}
*/
let log;
switch (severity) {
case "verbose":
log = logger.debug;
break;
case "warning":
log = logger.warn;
break;
case "error":
log = logger.error;
break;
default:
log = logger.info;
break;
}
if (name === "api") {
log = logger.debug;
}
log.call(logger, message.toString(), args);
},
},
},
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
],
});

View File

@@ -12,6 +12,7 @@ import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthent
import { msg } from "@lit/localize";
import { css, html, TemplateResult } from "lit";
import { customElement } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { until } from "lit/directives/until.js";
import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css";
@@ -56,21 +57,32 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
];
}
renderModal() {
#contentRef = createRef<HTMLDivElement>();
#backdropListener = (event: PointerEvent) => {
// We only want to close the modal when the backdrop is clicked, not when it's children are clicked.
if (this.#contentRef.value?.contains(event.target as Node)) {
return;
}
this.close();
};
protected override renderModal() {
let product = this.brandingTitle;
if (this.licenseSummary.status !== LicenseSummaryStatusEnum.Unlicensed) {
if (this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed) {
product += ` ${msg("Enterprise")}`;
}
return html`<div
class="pf-c-backdrop"
@click=${(e: PointerEvent) => {
e.stopPropagation();
this.closeModal();
}}
>
return html`<div class="pf-c-backdrop" @click=${this.#backdropListener}>
<div class="pf-l-bullseye">
<div class="pf-c-about-modal-box" role="dialog" aria-modal="true">
<div
${ref(this.#contentRef)}
class="pf-c-about-modal-box"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div class="pf-c-about-modal-box__brand">
<img
class="pf-c-about-modal-box__brand-image"
@@ -79,18 +91,12 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton))
/>
</div>
<div class="pf-c-about-modal-box__close">
<button
class="pf-c-button pf-m-plain"
type="button"
@click=${() => {
this.open = false;
}}
>
<button class="pf-c-button pf-m-plain" type="button" @click=${this.close}>
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-about-modal-box__header">
<h1 class="pf-c-title pf-m-4xl">${product}</h1>
<h1 class="pf-c-title pf-m-4xl" id="modal-title">${product}</h1>
</div>
<div class="pf-c-about-modal-box__hero"></div>
<div class="pf-c-about-modal-box__content">

View File

@@ -1,17 +1,45 @@
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "#elements/router/Route";
import { SidebarItemProperties } from "#elements/sidebar/SidebarItem";
import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize";
import { html, nothing, TemplateResult } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { repeat } from "lit/directives/repeat.js";
/**
* Given a record-like object, prefixes each key with a dot, allowing it to be spread into a
* template literal.
*
* ```ts
* interface MyElementProperties {
* foo: string;
* bar: number;
* }
*
* const properties {} as LitPropertyRecord<MyElementProperties>
*
* console.log(properties) // { '.foo': string; '.bar': number }
* ```
*/
export type LitPropertyRecord<T extends object> = {
[K in keyof T as K extends string ? LitPropertyKey<K> : never]: T[K];
};
/**
* A type that represents a property key that can be used in a LitPropertyRecord.
*
* @see {@linkcode LitPropertyRecord}
*/
export type LitPropertyKey<K> = K extends string ? `.${K}` | `?${K}` | K : K;
// The second attribute type is of string[] to help with the 'activeWhen' control, which was
// commonplace and singular enough to merit its own handler.
type SidebarEntry = [
path: string | null,
label: string,
attributes?: Record<string, any> | string[] | null, // eslint-disable-line
attributes?: LitPropertyRecord<SidebarItemProperties> | string[] | null,
children?: SidebarEntry[],
];
@@ -32,8 +60,7 @@ export function renderSidebarItem([
properties.path = path;
}
return html`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
return html`<ak-sidebar-item label=${ifDefined(label)} ${spread(properties)}>
${children ? renderSidebarItems(children) : nothing}
</ak-sidebar-item>`;
}

View File

@@ -31,6 +31,7 @@ import { ROUTES } from "#admin/Routes";
import { CapabilitiesEnum, SessionUser, UiThemeEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, CSSResult, html, nothing, TemplateResult } from "lit";
import { customElement, eventOptions, property, query } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
@@ -163,16 +164,18 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
}
async firstUpdated(): Promise<void> {
this.user = await me();
me().then((session) => {
this.user = session;
const canAccessAdmin =
this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface");
const canAccessAdmin =
this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/");
}
if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user/");
}
});
}
render(): TemplateResult {
@@ -191,13 +194,14 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
};
return html` <ak-locale-context>
<ak-skip-to-content></ak-skip-to-content>
<div class="pf-c-page">
<ak-page-navbar ?open=${this.sidebarOpen} @sidebar-toggle=${this.sidebarListener}>
<ak-version-banner></ak-version-banner>
<ak-enterprise-status interface="admin"></ak-enterprise-status>
</ak-page-navbar>
<ak-sidebar class="${classMap(sidebarClasses)}">
<ak-sidebar ?hidden=${!this.sidebarOpen} class="${classMap(sidebarClasses)}">
${renderSidebarItems(AdminSidebarEntries)}
${this.can(CapabilitiesEnum.IsEnterprise)
? renderSidebarItems(AdminSidebarEnterpriseEntries)
@@ -209,9 +213,10 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<main class="pf-c-page__main">
<div class="pf-c-page__main">
<ak-router-outlet
role="main"
aria-label="${msg("Main content")}"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
@@ -219,7 +224,7 @@ export class AdminInterface extends WithCapabilitiesConfig(AuthenticatedInterfac
.routes=${ROUTES}
>
</ak-router-outlet>
</main>
</div>
</div>
</div>
<ak-notification-drawer

View File

@@ -3,14 +3,15 @@ import { PFSize } from "#common/enums";
import { APIError, parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
import { AggregateCard } from "#elements/cards/AggregateCard";
import { SlottedTemplateResult } from "#elements/types";
import { msg } from "@lit/localize";
import { html, nothing, PropertyValues, TemplateResult } from "lit";
import { html, nothing, PropertyValues } from "lit";
import { state } from "lit/decorators.js";
export interface AdminStatus {
icon: string;
message?: TemplateResult;
message?: SlottedTemplateResult;
}
/**
@@ -95,8 +96,8 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
*
* @returns TemplateResult displaying the value
*/
protected renderValue(): TemplateResult {
return html`${this.value}`;
protected renderValue(): SlottedTemplateResult {
return this.value ? html`${this.value}` : nothing;
}
/**
@@ -105,7 +106,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* @param status - AdminStatus object containing icon and message
* @returns TemplateResult for status display
*/
private renderStatus(status: AdminStatus): TemplateResult {
private renderStatus(status: AdminStatus): SlottedTemplateResult {
return html`
<p><i class="${status.icon}"></i>&nbsp;${this.renderValue()}</p>
${status.message ? html`<p class="subtext">${status.message}</p>` : nothing}
@@ -118,9 +119,9 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
* @param error - Error message to display
* @returns TemplateResult for error display
*/
private renderError(error: string): TemplateResult {
private renderError(error: string): SlottedTemplateResult {
return html`
<p><i class="fa fa-times"></i>&nbsp;${msg("Failed to fetch")}</p>
<p><i aria-hidden="true" class="fa fa-times"></i>&nbsp;${msg("Failed to fetch")}</p>
<p class="subtext">${error}</p>
`;
}
@@ -130,7 +131,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
*
* @returns TemplateResult for loading spinner
*/
private renderLoading(): TemplateResult {
private renderLoading(): SlottedTemplateResult {
return html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`;
}
@@ -139,7 +140,7 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
*
* @returns TemplateResult for current component state
*/
renderInner(): TemplateResult {
renderInner(): SlottedTemplateResult {
return html`
<p class="center-value">
${

View File

@@ -1,11 +1,13 @@
import { DEFAULT_CONFIG } from "#common/api/config";
import { SlottedTemplateResult } from "#elements/types";
import { AdminStatus, AdminStatusCard } from "#admin/admin-overview/cards/AdminStatusCard";
import { AdminApi, OutpostsApi, SystemInfo } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html, TemplateResult } from "lit";
import { html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
@customElement("ak-admin-status-system")
@@ -82,12 +84,12 @@ export class SystemStatusCard extends AdminStatusCard<SystemInfo> {
});
}
renderHeader(): TemplateResult {
return html`${msg("System status")}`;
renderHeader(): SlottedTemplateResult {
return msg("System status");
}
renderValue(): TemplateResult {
return html`${this.statusSummary}`;
renderValue(): SlottedTemplateResult {
return this.statusSummary ? html`${this.statusSummary}` : nothing;
}
}

View File

@@ -1,4 +1,4 @@
import { AkControlElement } from "#elements/AkControlElement";
import { AkControlElement, formatFormElementAsJSON } from "#elements/AkControlElement";
import { type Spread } from "#elements/types";
import { FooterLink } from "@goauthentik/api";
@@ -37,18 +37,19 @@ export class FooterLinkInput extends AkControlElement<FooterLink> {
];
@property({ type: Object, attribute: false })
footerLink: FooterLink = {
public footerLink: FooterLink = {
name: "",
href: "",
};
@queryAll(".ak-form-control")
controls?: HTMLInputElement[];
@property({ type: String })
public name?: string | null;
json() {
return Object.fromEntries(
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
) as unknown as FooterLink;
@queryAll(".ak-form-control")
protected controls?: HTMLInputElement[];
public override json() {
return formatFormElementAsJSON<FooterLink>(this.controls);
}
get isValid() {

View File

@@ -2,42 +2,49 @@ import "../AdminSettingsFooterLinks.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { $, browser, expect } from "@wdio/globals";
import { html } from "lit";
describe("ak-admin-settings-footer-link", () => {
afterEach(async () => {
await browser.execute(async () => {
await document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body._$litPart$;
afterEach(() =>
browser.execute(() => {
document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if ("_$litPart$" in document.body) {
delete document.body._$litPart$;
}
});
});
}),
);
it("should render an empty control", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" });
const link = $("ak-admin-settings-footer-link");
await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "",
href: "",
});
});
it("should not be valid if just a name is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" });
await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo",
href: "",
});
});
it("should be valid if just a URL is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({
await expect(link.getProperty("isValid")).resolves.toStrictEqual(true);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "",
href: "https://foo.com",
});
@@ -45,11 +52,13 @@ describe("ak-admin-settings-footer-link", () => {
it("should be valid if both are filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({
await expect(link.getProperty("isValid")).resolves.toStrictEqual(true);
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo",
href: "https://foo.com",
});
@@ -57,13 +66,13 @@ describe("ak-admin-settings-footer-link", () => {
it("should not be valid if the URL is not valid", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
const link = $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("never://foo.com");
await expect(await link.getProperty("toJson")).toEqual({
await expect(link.getProperty("toJson")).resolves.toEqual({
name: "foo",
href: "never://foo.com",
});
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(link.getProperty("isValid")).resolves.toStrictEqual(false);
});
});

View File

@@ -179,9 +179,8 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
.options=${policyEngineModes}
.value=${this.instance?.policyEngineMode}
></ak-radio-input>
<ak-form-group>
<span slot="header"> ${msg("UI settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("UI settings")}">
<div class="pf-c-form">
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}

View File

@@ -84,7 +84,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
];
}
renderSidebarAfter(): TemplateResult {
protected renderSidebarAfter(): TemplateResult {
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card">
<div class="pf-c-card__body">

View File

@@ -28,13 +28,13 @@ export class ApplicationWizardStep<T = Record<string, unknown>> extends WizardSt
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
// these fields and provide them to all the child classes.
wizardTitle = msg("New application");
wizardDescription = msg("Create a new application and configure a provider for it.");
canCancel = true;
protected wizardTitle = msg("New application");
protected wizardDescription = msg("Create a new application and configure a provider for it.");
public cancelable = true;
// This should be overridden in the children for more precise targeting.
@query("form")
form!: HTMLFormElement;
protected form!: HTMLFormElement;
get formValues(): T {
return serializeForm<T>([

View File

@@ -20,7 +20,7 @@ export class AkWizardTitle extends AKElement {
render() {
return html`<div class="ak-bottom-spacing pf-c-content">
<h3><slot></slot></h3>
<h3 data-test-id="wizard-heading"><slot></slot></h3>
</div>`;
}
}
@@ -31,4 +31,12 @@ declare global {
interface HTMLElementTagNameMap {
"ak-wizard-title": AkWizardTitle;
}
interface WizardTestIDMap {
heading: HTMLHeadingElement;
}
interface TestIDSelectorMap {
wizard: WizardTestIDMap;
}
}

View File

@@ -8,8 +8,6 @@ import "#elements/forms/HorizontalFormElement";
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types.js";
import { camelToSnake } from "#common/utils";
import { isSlug } from "#elements/router/utils";
import { type NavigableButton, type WizardButton } from "#components/ak-wizard/types";
@@ -19,6 +17,8 @@ import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
import { type ApplicationRequest } from "@goauthentik/api";
import { snakeCase } from "change-case";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
@@ -29,8 +29,7 @@ const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
const trimMany = (o: Record<string, unknown>, vs: string[]) =>
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isStr = (v: any): v is string => typeof v === "string";
const isStr = (v: unknown): v is string => typeof v === "string";
@customElement("ak-application-wizard-application-step")
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
@@ -51,9 +50,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
errorMessages(name: string) {
return this.errors.has(name)
? [this.errors.get(name)]
: (this.wizard.errors?.app?.[name] ??
this.wizard.errors?.app?.[camelToSnake(name)] ??
[]);
: (this.wizard.errors?.app?.[name] ?? this.wizard.errors?.app?.[snakeCase(name)] ?? []);
}
get buttons(): WizardButton[] {
@@ -149,9 +146,8 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
.value=${app.policyEngineMode}
.errorMessages=${errors.policyEngineMode ?? []}
></ak-radio-input>
<ak-form-group aria-label=${msg("UI Settings")}>
<span slot="header"> ${msg("UI Settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("UI Settings")}>
<div class="pf-c-form">
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}

View File

@@ -8,11 +8,11 @@ import "#elements/forms/HorizontalFormElement";
import { styles as AwadStyles } from "../../ApplicationWizardFormStepStyles.styles.js";
import { type ApplicationWizardState, type OneOfProvider } from "../../types.js";
import { camelToSnake } from "#common/utils";
import { AKElement } from "#elements/Base";
import { serializeForm } from "#elements/forms/Form";
import { snakeCase } from "change-case";
import { CSSResult } from "lit";
import { property, query } from "lit/decorators.js";
@@ -46,7 +46,7 @@ export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKEl
return name in this.errors
? [this.errors[name]]
: (this.wizard.errors?.provider?.[name] ??
this.wizard.errors?.provider?.[camelToSnake(name)] ??
this.wizard.errors?.provider?.[snakeCase(name)] ??
[]);
}

View File

@@ -61,9 +61,8 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF
input-hint="code"
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label=" ${msg("Protocol settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"

View File

@@ -177,9 +177,8 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
</div>
</div>
<ak-form-group>
<span slot="header">${msg("Additional settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Additional settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Context")} name="context">
<ak-codemirror
mode=${CodeMirrorMode.YAML}

View File

@@ -91,9 +91,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</p>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header"> ${msg("Branding settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Branding settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Title")} required name="brandingTitle">
<input
type="text"
@@ -174,9 +173,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("External user settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("External user settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Default application")}
name="defaultApplication"
@@ -219,9 +217,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Default flows")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Default flows")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="flowAuthentication"
@@ -299,9 +296,8 @@ export class BrandForm extends ModelForm<Brand, string> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Other global settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Other global settings")} ">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Web Certificate")}
name="webCertificate"

View File

@@ -44,19 +44,18 @@ export class CoreGroupSearch extends CustomListenerElement(AKElement) {
* @attr
*/
@property({ type: String, reflect: true })
group?: string;
public group?: string;
@query("ak-search-select")
search!: SearchSelect<Group>;
public search!: SearchSelect<Group>;
@property({ type: String })
name: string | null | undefined;
public name?: string | null;
selectedGroup?: Group;
constructor() {
super();
this.selected = this.selected.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
}
@@ -83,9 +82,9 @@ export class CoreGroupSearch extends CustomListenerElement(AKElement) {
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
}
selected(group: Group) {
selected = (group: Group) => {
return this.group === group.pk;
}
};
render() {
return html`

View File

@@ -34,13 +34,19 @@ const renderValue = (item: CertificateKeyPair | undefined): string | undefined =
@customElement("ak-crypto-certificate-search")
export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) {
@property({ type: String, reflect: true })
certificate?: string;
public certificate?: string;
@query("ak-search-select")
search!: SearchSelect<CertificateKeyPair>;
public search!: SearchSelect<CertificateKeyPair>;
@property({ type: String })
name: string | null | undefined;
public name?: string | null;
@property({ type: String })
public label?: string | undefined;
@property({ type: String })
public placeholder?: string | undefined;
/**
* Set to `true` to allow certificates without private key to show up. When set to `false`,
@@ -48,7 +54,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
* @attr
*/
@property({ type: Boolean, attribute: "nokey" })
noKey = false;
public noKey = false;
/**
* Set this to true if, should there be only one certificate available, you want the system to
@@ -57,16 +63,12 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
* @attr
*/
@property({ type: Boolean, attribute: "singleton" })
singleton = false;
public singleton = false;
selectedKeypair?: CertificateKeyPair;
constructor() {
super();
this.selected = this.selected.bind(this);
this.fetchObjects = this.fetchObjects.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
}
/**
* @todo Document this.
*/
public selectedKeypair?: CertificateKeyPair;
get value() {
return this.selectedKeypair ? renderValue(this.selectedKeypair) : null;
@@ -85,13 +87,13 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
}
}
handleSearchUpdate(ev: CustomEvent) {
handleSearchUpdate = (ev: CustomEvent) => {
ev.stopPropagation();
this.selectedKeypair = ev.detail.value;
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
}
};
async fetchObjects(query?: string): Promise<CertificateKeyPair[]> {
fetchObjects = async (query?: string): Promise<CertificateKeyPair[]> => {
const args: CryptoCertificatekeypairsListRequest = {
ordering: "name",
hasKey: !this.noKey,
@@ -104,19 +106,21 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
args,
);
return certificates.results;
}
};
selected(item: CertificateKeyPair, items: CertificateKeyPair[]) {
selected = (item: CertificateKeyPair, items: CertificateKeyPair[]) => {
return (
(this.singleton && !this.certificate && items.length === 1) ||
(!!this.certificate && this.certificate === item.pk)
);
}
};
render() {
return html`
<ak-search-select
name=${ifDefined(this.name ?? undefined)}
label=${ifDefined(this.label ?? undefined)}
placeholder=${ifDefined(this.placeholder ?? undefined)}
.fetchObjects=${this.fetchObjects}
.renderElement=${renderElement}
.value=${renderValue}

View File

@@ -11,6 +11,7 @@ import { RenderFlowOption } from "#admin/flows/utils";
import type { Flow, FlowsInstancesListRequest } from "@goauthentik/api";
import { FlowsApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { html } from "lit";
import { property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -37,13 +38,15 @@ export function getFlowValue(flow: Flow | undefined): string | undefined {
*/
export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement) {
//#region Properties
/**
* The type of flow we're looking for.
*
* @attr
*/
@property({ type: String })
flowType?: FlowsInstancesListDesignationEnum;
public flowType?: FlowsInstancesListDesignationEnum;
/**
* The id of the current flow, if any. For stages where the flow is already defined.
@@ -51,7 +54,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
* @attr
*/
@property({ type: String })
currentFlow?: string | undefined;
public currentFlow?: string | undefined;
/**
* If true, it is not valid to leave the flow blank.
@@ -59,10 +62,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
* @attr
*/
@property({ type: Boolean })
required?: boolean = false;
@query("ak-search-select")
search!: SearchSelect<T>;
public required?: boolean = false;
/**
* When specified and the object instance does not have a flow selected, auto-select the flow with the given slug.
@@ -73,9 +73,29 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
defaultFlowSlug?: string;
@property({ type: String })
name: string | null | undefined;
public name?: string | null;
selectedFlow?: T;
/**
* The label of the input, for forms.
*
* @attr
*/
@property({ type: String })
public label?: string;
/**
* The textual placeholder for the search's <input> object, if currently empty. Used as the
* native <input> object's `placeholder` field.
*
* @attr
*/
@property({ type: String })
public placeholder: string = msg("Select a flow...");
@query("ak-search-select")
protected search!: SearchSelect<T>;
protected selectedFlow?: T;
get value() {
return this.selectedFlow ? getFlowValue(this.selectedFlow) : null;
@@ -83,18 +103,16 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
constructor() {
super();
this.fetchObjects = this.fetchObjects.bind(this);
this.selected = this.selected.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
}
handleSearchUpdate(ev: CustomEvent) {
handleSearchUpdate = (ev: CustomEvent) => {
ev.stopPropagation();
this.selectedFlow = ev.detail.value;
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
}
};
async fetchObjects(query?: string): Promise<Flow[]> {
fetchObjects = async (query?: string): Promise<Flow[]> => {
const args: FlowsInstancesListRequest = {
ordering: "slug",
designation: this.flowType,
@@ -102,7 +120,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
};
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args);
return flows.results;
}
};
/* This is the most commonly overridden method of this class. About half of the Flow Searches
* use this method, but several have more complex needs, such as relating to the brand, or just
@@ -137,6 +155,8 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
.renderElement=${renderElement}
.renderDescription=${renderDescription}
.value=${getFlowValue}
placeholder=${ifDefined(this.placeholder ?? undefined)}
label=${ifDefined(this.label ?? undefined)}
name=${ifDefined(this.name ?? undefined)}
@ak-change=${this.handleSearchUpdate}
?blankable=${!this.required}

View File

@@ -21,14 +21,9 @@ export class AkBrandedFlowSearch<T extends Flow> extends FlowSearch<T> {
@property({ attribute: false, type: String })
brandFlow?: string;
constructor() {
super();
this.selected = this.selected.bind(this);
}
selected(flow: Flow): boolean {
public selected = (flow: Flow): boolean => {
return super.selected(flow) || flow.pk === this.brandFlow;
}
};
}
declare global {

View File

@@ -31,19 +31,14 @@ export class AkSourceFlowSearch<T extends Flow> extends FlowSearch<T> {
@property({ type: String })
instanceId: string | undefined;
constructor() {
super();
this.selected = this.selected.bind(this);
}
// If there's no instance or no currentFlowId for it and the flow resembles the fallback,
// otherwise defer to the parent class.
selected(flow: Flow): boolean {
selected = (flow: Flow): boolean => {
return (
(!this.instanceId && !this.currentFlow && flow.slug === this.fallback) ||
super.selected(flow)
);
}
};
}
declare global {

View File

@@ -10,25 +10,35 @@ import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("ak-license-notice")
export class AkLicenceNotice extends WithLicenseSummary(AKElement) {
export class AKLicenceNotice extends WithLicenseSummary(AKElement) {
static styles = [$PFBase];
@property()
notice = msg("Enterprise only");
public label = msg("Enterprise only");
@property()
public description = msg("Learn more about the enterprise license.");
render() {
return this.hasEnterpriseLicense
? nothing
: html`
<ak-alert class="pf-c-radio__description" inline plain>
<a href="#/enterprise/licenses">${this.notice}</a>
</ak-alert>
`;
if (this.hasEnterpriseLicense) {
return nothing;
}
return html`
<ak-alert class="pf-c-radio__description" inline plain>
<a
aria-label="${this.label}"
aria-description="${this.description}"
href="#/enterprise/licenses"
>${this.label}</a
>
</ak-alert>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-license-notice": AkLicenceNotice;
"ak-license-notice": AKLicenceNotice;
}
}

View File

@@ -13,8 +13,8 @@ describe("ak-enterprise-status-card", () => {
it("should not error when no data is loaded", async () => {
render(html`<ak-enterprise-status-card></ak-enterprise-status-card>`);
const status = await $("ak-enterprise-status-card");
await expect(status).toHaveText(msg("Loading"));
const status = $("ak-enterprise-status-card");
await expect(status).resolves.toHaveText(msg("Loading"));
});
it("should render empty when unlicensed", async () => {
@@ -36,22 +36,22 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Unlicensed"));
await expect(status).resolves.toExist();
await expect(status).resolves.toHaveText(msg("Unlicensed"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "0");
const externalUserProgress = await $("ak-enterprise-status-card").$(
await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "0");
const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "0");
await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "0");
});
it("should show warnings when full", async () => {
@@ -73,34 +73,35 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Valid"));
await expect(status).resolves.toExist();
await expect(status).resolves.toHaveText(msg("Valid"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
await expect($("ak-enterprise-status-card").$(">>>#internalUsers")).toHaveElementClass(
"pf-m-warning",
);
const externalUserProgress = await $("ak-enterprise-status-card").$(
const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
$("ak-enterprise-status-card").$(">>>#internalUsers"),
).resolves.toHaveElementClass("pf-m-warning");
await expect(
await $("ak-enterprise-status-card").$(">>>#externalUsers"),
).toHaveElementClass("pf-m-warning");
$("ak-enterprise-status-card").$(">>>#externalUsers"),
).resolves.toHaveElementClass("pf-m-warning");
});
it("should show infinity when not licensed for a user type", async () => {
@@ -122,33 +123,33 @@ describe("ak-enterprise-status-card", () => {
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
const status = $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Valid"));
await expect(status).resolves.toExist();
await expect(status).resolves.toHaveText(msg("Valid"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
const internalUserProgress = $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(internalUserProgress).resolves.toExist();
await expect(internalUserProgress).resolves.toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
await expect($("ak-enterprise-status-card").$(">>>#internalUsers")).toHaveElementClass(
"pf-m-warning",
);
const externalUserProgress = await $("ak-enterprise-status-card").$(
const externalUserProgress = $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "∞");
await expect(externalUserProgress).resolves.toExist();
await expect(externalUserProgress).resolves.toHaveAttr("aria-valuenow", "∞");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
$("ak-enterprise-status-card").$(">>>#internalUsers"),
).resolves.toHaveElementClass("pf-m-warning");
await expect(
await $("ak-enterprise-status-card").$(">>>#externalUsers"),
).toHaveElementClass("pf-m-danger");
$("ak-enterprise-status-card").$(">>>#externalUsers"),
).resolves.toHaveElementClass("pf-m-danger");
});
});

View File

@@ -214,9 +214,8 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
${msg("Required authentication level for this flow.")}
</p>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header"> ${msg("Behavior settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Behavior settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="compatibilityMode">
<label class="pf-c-switch">
<input
@@ -289,9 +288,8 @@ export class FlowForm extends WithCapabilitiesConfig(ModelForm<Flow, string>) {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Appearance settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Appearance settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Layout")} required name="layout">
<select class="pf-c-form-control">
<option

View File

@@ -234,9 +234,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
selected-label="${msg("Selected Applications")}"
></ak-dual-select-provider>
</ak-form-element-horizontal>
<ak-form-group aria-label=${msg("Advanced settings")}>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("Advanced settings")}>
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Configuration")} name="config">
<ak-codemirror
mode=${CodeMirrorMode.YAML}

View File

@@ -66,9 +66,8 @@ export class DummyPolicyForm extends BasePolicyForm<DummyPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="result">
<label class="pf-c-switch">
<input

View File

@@ -78,9 +78,8 @@ export class EventMatcherPolicyForm extends BasePolicyForm<EventMatcherPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Action")} name="action">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<TypeCreate[]> => {

View File

@@ -66,9 +66,8 @@ export class PasswordExpiryPolicyForm extends BasePolicyForm<PasswordExpiryPolic
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Maximum age (in days)")}
required

View File

@@ -70,9 +70,8 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Expression")}
required

View File

@@ -81,9 +81,8 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header"> ${msg("Distance settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Distance settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="checkHistoryDistance">
<label class="pf-c-switch">
<input
@@ -188,9 +187,8 @@ export class GeoIPPolicyForm extends BasePolicyForm<GeoIPPolicy> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Static rule settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Static rule settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("ASNs")} name="asns">
<input
type="text"

View File

@@ -46,9 +46,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
}
renderStaticRules(): TemplateResult {
return html` <ak-form-group>
<span slot="header"> ${msg("Static rules")} </span>
<div slot="body" class="pf-c-form">
return html` <ak-form-group label="${msg("Static rules")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Minimum length")}
required
@@ -144,9 +143,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
renderHIBP(): TemplateResult {
return html`
<ak-form-group expanded>
<span slot="header"> ${msg("HaveIBeenPwned settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("HaveIBeenPwned settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Allowed count")}
required
@@ -169,9 +167,8 @@ export class PasswordPolicyForm extends BasePolicyForm<PasswordPolicy> {
renderZxcvbn(): TemplateResult {
return html`
<ak-form-group expanded>
<span slot="header"> ${msg("zxcvbn settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("zxcvbn settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Score threshold")}
required

View File

@@ -76,9 +76,8 @@ doesn't pass when either or both of the selected options are equal or above the
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Policy-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Policy-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="checkIp">
<label class="pf-c-switch">
<input

View File

@@ -65,9 +65,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("General settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("General settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Username")}
name="staticSettings.username"
@@ -92,9 +91,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("RDP settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("RDP settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Ignore server certificate")}
name="staticSettings.ignore-cert"
@@ -137,9 +135,8 @@ export class PropertyMappingProviderRACForm extends BasePropertyMappingForm<RACP
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Expression")}
required

View File

@@ -3,7 +3,7 @@ import { ModelForm } from "#elements/forms/ModelForm";
import { msg } from "@lit/localize";
export abstract class BaseProviderForm<T> extends ModelForm<T, number> {
getSuccessMessage(): string {
public override getSuccessMessage(): string {
return this.instance
? msg("Successfully updated provider.")
: msg("Successfully created provider.");

View File

@@ -29,32 +29,38 @@ import { customElement, property } from "lit/decorators.js";
@customElement("ak-provider-list")
export class ProviderListPage extends TablePage<Provider> {
searchEnabled(): boolean {
override searchEnabled(): boolean {
return true;
}
pageTitle(): string {
override pageTitle(): string {
return msg("Providers");
}
pageDescription(): string {
override pageDescription(): string {
return msg("Provide support for protocols like SAML and OAuth to assigned applications.");
}
pageIcon(): string {
override pageIcon(): string {
return "pf-icon pf-icon-integration";
}
checkbox = true;
clearOnRefresh = true;
override checkbox = true;
override clearOnRefresh = true;
@property()
order = "name";
public order = "name";
async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
public searchLabel = msg("Provider name");
public searchPlaceholder = msg("Search for providers…");
override async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
return new ProvidersApi(DEFAULT_CONFIG).providersAllList(
await this.defaultEndpointConfig(),
);
}
columns(): TableColumn[] {
override columns(): TableColumn[] {
return [
new TableColumn(msg("Name"), "name"),
new TableColumn(msg("Application")),
@@ -63,8 +69,9 @@ export class ProviderListPage extends TablePage<Provider> {
];
}
renderToolbarSelected(): TemplateResult {
override renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Provider(s)")}
.objects=${this.selectedElements}
@@ -85,7 +92,7 @@ export class ProviderListPage extends TablePage<Provider> {
</ak-forms-delete-bulk>`;
}
rowApp(item: Provider): TemplateResult {
#rowApp(item: Provider): TemplateResult {
if (item.assignedApplicationName) {
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${msg("Assigned to application ")}
@@ -93,6 +100,7 @@ export class ProviderListPage extends TablePage<Provider> {
>${item.assignedApplicationName}</a
>`;
}
if (item.assignedBackchannelApplicationName) {
return html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${msg("Assigned to application (backchannel) ")}
@@ -100,15 +108,15 @@ export class ProviderListPage extends TablePage<Provider> {
>${item.assignedBackchannelApplicationName}</a
>`;
}
return html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i> ${msg(
"Warning: Provider not assigned to any application.",
)}`;
return html`<i aria-hidden="true" class="pf-icon pf-icon-warning-triangle pf-m-warning"></i>
${msg("Warning: Provider not assigned to any application.")}`;
}
row(item: Provider): TemplateResult[] {
override row(item: Provider): TemplateResult[] {
return [
html`<a href="#/core/providers/${item.pk}"> ${item.name} </a>`,
this.rowApp(item),
this.#rowApp(item),
html`${item.verboseName}`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
@@ -121,16 +129,20 @@ export class ProviderListPage extends TablePage<Provider> {
type=${item.component}
>
</ak-proxy-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<button
aria-label=${msg("Edit provider")}
slot="trigger"
class="pf-c-button pf-m-plain"
>
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
<i aria-hidden="true" class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
override renderObjectCreate(): TemplateResult {
return html`<ak-provider-wizard> </ak-provider-wizard> `;
}
}

View File

@@ -30,18 +30,16 @@ export class ProviderWizard extends AKElement {
static styles: CSSResult[] = [PFBase, PFButton];
@property()
createText = msg("Create");
public createText = msg("Create");
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
public providerTypes: TypeCreate[] = [];
@property({ attribute: false })
finalHandler: () => Promise<void> = () => {
return Promise.resolve();
};
public finalHandler?: () => Promise<void>;
@query("ak-wizard")
wizard?: Wizard;
private wizard?: Wizard;
connectedCallback() {
super.connectedCallback();
@@ -56,9 +54,7 @@ export class ProviderWizard extends AKElement {
.steps=${["initial"]}
header=${msg("New provider")}
description=${msg("Create a new provider.")}
.finalHandler=${() => {
return this.finalHandler();
}}
.finalHandler=${this.finalHandler}
>
<ak-wizard-page-type-create
name="selectProviderType"
@@ -82,7 +78,15 @@ export class ProviderWizard extends AKElement {
</ak-wizard-page-form>
`;
})}
<button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button>
<button
aria-label=${msg("New Provider")}
aria-description="${msg("Open the wizard to create a new provider.")}"
type="button"
slot="trigger"
class="pf-c-button pf-m-primary"
>
${msg("Create")}
</button>
</ak-wizard>
`;
}

View File

@@ -59,9 +59,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Credentials")}
required
@@ -184,9 +183,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("User filtering")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
@@ -237,9 +235,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"

View File

@@ -47,7 +47,9 @@ export function renderForm(
) {
return html`
<ak-text-input
autocomplete="on"
name="name"
placeholder=${msg("Provider name")}
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
@@ -80,10 +82,8 @@ export function renderForm(
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Bind flow")}
required
@@ -91,6 +91,7 @@ export function renderForm(
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
label=${msg("Bind flow")}
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication}
@@ -119,9 +120,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-text-input
name="baseDn"
label=${msg("Base DN")}
@@ -141,6 +141,8 @@ export function renderForm(
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
label=${msg("Certificate")}
placeholder=${msg("Select a certificate...")}
certificate=${ifDefined(provider?.certificate ?? nothing)}
name="certificate"
>

View File

@@ -57,9 +57,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Client ID")} required name="clientId">
<input
type="text"
@@ -160,9 +159,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("User filtering")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
@@ -213,9 +211,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"

View File

@@ -23,16 +23,14 @@ export async function oauth2ProvidersProvider(page = 1, search = "") {
return {
pagination: oauthProviders.pagination,
options: oauthProviders.results.map((provider) => providerToSelect(provider)),
options: oauthProviders.results.map(providerToSelect),
};
}
export function oauth2ProviderSelector(instanceProviders: number[] | undefined) {
if (!instanceProviders) {
return async (mappings: DualSelectPair<OAuth2Provider>[]) =>
mappings.filter(
([_0, _1, _2, source]: DualSelectPair<OAuth2Provider>) => source !== undefined,
);
mappings.filter(([, , , source]: DualSelectPair<OAuth2Provider>) => !source);
}
return async () => {
@@ -59,9 +57,6 @@ export function oauth2ProviderSelector(instanceProviders: number[] | undefined)
@customElement("ak-provider-oauth2-form")
export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
@state()
showClientSecret = true;
static styles = [
...super.styles,
css`
@@ -71,30 +66,37 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
`,
];
async loadInstance(pk: number): Promise<OAuth2Provider> {
@state()
protected showClientSecret = true;
override async loadInstance(pk: number): Promise<OAuth2Provider> {
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Retrieve({
id: pk,
});
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
return provider;
}
async send(data: OAuth2Provider): Promise<OAuth2Provider> {
override async send(data: OAuth2Provider): Promise<OAuth2Provider> {
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({
id: this.instance.pk,
oAuth2ProviderRequest: data,
});
}
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({
oAuth2ProviderRequest: data,
});
}
renderForm() {
override renderForm() {
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};
return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback);
}
}

View File

@@ -127,7 +127,9 @@ export function renderForm(
showClientSecretCallback: ShowClientSecret = defaultShowClientSecret,
) {
return html` <ak-text-input
autocomplete="on"
name="name"
placeholder=${msg("Provider name")}
label=${msg("Name")}
value=${ifDefined(provider?.name)}
required
@@ -139,6 +141,8 @@ export function renderForm(
required
>
<ak-flow-search
label=${msg("Authorization flow")}
placeholder=${msg("Select an authorization flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
@@ -147,9 +151,8 @@ export function renderForm(
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-radio-input
name="clientType"
label=${msg("Client type")}
@@ -199,6 +202,8 @@ export function renderForm(
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
label=${msg("Signing Key")}
placeholder=${msg("Select a signing key...")}
certificate=${ifDefined(provider?.signingKey ?? undefined)}
singleton
></ak-crypto-certificate-search>
@@ -207,6 +212,8 @@ export function renderForm(
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
label=${msg("Encryption Key")}
placeholder=${msg("Select an encryption key...")}
certificate=${ifDefined(provider?.encryptionKey ?? undefined)}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to encrypt the tokens.")}</p>
@@ -214,14 +221,15 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label=${msg("Advanced flow settings")}>
<div class="pf-c-form">
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
label=${msg("Authentication flow")}
placeHolder=${msg("Select an authentication flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
@@ -237,6 +245,8 @@ export function renderForm(
required
>
<ak-flow-search
label=${msg("Invalidation flow")}
placeHolder=${msg("Select an invalidation flow...")}
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
@@ -249,9 +259,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-text-input
name="accessCodeValidity"
label=${msg("Access code validity")}
@@ -334,9 +343,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Machine-to-Machine authentication settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Federated OIDC Sources")}
name="jwtFederationSources"

View File

@@ -1,6 +1,6 @@
import "#admin/providers/oauth2/OAuth2ProviderRedirectURI";
import { AkControlElement } from "#elements/AkControlElement";
import { AkControlElement, formatFormElementAsJSON } from "#elements/AkControlElement";
import { type Spread } from "#elements/types";
import { MatchingModeEnum, RedirectURI } from "@goauthentik/api";
@@ -43,9 +43,7 @@ export class OAuth2ProviderRedirectURI extends AkControlElement<RedirectURI> {
controls?: HTMLInputElement[];
json() {
return Object.fromEntries(
Array.from(this.controls ?? []).map((control) => [control.name, control.value]),
) as unknown as RedirectURI;
return formatFormElementAsJSON<RedirectURI>(this.controls);
}
get isValid() {

View File

@@ -26,6 +26,7 @@ import {
RbacPermissionsAssignedByUsersListModelEnum,
User,
} from "@goauthentik/api";
import { IDGenerator } from "@goauthentik/core/id";
import MDProviderOAuth2 from "~docs/add-secure-apps/providers/oauth2/index.mdx";
@@ -267,12 +268,16 @@ export class OAuth2ProviderViewPage extends AKElement {
<div class="pf-c-card__body">
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("providerInfo")}"
>
<span class="pf-c-form__label-text"
>${msg("OpenID Configuration URL")}</span
>
</label>
<input
id="${IDGenerator.elementID("providerInfo")}"
class="pf-c-form-control"
readonly
type="text"
@@ -280,12 +285,16 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("issuer")}"
>
<span class="pf-c-form__label-text"
>${msg("OpenID Configuration Issuer")}</span
>
</label>
<input
id="${IDGenerator.elementID("issuer")}"
class="pf-c-form-control"
readonly
type="text"
@@ -294,12 +303,16 @@ export class OAuth2ProviderViewPage extends AKElement {
</div>
<hr class="pf-c-divider" />
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("authorize")}"
>
<span class="pf-c-form__label-text"
>${msg("Authorize URL")}</span
>
</label>
<input
id="${IDGenerator.elementID("authorize")}"
class="pf-c-form-control"
readonly
type="text"
@@ -307,10 +320,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("token")}"
>
<span class="pf-c-form__label-text">${msg("Token URL")}</span>
</label>
<input
id="${IDGenerator.elementID("token")}"
class="pf-c-form-control"
readonly
type="text"
@@ -318,12 +335,16 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("userInfo")}"
>
<span class="pf-c-form__label-text"
>${msg("Userinfo URL")}</span
>
</label>
<input
id="${IDGenerator.elementID("userInfo")}"
class="pf-c-form-control"
readonly
type="text"
@@ -331,10 +352,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("logout")}"
>
<span class="pf-c-form__label-text">${msg("Logout URL")}</span>
</label>
<input
id="${IDGenerator.elementID("logout")}"
class="pf-c-form-control"
readonly
type="text"
@@ -342,10 +367,14 @@ export class OAuth2ProviderViewPage extends AKElement {
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<label
class="pf-c-form__label"
for="${IDGenerator.elementID("jwks")}"
>
<span class="pf-c-form__label-text">${msg("JWKS URL")}</span>
</label>
<input
id="${IDGenerator.elementID("jwks")}"
class="pf-c-form-control"
readonly
type="text"
@@ -391,9 +420,12 @@ export class OAuth2ProviderViewPage extends AKElement {
${renderDescriptionList(
[
[
msg("Preview for user"),
html`<label for="${IDGenerator.elementID("preview-user")}"
>${msg("Preview for user")}</label
>`,
html`
<ak-search-select
id="${IDGenerator.elementID("preview-user")}"
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
ordering: "username",

View File

@@ -230,9 +230,8 @@ export function renderForm(
input-hint="code"
></ak-text-input>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${provider?.certificate}
@@ -275,9 +274,8 @@ ${provider?.skipPathRegex}</textarea
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Authentication settings")}">
<div class="pf-c-form">
<ak-switch-input
name="interceptHeaderAuth"
label=${msg("Intercept header authentication")}
@@ -335,9 +333,8 @@ ${provider?.skipPathRegex}</textarea
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"

View File

@@ -118,9 +118,8 @@ export class EndpointForm extends ModelForm<Endpoint, string> {
selected-label="${msg("Selected User Property Mappings")}"
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header"> ${msg("Advanced settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Settings")} name="settings">
<ak-codemirror
mode="yaml"

View File

@@ -118,9 +118,8 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"

View File

@@ -45,6 +45,7 @@ export function renderForm(
<ak-text-input
name="name"
label=${msg("Name")}
placeholder=${msg("Provider name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
@@ -58,6 +59,8 @@ export function renderForm(
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
label=${msg("Authentication flow")}
placeholder=${msg("Select an authentication flow...")}
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication}
@@ -74,17 +77,14 @@ export function renderForm(
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-hidden-text-input
name="sharedSecret"
label=${msg("Shared secret")}
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-hidden-text-input>
name="sharedSecret" label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret ?? []}
value=${provider?.sharedSecret ?? randomString(128, ascii_letters + digits)}
required
input-hint="code"
></ak-hidden-text-input>
required input-hint="code" ></ak-hidden-text-input
>
<ak-text-input
name="clientNetworks"
label=${msg("Client Networks")}
@@ -107,15 +107,16 @@ export function renderForm(
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
label=${msg("Invalidation flow")}
placeholder=${msg("Select an invalidation flow...")}
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.errorMessages=${errors?.invalidationFlow ?? []}

View File

@@ -85,9 +85,8 @@ export function renderForm(
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-text-input
name="acsUrl"
label=${msg("ACS URL")}
@@ -123,9 +122,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
@@ -158,9 +156,8 @@ export function renderForm(
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Signing Certificate")} name="signingKp">
<ak-crypto-certificate-search
.certificate=${provider?.signingKp}

View File

@@ -32,9 +32,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-text-input
name="url"
label=${msg("URL")}
@@ -114,9 +113,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("User filtering")}">
<div class="pf-c-form">
<ak-switch-input
name="excludeUsersServiceAccount"
label=${msg("Exclude service accounts")}
@@ -156,9 +154,8 @@ export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationE
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"

View File

@@ -59,9 +59,8 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
value=${ifDefined(provider?.name)}
required
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Signing Key")}
name="signingKey"
@@ -95,9 +94,8 @@ export class SSFProviderFormPage extends BaseProviderForm<SSFProvider> {
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Authentication settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("OIDC Providers")}
name="oidcAuthProviders"

View File

@@ -68,7 +68,7 @@ export class InitialPermissionsListPage extends TablePage<InitialPermissions> {
</ak-forms-delete-bulk>`;
}
render(): TemplateResult {
render() {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}

View File

@@ -66,7 +66,7 @@ export class RoleListPage extends TablePage<Role> {
</ak-forms-delete-bulk>`;
}
render(): TemplateResult {
render() {
return html`<ak-page-header
icon=${this.pageIcon()}
header=${this.pageTitle()}

View File

@@ -121,9 +121,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
"Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.",
)}
></ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Realm settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Realm settings")}">
<div class="pf-c-form">
<ak-text-input
name="realm"
label=${msg("Realm")}
@@ -213,9 +212,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Sync connection settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Sync connection settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("KAdmin type")}
required
@@ -276,9 +274,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("SPNEGO settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("SPNEGO settings")}">
<div class="pf-c-form">
<ak-text-input
name="spnegoServerName"
label=${msg("SPNEGO server name")}
@@ -305,9 +302,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Kerberos Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Kerberos Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@@ -344,9 +340,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
@@ -377,9 +372,8 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<Ke
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Additional settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Additional settings")}">
<div class="pf-c-form">
<ak-text-input
name="userPathTemplate"
label=${msg("User path")}

View File

@@ -173,9 +173,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Connection settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Connection settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Server URI")}
required
@@ -279,9 +278,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("LDAP Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("LDAP Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@@ -316,9 +314,8 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Additional settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Additional settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Parent Group")} name="syncParentGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {

View File

@@ -126,9 +126,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
if (!this.providerType?.urlsCustomizable) {
return html``;
}
return html` <ak-form-group expanded>
<span slot="header"> ${msg("URL settings")} </span>
<div slot="body" class="pf-c-form">
return html` <ak-form-group open label="${msg("URL settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authorization URL")}
name="authorizationUrl"
@@ -421,9 +420,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
<p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`}
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Consumer key")}
required
@@ -464,9 +462,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
</div>
</ak-form-group>
${this.renderUrlOptions()}
<ak-form-group expanded>
<span slot="header"> ${msg("OAuth Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("OAuth Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@@ -501,9 +498,8 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm<OAuth
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"

View File

@@ -333,9 +333,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
/>
<p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`}
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Client ID")} required name="clientId">
<input
type="text"
@@ -347,9 +346,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
${this.renderSettings()}
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
@@ -380,9 +378,8 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Plex Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Plex Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"

View File

@@ -233,9 +233,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
<p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`}
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("SSO URL")} required name="ssoUrl">
<input
type="text"
@@ -321,9 +320,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal name="allowIdpInitiated">
<label class="pf-c-switch">
<input
@@ -493,9 +491,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("SAML Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("SAML Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@@ -530,9 +527,8 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Flow settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Pre-authentication flow")}
required

View File

@@ -70,9 +70,8 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
<label class="pf-c-check__label"> ${msg("Enabled")} </label>
</div>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("SCIM Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("SCIM Attribute mapping")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
@@ -107,9 +106,8 @@ export class SCIMSourceForm extends BaseSourceForm<SCIMSource> {
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group label="${msg("Advanced protocol settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("User path")} name="userPathTemplate">
<input
type="text"

View File

@@ -82,9 +82,8 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Duo Auth API")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Duo Auth API")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Integration key")}
required
@@ -106,15 +105,13 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
></ak-secret-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Duo Admin API (optional)")}</span>
<span slot="description">
${msg(
`When using a Duo MFA, Access or Beyond plan, an Admin API application can be created.
This will allow authentik to import devices automatically.`,
)}
</span>
<div slot="body" class="pf-c-form">
<ak-form-group
label=${msg("Duo Admin API (optional)")}
description="${msg(
`When using a Duo MFA, Access or Beyond plan, an Admin API application can be created. This will allow authentik to import devices automatically.`,
)}"
>
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Integration key")}
name="adminIntegrationKey"
@@ -135,9 +132,8 @@ export class AuthenticatorDuoStageForm extends BaseStageForm<AuthenticatorDuoSta
></ak-secret-text-input>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Configuration flow")}
name="configureFlow"

View File

@@ -52,9 +52,8 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
if (!this.showConnectionSettings) {
return html``;
}
return html`<ak-form-group expanded>
<span slot="header"> ${msg("Connection settings")} </span>
<div slot="body" class="pf-c-form">
return html`<ak-form-group open label="${msg("Connection settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("SMTP Host")} required name="host">
<input
type="text"
@@ -193,9 +192,8 @@ export class AuthenticatorEmailStageForm extends BaseStageForm<AuthenticatorEmai
</p>
</ak-form-element-horizontal>
${this.renderConnectionSettings()}
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Subject")} required name="subject">
<input
type="text"

View File

@@ -56,9 +56,8 @@ export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm<Authentica
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Google Verified Access API")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Google Verified Access API")}">
<div class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Credentials")}
required

View File

@@ -192,9 +192,8 @@ export class AuthenticatorSMSStageForm extends BaseStageForm<AuthenticatorSMSSta
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-group open label="${msg("Stage-specific settings")}">
<div class="pf-c-form">
<ak-form-element-horizontal label=${msg("Provider")} required name="provider">
<select
class="pf-c-form-control"

Some files were not shown because too many files have changed in this diff Show More