mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 15:12:13 +02:00
Compare commits
81 Commits
webdriver-
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7614b8c608 | ||
|
|
99fa5ec7cc | ||
|
|
2800211dd0 | ||
|
|
2d5247f1bc | ||
|
|
2c2d7c38d2 | ||
|
|
d3963172cc | ||
|
|
08abe34b81 | ||
|
|
47263aef3f | ||
|
|
162934679d | ||
|
|
1903c35ee0 | ||
|
|
b97635f710 | ||
|
|
fd1f65eefc | ||
|
|
1e8eff4cb2 | ||
|
|
cfe113b36a | ||
|
|
90274b357a | ||
|
|
9848e4fbe0 | ||
|
|
a9deefe481 | ||
|
|
d29896cfe1 | ||
|
|
30670bb547 | ||
|
|
0f64471115 | ||
|
|
249b22963a | ||
|
|
b3a513273b | ||
|
|
7ca013d527 | ||
|
|
2e65e307fe | ||
|
|
0c07bad6f6 | ||
|
|
eb1c56dbeb | ||
|
|
766a294e55 | ||
|
|
db84a29ad7 | ||
|
|
58e65e4612 | ||
|
|
95b2d15476 | ||
|
|
2bdc5ef8b1 | ||
|
|
83cae926f7 | ||
|
|
213cf44928 | ||
|
|
3c97b081b0 | ||
|
|
ba725365ec | ||
|
|
e5e9708ec2 | ||
|
|
6a604e42ca | ||
|
|
ab1f87cfd6 | ||
|
|
de9b795c97 | ||
|
|
0377e3593e | ||
|
|
951c24dab5 | ||
|
|
707eca883e | ||
|
|
8bc64ea478 | ||
|
|
8b1240ff0b | ||
|
|
56ff8b1f97 | ||
|
|
cf26aace7b | ||
|
|
46021e904a | ||
|
|
a47196776d | ||
|
|
58ce20c840 | ||
|
|
29b0177235 | ||
|
|
f15ddfcccd | ||
|
|
7e4bdac093 | ||
|
|
5f16ea4718 | ||
|
|
d50a266d74 | ||
|
|
41ebfa24da | ||
|
|
8b7cc18988 | ||
|
|
e780b7d519 | ||
|
|
25894592ae | ||
|
|
5021d08c69 | ||
|
|
cb74b47674 | ||
|
|
aafd81ca09 | ||
|
|
a4f8e15f91 | ||
|
|
0c20169739 | ||
|
|
24ca89c439 | ||
|
|
0352d31af0 | ||
|
|
5bdbf06351 | ||
|
|
b3f1e7b1a2 | ||
|
|
2dfda8833d | ||
|
|
9094b30860 | ||
|
|
7e52e932fc | ||
|
|
b5fc28a3fd | ||
|
|
df49dd4ec8 | ||
|
|
3f4c58a05b | ||
|
|
46c9bfb0aa | ||
|
|
6d325d566c | ||
|
|
5689336f61 | ||
|
|
e04ca70cb2 | ||
|
|
6a5342f621 | ||
|
|
e250c8f514 | ||
|
|
a4e7aa0adc | ||
|
|
ac79acd2bc |
@@ -4,7 +4,7 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
GITHUB_OUTPUT=/dev/stdout \
|
||||
GITHUB_REF=ref \
|
||||
GITHUB_SHA=sha \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,authentik/server \
|
||||
GITHUB_REPOSITORY=goauthentik/authentik \
|
||||
python $SCRIPT_DIR/push_vars.py
|
||||
|
||||
@@ -12,7 +12,7 @@ GITHUB_OUTPUT=/dev/stdout \
|
||||
GITHUB_OUTPUT=/dev/stdout \
|
||||
GITHUB_REF=ref \
|
||||
GITHUB_SHA=sha \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,authentik/server \
|
||||
GITHUB_REPOSITORY=goauthentik/authentik \
|
||||
DOCKER_USERNAME=foo \
|
||||
python $SCRIPT_DIR/push_vars.py
|
||||
|
||||
2
.github/workflows/ci-api-docs.yml
vendored
2
.github/workflows/ci-api-docs.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: api-docs
|
||||
path: website/api/build
|
||||
|
||||
14
.github/workflows/release-publish.yml
vendored
14
.github/workflows/release-publish.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
with:
|
||||
image_name: ghcr.io/goauthentik/server,beryju/authentik
|
||||
image_name: ghcr.io/goauthentik/server,authentik/server
|
||||
release: true
|
||||
registry_dockerhub: true
|
||||
registry_ghcr: true
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/docs
|
||||
- name: Login to GitHub Container Registry
|
||||
@@ -92,9 +92,9 @@ jobs:
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }}
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},authentik/${{ matrix.type }}
|
||||
- name: make empty clients
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
@@ -102,8 +102,8 @@ jobs:
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -220,7 +220,7 @@ jobs:
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_CORP_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/server
|
||||
- name: Get static files from docker image
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@@ -76,7 +76,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.8.3 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.5 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
|
||||
@@ -134,11 +134,16 @@ ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
LABEL org.opencontainers.image.url=https://goauthentik.io
|
||||
LABEL org.opencontainers.image.description="goauthentik.io Main server image, see https://goauthentik.io for more info."
|
||||
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
|
||||
LABEL org.opencontainers.image.version=${VERSION}
|
||||
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
|
||||
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.description="goauthentik.io Main server image, see https://goauthentik.io for more info." \
|
||||
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
|
||||
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
|
||||
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
|
||||
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
|
||||
org.opencontainers.image.title="authentik server image" \
|
||||
org.opencontainers.image.url="https://goauthentik.io" \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
org.opencontainers.image.version=${VERSION}
|
||||
|
||||
WORKDIR /
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
|
||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
|
||||
[](https://codecov.io/gh/goauthentik/authentik)
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://www.transifex.com/authentik/authentik/)
|
||||
|
||||
## What is authentik?
|
||||
|
||||
@@ -49,11 +49,28 @@ class GroupMemberSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class GroupChildSerializer(ModelSerializer):
|
||||
"""Stripped down group serializer to show relevant children for groups"""
|
||||
|
||||
attributes = JSONDictField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"is_superuser",
|
||||
"attributes",
|
||||
"group_uuid",
|
||||
]
|
||||
|
||||
|
||||
class GroupSerializer(ModelSerializer):
|
||||
"""Group Serializer"""
|
||||
|
||||
attributes = JSONDictField(required=False)
|
||||
users_obj = SerializerMethodField(allow_null=True)
|
||||
children_obj = SerializerMethodField(allow_null=True)
|
||||
roles_obj = ListSerializer(
|
||||
child=RoleSerializer(),
|
||||
read_only=True,
|
||||
@@ -61,7 +78,6 @@ class GroupSerializer(ModelSerializer):
|
||||
required=False,
|
||||
)
|
||||
parent_name = CharField(source="parent.name", read_only=True, allow_null=True)
|
||||
|
||||
num_pk = IntegerField(read_only=True)
|
||||
|
||||
@property
|
||||
@@ -71,12 +87,25 @@ class GroupSerializer(ModelSerializer):
|
||||
return True
|
||||
return str(request.query_params.get("include_users", "true")).lower() == "true"
|
||||
|
||||
@property
|
||||
def _should_include_children(self) -> bool:
|
||||
request: Request = self.context.get("request", None)
|
||||
if not request:
|
||||
return True
|
||||
return str(request.query_params.get("include_children", "false")).lower() == "true"
|
||||
|
||||
@extend_schema_field(GroupMemberSerializer(many=True))
|
||||
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
|
||||
if not self._should_include_users:
|
||||
return None
|
||||
return GroupMemberSerializer(instance.users, many=True).data
|
||||
|
||||
@extend_schema_field(GroupChildSerializer(many=True))
|
||||
def get_children_obj(self, instance: Group) -> list[GroupChildSerializer] | None:
|
||||
if not self._should_include_children:
|
||||
return None
|
||||
return GroupChildSerializer(instance.children, many=True).data
|
||||
|
||||
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:
|
||||
@@ -126,11 +155,17 @@ class GroupSerializer(ModelSerializer):
|
||||
"attributes",
|
||||
"roles",
|
||||
"roles_obj",
|
||||
"children",
|
||||
"children_obj",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"users": {
|
||||
"default": list,
|
||||
},
|
||||
"children": {
|
||||
"required": False,
|
||||
"default": list,
|
||||
},
|
||||
# TODO: This field isn't unique on the database which is hard to backport
|
||||
# hence we just validate the uniqueness here
|
||||
"name": {"validators": [UniqueValidator(Group.objects.all())]},
|
||||
@@ -203,11 +238,15 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
Prefetch("users", queryset=User.objects.all().only("id"))
|
||||
)
|
||||
|
||||
if self.serializer_class(context={"request": self.request})._should_include_children:
|
||||
base_qs = base_qs.prefetch_related("children")
|
||||
|
||||
return base_qs
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("include_users", bool, default=True),
|
||||
OpenApiParameter("include_children", bool, default=False),
|
||||
]
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
@@ -216,6 +255,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("include_users", bool, default=True),
|
||||
OpenApiParameter("include_children", bool, default=False),
|
||||
]
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
|
||||
@@ -5,7 +5,7 @@ from json import loads
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import AnonymousUser, Permission
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
from django.urls import reverse_lazy
|
||||
@@ -16,6 +16,7 @@ from django.utils.translation import gettext as _
|
||||
from django_filters.filters import (
|
||||
BooleanFilter,
|
||||
CharFilter,
|
||||
IsoDateTimeFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
MultipleChoiceFilter,
|
||||
UUIDFilter,
|
||||
@@ -241,6 +242,7 @@ class UserSerializer(ModelSerializer):
|
||||
"type",
|
||||
"uuid",
|
||||
"password_change_date",
|
||||
"last_updated",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"allow_blank": True},
|
||||
@@ -331,6 +333,14 @@ class UsersFilter(FilterSet):
|
||||
method="filter_attributes",
|
||||
)
|
||||
|
||||
date_joined__lt = IsoDateTimeFilter(field_name="date_joined", lookup_expr="lt")
|
||||
date_joined = IsoDateTimeFilter(field_name="date_joined")
|
||||
date_joined__gt = IsoDateTimeFilter(field_name="date_joined", lookup_expr="gt")
|
||||
|
||||
last_updated__lt = IsoDateTimeFilter(field_name="last_updated", lookup_expr="lt")
|
||||
last_updated = IsoDateTimeFilter(field_name="last_updated")
|
||||
last_updated__gt = IsoDateTimeFilter(field_name="last_updated", lookup_expr="gt")
|
||||
|
||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
||||
uuid = UUIDFilter(field_name="uuid")
|
||||
|
||||
@@ -376,6 +386,8 @@ class UsersFilter(FilterSet):
|
||||
fields = [
|
||||
"username",
|
||||
"email",
|
||||
"date_joined",
|
||||
"last_updated",
|
||||
"name",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
@@ -390,10 +402,19 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
"""User Viewset"""
|
||||
|
||||
queryset = User.objects.none()
|
||||
ordering = ["username"]
|
||||
ordering = ["username", "date_joined", "last_updated"]
|
||||
serializer_class = UserSerializer
|
||||
filterset_class = UsersFilter
|
||||
search_fields = ["username", "name", "is_active", "email", "uuid", "attributes"]
|
||||
search_fields = [
|
||||
"username",
|
||||
"name",
|
||||
"is_active",
|
||||
"email",
|
||||
"uuid",
|
||||
"attributes",
|
||||
"date_joined",
|
||||
"last_updated",
|
||||
]
|
||||
|
||||
def get_ql_fields(self):
|
||||
from djangoql.schema import BoolField, StrField
|
||||
@@ -435,6 +456,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
user: User = self.get_object()
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
self.request._request.user = AnonymousUser()
|
||||
try:
|
||||
plan = planner.plan(
|
||||
self.request._request,
|
||||
|
||||
27
authentik/core/migrations/0050_user_last_updated_and_more.py
Normal file
27
authentik/core/migrations/0050_user_last_updated_and_more.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-15 15:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("authentik_core", "0049_alter_token_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="last_updated",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(fields=["last_updated"], name="authentik_c_last_up_ed7486_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="user",
|
||||
index=models.Index(fields=["date_joined"], name="authentik_c_date_jo_58c256_idx"),
|
||||
),
|
||||
]
|
||||
@@ -274,6 +274,8 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
||||
ak_groups = models.ManyToManyField("Group", related_name="users")
|
||||
password_change_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
class Meta:
|
||||
@@ -293,6 +295,8 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
||||
models.Index(fields=["uuid"]),
|
||||
models.Index(fields=["path"]),
|
||||
models.Index(fields=["type"]),
|
||||
models.Index(fields=["date_joined"]),
|
||||
models.Index(fields=["last_updated"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -21,7 +21,7 @@ from authentik.core.tests.utils import (
|
||||
create_test_flow,
|
||||
create_test_user,
|
||||
)
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.stages.email.models import EmailStage
|
||||
|
||||
@@ -103,8 +103,11 @@ class TestUsersAPI(APITestCase):
|
||||
self.assertTrue(self.admin.check_password(new_pw))
|
||||
|
||||
def test_recovery(self):
|
||||
"""Test user recovery link (no recovery flow set)"""
|
||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||
"""Test user recovery link"""
|
||||
flow = create_test_flow(
|
||||
FlowDesignation.RECOVERY,
|
||||
authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED,
|
||||
)
|
||||
brand: Brand = create_test_brand()
|
||||
brand.flow_recovery = flow
|
||||
brand.save()
|
||||
@@ -387,3 +390,72 @@ class TestUsersAPI(APITestCase):
|
||||
self.assertFalse(
|
||||
AuthenticatedSession.objects.filter(session__session_key=session_id).exists()
|
||||
)
|
||||
|
||||
def test_sort_by_last_updated(self):
|
||||
"""Test API sorting by last_updated"""
|
||||
User.objects.all().delete()
|
||||
admin = create_test_admin_user()
|
||||
self.client.force_login(admin)
|
||||
|
||||
user = create_test_user()
|
||||
admin.first_name = "Sample change"
|
||||
admin.last_name = "To trigger an update"
|
||||
admin.save()
|
||||
|
||||
# Ascending
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"ordering": "last_updated",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
self.assertEqual(body["results"][0]["pk"], user.pk)
|
||||
|
||||
# Descending
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"ordering": "-last_updated",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
self.assertEqual(body["results"][0]["pk"], admin.pk)
|
||||
|
||||
def test_sort_by_date_joined(self):
|
||||
"""Test API sorting by date_joined"""
|
||||
User.objects.all().delete()
|
||||
admin = create_test_admin_user()
|
||||
self.client.force_login(admin)
|
||||
|
||||
user = create_test_user()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"ordering": "date_joined",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
self.assertEqual(body["results"][0]["pk"], admin.pk)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:user-list"),
|
||||
data={
|
||||
"ordering": "-date_joined",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
body = loads(response.content)
|
||||
self.assertEqual(len(body["results"]), 2)
|
||||
self.assertEqual(body["results"][0]["pk"], user.pk)
|
||||
|
||||
@@ -55,6 +55,7 @@ class TestEnterpriseAudit(APITestCase):
|
||||
self.assertIsNotNone(event)
|
||||
self.assertIsNotNone(event.context["diff"])
|
||||
diff = event.context["diff"]
|
||||
diff.pop("last_updated")
|
||||
self.assertEqual(
|
||||
diff,
|
||||
{
|
||||
@@ -116,6 +117,7 @@ class TestEnterpriseAudit(APITestCase):
|
||||
self.assertIsNotNone(event)
|
||||
self.assertIsNotNone(event.context["diff"])
|
||||
diff = event.context["diff"]
|
||||
diff.pop("last_updated")
|
||||
self.assertEqual(
|
||||
diff,
|
||||
{
|
||||
|
||||
@@ -301,6 +301,7 @@ class SessionEndStage(ChallengeStageView):
|
||||
"flow_slug": self.request.brand.flow_invalidation.slug,
|
||||
},
|
||||
)
|
||||
|
||||
return SessionEndChallenge(data=data)
|
||||
|
||||
# This can never be reached since this challenge is created on demand and only the
|
||||
|
||||
@@ -42,7 +42,7 @@ class TestBindingsAPI(APITestCase):
|
||||
)
|
||||
|
||||
def test_invalid_too_little(self):
|
||||
"""Test invvalid binding (too little)"""
|
||||
"""Test invalid binding (too little)"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:policybinding-list"),
|
||||
data={"target": self.pbm.pk, "order": 0},
|
||||
|
||||
@@ -70,6 +70,7 @@ class OAuth2ProviderSerializer(ProviderSerializer):
|
||||
"signing_key",
|
||||
"encryption_key",
|
||||
"redirect_uris",
|
||||
"backchannel_logout_uri",
|
||||
"sub_mode",
|
||||
"property_mappings",
|
||||
"issuer_mode",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""OAuth/OpenID Constants"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"
|
||||
GRANT_TYPE_IMPLICIT = "implicit"
|
||||
GRANT_TYPE_REFRESH_TOKEN = "refresh_token" # nosec
|
||||
@@ -51,3 +54,23 @@ AMR_MFA = "mfa"
|
||||
AMR_OTP = "otp"
|
||||
AMR_WEBAUTHN = "user"
|
||||
AMR_SMART_CARD = "sc"
|
||||
|
||||
|
||||
class SubModes(models.TextChoices):
|
||||
"""Mode after which 'sub' attribute is generated, for compatibility reasons"""
|
||||
|
||||
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
|
||||
USER_ID = "user_id", _("Based on user ID")
|
||||
USER_UUID = "user_uuid", _("Based on user UUID")
|
||||
USER_USERNAME = "user_username", _("Based on the username")
|
||||
USER_EMAIL = (
|
||||
"user_email",
|
||||
_("Based on the User's Email. This is recommended over the UPN method."),
|
||||
)
|
||||
USER_UPN = (
|
||||
"user_upn",
|
||||
_(
|
||||
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
|
||||
"Use this method only if you have different UPN and Mail domains."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,10 +4,8 @@ from dataclasses import asdict, dataclass, field
|
||||
from hashlib import sha256
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.core.models import default_token_duration
|
||||
from authentik.events.signals import get_login_event
|
||||
@@ -18,6 +16,7 @@ from authentik.providers.oauth2.constants import (
|
||||
AMR_PASSWORD,
|
||||
AMR_SMART_CARD,
|
||||
AMR_WEBAUTHN,
|
||||
SubModes,
|
||||
)
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
@@ -30,26 +29,6 @@ def hash_session_key(session_key: str) -> str:
|
||||
return sha256(session_key.encode("ascii")).hexdigest()
|
||||
|
||||
|
||||
class SubModes(models.TextChoices):
|
||||
"""Mode after which 'sub' attribute is generated, for compatibility reasons"""
|
||||
|
||||
HASHED_USER_ID = "hashed_user_id", _("Based on the Hashed User ID")
|
||||
USER_ID = "user_id", _("Based on user ID")
|
||||
USER_UUID = "user_uuid", _("Based on user UUID")
|
||||
USER_USERNAME = "user_username", _("Based on the username")
|
||||
USER_EMAIL = (
|
||||
"user_email",
|
||||
_("Based on the User's Email. This is recommended over the UPN method."),
|
||||
)
|
||||
USER_UPN = (
|
||||
"user_upn",
|
||||
_(
|
||||
"Based on the User's UPN, only works if user has a 'upn' attribute set. "
|
||||
"Use this method only if you have different UPN and Mail domains."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class IDToken:
|
||||
"""The primary extension that OpenID Connect makes to OAuth 2.0 to enable End-Users to be
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-04 03:23
|
||||
|
||||
import authentik.lib.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0028_migrate_session"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="oauth2provider",
|
||||
name="backchannel_logout_uri",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
|
||||
verbose_name="Back-Channel Logout URI",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="oauth2provider",
|
||||
name="_redirect_uris",
|
||||
field=models.JSONField(default=list, verbose_name="Redirect URIs"),
|
||||
),
|
||||
]
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
from dataclasses import asdict, dataclass
|
||||
from functools import cached_property
|
||||
from hashlib import sha256
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
@@ -42,11 +42,14 @@ from authentik.core.models import (
|
||||
)
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.models import DomainlessURLValidator, SerializerModel
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.providers.oauth2.id_token import IDToken, SubModes
|
||||
from authentik.providers.oauth2.constants import SubModes
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@@ -193,9 +196,14 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
||||
default=generate_client_secret,
|
||||
)
|
||||
_redirect_uris = models.JSONField(
|
||||
default=dict,
|
||||
default=list,
|
||||
verbose_name=_("Redirect URIs"),
|
||||
)
|
||||
backchannel_logout_uri = models.TextField(
|
||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
||||
verbose_name=_("Back-Channel Logout URI"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
include_claims_in_id_token = models.BooleanField(
|
||||
default=True,
|
||||
@@ -480,13 +488,15 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
||||
return f"Access Token for {self.provider_id} for user {self.user_id}"
|
||||
|
||||
@property
|
||||
def id_token(self) -> IDToken:
|
||||
def id_token(self) -> "IDToken":
|
||||
"""Load ID Token from json"""
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
|
||||
raw_token = json.loads(self._id_token)
|
||||
return from_dict(IDToken, raw_token)
|
||||
|
||||
@id_token.setter
|
||||
def id_token(self, value: IDToken):
|
||||
def id_token(self, value: "IDToken"):
|
||||
self.token = value.to_access_token(self.provider)
|
||||
self._id_token = json.dumps(asdict(value))
|
||||
|
||||
@@ -531,13 +541,15 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
|
||||
return f"Refresh Token for {self.provider_id} for user {self.user_id}"
|
||||
|
||||
@property
|
||||
def id_token(self) -> IDToken:
|
||||
def id_token(self) -> "IDToken":
|
||||
"""Load ID Token from json"""
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
|
||||
raw_token = json.loads(self._id_token)
|
||||
return from_dict(IDToken, raw_token)
|
||||
|
||||
@id_token.setter
|
||||
def id_token(self, value: IDToken):
|
||||
def id_token(self, value: "IDToken"):
|
||||
self._id_token = json.dumps(asdict(value))
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
|
||||
from authentik.providers.oauth2.tasks import backchannel_logout_notification_dispatch
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
|
||||
def user_session_deleted_oauth_backchannel_logout_and_tokens_removal(
|
||||
sender, instance: AuthenticatedSession, **_
|
||||
):
|
||||
"""Revoke tokens upon user logout"""
|
||||
AccessToken.objects.filter(
|
||||
LOGGER.debug("Sending back-channel logout notifications signal!", session=instance)
|
||||
|
||||
access_tokens = AccessToken.objects.filter(
|
||||
user=instance.user,
|
||||
session__session__session_key=instance.session.session_key,
|
||||
).delete()
|
||||
)
|
||||
|
||||
backchannel_logout_notification_dispatch.send(
|
||||
revocations=[
|
||||
(token.provider_id, token.id_token.iss, token.session.user.uid)
|
||||
for token in access_tokens
|
||||
],
|
||||
)
|
||||
|
||||
access_tokens.delete()
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
|
||||
68
authentik/providers/oauth2/tasks.py
Normal file
68
authentik/providers/oauth2/tasks.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""OAuth2 Provider Tasks"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_dramatiq_postgres.middleware import CurrentTask
|
||||
from dramatiq.actor import actor
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
from authentik.providers.oauth2.utils import create_logout_token
|
||||
from authentik.tasks.models import Task
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@actor(description=_("Send a back-channel logout request to the registered client"))
|
||||
def send_backchannel_logout_request(provider_pk: int, iss: str, sub: str = None) -> bool:
|
||||
"""Send a back-channel logout request to the registered client
|
||||
|
||||
Args:
|
||||
provider_pk: The OAuth2 provider's primary key
|
||||
iss: The issuer URL for the logout token
|
||||
sub: The subject identifier to include in the logout token
|
||||
|
||||
Returns:
|
||||
bool: True if the request was sent successfully, False otherwise
|
||||
"""
|
||||
self: Task = CurrentTask.get_task()
|
||||
LOGGER.debug("Sending back-channel logout request", provider_pk=provider_pk, sub=sub)
|
||||
|
||||
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
|
||||
if provider is None:
|
||||
return
|
||||
|
||||
# Generate the logout token
|
||||
logout_token = create_logout_token(iss, provider, None, sub)
|
||||
|
||||
# Get the back-channel logout URI from the provider's dedicated backchannel_logout_uri field
|
||||
# Back-channel logout requires explicit configuration - no fallback to redirect URIs
|
||||
|
||||
backchannel_logout_uri = provider.backchannel_logout_uri
|
||||
if not backchannel_logout_uri:
|
||||
self.info("No back-channel logout URI found for provider")
|
||||
return
|
||||
|
||||
# Send the back-channel logout request
|
||||
response = get_http_session().post(
|
||||
backchannel_logout_uri,
|
||||
data={"logout_token": logout_token},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
allow_redirects=True,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
self.info("Back-channel logout successful", sub=sub)
|
||||
return True
|
||||
|
||||
|
||||
@actor(description=_("Handle backchannel logout notifications dispatched via signal"))
|
||||
def backchannel_logout_notification_dispatch(revocations: list, **kwargs):
|
||||
"""Handle backchannel logout notifications dispatched via signal"""
|
||||
for revocation in revocations:
|
||||
provider_pk, iss, sub = revocation
|
||||
provider = OAuth2Provider.objects.filter(pk=provider_pk).first()
|
||||
send_backchannel_logout_request.send_with_options(
|
||||
args=(provider_pk, iss, sub),
|
||||
rel_obj=provider,
|
||||
)
|
||||
@@ -81,4 +81,46 @@ class TestAPI(APITestCase):
|
||||
},
|
||||
)
|
||||
self.assertJSONEqual(response.content, {"redirect_uris": ["Invalid Regex Pattern: **"]})
|
||||
|
||||
def test_backchannel_logout_uri_validation(self):
|
||||
"""Test backchannel_logout_uri API validation"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:oauth2provider-list"),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"authorization_flow": create_test_flow().pk,
|
||||
"invalidation_flow": create_test_flow().pk,
|
||||
"redirect_uris": [
|
||||
{"matching_mode": "strict", "url": "http://goauthentik.io"},
|
||||
],
|
||||
"backchannel_logout_uri": "invalid-url",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_backchannel_logout_uri_create_and_retrieve(self):
|
||||
"""Test creating and retrieving backchannel logout URI"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:oauth2provider-list"),
|
||||
data={
|
||||
"name": generate_id(),
|
||||
"authorization_flow": create_test_flow().pk,
|
||||
"invalidation_flow": create_test_flow().pk,
|
||||
"redirect_uris": [
|
||||
{"matching_mode": "strict", "url": "http://goauthentik.io"},
|
||||
],
|
||||
"backchannel_logout_uri": "http://goauthentik.io/logout",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
provider_data = response.json()
|
||||
self.assertEqual(provider_data["backchannel_logout_uri"], "http://goauthentik.io/logout")
|
||||
|
||||
# Test retrieving the provider
|
||||
provider_pk = provider_data["pk"]
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:oauth2provider-detail", kwargs={"pk": provider_pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
retrieved_data = response.json()
|
||||
self.assertEqual(retrieved_data["backchannel_logout_uri"], "http://goauthentik.io/logout")
|
||||
|
||||
223
authentik/providers/oauth2/tests/test_backchannel_logout.py
Normal file
223
authentik/providers/oauth2/tests/test_backchannel_logout.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""Test OAuth2 Back-Channel Logout implementation"""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import jwt
|
||||
from django.test import RequestFactory
|
||||
from django.utils import timezone
|
||||
from dramatiq.results.errors import ResultFailure
|
||||
from requests import Response
|
||||
from requests.exceptions import HTTPError, Timeout
|
||||
|
||||
from authentik.core.models import Application, AuthenticatedSession, Session
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.id_token import hash_session_key
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
RefreshToken,
|
||||
)
|
||||
from authentik.providers.oauth2.tasks import send_backchannel_logout_request
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
from authentik.providers.oauth2.utils import create_logout_token
|
||||
|
||||
|
||||
class TestBackChannelLogout(OAuthTestCase):
|
||||
"""Test Back-Channel Logout functionality"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_test_admin_user()
|
||||
self.app = Application.objects.create(name=generate_id(), slug="test-app")
|
||||
self.provider = OAuth2Provider.objects.create(
|
||||
name=generate_id(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, "http://testserver/callback"),
|
||||
],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
self.app.provider = self.provider
|
||||
self.app.save()
|
||||
|
||||
def _create_session(self, session_key=None):
|
||||
"""Create a session with the given key or a generated one"""
|
||||
session_key = session_key or f"session-{generate_id()}"
|
||||
session = Session.objects.create(
|
||||
session_key=session_key,
|
||||
expires=timezone.now() + timezone.timedelta(hours=1),
|
||||
last_ip="255.255.255.255",
|
||||
)
|
||||
auth_session = AuthenticatedSession.objects.create(
|
||||
session=session,
|
||||
user=self.user,
|
||||
)
|
||||
return auth_session
|
||||
|
||||
def _create_token(
|
||||
self, provider, user, session=None, token_type="access", token_id=None
|
||||
): # nosec
|
||||
"""Create a token of the specified type"""
|
||||
token_id = token_id or f"{token_type}-token-{generate_id()}"
|
||||
kwargs = {
|
||||
"provider": provider,
|
||||
"user": user,
|
||||
"session": session,
|
||||
"token": token_id,
|
||||
"_id_token": "{}",
|
||||
"auth_time": timezone.now(),
|
||||
}
|
||||
|
||||
if token_type == "access": # nosec
|
||||
return AccessToken.objects.create(**kwargs)
|
||||
else: # refresh
|
||||
return RefreshToken.objects.create(**kwargs)
|
||||
|
||||
def _create_provider(self, name=None):
|
||||
"""Create an OAuth2 provider"""
|
||||
name = name or f"provider-{generate_id()}"
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name=name,
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris=[
|
||||
RedirectURI(RedirectURIMatchingMode.STRICT, f"http://{name}/callback"),
|
||||
],
|
||||
signing_key=self.keypair,
|
||||
)
|
||||
return provider
|
||||
|
||||
def _create_logout_token(
|
||||
self,
|
||||
provider: OAuth2Provider | None = None,
|
||||
session_id: str | None = None,
|
||||
sub: str | None = None,
|
||||
):
|
||||
"""Create a logout token with the given parameters"""
|
||||
provider = provider or self.provider
|
||||
|
||||
# Create a token with the same issuer that the view will expect
|
||||
# Use the same request object that will be used in the test
|
||||
request = self.factory.post("/backchannel_logout")
|
||||
|
||||
return create_logout_token(
|
||||
iss=provider.get_issuer(request),
|
||||
provider=provider,
|
||||
session_key=session_id,
|
||||
sub=sub,
|
||||
)
|
||||
|
||||
def _decode_token(self, token, provider=None):
|
||||
"""Helper to decode and validate a JWT token"""
|
||||
provider = provider or self.provider
|
||||
key, alg = provider.jwt_key
|
||||
if alg != "HS256":
|
||||
key = provider.signing_key.public_key
|
||||
return jwt.decode(
|
||||
token, key, algorithms=[alg], options={"verify_exp": False, "verify_aud": False}
|
||||
)
|
||||
|
||||
def test_create_logout_token_variants(self):
|
||||
"""Test creating logout tokens with different combinations of parameters"""
|
||||
# Test case 1: With session_id only
|
||||
session_id = "test-session-123"
|
||||
token1 = self._create_logout_token(session_id=session_id)
|
||||
decoded1 = self._decode_token(token1)
|
||||
|
||||
self.assertIn("iss", decoded1)
|
||||
self.assertEqual(decoded1["aud"], self.provider.client_id)
|
||||
self.assertIn("iat", decoded1)
|
||||
self.assertIn("jti", decoded1)
|
||||
self.assertEqual(decoded1["sid"], hash_session_key(session_id))
|
||||
self.assertIn("events", decoded1)
|
||||
self.assertIn("http://schemas.openid.net/event/backchannel-logout", decoded1["events"])
|
||||
self.assertNotIn("sub", decoded1)
|
||||
|
||||
# Test case 2: With sub only
|
||||
sub = "user-123"
|
||||
token2 = self._create_logout_token(sub=sub)
|
||||
decoded2 = self._decode_token(token2)
|
||||
|
||||
self.assertEqual(decoded2["sub"], sub)
|
||||
self.assertIn("events", decoded2)
|
||||
self.assertIn("http://schemas.openid.net/event/backchannel-logout", decoded2["events"])
|
||||
self.assertNotIn("sid", decoded2)
|
||||
|
||||
# Test case 3: With both session_id and sub
|
||||
token3 = self._create_logout_token(session_id=session_id, sub=sub)
|
||||
decoded3 = self._decode_token(token3)
|
||||
|
||||
self.assertEqual(decoded3["sid"], hash_session_key(session_id))
|
||||
self.assertEqual(decoded3["sub"], sub)
|
||||
self.assertIn("events", decoded3)
|
||||
|
||||
@patch("authentik.providers.oauth2.tasks.get_http_session")
|
||||
def test_send_backchannel_logout_request_scenarios(self, mock_get_session):
|
||||
"""Test various scenarios for backchannel logout request task"""
|
||||
# Setup provider with backchannel logout URI
|
||||
self.provider.backchannel_logout_uri = "http://testserver/backchannel_logout"
|
||||
self.provider.save()
|
||||
|
||||
# Setup mock session and response
|
||||
mock_session = Mock()
|
||||
mock_get_session.return_value = mock_session
|
||||
mock_response = Mock(spec=Response)
|
||||
mock_response.status_code = 200
|
||||
mock_response.raise_for_status.return_value = None # No exception for successful request
|
||||
mock_session.post.return_value = mock_response
|
||||
|
||||
result = send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver", sub="test-user-uid"
|
||||
)
|
||||
self.assertTrue(result)
|
||||
mock_session.post.assert_called_once()
|
||||
call_args = mock_session.post.call_args
|
||||
self.assertIn("logout_token", call_args[1]["data"])
|
||||
self.assertEqual(
|
||||
call_args[1]["headers"]["Content-Type"], "application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
# Scenario 2: Failed request (400 response) - should raise exception
|
||||
mock_session.post.reset_mock()
|
||||
error_response = Mock(spec=Response)
|
||||
error_response.status_code = 400
|
||||
error_response.raise_for_status.side_effect = HTTPError("HTTP 400")
|
||||
mock_session.post.return_value = error_response
|
||||
with self.assertRaises(ResultFailure):
|
||||
send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver", sub="test-user-uid"
|
||||
).get_result()
|
||||
|
||||
# Scenario 3: No URI configured
|
||||
mock_session.post.reset_mock()
|
||||
self.provider.backchannel_logout_uri = ""
|
||||
self.provider.save()
|
||||
result = send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver", sub="test-user-uid"
|
||||
).get_result()
|
||||
self.assertIsNone(result)
|
||||
mock_session.post.assert_not_called()
|
||||
|
||||
# Scenario 4: No sub provided - should fail
|
||||
result = send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver"
|
||||
).get_result()
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Scenario 5: Non-existent provider
|
||||
result = send_backchannel_logout_request.send(
|
||||
99999, "http://testserver", sub="test-user-uid"
|
||||
).get_result()
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Scenario 6: Request timeout
|
||||
mock_session.post.side_effect = Timeout("Request timed out")
|
||||
self.provider.backchannel_logout_uri = "http://testserver/backchannel_logout"
|
||||
self.provider.save()
|
||||
with self.assertRaises(ResultFailure):
|
||||
send_backchannel_logout_request.send(
|
||||
self.provider.pk, "http://testserver", sub="test-user-uid"
|
||||
).get_result()
|
||||
@@ -11,9 +11,9 @@ from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
IDToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
|
||||
@@ -10,11 +10,11 @@ from django.utils import timezone
|
||||
from authentik.core.models import Application, AuthenticatedSession, Session
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
ClientTypes,
|
||||
DeviceToken,
|
||||
IDToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
|
||||
@@ -11,9 +11,9 @@ from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import (
|
||||
AccessToken,
|
||||
IDToken,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
RedirectURIMatchingMode,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""OAuth2/OpenID Utils"""
|
||||
|
||||
import re
|
||||
import uuid
|
||||
from base64 import b64decode
|
||||
from binascii import Error
|
||||
from time import time
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -14,6 +16,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.core.middleware import CTX_AUTH_VIA, KEY_USER
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.providers.oauth2.errors import BearerTokenError
|
||||
from authentik.providers.oauth2.id_token import hash_session_key
|
||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
|
||||
|
||||
LOGGER = get_logger()
|
||||
@@ -211,3 +214,36 @@ class HttpResponseRedirectScheme(HttpResponseRedirect):
|
||||
) -> None:
|
||||
self.allowed_schemes = allowed_schemes or ["http", "https", "ftp"]
|
||||
super().__init__(redirect_to, *args, **kwargs)
|
||||
|
||||
|
||||
def create_logout_token(
|
||||
iss: str,
|
||||
provider: OAuth2Provider,
|
||||
session_key: str | None = None,
|
||||
sub: str | None = None,
|
||||
) -> str:
|
||||
"""Create a logout token for Back-Channel Logout
|
||||
|
||||
As per https://openid.net/specs/openid-connect-backchannel-1_0.html
|
||||
"""
|
||||
|
||||
LOGGER.debug("Creating logout token", provider=provider, session_key=session_key, sub=sub)
|
||||
|
||||
# Create the logout token payload
|
||||
payload = {
|
||||
"iss": str(iss),
|
||||
"aud": provider.client_id,
|
||||
"iat": int(time()),
|
||||
"jti": str(uuid.uuid4()),
|
||||
"events": {
|
||||
"http://schemas.openid.net/event/backchannel-logout": {},
|
||||
},
|
||||
}
|
||||
|
||||
# Add either sub or sid (or both)
|
||||
if sub:
|
||||
payload["sub"] = sub
|
||||
if session_key:
|
||||
payload["sid"] = hash_session_key(session_key)
|
||||
# Encode the token
|
||||
return provider.encode(payload)
|
||||
|
||||
@@ -9,7 +9,8 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.providers.oauth2.errors import TokenIntrospectionError
|
||||
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, RefreshToken
|
||||
from authentik.providers.oauth2.utils import TokenResponse, authenticate_provider
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -72,6 +72,8 @@ class ProviderInfoView(View):
|
||||
"device_authorization_endpoint": self.request.build_absolute_uri(
|
||||
reverse("authentik_providers_oauth2:device")
|
||||
),
|
||||
"backchannel_logout_supported": True,
|
||||
"backchannel_logout_session_supported": True,
|
||||
"response_types_supported": [
|
||||
ResponseTypes.CODE,
|
||||
ResponseTypes.ID_TOKEN,
|
||||
|
||||
@@ -190,6 +190,7 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
"sign_response",
|
||||
"sp_binding",
|
||||
"default_relay_state",
|
||||
"default_name_id_policy",
|
||||
"url_download_metadata",
|
||||
"url_sso_post",
|
||||
"url_sso_redirect",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.1.11 on 2025-06-18 09:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_saml", "0018_alter_samlprovider_acs_url"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="samlprovider",
|
||||
name="default_name_id_policy",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "Email"),
|
||||
("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "Persistent"),
|
||||
("urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName", "X509"),
|
||||
(
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName",
|
||||
"Windows",
|
||||
),
|
||||
("urn:oasis:names:tc:SAML:2.0:nameid-format:transient", "Transient"),
|
||||
("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "Unspecified"),
|
||||
],
|
||||
default="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -12,6 +12,7 @@ from authentik.core.models import PropertyMapping, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.models import DomainlessURLValidator
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.sources.saml.models import SAMLNameIDPolicy
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
ECDSA_SHA1,
|
||||
@@ -179,6 +180,9 @@ class SAMLProvider(Provider):
|
||||
default_relay_state = models.TextField(
|
||||
default="", blank=True, help_text=_("Default relay_state value for IDP-initiated logins")
|
||||
)
|
||||
default_name_id_policy = models.TextField(
|
||||
choices=SAMLNameIDPolicy.choices, default=SAMLNameIDPolicy.UNSPECIFIED
|
||||
)
|
||||
|
||||
sign_assertion = models.BooleanField(default=True)
|
||||
sign_response = models.BooleanField(default=False)
|
||||
|
||||
@@ -205,6 +205,13 @@ class AssertionProcessor:
|
||||
def get_name_id(self) -> Element:
|
||||
"""Get NameID Element"""
|
||||
name_id = Element(f"{{{NS_SAML_ASSERTION}}}NameID")
|
||||
# For requests that don't specify a NameIDPolicy, check if we
|
||||
# can fall back to the provider default
|
||||
if (
|
||||
self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_UNSPECIFIED
|
||||
and self.provider.default_name_id_policy != SAML_NAME_ID_FORMAT_UNSPECIFIED
|
||||
):
|
||||
self.auth_n_request.name_id_policy = self.provider.default_name_id_policy
|
||||
name_id.attrib["Format"] = self.auth_n_request.name_id_policy
|
||||
# persistent is used as a fallback, so always generate it
|
||||
persistent = self.http_request.user.uid
|
||||
|
||||
@@ -13,6 +13,7 @@ from authentik.lib.xml import lxml_from_string
|
||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||
from authentik.providers.saml.models import SAMLProvider
|
||||
from authentik.providers.saml.utils.encoding import decode_base64_and_inflate
|
||||
from authentik.sources.saml.models import SAMLNameIDPolicy
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
NS_MAP,
|
||||
@@ -175,7 +176,9 @@ class AuthNRequestParser:
|
||||
|
||||
def idp_initiated(self) -> AuthNRequest:
|
||||
"""Create IdP Initiated AuthNRequest"""
|
||||
relay_state = None
|
||||
request = AuthNRequest(relay_state=None)
|
||||
if self.provider.default_relay_state != "":
|
||||
relay_state = self.provider.default_relay_state
|
||||
return AuthNRequest(relay_state=relay_state)
|
||||
request.relay_state = self.provider.default_relay_state
|
||||
if self.provider.default_name_id_policy != SAMLNameIDPolicy.UNSPECIFIED:
|
||||
request.name_id_policy = self.provider.default_name_id_policy
|
||||
return request
|
||||
|
||||
@@ -13,6 +13,7 @@ from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.utils.encoding import PEM_FOOTER, PEM_HEADER
|
||||
from authentik.sources.saml.models import SAMLNameIDPolicy
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
NS_MAP,
|
||||
NS_SAML_METADATA,
|
||||
@@ -46,6 +47,7 @@ class ServiceProviderMetadata:
|
||||
|
||||
auth_n_request_signed: bool
|
||||
assertion_signed: bool
|
||||
name_id_policy: SAMLNameIDPolicy
|
||||
|
||||
signing_keypair: CertificateKeyPair | None = None
|
||||
|
||||
@@ -60,6 +62,7 @@ class ServiceProviderMetadata:
|
||||
provider.issuer = self.entity_id
|
||||
provider.sp_binding = self.acs_binding
|
||||
provider.acs_url = self.acs_location
|
||||
provider.default_name_id_policy = self.name_id_policy
|
||||
if self.signing_keypair and self.auth_n_request_signed:
|
||||
self.signing_keypair.name = f"Provider {name} - SAML Signing Certificate"
|
||||
self.signing_keypair.save()
|
||||
@@ -148,6 +151,11 @@ class ServiceProviderMetadataParser:
|
||||
if signing_keypair:
|
||||
self.check_signature(root, signing_keypair)
|
||||
|
||||
name_id_format = descriptor.findall(f"{{{NS_SAML_METADATA}}}NameIDFormat")
|
||||
name_id_policy = SAMLNameIDPolicy.UNSPECIFIED
|
||||
if len(name_id_format) > 0:
|
||||
name_id_policy = SAMLNameIDPolicy(name_id_format[0].text)
|
||||
|
||||
return ServiceProviderMetadata(
|
||||
entity_id=entity_id,
|
||||
acs_binding=acs_binding,
|
||||
@@ -155,4 +163,5 @@ class ServiceProviderMetadataParser:
|
||||
auth_n_request_signed=auth_n_request_signed,
|
||||
assertion_signed=assertion_signed,
|
||||
signing_keypair=signing_keypair,
|
||||
name_id_policy=name_id_policy,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
cacheDuration="PT604800S"
|
||||
entityID="http://localhost:8080/saml/metadata">
|
||||
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
|
||||
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
|
||||
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
Location="http://localhost:8080/saml/acs"
|
||||
index="1" />
|
||||
|
||||
@@ -14,6 +14,7 @@ from authentik.lib.xml import lxml_from_string
|
||||
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
||||
from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
|
||||
from authentik.sources.saml.models import SAMLNameIDPolicy
|
||||
from authentik.sources.saml.processors.constants import ECDSA_SHA256, NS_MAP, NS_SAML_METADATA
|
||||
|
||||
|
||||
@@ -86,6 +87,7 @@ class TestServiceProviderMetadataParser(TestCase):
|
||||
self.assertEqual(provider.acs_url, "http://localhost:8080/saml/acs")
|
||||
self.assertEqual(provider.issuer, "http://localhost:8080/saml/metadata")
|
||||
self.assertEqual(provider.sp_binding, SAMLBindings.POST)
|
||||
self.assertEqual(provider.default_name_id_policy, SAMLNameIDPolicy.EMAIL)
|
||||
self.assertEqual(
|
||||
len(provider.property_mappings.all()),
|
||||
len(SAMLPropertyMapping.objects.exclude(managed__isnull=True)),
|
||||
|
||||
@@ -75,7 +75,9 @@ TENANT_APPS = [
|
||||
"pgtrigger",
|
||||
"authentik.admin",
|
||||
"authentik.api",
|
||||
"authentik.core",
|
||||
"authentik.crypto",
|
||||
"authentik.enterprise",
|
||||
"authentik.events",
|
||||
"authentik.flows",
|
||||
"authentik.outposts",
|
||||
@@ -171,6 +173,7 @@ SPECTACULAR_SETTINGS = {
|
||||
"PromptTypeEnum": "authentik.stages.prompt.models.FieldTypes",
|
||||
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
|
||||
"TaskAggregatedStatusEnum": "authentik.tasks.models.TaskStatus",
|
||||
"SAMLNameIDPolicyEnum": "authentik.sources.saml.models.SAMLNameIDPolicy",
|
||||
"UserTypeEnum": "authentik.core.models.UserTypes",
|
||||
"UserVerificationEnum": "authentik.stages.authenticator_webauthn.models.UserVerification",
|
||||
},
|
||||
@@ -245,10 +248,12 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
|
||||
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
|
||||
|
||||
MIDDLEWARE_FIRST = [
|
||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||
]
|
||||
MIDDLEWARE = [
|
||||
"django_tenants.middleware.default.DefaultTenantMiddleware",
|
||||
"authentik.root.middleware.LoggingMiddleware",
|
||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||
"authentik.root.middleware.ClientIPMiddleware",
|
||||
"authentik.stages.user_login.middleware.BoundSessionMiddleware",
|
||||
"authentik.core.middleware.AuthenticationMiddleware",
|
||||
@@ -261,6 +266,8 @@ MIDDLEWARE = [
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"authentik.core.middleware.ImpersonateMiddleware",
|
||||
]
|
||||
MIDDLEWARE_LAST = [
|
||||
"django_prometheus.middleware.PrometheusAfterMiddleware",
|
||||
]
|
||||
|
||||
@@ -496,7 +503,9 @@ _DISALLOWED_ITEMS = [
|
||||
"SHARED_APPS",
|
||||
"TENANT_APPS",
|
||||
"INSTALLED_APPS",
|
||||
"MIDDLEWARE_FIRST",
|
||||
"MIDDLEWARE",
|
||||
"MIDDLEWARE_LAST",
|
||||
"AUTHENTICATION_BACKENDS",
|
||||
"SPECTACULAR_SETTINGS",
|
||||
"REST_FRAMEWORK",
|
||||
@@ -514,16 +523,35 @@ SILENCED_SYSTEM_CHECKS = [
|
||||
]
|
||||
|
||||
|
||||
def _update_settings(app_path: str):
|
||||
def subtract_list(a: list, b: list) -> list:
|
||||
return [item for item in a if item not in b]
|
||||
|
||||
|
||||
def _filter_and_update(apps: list[str]) -> None:
|
||||
for _app in set(apps):
|
||||
if not _app.startswith("authentik"):
|
||||
continue
|
||||
_update_settings(f"{_app}.settings")
|
||||
|
||||
|
||||
def _update_settings(app_path: str) -> None:
|
||||
try:
|
||||
settings_module = importlib.import_module(app_path)
|
||||
CONFIG.log("debug", "Loaded app settings", path=app_path)
|
||||
SHARED_APPS.extend(getattr(settings_module, "SHARED_APPS", []))
|
||||
TENANT_APPS.extend(getattr(settings_module, "TENANT_APPS", []))
|
||||
|
||||
new_shared_apps = subtract_list(getattr(settings_module, "SHARED_APPS", []), SHARED_APPS)
|
||||
new_tenant_apps = subtract_list(getattr(settings_module, "TENANT_APPS", []), TENANT_APPS)
|
||||
SHARED_APPS.extend(new_shared_apps)
|
||||
TENANT_APPS.extend(new_tenant_apps)
|
||||
_filter_and_update(new_shared_apps + new_tenant_apps)
|
||||
|
||||
MIDDLEWARE_FIRST.extend(getattr(settings_module, "MIDDLEWARE_FIRST", []))
|
||||
MIDDLEWARE.extend(getattr(settings_module, "MIDDLEWARE", []))
|
||||
|
||||
AUTHENTICATION_BACKENDS.extend(getattr(settings_module, "AUTHENTICATION_BACKENDS", []))
|
||||
SPECTACULAR_SETTINGS.update(getattr(settings_module, "SPECTACULAR_SETTINGS", {}))
|
||||
REST_FRAMEWORK.update(getattr(settings_module, "REST_FRAMEWORK", {}))
|
||||
|
||||
for _attr in dir(settings_module):
|
||||
if not _attr.startswith("__") and _attr not in _DISALLOWED_ITEMS:
|
||||
globals()[_attr] = getattr(settings_module, _attr)
|
||||
@@ -538,26 +566,13 @@ if DEBUG:
|
||||
SHARED_APPS.insert(SHARED_APPS.index("django.contrib.staticfiles"), "daphne")
|
||||
enable_debug_trace(True)
|
||||
|
||||
TENANT_APPS.append("authentik.core")
|
||||
|
||||
CONFIG.log("info", "Booting authentik", version=__version__)
|
||||
|
||||
# Attempt to load enterprise app, if available
|
||||
try:
|
||||
importlib.import_module("authentik.enterprise.apps")
|
||||
CONFIG.log("info", "Enabled authentik enterprise")
|
||||
TENANT_APPS.append("authentik.enterprise")
|
||||
_update_settings("authentik.enterprise.settings")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# Load subapps's settings
|
||||
for _app in set(SHARED_APPS + TENANT_APPS):
|
||||
if not _app.startswith("authentik"):
|
||||
continue
|
||||
_update_settings(f"{_app}.settings")
|
||||
_filter_and_update(SHARED_APPS + TENANT_APPS)
|
||||
_update_settings("data.user_settings")
|
||||
|
||||
MIDDLEWARE = list(OrderedDict.fromkeys(MIDDLEWARE_FIRST + MIDDLEWARE + MIDDLEWARE_LAST))
|
||||
SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS))
|
||||
INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS))
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.1.11 on 2025-06-18 09:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_sources_saml", "0019_migrate_usersamlsourceconnection_identifier"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="samlsource",
|
||||
name="name_id_policy",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "Email"),
|
||||
("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "Persistent"),
|
||||
("urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName", "X509"),
|
||||
(
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName",
|
||||
"Windows",
|
||||
),
|
||||
("urn:oasis:names:tc:SAML:2.0:nameid-format:transient", "Transient"),
|
||||
("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "Unspecified"),
|
||||
],
|
||||
default="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
|
||||
help_text="NameID Policy sent to the IdP. Can be unset, in which case no Policy is sent.",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -39,6 +39,7 @@ from authentik.sources.saml.processors.constants import (
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||
SAML_NAME_ID_FORMAT_TRANSIENT,
|
||||
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||
SAML_NAME_ID_FORMAT_WINDOWS,
|
||||
SAML_NAME_ID_FORMAT_X509,
|
||||
SHA1,
|
||||
@@ -73,6 +74,7 @@ class SAMLNameIDPolicy(models.TextChoices):
|
||||
X509 = SAML_NAME_ID_FORMAT_X509
|
||||
WINDOWS = SAML_NAME_ID_FORMAT_WINDOWS
|
||||
TRANSIENT = SAML_NAME_ID_FORMAT_TRANSIENT
|
||||
UNSPECIFIED = SAML_NAME_ID_FORMAT_UNSPECIFIED
|
||||
|
||||
|
||||
class SAMLSource(Source):
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -44,6 +44,8 @@ class EmailStageSerializer(StageSerializer):
|
||||
"subject",
|
||||
"template",
|
||||
"activate_user_on_success",
|
||||
"recovery_max_attempts",
|
||||
"recovery_cache_timeout",
|
||||
]
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.11 on 2025-07-23 11:26
|
||||
|
||||
import authentik.lib.utils.time
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_email", "0005_alter_emailstage_token_expiry"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="emailstage",
|
||||
name="recovery_cache_timeout",
|
||||
field=models.TextField(
|
||||
default="minutes=5",
|
||||
help_text="The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="emailstage",
|
||||
name="recovery_max_attempts",
|
||||
field=models.PositiveIntegerField(default=5),
|
||||
),
|
||||
]
|
||||
@@ -16,6 +16,8 @@ from authentik.flows.models import Stage
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
|
||||
EMAIL_RECOVERY_MAX_ATTEMPTS = 5
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@@ -70,6 +72,17 @@ class EmailStage(Stage):
|
||||
use_ssl = models.BooleanField(default=False)
|
||||
timeout = models.IntegerField(default=10)
|
||||
from_address = models.EmailField(default="system@authentik.local")
|
||||
recovery_max_attempts = models.PositiveIntegerField(default=EMAIL_RECOVERY_MAX_ATTEMPTS)
|
||||
recovery_cache_timeout = models.TextField(
|
||||
default="minutes=5",
|
||||
validators=[timedelta_string_validator],
|
||||
help_text=_(
|
||||
"The time window used to count recent account recovery attempts. "
|
||||
"If the number of attempts exceed recovery_max_attempts within "
|
||||
"this period, further attempts will be rate-limited. "
|
||||
"(Format: hours=1;minutes=2;seconds=3)."
|
||||
),
|
||||
)
|
||||
|
||||
activate_user_on_success = models.BooleanField(
|
||||
default=False, help_text=_("Activate users upon completion of stage.")
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""authentik multi-stage authentication engine"""
|
||||
|
||||
from datetime import timedelta
|
||||
import math
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from hashlib import sha256
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
@@ -27,6 +30,8 @@ from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
EMAIL_RECOVERY_CACHE_KEY = "goauthentik.io/stages/email/stage/"
|
||||
|
||||
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
|
||||
PLAN_CONTEXT_EMAIL_OVERRIDE = "email"
|
||||
|
||||
@@ -170,10 +175,66 @@ class EmailStageView(ChallengeStageView):
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
return super().challenge_invalid(response)
|
||||
|
||||
def _get_cache_key(self) -> str:
|
||||
"""Return the cache key used for rate limiting email recovery attempts."""
|
||||
user = self.get_pending_user()
|
||||
user_email_hashed = sha256(user.email.lower().encode("utf-8")).hexdigest()
|
||||
return EMAIL_RECOVERY_CACHE_KEY + user_email_hashed
|
||||
|
||||
def _is_rate_limited(self) -> int | None:
|
||||
"""Check whether the email recovery attempt should be rate limited.
|
||||
|
||||
If the request should be rate limited, update the cache and return the
|
||||
remaining time in minutes before the user is allowed to try again.
|
||||
Otherwise, return None."""
|
||||
cache_key = self._get_cache_key()
|
||||
attempts = cache.get(cache_key, [])
|
||||
|
||||
stage = self.executor.current_stage
|
||||
stage.refresh_from_db()
|
||||
max_attempts = stage.recovery_max_attempts
|
||||
cache_timeout_delta = timedelta_from_string(stage.recovery_cache_timeout)
|
||||
|
||||
_now = now()
|
||||
start_window = _now - cache_timeout_delta
|
||||
|
||||
# Convert unix timestamps to datetime objects for comparison
|
||||
recent_attempts_in_window = [
|
||||
datetime.fromtimestamp(attempt, UTC)
|
||||
for attempt in attempts
|
||||
if datetime.fromtimestamp(attempt, UTC) > start_window
|
||||
]
|
||||
|
||||
if len(recent_attempts_in_window) >= max_attempts:
|
||||
retry_after = (min(recent_attempts_in_window) + cache_timeout_delta) - _now
|
||||
minutes_left = max(1, math.ceil(retry_after.total_seconds() / 60))
|
||||
return minutes_left
|
||||
|
||||
recent_attempts_in_window.append(_now)
|
||||
|
||||
# Convert datetime objects back to unix timestamps to update cache
|
||||
recent_attempts_in_window = [attempt.timestamp() for attempt in recent_attempts_in_window]
|
||||
|
||||
cache.set(
|
||||
cache_key,
|
||||
recent_attempts_in_window,
|
||||
int(cache_timeout_delta.total_seconds()),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def challenge_invalid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
if minutes_left := self._is_rate_limited():
|
||||
error = _(
|
||||
"Too many account verification attempts. Please try again after {minutes} minutes."
|
||||
).format(minutes=minutes_left)
|
||||
messages.error(self.request, error)
|
||||
return super().challenge_invalid(response)
|
||||
|
||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||
messages.error(self.request, _("No pending user."))
|
||||
return super().challenge_invalid(response)
|
||||
|
||||
self.send_email()
|
||||
messages.success(self.request, _("Email Successfully sent."))
|
||||
# We can't call stage_ok yet, as we're still waiting
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""email tests"""
|
||||
|
||||
from hashlib import sha256
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.locmem import EmailBackend
|
||||
from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
|
||||
@@ -9,6 +11,7 @@ from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.flows.markers import StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
|
||||
@@ -17,6 +20,7 @@ from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_PLAN, FlowExecutorView
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.stages.consent.stage import SESSION_KEY_CONSENT_TOKEN
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.stage import PLAN_CONTEXT_EMAIL_OVERRIDE, EmailStageView
|
||||
@@ -291,3 +295,173 @@ class TestEmailStage(FlowTestCase):
|
||||
stage_view.get_full_url(**{QS_KEY_TOKEN: token}),
|
||||
f"http://testserver/if/flow/{self.flow.slug}/?foo=bar&flow_token={token}",
|
||||
)
|
||||
|
||||
def test_get_cache_key(self):
|
||||
"""Test to ensure that the correct cache key is returned."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
request = self.factory.post(url)
|
||||
request.user = self.user
|
||||
request.session = session
|
||||
|
||||
executor = FlowExecutorView(request=request, flow=self.flow)
|
||||
executor.plan = plan
|
||||
|
||||
stage_view = EmailStageView(executor, request=request)
|
||||
|
||||
cache_key = stage_view._get_cache_key()
|
||||
|
||||
expected_hash = sha256(self.user.email.lower().encode("utf-8")).hexdigest()
|
||||
expected_cache_key = "goauthentik.io/stages/email/stage/" + expected_hash
|
||||
|
||||
self.assertEqual(cache_key, expected_cache_key)
|
||||
|
||||
def test_is_rate_limited_returns_none(self):
|
||||
"""Test to ensure None is returned if the request shouldn't be rate limited."""
|
||||
self.stage.recovery_max_attempts = 2
|
||||
self.stage.recovery_cache_timeout = "minutes=10"
|
||||
self.stage.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
request = self.factory.post(url)
|
||||
request.user = self.user
|
||||
request.session = session
|
||||
|
||||
executor = FlowExecutorView(request=request, flow=self.flow)
|
||||
executor.current_stage = self.stage
|
||||
executor.plan = plan
|
||||
|
||||
stage_view = EmailStageView(executor, request=request)
|
||||
|
||||
result = stage_view._is_rate_limited()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_is_rate_limited_returns_remaining_time(self):
|
||||
"""Test to ensure the remaining time is returned if the request
|
||||
should be rate limited."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
request = self.factory.post(url)
|
||||
request.user = self.user
|
||||
request.session = session
|
||||
|
||||
executor = FlowExecutorView(request=request, flow=self.flow)
|
||||
executor.current_stage = self.stage
|
||||
executor.plan = plan
|
||||
|
||||
stage_view = EmailStageView(executor, request=request)
|
||||
|
||||
test_cases = [
|
||||
# 2 attempts within 2 minutes
|
||||
(2, "seconds=120", 2),
|
||||
# 4 attempts within 5 minutes
|
||||
(4, "minutes=5", 5),
|
||||
# 6 attempts within 5 minutes. Although 299 seconds is less than
|
||||
# 5 minutes, the user is intentionally shown "5 minutes". This is
|
||||
# because an initial rate limiting message like "Try again after 4 minutes"
|
||||
# can be confusing.
|
||||
(6, "seconds=299", 5),
|
||||
]
|
||||
for test_case in test_cases:
|
||||
max_attempts, cache_timeout, minutes_remaining = test_case
|
||||
with self.subTest(
|
||||
f"Test recovery with {max_attempts} max attempts and "
|
||||
f"{cache_timeout} cache timeout seconds"
|
||||
):
|
||||
self.stage.recovery_max_attempts = max_attempts
|
||||
self.stage.recovery_cache_timeout = cache_timeout
|
||||
self.stage.save()
|
||||
|
||||
# Simulate multiple requests
|
||||
for _ in range(max_attempts):
|
||||
stage_view._is_rate_limited()
|
||||
|
||||
# The following request should be rate-limited
|
||||
result = stage_view._is_rate_limited()
|
||||
|
||||
self.assertEqual(result, minutes_remaining)
|
||||
|
||||
def _challenge_invalid_helper(self):
|
||||
"""Helper to test the challenge_invalid() method."""
|
||||
self.stage.recovery_max_attempts = 1
|
||||
self.stage.recovery_cache_timeout = "seconds=300"
|
||||
self.stage.save()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
request = get_request(url, user=self.user)
|
||||
request.session = session
|
||||
|
||||
request.brand = Brand.objects.create(domain="foo-domain.com", default=True)
|
||||
|
||||
executor = FlowExecutorView(request=request, flow=self.flow)
|
||||
executor.current_stage = self.stage
|
||||
executor.plan = plan
|
||||
|
||||
stage_view = EmailStageView(executor, request=request)
|
||||
challenge_response = stage_view.get_response_instance(data={})
|
||||
challenge_response.is_valid()
|
||||
|
||||
return challenge_response, stage_view, request
|
||||
|
||||
def test_challenge_invalid_not_rate_limited(self):
|
||||
"""Tests that the request is not rate limited and email is sent."""
|
||||
challenge_response, stage_view, request = self._challenge_invalid_helper()
|
||||
|
||||
with patch.object(stage_view, "send_email") as mock_send_email:
|
||||
result = stage_view.challenge_invalid(challenge_response)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
mock_send_email.assert_called_once()
|
||||
|
||||
message_list = list(messages.get_messages(request))
|
||||
self.assertEqual(len(message_list), 1)
|
||||
self.assertEqual(
|
||||
"Email Successfully sent.",
|
||||
message_list[-1].message,
|
||||
)
|
||||
|
||||
def test_challenge_invalid_returns_error_if_rate_limited(self):
|
||||
"""Tests that an error is returned if the request is rate limited. Ensure
|
||||
that an email is not sent."""
|
||||
challenge_response, stage_view, request = self._challenge_invalid_helper()
|
||||
|
||||
# Initial request that shouldn't be rate limited
|
||||
stage_view.challenge_invalid(challenge_response)
|
||||
|
||||
with patch.object(stage_view, "send_email") as mock_send_email:
|
||||
# This next request should be rate limited
|
||||
result = stage_view.challenge_invalid(challenge_response)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
mock_send_email.assert_not_called()
|
||||
|
||||
message_list = list(messages.get_messages(request))
|
||||
self.assertEqual(len(message_list), 2)
|
||||
self.assertEqual(
|
||||
"Too many account verification attempts. Please try again after 5 minutes.",
|
||||
message_list[-1].message,
|
||||
)
|
||||
|
||||
@@ -61,6 +61,8 @@ entries:
|
||||
subject: authentik
|
||||
template: email/password_reset.html
|
||||
activate_user_on_success: true
|
||||
recovery_max_attempts: 5
|
||||
recovery_cache_timeout: minutes=5
|
||||
- identifiers:
|
||||
name: default-recovery-user-write
|
||||
id: default-recovery-user-write
|
||||
|
||||
@@ -4689,6 +4689,14 @@
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "Roles"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "Children"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@@ -8465,6 +8473,10 @@
|
||||
},
|
||||
"title": "Redirect uris"
|
||||
},
|
||||
"backchannel_logout_uri": {
|
||||
"type": "string",
|
||||
"title": "Back-Channel Logout URI"
|
||||
},
|
||||
"sub_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -9287,6 +9299,18 @@
|
||||
"type": "string",
|
||||
"title": "Default relay state",
|
||||
"description": "Default relay_state value for IDP-initiated logins"
|
||||
},
|
||||
"default_name_id_policy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||
],
|
||||
"title": "Default name id policy"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
@@ -11714,7 +11738,8 @@
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName",
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
|
||||
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
|
||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||
],
|
||||
"title": "Name id policy",
|
||||
"description": "NameID Policy sent to the IdP. Can be unset, in which case no Policy is sent."
|
||||
@@ -14311,6 +14336,18 @@
|
||||
"type": "boolean",
|
||||
"title": "Activate user on success",
|
||||
"description": "Activate users upon completion of stage."
|
||||
},
|
||||
"recovery_max_attempts": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 2147483647,
|
||||
"title": "Recovery max attempts"
|
||||
},
|
||||
"recovery_cache_timeout": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Recovery cache timeout",
|
||||
"description": "The time window used to count recent account recovery attempts. If the number of attempts exceed recovery_max_attempts within this period, further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3)."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
||||
19
go.mod
19
go.mod
@@ -5,12 +5,12 @@ go 1.24.0
|
||||
require (
|
||||
beryju.io/ldap v0.1.0
|
||||
github.com/avast/retry-go/v4 v4.6.1
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/getsentry/sentry-go v0.34.1
|
||||
github.com/coreos/go-oidc/v3 v3.15.0
|
||||
github.com/getsentry/sentry-go v0.35.0
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/go-openapi/runtime v0.28.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
@@ -22,14 +22,14 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.8.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/sethvargo/go-envconfig v1.3.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025064.2
|
||||
goauthentik.io/api/v3 v3.2025064.6
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.16.0
|
||||
@@ -69,18 +69,17 @@ require (
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
36
go.sum
36
go.sum
@@ -16,8 +16,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
|
||||
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -26,8 +26,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/getsentry/sentry-go v0.34.1 h1:HSjc1C/OsnZttohEPrrqKH42Iud0HuLCXpv8cU1pWcw=
|
||||
github.com/getsentry/sentry-go v0.34.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
|
||||
github.com/getsentry/sentry-go v0.35.0 h1:+FJNlnjJsZMG3g0/rmmP7GiKjQoUF5EXfEtBwtPtkzY=
|
||||
github.com/getsentry/sentry-go v0.35.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
@@ -67,8 +67,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
@@ -140,14 +140,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
@@ -185,8 +185,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025064.2 h1:WFXe12hfsRe29EkLCxWCvrdK6peAkCA6ftdEh04hKLg=
|
||||
goauthentik.io/api/v3 v3.2025064.2/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
goauthentik.io/api/v3 v3.2025064.6 h1:s9DaQ8x93T9IjDBqSX69VTuB5kBH3nHyI3/2Mlhlf08=
|
||||
goauthentik.io/api/v3 v3.2025064.6/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
@@ -211,8 +211,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -17,6 +17,7 @@ type LDAPGroup struct {
|
||||
Uid string
|
||||
GidNumber string
|
||||
Member []string
|
||||
MemberOf []string
|
||||
IsSuperuser bool
|
||||
IsVirtualGroup bool
|
||||
Attributes map[string]interface{}
|
||||
@@ -38,6 +39,7 @@ func (lg *LDAPGroup) Entry() *ldap.Entry {
|
||||
"ak-superuser": {strconv.FormatBool(lg.IsSuperuser)},
|
||||
"objectClass": objectClass,
|
||||
"member": lg.Member,
|
||||
"memberOf": lg.MemberOf,
|
||||
"cn": {lg.CN},
|
||||
"uid": {lg.Uid},
|
||||
"sAMAccountName": {lg.CN},
|
||||
@@ -52,7 +54,8 @@ func FromAPIGroup(g api.Group, si server.LDAPServerInstance) *LDAPGroup {
|
||||
CN: g.Name,
|
||||
Uid: string(g.Pk),
|
||||
GidNumber: si.GetGroupGidNumber(g),
|
||||
Member: si.UsersForGroup(g),
|
||||
Member: si.MembersForGroup(g),
|
||||
MemberOf: si.MemberOfForGroup(g),
|
||||
IsVirtualGroup: false,
|
||||
IsSuperuser: *g.IsSuperuser,
|
||||
Attributes: g.Attributes,
|
||||
|
||||
@@ -155,7 +155,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
if needGroups {
|
||||
errs.Go(func() error {
|
||||
gapisp := sentry.StartSpan(errCtx, "authentik.providers.ldap.search.api_group")
|
||||
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()).IncludeUsers(true), parsedFilter, false)
|
||||
searchReq, skip := utils.ParseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()).IncludeUsers(true).IncludeChildren(true), parsedFilter, false)
|
||||
if skip {
|
||||
req.Log().Trace("Skip backend request")
|
||||
return nil
|
||||
|
||||
@@ -165,7 +165,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
for _, u := range g.UsersObj {
|
||||
if flag.UserPk == u.Pk {
|
||||
// TODO: Is there a better way to clone this object?
|
||||
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, g.ParentName, []api.GroupMember{u}, []api.Role{})
|
||||
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, g.ParentName, []api.GroupMember{u}, []api.Role{}, []api.GroupChild{})
|
||||
fg.SetUsers([]int32{flag.UserPk})
|
||||
if g.Parent.IsSet() {
|
||||
if p := g.Parent.Get(); p != nil {
|
||||
|
||||
@@ -32,7 +32,8 @@ type LDAPServerInstance interface {
|
||||
GetUserGidNumber(api.User) string
|
||||
GetGroupGidNumber(api.Group) string
|
||||
|
||||
UsersForGroup(api.Group) []string
|
||||
MembersForGroup(api.Group) []string
|
||||
MemberOfForGroup(api.Group) []string
|
||||
|
||||
GetFlags(dn string) *flags.UserFlags
|
||||
SetFlags(dn string, flags *flags.UserFlags)
|
||||
|
||||
@@ -15,12 +15,27 @@ func (pi *ProviderInstance) GroupsForUser(user api.User) []string {
|
||||
return groups
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) UsersForGroup(group api.Group) []string {
|
||||
func (pi *ProviderInstance) MembersForGroup(group api.Group) []string {
|
||||
users := make([]string, len(group.UsersObj))
|
||||
for i, user := range group.UsersObj {
|
||||
users[i] = pi.GetUserDN(user.Username)
|
||||
}
|
||||
return users
|
||||
children := make([]string, len(group.ChildrenObj))
|
||||
for i, child := range group.ChildrenObj {
|
||||
children[i] = pi.GetGroupDN(child.Name)
|
||||
}
|
||||
return append(users, children...)
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) MemberOfForGroup(group api.Group) []string {
|
||||
if group.ParentName.IsSet() {
|
||||
parent := group.ParentName.Get()
|
||||
if parent != nil {
|
||||
return []string{pi.GetGroupDN(*group.ParentName.Get())}
|
||||
}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetUserDN(user string) string {
|
||||
|
||||
@@ -37,11 +37,16 @@ ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
LABEL org.opencontainers.image.url=https://goauthentik.io
|
||||
LABEL org.opencontainers.image.description="goauthentik.io LDAP outpost, see https://goauthentik.io for more info."
|
||||
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
|
||||
LABEL org.opencontainers.image.version=${VERSION}
|
||||
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
|
||||
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.description="goauthentik.io LDAP outpost, see https://goauthentik.io for more info." \
|
||||
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
|
||||
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
|
||||
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
|
||||
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
|
||||
org.opencontainers.image.title="authentik LDAP outpost image" \
|
||||
org.opencontainers.image.url="https://goauthentik.io" \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
org.opencontainers.image.version=${VERSION}
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
|
||||
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1022.0",
|
||||
"aws-cdk": "^2.1023.0",
|
||||
"cross-env": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -24,9 +24,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1022.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1022.0.tgz",
|
||||
"integrity": "sha512-GHCu+tDtYMqCiElCl7Fad2/Bt2GmtXEV3dynudoAsV9PlL5ETeLmEN7jflDQxhmr7KhKpQeZJo/PM0DoWCvoHw==",
|
||||
"version": "2.1023.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1023.0.tgz",
|
||||
"integrity": "sha512-DWMA+IrAsBUNF2RvH7ujpDp7wSJkqTkRL8yfK4AYpEjoGY1KMaKIfxz3M3+Nk3ogM7VhZiW3OGWEOgyDF47HOQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1022.0",
|
||||
"aws-cdk": "^2.1023.0",
|
||||
"cross-env": "^10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-07-28 16:09+0000\n"
|
||||
"POT-Creation-Date: 2025-08-06 00:11+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -1483,27 +1483,27 @@ msgstr ""
|
||||
msgid "Invalid Regex Pattern: {url}"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
msgid "Based on the Hashed User ID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
msgid "Based on user ID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
msgid "Based on user UUID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
msgid "Based on the username"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
msgid "Based on the User's Email. This is recommended over the UPN method."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
msgid ""
|
||||
"Based on the User's UPN, only works if user has a 'upn' attribute set. Use "
|
||||
"this method only if you have different UPN and Mail domains."
|
||||
@@ -1617,6 +1617,10 @@ msgstr ""
|
||||
msgid "Redirect URIs"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Back-Channel Logout URI"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Include claims in id_token"
|
||||
msgstr ""
|
||||
@@ -1732,6 +1736,14 @@ msgstr ""
|
||||
msgid "Device Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Send a back-channel logout request to the registered client"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/tasks.py
|
||||
msgid "Handle backchannel logout notifications dispatched via signal"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/views/authorize.py
|
||||
#: authentik/providers/saml/views/flows.py
|
||||
#, python-brace-format
|
||||
|
||||
Binary file not shown.
@@ -19,7 +19,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
|
||||
"POT-Creation-Date: 2025-07-28 16:09+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2025\n"
|
||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||
@@ -33,6 +33,10 @@ msgstr ""
|
||||
msgid "Version history"
|
||||
msgstr "Historique des versions"
|
||||
|
||||
#: authentik/admin/tasks.py
|
||||
msgid "Update latest version info."
|
||||
msgstr "Mettre à jour les dernières informations de version."
|
||||
|
||||
#: authentik/admin/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "New version {version} available!"
|
||||
@@ -88,10 +92,25 @@ msgstr "Instances du plan"
|
||||
msgid "authentik Export - {date}"
|
||||
msgstr "Export authentik - {date}"
|
||||
|
||||
#: authentik/blueprints/v1/tasks.py authentik/crypto/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Successfully imported {count} files."
|
||||
msgstr "{count} fichiers importés avec succès."
|
||||
#: authentik/blueprints/v1/tasks.py
|
||||
msgid "Find blueprints as `blueprints_find` does, but return a safe dict."
|
||||
msgstr ""
|
||||
"Cherche les plans comme le fait `blueprints_find`, mais renvoie un safe "
|
||||
"dict."
|
||||
|
||||
#: authentik/blueprints/v1/tasks.py
|
||||
msgid "Find blueprints and check if they need to be created in the database."
|
||||
msgstr ""
|
||||
"Cherche les plans et vérifie s'ils doivent être créés dans la base de "
|
||||
"données."
|
||||
|
||||
#: authentik/blueprints/v1/tasks.py
|
||||
msgid "Apply single blueprint."
|
||||
msgstr "Applique un seul plan."
|
||||
|
||||
#: authentik/blueprints/v1/tasks.py
|
||||
msgid "Remove blueprints which couldn't be fetched."
|
||||
msgstr "Supprime les plans qui n'ont pas pu être récupérés."
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid ""
|
||||
@@ -129,10 +148,6 @@ msgstr "Marques"
|
||||
msgid "User does not have access to application."
|
||||
msgstr "L'utilisateur n'a pas accès à l'application."
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr "Description supplémentaire indisponible"
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "Impossible de définir le groupe en tant que parent de lui-même."
|
||||
@@ -379,6 +394,10 @@ msgstr "Jetons"
|
||||
msgid "View token's key"
|
||||
msgstr "Voir la clé du jeton"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Set a token's key"
|
||||
msgstr "Définir la clé d'un jeton"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Property Mapping"
|
||||
msgstr "Mappage de propriété"
|
||||
@@ -434,6 +453,14 @@ msgstr "{source} liée avec succès !"
|
||||
msgid "Source is not configured for enrollment."
|
||||
msgstr "La source n'est pas configurée pour l'inscription."
|
||||
|
||||
#: authentik/core/tasks.py
|
||||
msgid "Remove expired objects."
|
||||
msgstr "Supprime les objets expirés"
|
||||
|
||||
#: authentik/core/tasks.py
|
||||
msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr "Supprime les utilisateurs temporaires créés par les sources SAML."
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
msgid "Go home"
|
||||
msgstr "Retourner à l'accueil"
|
||||
@@ -486,6 +513,12 @@ msgstr "Paire de clé/certificat"
|
||||
msgid "Certificate-Key Pairs"
|
||||
msgstr "Paires de clé/certificat"
|
||||
|
||||
#: authentik/crypto/tasks.py
|
||||
msgid "Discover, import and update certificates from the filesystem."
|
||||
msgstr ""
|
||||
"Découvre, importe et met à jour les certificats depuis le système de "
|
||||
"fichiers."
|
||||
|
||||
#: authentik/enterprise/api.py
|
||||
msgid "Enterprise is required to create/update this object."
|
||||
msgstr "Entreprise est requis pour créer/mettre à jour cet objet."
|
||||
@@ -538,6 +571,18 @@ msgstr "Politiques d'unicité des mots de passe"
|
||||
msgid "User Password History"
|
||||
msgstr "Historique des mots de passe utilisateur"
|
||||
|
||||
#: authentik/enterprise/policies/unique_password/tasks.py
|
||||
msgid ""
|
||||
"Check if any UniquePasswordPolicy exists, and if not, purge the password "
|
||||
"history table."
|
||||
msgstr ""
|
||||
"Vérifie si une politique de mot de passe unique existe et, si ce n'est pas "
|
||||
"le cas, purge la table de l'historique des mots de passe."
|
||||
|
||||
#: authentik/enterprise/policies/unique_password/tasks.py
|
||||
msgid "Remove user password history that are too old."
|
||||
msgstr "Supprime l'historique des mots de passe utilisateur trop anciens."
|
||||
|
||||
#: authentik/enterprise/policy.py
|
||||
msgid "Enterprise required to access this feature."
|
||||
msgstr "Entreprise est requis pour accéder à cette fonctionnalité."
|
||||
@@ -586,6 +631,42 @@ msgstr "Mappage de propriété Google Workspace"
|
||||
msgid "Google Workspace Provider Mappings"
|
||||
msgstr "Mappages de propriété Google Workspace"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/tasks.py
|
||||
msgid "Sync Google Workspace provider objects."
|
||||
msgstr "Synchronise les objets du fournisseur Google Workspace."
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/tasks.py
|
||||
msgid "Full sync for Google Workspace provider."
|
||||
msgstr "Synchronisation complète pour le fournisseur Google Workspace."
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/tasks.py
|
||||
msgid "Sync a direct object (user, group) for Google Workspace provider."
|
||||
msgstr ""
|
||||
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur Google"
|
||||
" Workspace."
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/tasks.py
|
||||
msgid ""
|
||||
"Dispatch syncs for a direct object (user, group) for Google Workspace "
|
||||
"providers."
|
||||
msgstr ""
|
||||
"Déclenche des synchronisations pour un objet direct (utilisateur, groupe) "
|
||||
"pour les fournisseurs Google Workspace."
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/tasks.py
|
||||
msgid "Sync a related object (memberships) for Google Workspace provider."
|
||||
msgstr ""
|
||||
"Synchronise un objet lié (appartenances) pour le fournisseur Google "
|
||||
"Workspace."
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/tasks.py
|
||||
msgid ""
|
||||
"Dispatch syncs for a related object (memberships) for Google Workspace "
|
||||
"providers."
|
||||
msgstr ""
|
||||
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
|
||||
"fournisseurs Google Workspace."
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
msgid "Microsoft Entra Provider User"
|
||||
msgstr "Utilisateur du fournisseur Microsoft Entra"
|
||||
@@ -614,6 +695,42 @@ msgstr "Mappage de propriété Microsoft Entra"
|
||||
msgid "Microsoft Entra Provider Mappings"
|
||||
msgstr "Mappages de propriété Microsoft Entra"
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/tasks.py
|
||||
msgid "Sync Microsoft Entra provider objects."
|
||||
msgstr "Synchronise les objets du fournisseur Microsoft Entra."
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/tasks.py
|
||||
msgid "Full sync for Microsoft Entra provider."
|
||||
msgstr "Synchronisation complète pour le fournisseur Microsoft Entra."
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/tasks.py
|
||||
msgid "Sync a direct object (user, group) for Microsoft Entra provider."
|
||||
msgstr ""
|
||||
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur "
|
||||
"Microsoft Entra."
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/tasks.py
|
||||
msgid ""
|
||||
"Dispatch syncs for a direct object (user, group) for Microsoft Entra "
|
||||
"providers."
|
||||
msgstr ""
|
||||
"Déclenche les synchronisations pour un objet direct (utilisateur, groupe) "
|
||||
"pour les fournisseurs Microsoft Entra."
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/tasks.py
|
||||
msgid "Sync a related object (memberships) for Microsoft Entra provider."
|
||||
msgstr ""
|
||||
"Synchronise un objet lié (appartenances) pour le fournisseur Microsoft "
|
||||
"Entra."
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/tasks.py
|
||||
msgid ""
|
||||
"Dispatch syncs for a related object (memberships) for Microsoft Entra "
|
||||
"providers."
|
||||
msgstr ""
|
||||
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
|
||||
"fournisseurs Microsoft Entra."
|
||||
|
||||
#: authentik/enterprise/providers/ssf/models.py
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Signing Key"
|
||||
@@ -652,8 +769,12 @@ msgid "SSF Stream Events"
|
||||
msgstr "Évènements du flux SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/tasks.py
|
||||
msgid "Failed to send request"
|
||||
msgstr "Échec de l'envoi de la requête"
|
||||
msgid "Dispatch SSF events."
|
||||
msgstr "Distribue les événements SSF."
|
||||
|
||||
#: authentik/enterprise/providers/ssf/tasks.py
|
||||
msgid "Send an SSF event."
|
||||
msgstr "Envoye un événement SSF."
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||
@@ -725,10 +846,9 @@ msgstr "Étape Source"
|
||||
msgid "Source Stages"
|
||||
msgstr "Étapes Source"
|
||||
|
||||
#: authentik/events/api/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Successfully started task {name}."
|
||||
msgstr "La tâche {name} a été démarrée avec succès."
|
||||
#: authentik/enterprise/tasks.py
|
||||
msgid "Update enterprise license status."
|
||||
msgstr "Mettre à jour le statut de licence entreprise."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Event"
|
||||
@@ -840,6 +960,15 @@ msgstr ""
|
||||
"Définir à quel groupe d'utilisateur cette notification doit être envoyée et "
|
||||
"affichée. Si laissé vide, les notifications ne seront pas envoyées."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"When enabled, notification will be sent to user the user that triggered the "
|
||||
"event.When destination_group is configured, notification is sent to both."
|
||||
msgstr ""
|
||||
"Lorsque cette option est activée, une notification est envoyée à "
|
||||
"l'utilisateur qui a déclenché l'événement. Si destination_group est "
|
||||
"configuré, la notification est envoyée aux deux."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rule"
|
||||
msgstr "Règle de Notification"
|
||||
@@ -856,10 +985,6 @@ msgstr "Mappage de Webhook"
|
||||
msgid "Webhook Mappings"
|
||||
msgstr "Mappages de Webhook"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Run task"
|
||||
msgstr "Lancer la tâche"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "System Task"
|
||||
msgstr "Tâches du système"
|
||||
@@ -868,9 +993,31 @@ msgstr "Tâches du système"
|
||||
msgid "System Tasks"
|
||||
msgstr "Tâches du système"
|
||||
|
||||
#: authentik/events/system_tasks.py
|
||||
msgid "Task has not been run yet."
|
||||
msgstr "Tâche pas encore exécutée."
|
||||
#: authentik/events/tasks.py
|
||||
msgid "Dispatch new event notifications."
|
||||
msgstr "Envoye les notifications d'un nouvel événement."
|
||||
|
||||
#: authentik/events/tasks.py
|
||||
msgid ""
|
||||
"Check if policies attached to NotificationRule match event and dispatch "
|
||||
"notification tasks."
|
||||
msgstr ""
|
||||
"Vérifier si les politiques attachées à une règle de notifications "
|
||||
"correspondent à l'événement et déclenche les tâches de notification."
|
||||
|
||||
#: authentik/events/tasks.py
|
||||
msgid "Send notification."
|
||||
msgstr "Envoye une notification."
|
||||
|
||||
#: authentik/events/tasks.py
|
||||
msgid "Cleanup events for GDPR compliance."
|
||||
msgstr "Nettoye les événements pour la conformité au RGPD."
|
||||
|
||||
#: authentik/events/tasks.py
|
||||
msgid "Cleanup seen notifications and notifications whose event expired."
|
||||
msgstr ""
|
||||
"Nettoye les notifications vues et les notifications dont l'événement a "
|
||||
"expiré."
|
||||
|
||||
#: authentik/flows/api/flows.py
|
||||
#, python-brace-format
|
||||
@@ -1051,32 +1198,6 @@ msgstr ""
|
||||
"Si activé, le fournisseur ne changera ou ne créera pas d'objets auprès du "
|
||||
"système distant."
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Starting full provider sync"
|
||||
msgstr "Démarrage d'une synchronisation complète du fournisseur"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Syncing users"
|
||||
msgstr "Synchronisation des utilisateurs"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Syncing groups"
|
||||
msgstr "Synchronisation des groupes"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Syncing page {page} of {object_type}"
|
||||
msgstr "Synchronisation de la page {page} de {object_type}"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
msgstr "Abandon de la requête de mutation en raison d'une simulation"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Stopping sync due to error: {error}"
|
||||
msgstr "Arrêt de la synchronisation due à l'erreur : {error}"
|
||||
|
||||
#: authentik/lib/utils/time.py
|
||||
#, python-format
|
||||
msgid "%(value)s is not in the correct format of 'hours=3;minutes=1'."
|
||||
@@ -1183,6 +1304,32 @@ msgstr "Avant-poste"
|
||||
msgid "Outposts"
|
||||
msgstr "Avant-postes"
|
||||
|
||||
#: authentik/outposts/tasks.py
|
||||
msgid "Update cached state of service connection."
|
||||
msgstr "Met à jour l'état mis en cache de la connexion de service."
|
||||
|
||||
#: authentik/outposts/tasks.py
|
||||
msgid "Create/update/monitor/delete the deployment of an Outpost."
|
||||
msgstr "Crée/met à jour/surveille/supprime le déploiement d'un avant-poste."
|
||||
|
||||
#: authentik/outposts/tasks.py
|
||||
msgid "Ensure that all Outposts have valid Service Accounts and Tokens."
|
||||
msgstr ""
|
||||
"S'assure que tous les avant-postes ont des comptes de service et des jetons "
|
||||
"valides."
|
||||
|
||||
#: authentik/outposts/tasks.py
|
||||
msgid "Send update to outpost"
|
||||
msgstr "Envoye une mise à jour à un avant-poste"
|
||||
|
||||
#: authentik/outposts/tasks.py
|
||||
msgid "Checks the local environment and create Service connections."
|
||||
msgstr "Vérifie l'environnement local et crée les connexions de service."
|
||||
|
||||
#: authentik/outposts/tasks.py
|
||||
msgid "Terminate session on all outposts."
|
||||
msgstr "Met fin à la session sur tous les avant-postes."
|
||||
|
||||
#: authentik/policies/denied.py
|
||||
msgid "Access denied"
|
||||
msgstr "Accès refusé"
|
||||
@@ -1901,6 +2048,10 @@ msgstr "Fournisseur Proxy"
|
||||
msgid "Proxy Providers"
|
||||
msgstr "Fournisseur de Proxy"
|
||||
|
||||
#: authentik/providers/proxy/tasks.py
|
||||
msgid "Terminate session on Proxy outpost."
|
||||
msgstr "Met fin à la session sur l'avant-poste Proxy."
|
||||
|
||||
#: authentik/providers/rac/models.py authentik/stages/user_login/models.py
|
||||
msgid ""
|
||||
"Determines how long a session lasts. Default of 0 means that the sessions "
|
||||
@@ -2245,6 +2396,35 @@ msgstr "Mappage fournisseur SCIM"
|
||||
msgid "SCIM Provider Mappings"
|
||||
msgstr "Mappages fournisseur SCIM"
|
||||
|
||||
#: authentik/providers/scim/tasks.py
|
||||
msgid "Sync SCIM provider objects."
|
||||
msgstr "Synchronise les objets du fournisseur SCIM."
|
||||
|
||||
#: authentik/providers/scim/tasks.py
|
||||
msgid "Full sync for SCIM provider."
|
||||
msgstr "Synchronisation complète pour le fournisseur SCIM."
|
||||
|
||||
#: authentik/providers/scim/tasks.py
|
||||
msgid "Sync a direct object (user, group) for SCIM provider."
|
||||
msgstr ""
|
||||
"Synchronise un objet direct (utilisateur, groupe) pour le fournisseur SCIM."
|
||||
|
||||
#: authentik/providers/scim/tasks.py
|
||||
msgid "Dispatch syncs for a direct object (user, group) for SCIM providers."
|
||||
msgstr ""
|
||||
"Déclenche les synchronisations pour un objet direct (utilisateur, groupe) "
|
||||
"pour les fournisseurs SCIM."
|
||||
|
||||
#: authentik/providers/scim/tasks.py
|
||||
msgid "Sync a related object (memberships) for SCIM provider."
|
||||
msgstr "Synchronise un objet lié (appartenances) pour le fournisseur SCIM."
|
||||
|
||||
#: authentik/providers/scim/tasks.py
|
||||
msgid "Dispatch syncs for a related object (memberships) for SCIM providers."
|
||||
msgstr ""
|
||||
"Déclenche des synchronisations pour un objet lié (appartenances) pour les "
|
||||
"fournisseurs SCIM."
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Role"
|
||||
msgstr "Rôle"
|
||||
@@ -2399,6 +2579,14 @@ msgstr "Connexion du groupe à la source Kerberos"
|
||||
msgid "Group Kerberos Source Connections"
|
||||
msgstr "Connexions du groupe à la source Kerberos"
|
||||
|
||||
#: authentik/sources/kerberos/tasks.py
|
||||
msgid "Check connectivity for Kerberos sources."
|
||||
msgstr "Vérifie la connectivité des sources Kerberos."
|
||||
|
||||
#: authentik/sources/kerberos/tasks.py
|
||||
msgid "Sync Kerberos source."
|
||||
msgstr "Synchronise la source Kerberos."
|
||||
|
||||
#: authentik/sources/kerberos/views.py
|
||||
msgid "SPNEGO authentication required"
|
||||
msgstr "Authentification SPNEGO requise"
|
||||
@@ -2566,6 +2754,18 @@ msgstr "Connexions du groupe à la source LDAP"
|
||||
msgid "Password does not match Active Directory Complexity."
|
||||
msgstr "Le mot de passe ne correspond pas à la complexité d'Active Directory."
|
||||
|
||||
#: authentik/sources/ldap/tasks.py
|
||||
msgid "Check connectivity for LDAP source."
|
||||
msgstr "Vérifie la connectivité des sources LDAP."
|
||||
|
||||
#: authentik/sources/ldap/tasks.py
|
||||
msgid "Sync LDAP source."
|
||||
msgstr "Synchronise la source LDAP."
|
||||
|
||||
#: authentik/sources/ldap/tasks.py
|
||||
msgid "Sync page for LDAP source."
|
||||
msgstr "Synchronise une page pour la source LDAP."
|
||||
|
||||
#: authentik/sources/oauth/clients/oauth2.py
|
||||
msgid "No token received."
|
||||
msgstr "Pas de jeton reçu."
|
||||
@@ -2715,6 +2915,14 @@ msgstr "Source d'OAuth Azure AD"
|
||||
msgid "Azure AD OAuth Sources"
|
||||
msgstr "Source d'OAuth Azure AD"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Entra ID OAuth Source"
|
||||
msgstr "Source d'OAuth Entra ID"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Entra ID OAuth Sources"
|
||||
msgstr "Sources d'OAuth Entra ID"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "OpenID OAuth Source"
|
||||
msgstr "Source d'OAuth OpenID"
|
||||
@@ -2771,6 +2979,14 @@ msgstr "Connexion du groupe à la source OAuth"
|
||||
msgid "Group OAuth Source Connections"
|
||||
msgstr "Connexions du groupe à la source OAuth"
|
||||
|
||||
#: authentik/sources/oauth/tasks.py
|
||||
msgid ""
|
||||
"Update OAuth sources' config from well_known, and JWKS info from the "
|
||||
"configured URL."
|
||||
msgstr ""
|
||||
"Met à jour la configuration des sources OAuth à partir de well_known, et les"
|
||||
" informations JWKS à partir de l'URL configurée."
|
||||
|
||||
#: authentik/sources/oauth/views/callback.py
|
||||
#, python-brace-format
|
||||
msgid "Authentication failed: {reason}"
|
||||
@@ -2829,6 +3045,10 @@ msgstr "Connexion du groupe à la source Plex"
|
||||
msgid "Group Plex Source Connections"
|
||||
msgstr "Connexions du groupe à la source OAuth"
|
||||
|
||||
#: authentik/sources/plex/tasks.py
|
||||
msgid "Check the validity of a Plex source."
|
||||
msgstr "Vérifie la validité d'une source Plex."
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "Redirect Binding"
|
||||
msgstr "Liaison de Redirection"
|
||||
@@ -3273,6 +3493,13 @@ msgstr "Type d'appareil WebAuthn"
|
||||
msgid "WebAuthn Device types"
|
||||
msgstr "Types d'appareil WebAuthn"
|
||||
|
||||
#: authentik/stages/authenticator_webauthn/tasks.py
|
||||
msgid ""
|
||||
"Background task to import FIDO Alliance MDS blob and AAGUIDs into database."
|
||||
msgstr ""
|
||||
"Tâche de fond pour importer le blob MDS de la FIDO Alliance et les AAGUID "
|
||||
"dans la base de données."
|
||||
|
||||
#: authentik/stages/captcha/models.py
|
||||
msgid "Public key, acquired your captcha Provider."
|
||||
msgstr "Clé publique, acquise auprès de votre fournisseur captcha."
|
||||
@@ -3399,6 +3626,10 @@ msgstr "Email envoyé."
|
||||
msgid "Email Successfully sent."
|
||||
msgstr "Couriel envoyé avec succès."
|
||||
|
||||
#: authentik/stages/email/tasks.py
|
||||
msgid "Send email."
|
||||
msgstr "Envoye un courriel."
|
||||
|
||||
#: authentik/stages/email/templates/email/account_confirmation.html
|
||||
#: authentik/stages/email/templates/email/account_confirmation.txt
|
||||
msgid "Welcome!"
|
||||
@@ -3867,6 +4098,16 @@ msgstr ""
|
||||
"souvenir de moi ne sera pas proposée. (Format: "
|
||||
"hours=-1;minutes=-2;seconds=-3)"
|
||||
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid ""
|
||||
"When set to a non-zero value, authentik will save a cookie with a longer "
|
||||
"expiry,to remember the device the user is logging in from. (Format: "
|
||||
"hours=-1;minutes=-2;seconds=-3)"
|
||||
msgstr ""
|
||||
"Si cette valeur est différente de zéro, authentik enregistrera un cookie "
|
||||
"avec une expiration plus longue, afin de se souvenir de l'appareil à partir "
|
||||
"duquel l'utilisateur se connecte. (Format : hours=-1;minutes=-2;seconds=-3)"
|
||||
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid "User Login Stage"
|
||||
msgstr "Étape de connexion utlisateur"
|
||||
@@ -3918,6 +4159,38 @@ msgid "Failed to update user. Please try again later."
|
||||
msgstr ""
|
||||
"Échec de mise à jour de l'utilisateur. Merci de réessayer ultérieurement,"
|
||||
|
||||
#: authentik/tasks/models.py
|
||||
msgid "Tenant this task belongs to"
|
||||
msgstr "Tenant auquel cette tâche appartient"
|
||||
|
||||
#: authentik/tasks/models.py
|
||||
msgid "Retry failed task"
|
||||
msgstr "Relancer la tâche échouée"
|
||||
|
||||
#: authentik/tasks/models.py
|
||||
msgid "Worker status"
|
||||
msgstr "État du worker"
|
||||
|
||||
#: authentik/tasks/models.py
|
||||
msgid "Worker statuses"
|
||||
msgstr "États du worker"
|
||||
|
||||
#: authentik/tasks/schedules/models.py
|
||||
msgid "Unique schedule identifier"
|
||||
msgstr "Identifiant unique des planifications"
|
||||
|
||||
#: authentik/tasks/schedules/models.py
|
||||
msgid "User schedule identifier"
|
||||
msgstr "Identifiant utilisateur des planifications"
|
||||
|
||||
#: authentik/tasks/schedules/models.py
|
||||
msgid "Manually trigger a schedule"
|
||||
msgstr "Déclencher manuellement une planification"
|
||||
|
||||
#: authentik/tasks/tasks.py
|
||||
msgid "Remove old worker statuses."
|
||||
msgstr "Supprime les anciens statuts des workers."
|
||||
|
||||
#: authentik/tenants/models.py
|
||||
msgid ""
|
||||
"Schema name must start with t_, only contain lowercase letters and numbers "
|
||||
@@ -4010,3 +4283,76 @@ msgstr "Domaine"
|
||||
#: authentik/tenants/models.py
|
||||
msgid "Domains"
|
||||
msgstr "Domaines"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Queue name"
|
||||
msgstr "Nom de la file"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Dramatiq actor name"
|
||||
msgstr "Nom de l'acteur Dramatiq"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Message body"
|
||||
msgstr "Corps du message"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Task status"
|
||||
msgstr "État de la tâche"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Task last modified time"
|
||||
msgstr "Heure de dernière modification de la tâche"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Task result"
|
||||
msgstr "Résultat de la tâche"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Result expiry time"
|
||||
msgstr "Délai d'expiration du résultat"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Task"
|
||||
msgstr "Tâche"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Tasks"
|
||||
msgstr "Tâches"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
#, python-format
|
||||
msgid "%(value)s is not a valid crontab"
|
||||
msgstr "%(value)s n'est pas un crontab valide"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Dramatiq actor to call"
|
||||
msgstr "Acteur Dramatiq à invoquer"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Args to send to the actor"
|
||||
msgstr "Args à passer à l'acteur"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Kwargs to send to the actor"
|
||||
msgstr "Kwargs à passer à l'acteur"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Options to send to the actor"
|
||||
msgstr "Options à passer à l'acteur"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "When to schedule tasks"
|
||||
msgstr "Quand planifier les tâches"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Pause this schedule"
|
||||
msgstr "Mettre cette planification en pause"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Schedule"
|
||||
msgstr "Planification"
|
||||
|
||||
#: packages/django-dramatiq-postgres/django_dramatiq_postgres/models.py
|
||||
msgid "Schedules"
|
||||
msgstr "Planifications"
|
||||
|
||||
@@ -186,19 +186,19 @@ class MetricsMiddleware(Middleware):
|
||||
"The total number of dead-lettered tasks.",
|
||||
self.labels,
|
||||
)
|
||||
self.inprogress_messages = Gauge(
|
||||
f"{self.prefix}_tasks_inprogress",
|
||||
self.in_progress_messages = Gauge(
|
||||
f"{self.prefix}_tasks_in_progress",
|
||||
"The number of tasks in progress.",
|
||||
self.labels,
|
||||
multiprocess_mode="livesum",
|
||||
)
|
||||
self.inprogress_delayed_messages = Gauge(
|
||||
f"{self.prefix}_tasks_delayed_inprogress",
|
||||
self.in_progress_delayed_messages = Gauge(
|
||||
f"{self.prefix}_tasks_delayed_in_progress",
|
||||
"The number of delayed tasks in memory.",
|
||||
self.labels,
|
||||
)
|
||||
self.messages_durations = Histogram(
|
||||
f"{self.prefix}_tasks_duration_miliseconds",
|
||||
f"{self.prefix}_tasks_duration_milliseconds",
|
||||
"The time spent processing tasks.",
|
||||
self.labels,
|
||||
buckets=(
|
||||
@@ -244,15 +244,15 @@ class MetricsMiddleware(Middleware):
|
||||
|
||||
def before_delay_message(self, broker: Broker, message: Message):
|
||||
self.delayed_messages.add(message.message_id)
|
||||
self.inprogress_delayed_messages.labels(*self._make_labels(message)).inc()
|
||||
self.in_progress_delayed_messages.labels(*self._make_labels(message)).inc()
|
||||
|
||||
def before_process_message(self, broker: Broker, message: Message):
|
||||
labels = self._make_labels(message)
|
||||
if message.message_id in self.delayed_messages:
|
||||
self.delayed_messages.remove(message.message_id)
|
||||
self.inprogress_delayed_messages.labels(*labels).dec()
|
||||
self.in_progress_delayed_messages.labels(*labels).dec()
|
||||
|
||||
self.inprogress_messages.labels(*labels).inc()
|
||||
self.in_progress_messages.labels(*labels).inc()
|
||||
self.message_start_times[message.message_id] = current_millis()
|
||||
|
||||
def after_process_message(
|
||||
@@ -269,7 +269,7 @@ class MetricsMiddleware(Middleware):
|
||||
message_duration = current_millis() - message_start_time
|
||||
self.messages_durations.labels(*labels).observe(message_duration)
|
||||
|
||||
self.inprogress_messages.labels(*labels).dec()
|
||||
self.in_progress_messages.labels(*labels).dec()
|
||||
self.total_messages.labels(*labels).inc()
|
||||
if exception is not None:
|
||||
self.total_errored_messages.labels(*labels).inc()
|
||||
|
||||
6
packages/docusaurus-config/package-lock.json
generated
6
packages/docusaurus-config/package-lock.json
generated
@@ -17958,9 +17958,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -2728,9 +2728,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc-plugin-markdown": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.7.1.tgz",
|
||||
"integrity": "sha512-HN/fHLm2S6MD4HX8txfB4eWvVBzX/mEYy5U5s1KTAdh3E5uX5/lilswqTzZlPTT6fNZInAboAdFGpbAuBKnE4A==",
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.8.0.tgz",
|
||||
"integrity": "sha512-BQqXnT9PETe6WEFf8bcsvvGEGQHbwTo/BFyY+RUIsSB05Y0Wn56iF+fK1PY2OKJJIhV4kp4dp7osaP9Bm5a0Zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2741,9 +2741,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
448
packages/eslint-config/package-lock.json
generated
448
packages/eslint-config/package-lock.json
generated
@@ -500,93 +500,6 @@
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
||||
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/type-utils": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
|
||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
|
||||
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.38.0",
|
||||
"@typescript-eslint/types": "^8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
|
||||
@@ -605,48 +518,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
|
||||
@@ -661,98 +532,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
|
||||
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.38.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
|
||||
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
|
||||
@@ -4694,9 +4473,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -4731,6 +4510,227 @@
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
||||
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/type-utils": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
|
||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
|
||||
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.38.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
|
||||
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.38.0",
|
||||
"@typescript-eslint/types": "^8.38.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
|
||||
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||
|
||||
6
packages/prettier-config/package-lock.json
generated
6
packages/prettier-config/package-lock.json
generated
@@ -1711,9 +1711,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -53,11 +53,16 @@ ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
LABEL org.opencontainers.image.url=https://goauthentik.io
|
||||
LABEL org.opencontainers.image.description="goauthentik.io Proxy outpost image, see https://goauthentik.io for more info."
|
||||
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
|
||||
LABEL org.opencontainers.image.version=${VERSION}
|
||||
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
|
||||
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.description="goauthentik.io Proxy outpost image, see https://goauthentik.io for more info." \
|
||||
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
|
||||
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
|
||||
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
|
||||
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
|
||||
org.opencontainers.image.title="authentik proxy outpost image" \
|
||||
org.opencontainers.image.url="https://goauthentik.io" \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
org.opencontainers.image.version=${VERSION}
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
|
||||
@@ -37,11 +37,16 @@ ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
LABEL org.opencontainers.image.url=https://goauthentik.io
|
||||
LABEL org.opencontainers.image.description="goauthentik.io RAC outpost, see https://goauthentik.io for more info."
|
||||
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
|
||||
LABEL org.opencontainers.image.version=${VERSION}
|
||||
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
|
||||
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.description="goauthentik.io RAC outpost, see https://goauthentik.io for more info." \
|
||||
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
|
||||
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
|
||||
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
|
||||
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
|
||||
org.opencontainers.image.title="authentik RAC outpost image" \
|
||||
org.opencontainers.image.url="https://goauthentik.io" \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
org.opencontainers.image.version=${VERSION}
|
||||
|
||||
USER root
|
||||
RUN apt-get update && \
|
||||
|
||||
@@ -37,11 +37,16 @@ ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
LABEL org.opencontainers.image.url=https://goauthentik.io
|
||||
LABEL org.opencontainers.image.description="goauthentik.io Radius outpost, see https://goauthentik.io for more info."
|
||||
LABEL org.opencontainers.image.source=https://github.com/goauthentik/authentik
|
||||
LABEL org.opencontainers.image.version=${VERSION}
|
||||
LABEL org.opencontainers.image.revision=${GIT_BUILD_HASH}
|
||||
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
|
||||
org.opencontainers.image.description="goauthentik.io Radius outpost, see https://goauthentik.io for more info." \
|
||||
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
|
||||
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \
|
||||
org.opencontainers.image.revision=${GIT_BUILD_HASH} \
|
||||
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
|
||||
org.opencontainers.image.title="authentik RADIUS outpost image" \
|
||||
org.opencontainers.image.url="https://goauthentik.io" \
|
||||
org.opencontainers.image.vendor="Authentik Security Inc." \
|
||||
org.opencontainers.image.version=${VERSION}
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
|
||||
174
schema.yml
174
schema.yml
@@ -4718,6 +4718,11 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
description: Attributes
|
||||
- in: query
|
||||
name: include_children
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- in: query
|
||||
name: include_users
|
||||
schema:
|
||||
@@ -4840,6 +4845,11 @@ paths:
|
||||
format: uuid
|
||||
description: A UUID string identifying this Group.
|
||||
required: true
|
||||
- in: query
|
||||
name: include_children
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
- in: query
|
||||
name: include_users
|
||||
schema:
|
||||
@@ -5654,6 +5664,21 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
description: Attributes
|
||||
- in: query
|
||||
name: date_joined
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: date_joined__gt
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: date_joined__lt
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: email
|
||||
schema:
|
||||
@@ -5688,6 +5713,21 @@ paths:
|
||||
name: is_superuser
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: last_updated
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: last_updated__gt
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: last_updated__lt
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
@@ -22282,6 +22322,17 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- in: query
|
||||
name: default_name_id_policy
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
||||
- in: query
|
||||
name: default_relay_state
|
||||
schema:
|
||||
@@ -29498,6 +29549,7 @@ paths:
|
||||
enum:
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
||||
@@ -44793,6 +44845,15 @@ components:
|
||||
activate_user_on_success:
|
||||
type: boolean
|
||||
description: Activate users upon completion of stage.
|
||||
recovery_max_attempts:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
recovery_cache_timeout:
|
||||
type: string
|
||||
description: 'The time window used to count recent account recovery attempts.
|
||||
If the number of attempts exceed recovery_max_attempts within this period,
|
||||
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
|
||||
required:
|
||||
- component
|
||||
- meta_model_name
|
||||
@@ -44853,6 +44914,16 @@ components:
|
||||
activate_user_on_success:
|
||||
type: boolean
|
||||
description: Activate users upon completion of stage.
|
||||
recovery_max_attempts:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
recovery_cache_timeout:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: 'The time window used to count recent account recovery attempts.
|
||||
If the number of attempts exceed recovery_max_attempts within this period,
|
||||
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
|
||||
required:
|
||||
- name
|
||||
Endpoint:
|
||||
@@ -46466,13 +46537,50 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/Role'
|
||||
readOnly: true
|
||||
children:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
children_obj:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GroupChild'
|
||||
readOnly: true
|
||||
nullable: true
|
||||
required:
|
||||
- children_obj
|
||||
- name
|
||||
- num_pk
|
||||
- parent_name
|
||||
- pk
|
||||
- roles_obj
|
||||
- users_obj
|
||||
GroupChild:
|
||||
type: object
|
||||
description: Stripped down group serializer to show relevant children for groups
|
||||
properties:
|
||||
pk:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
title: Group uuid
|
||||
name:
|
||||
type: string
|
||||
is_superuser:
|
||||
type: boolean
|
||||
description: Users added to this group will be superusers.
|
||||
attributes:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
group_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
required:
|
||||
- group_uuid
|
||||
- name
|
||||
- pk
|
||||
GroupKerberosSourceConnection:
|
||||
type: object
|
||||
description: Group Source Connection
|
||||
@@ -46794,6 +46902,11 @@ components:
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
children:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- name
|
||||
GroupSAMLSourceConnection:
|
||||
@@ -49063,14 +49176,6 @@ components:
|
||||
- mode
|
||||
- name
|
||||
- user_attribute
|
||||
NameIdPolicyEnum:
|
||||
enum:
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
||||
type: string
|
||||
NetworkBindingEnum:
|
||||
enum:
|
||||
- no_binding
|
||||
@@ -49417,6 +49522,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RedirectURI'
|
||||
backchannel_logout_uri:
|
||||
type: string
|
||||
title: Back-Channel Logout URI
|
||||
format: uri
|
||||
sub_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SubModeEnum'
|
||||
@@ -49524,6 +49633,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RedirectURIRequest'
|
||||
backchannel_logout_uri:
|
||||
type: string
|
||||
title: Back-Channel Logout URI
|
||||
format: uri
|
||||
sub_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SubModeEnum'
|
||||
@@ -53300,6 +53413,16 @@ components:
|
||||
activate_user_on_success:
|
||||
type: boolean
|
||||
description: Activate users upon completion of stage.
|
||||
recovery_max_attempts:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
recovery_cache_timeout:
|
||||
type: string
|
||||
minLength: 1
|
||||
description: 'The time window used to count recent account recovery attempts.
|
||||
If the number of attempts exceed recovery_max_attempts within this period,
|
||||
further attempts will be rate-limited. (Format: hours=1;minutes=2;seconds=3).'
|
||||
PatchedEndpointDeviceRequest:
|
||||
type: object
|
||||
description: Serializer for Endpoint authenticator devices
|
||||
@@ -53685,6 +53808,11 @@ components:
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
children:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
PatchedGroupSAMLSourceConnectionRequest:
|
||||
type: object
|
||||
description: Group Source Connection
|
||||
@@ -54428,6 +54556,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RedirectURIRequest'
|
||||
backchannel_logout_uri:
|
||||
type: string
|
||||
title: Back-Channel Logout URI
|
||||
format: uri
|
||||
sub_mode:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SubModeEnum'
|
||||
@@ -55289,6 +55421,8 @@ components:
|
||||
default_relay_state:
|
||||
type: string
|
||||
description: Default relay_state value for IDP-initiated logins
|
||||
default_name_id_policy:
|
||||
$ref: '#/components/schemas/SAMLNameIDPolicyEnum'
|
||||
PatchedSAMLSourcePropertyMappingRequest:
|
||||
type: object
|
||||
description: SAMLSourcePropertyMapping Serializer
|
||||
@@ -55382,7 +55516,7 @@ components:
|
||||
be a security risk, as no validation of the request ID is done.
|
||||
name_id_policy:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/NameIdPolicyEnum'
|
||||
- $ref: '#/components/schemas/SAMLNameIDPolicyEnum'
|
||||
description: NameID Policy sent to the IdP. Can be unset, in which case
|
||||
no Policy is sent.
|
||||
binding_type:
|
||||
@@ -58130,6 +58264,15 @@ components:
|
||||
required:
|
||||
- download_url
|
||||
- metadata
|
||||
SAMLNameIDPolicyEnum:
|
||||
enum:
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:WindowsDomainQualifiedName
|
||||
- urn:oasis:names:tc:SAML:2.0:nameid-format:transient
|
||||
- urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
type: string
|
||||
SAMLPropertyMapping:
|
||||
type: object
|
||||
description: SAMLPropertyMapping Serializer
|
||||
@@ -58347,6 +58490,8 @@ components:
|
||||
default_relay_state:
|
||||
type: string
|
||||
description: Default relay_state value for IDP-initiated logins
|
||||
default_name_id_policy:
|
||||
$ref: '#/components/schemas/SAMLNameIDPolicyEnum'
|
||||
url_download_metadata:
|
||||
type: string
|
||||
description: Get metadata download URL
|
||||
@@ -58519,6 +58664,8 @@ components:
|
||||
default_relay_state:
|
||||
type: string
|
||||
description: Default relay_state value for IDP-initiated logins
|
||||
default_name_id_policy:
|
||||
$ref: '#/components/schemas/SAMLNameIDPolicyEnum'
|
||||
required:
|
||||
- acs_url
|
||||
- authorization_flow
|
||||
@@ -58627,7 +58774,7 @@ components:
|
||||
be a security risk, as no validation of the request ID is done.
|
||||
name_id_policy:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/NameIdPolicyEnum'
|
||||
- $ref: '#/components/schemas/SAMLNameIDPolicyEnum'
|
||||
description: NameID Policy sent to the IdP. Can be unset, in which case
|
||||
no Policy is sent.
|
||||
binding_type:
|
||||
@@ -58817,7 +58964,7 @@ components:
|
||||
be a security risk, as no validation of the request ID is done.
|
||||
name_id_policy:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/NameIdPolicyEnum'
|
||||
- $ref: '#/components/schemas/SAMLNameIDPolicyEnum'
|
||||
description: NameID Policy sent to the IdP. Can be unset, in which case
|
||||
no Policy is sent.
|
||||
binding_type:
|
||||
@@ -61075,11 +61222,16 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
required:
|
||||
- avatar
|
||||
- date_joined
|
||||
- groups_obj
|
||||
- is_superuser
|
||||
- last_updated
|
||||
- name
|
||||
- password_change_date
|
||||
- pk
|
||||
|
||||
117
uv.lock
generated
117
uv.lock
generated
@@ -19,7 +19,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.12.14"
|
||||
version = "3.12.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -30,25 +30,25 @@ dependencies = [
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921, upload-time = "2025-07-10T13:05:33.968Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/06/48/e0d2fa8ac778008071e7b79b93ab31ef14ab88804d7ba71b5c964a7c844e/aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767", size = 695471, upload-time = "2025-07-10T13:04:20.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/e7/f73206afa33100804f790b71092888f47df65fd9a4cd0e6800d7c6826441/aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e", size = 473128, upload-time = "2025-07-10T13:04:21.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/e2/4dd00180be551a6e7ee979c20fc7c32727f4889ee3fd5b0586e0d47f30e1/aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63", size = 465426, upload-time = "2025-07-10T13:04:24.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/dd/525ed198a0bb674a323e93e4d928443a680860802c44fa7922d39436b48b/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d", size = 1704252, upload-time = "2025-07-10T13:04:26.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/b1/01e542aed560a968f692ab4fc4323286e8bc4daae83348cd63588e4f33e3/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab", size = 1685514, upload-time = "2025-07-10T13:04:28.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/06/93669694dc5fdabdc01338791e70452d60ce21ea0946a878715688d5a191/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4", size = 1737586, upload-time = "2025-07-10T13:04:30.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/3a/18991048ffc1407ca51efb49ba8bcc1645961f97f563a6c480cdf0286310/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026", size = 1786958, upload-time = "2025-07-10T13:04:32.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a8/81e237f89a32029f9b4a805af6dffc378f8459c7b9942712c809ff9e76e5/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd", size = 1709287, upload-time = "2025-07-10T13:04:34.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/e3/bd67a11b0fe7fc12c6030473afd9e44223d456f500f7cf526dbaa259ae46/aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88", size = 1622990, upload-time = "2025-07-10T13:04:36.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ba/e0cc8e0f0d9ce0904e3cf2d6fa41904e379e718a013c721b781d53dcbcca/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086", size = 1676015, upload-time = "2025-07-10T13:04:38.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/b3/1e6c960520bda094c48b56de29a3d978254637ace7168dd97ddc273d0d6c/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933", size = 1707678, upload-time = "2025-07-10T13:04:41.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/19/929a3eb8c35b7f9f076a462eaa9830b32c7f27d3395397665caa5e975614/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151", size = 1650274, upload-time = "2025-07-10T13:04:43.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e5/81682a6f20dd1b18ce3d747de8eba11cbef9b270f567426ff7880b096b48/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8", size = 1726408, upload-time = "2025-07-10T13:04:45.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/17/884938dffaa4048302985483f77dfce5ac18339aad9b04ad4aaa5e32b028/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3", size = 1759879, upload-time = "2025-07-10T13:04:47.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/78/53b081980f50b5cf874359bde707a6eacd6c4be3f5f5c93937e48c9d0025/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758", size = 1708770, upload-time = "2025-07-10T13:04:49.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/91/228eeddb008ecbe3ffa6c77b440597fdf640307162f0c6488e72c5a2d112/aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5", size = 421688, upload-time = "2025-07-10T13:04:51.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/5f/8427618903343402fdafe2850738f735fd1d9409d2a8f9bcaae5e630d3ba/aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa", size = 448098, upload-time = "2025-07-10T13:04:53.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -86,15 +86,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -111,23 +111,23 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "argon2-cffi-bindings"
|
||||
version = "21.2.0"
|
||||
version = "25.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658, upload-time = "2021-12-01T09:09:17.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583, upload-time = "2021-12-01T09:09:19.546Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168, upload-time = "2021-12-01T09:09:21.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709, upload-time = "2021-12-01T09:09:18.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613, upload-time = "2021-12-01T09:09:22.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583, upload-time = "2021-12-01T09:09:24.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475, upload-time = "2021-12-01T09:09:26.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698, upload-time = "2021-12-01T09:09:27.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817, upload-time = "2021-12-01T09:09:30.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -557,30 +557,30 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.39.15"
|
||||
version = "1.40.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/65/ddd4f52d138e52c1345c2d2421281a98449a6e4365290477befe06fa649a/boto3-1.39.15.tar.gz", hash = "sha256:b4483625f0d8c35045254dee46cd3c851bbc0450814f20b9b25bee1b5c0d8409", size = 111856, upload-time = "2025-07-28T19:56:49.504Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/c0/9ceff05d2243f169765ae9db08fa6f085d026af71a778cd083dc972f0f2b/boto3-1.40.2.tar.gz", hash = "sha256:2dfbc214fdbf94abfd61eec687ea39089d05af43bb00be792c76f3a6c1393f7b", size = 111826, upload-time = "2025-08-04T19:31:51.959Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/c5/27f50a31317041dc3ad79d62f37d5fcfb3f349c2fba8ea3e81de169db870/boto3-1.39.15-py3-none-any.whl", hash = "sha256:38fc54576b925af0075636752de9974e172c8a2cf7133400e3e09b150d20fb6a", size = 139901, upload-time = "2025-07-28T19:56:47.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/66/01bccaaebcd1365ce1334be042765e49ccf23787887afb8e43c6d4bc2f6e/boto3-1.40.2-py3-none-any.whl", hash = "sha256:3d99325ee874190e8f3bfd38823987327c826cdfbab943420851bdb7684d727c", size = 139882, upload-time = "2025-08-04T19:31:50.493Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.39.15"
|
||||
version = "1.40.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/e2/8cd9560e7e44cf977dc0cc2e48da7634e78b7104ae6e47f4e1dfc1093965/botocore-1.39.15.tar.gz", hash = "sha256:2aa29a717f14f8c7ca058c2e297aaed0aa10ecea24b91514eee802814d1b7600", size = 14237556, upload-time = "2025-07-28T19:56:39.397Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/e7d68381042a6d50510c8d4629f39922ce27ff32f45baf852ba6534342c5/botocore-1.40.2.tar.gz", hash = "sha256:77c4710bf37b28e897833b5b1f47d6a83e45a29985cd01a560dfdb8b6ad524e5", size = 14284599, upload-time = "2025-08-04T19:31:42.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/6e/f25b8633e7ab2008de4c27466c9bc39e32dc73816619ffebbea12936135a/botocore-1.39.15-py3-none-any.whl", hash = "sha256:eb9cfe918ebfbfb8654e1b153b29f0c129d586d2c0d7fb4032731d49baf04cff", size = 13894884, upload-time = "2025-07-28T19:56:33.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/56/dd25fb9e47060e8f7e353208678fefb65d1b06704ea30983cad8bdd81370/botocore-1.40.2-py3-none-any.whl", hash = "sha256:a31e6269af05498f8dc1c7f2b3f34448a0f16c79a8601c0389ecddab51b2c2ab", size = 13944886, upload-time = "2025-08-04T19:31:37.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -603,14 +603,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cattrs"
|
||||
version = "24.1.3"
|
||||
version = "25.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/29/7b/da4aa2f95afb2f28010453d03d6eedf018f9e085bd001f039e15731aba89/cattrs-24.1.3.tar.gz", hash = "sha256:981a6ef05875b5bb0c7fb68885546186d306f10f0f6718fe9b96c226e68821ff", size = 426684, upload-time = "2025-03-25T15:01:00.325Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/2b/561d78f488dcc303da4639e02021311728fb7fda8006dd2835550cddd9ed/cattrs-25.1.1.tar.gz", hash = "sha256:c914b734e0f2d59e5b720d145ee010f1fd9a13ee93900922a2f3f9d593b8382c", size = 435016, upload-time = "2025-06-04T20:27:15.44Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/ee/d68a3de23867a9156bab7e0a22fb9a0305067ee639032a22982cf7f725e7/cattrs-24.1.3-py3-none-any.whl", hash = "sha256:adf957dddd26840f27ffbd060a6c4dd3b2192c5b7c2c0525ef1bd8131d8a83f5", size = 66462, upload-time = "2025-03-25T15:00:58.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/b0/215274ef0d835bbc1056392a367646648b6084e39d489099959aefcca2af/cattrs-25.1.1-py3-none-any.whl", hash = "sha256:1b40b2d3402af7be79a7e7e097a9b4cd16d4c06e6d526644b0b26a063a1cc064", size = 69386, upload-time = "2025-06-04T20:27:13.969Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -631,11 +632,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.7.14"
|
||||
version = "2025.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1691,7 +1692,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "jsii"
|
||||
version = "1.112.0"
|
||||
version = "1.113.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
@@ -1702,9 +1703,9 @@ dependencies = [
|
||||
{ name = "typeguard" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/3e/270b5236035fc7bb2cdd7f55ea25f85489d35d971870cbec32c3d9e99d7f/jsii-1.112.0.tar.gz", hash = "sha256:6b7d19f361c2565b76828ecbe8cbed8b8d6028a82aa98a46b206a4ee5083157e", size = 624533, upload-time = "2025-05-07T14:45:52.574Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/37/9b/ff11800e2edc2860c9eddd7ea7c7a8849f69cbb16b1aae803dae7dafa86e/jsii-1.113.0.tar.gz", hash = "sha256:2dedea9d6006af53467a7a67f1d35a56ab3f75a3d6ed4b4536fffc3e1d1fe476", size = 623541, upload-time = "2025-07-31T12:55:42.888Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/af/8554b632e2b82f37a7422782aba5db2a1fbff4887faa7ec850818def8407/jsii-1.112.0-py3-none-any.whl", hash = "sha256:6510c223074d9b206fd0570849a791e4d9ecfff7ffe68428de73870cea9f55a1", size = 600681, upload-time = "2025-05-07T14:45:51.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/59/bbbdcc7e0adc32e2362dbb2398949ac013f79dc3468cdf2b5ac411b0f5e8/jsii-1.113.0-py3-none-any.whl", hash = "sha256:62377c651554234ea945693f7c03cb96a969ba425a686950c88d43b0d4d76b07", size = 599669, upload-time = "2025-07-31T12:55:40.874Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2154,42 +2155,42 @@ source = { git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.35.0"
|
||||
version = "1.36.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/c9/4509bfca6bb43220ce7f863c9f791e0d5001c2ec2b5867d48586008b3d96/opentelemetry_api-1.35.0.tar.gz", hash = "sha256:a111b959bcfa5b4d7dffc2fbd6a241aa72dd78dd8e79b5b1662bda896c5d2ffe", size = 64778, upload-time = "2025-07-11T12:23:28.804Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/27/d2/c782c88b8afbf961d6972428821c302bd1e9e7bc361352172f0ca31296e2/opentelemetry_api-1.36.0.tar.gz", hash = "sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0", size = 64780, upload-time = "2025-07-29T15:12:06.02Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/5a/3f8d078dbf55d18442f6a2ecedf6786d81d7245844b2b20ce2b8ad6f0307/opentelemetry_api-1.35.0-py3-none-any.whl", hash = "sha256:c4ea7e258a244858daf18474625e9cc0149b8ee354f37843415771a40c25ee06", size = 65566, upload-time = "2025-07-11T12:23:07.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl", hash = "sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c", size = 65564, upload-time = "2025-07-29T15:11:47.998Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-sdk"
|
||||
version = "1.35.0"
|
||||
version = "1.36.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/1eb2ed2ce55e0a9aa95b3007f26f55c7943aeef0a783bb006bdd92b3299e/opentelemetry_sdk-1.35.0.tar.gz", hash = "sha256:2a400b415ab68aaa6f04e8a6a9f6552908fb3090ae2ff78d6ae0c597ac581954", size = 160871, upload-time = "2025-07-11T12:23:39.566Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/85/8567a966b85a2d3f971c4d42f781c305b2b91c043724fa08fd37d158e9dc/opentelemetry_sdk-1.36.0.tar.gz", hash = "sha256:19c8c81599f51b71670661ff7495c905d8fdf6976e41622d5245b791b06fa581", size = 162557, upload-time = "2025-07-29T15:12:16.76Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/4f/8e32b757ef3b660511b638ab52d1ed9259b666bdeeceba51a082ce3aea95/opentelemetry_sdk-1.35.0-py3-none-any.whl", hash = "sha256:223d9e5f5678518f4842311bb73966e0b6db5d1e0b74e35074c052cd2487f800", size = 119379, upload-time = "2025-07-11T12:23:24.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/59/7bed362ad1137ba5886dac8439e84cd2df6d087be7c09574ece47ae9b22c/opentelemetry_sdk-1.36.0-py3-none-any.whl", hash = "sha256:19fe048b42e98c5c1ffe85b569b7073576ad4ce0bcb6e9b4c6a39e890a6c45fb", size = 119995, upload-time = "2025-07-29T15:12:03.181Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions"
|
||||
version = "0.56b0"
|
||||
version = "0.57b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/8e/214fa817f63b9f068519463d8ab46afd5d03b98930c39394a37ae3e741d0/opentelemetry_semantic_conventions-0.56b0.tar.gz", hash = "sha256:c114c2eacc8ff6d3908cb328c811eaf64e6d68623840be9224dc829c4fd6c2ea", size = 124221, upload-time = "2025-07-11T12:23:40.71Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/31/67dfa252ee88476a29200b0255bda8dfc2cf07b56ad66dc9a6221f7dc787/opentelemetry_semantic_conventions-0.57b0.tar.gz", hash = "sha256:609a4a79c7891b4620d64c7aac6898f872d790d75f22019913a660756f27ff32", size = 124225, upload-time = "2025-07-29T15:12:17.873Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/3f/e80c1b017066a9d999efffe88d1cce66116dcf5cb7f80c41040a83b6e03b/opentelemetry_semantic_conventions-0.56b0-py3-none-any.whl", hash = "sha256:df44492868fd6b482511cc43a942e7194be64e94945f572db24df2e279a001a2", size = 201625, upload-time = "2025-07-11T12:23:25.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/75/7d591371c6c39c73de5ce5da5a2cc7b72d1d1cd3f8f4638f553c01c37b11/opentelemetry_semantic_conventions-0.57b0-py3-none-any.whl", hash = "sha256:757f7e76293294f124c827e514c2a3144f191ef175b069ce8d1211e1e38e9e78", size = 201627, upload-time = "2025-07-29T15:12:04.174Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -15,6 +15,7 @@ export function addCommands(browser) {
|
||||
/**
|
||||
* @this {HTMLElement}
|
||||
*/
|
||||
// @ts-ignore
|
||||
function () {
|
||||
this.focus();
|
||||
|
||||
@@ -28,6 +29,7 @@ export function addCommands(browser) {
|
||||
/**
|
||||
* @this {HTMLElement}
|
||||
*/
|
||||
// @ts-ignore
|
||||
function () {
|
||||
this.blur();
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/* 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>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PageFixture } from "#e2e/fixtures/PageFixture";
|
||||
|
||||
import { expect, Page } from "@playwright/test";
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export const GOOD_USERNAME = "test-admin@goauthentik.io";
|
||||
export const GOOD_PASSWORD = "test-runner";
|
||||
@@ -26,21 +26,16 @@ export class SessionFixture extends PageFixture {
|
||||
/**
|
||||
* 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 $usernameField = this.page.getByLabel("Username");
|
||||
|
||||
public $passwordStage = this.page.locator("ak-stage-password");
|
||||
public $passwordField = this.$passwordStage.locator('input[name="password"]');
|
||||
public $passwordField = this.page.getByLabel("Password");
|
||||
|
||||
/**
|
||||
* The button to submit the the login flow,
|
||||
* typically redirecting to the authenticated interface.
|
||||
*/
|
||||
public $submitPasswordStageButton = this.$passwordStage.locator('button[type="submit"]');
|
||||
public $submitButton = this.page.locator('button[type="submit"]');
|
||||
|
||||
/**
|
||||
* A possible authentication failure message.
|
||||
@@ -55,26 +50,6 @@ export class SessionFixture extends PageFixture {
|
||||
|
||||
//#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;
|
||||
@@ -98,11 +73,19 @@ export class SessionFixture extends PageFixture {
|
||||
await this.page.goto(to.toString());
|
||||
}
|
||||
|
||||
await this.submitUsernameStage(username);
|
||||
await this.$usernameField.fill(username);
|
||||
|
||||
await this.$passwordField.waitFor({ state: "visible" });
|
||||
const passwordFieldVisible = await this.$passwordField.isVisible();
|
||||
|
||||
await this.submitPasswordStage(password);
|
||||
if (!passwordFieldVisible) {
|
||||
await this.$submitButton.click();
|
||||
|
||||
await this.$passwordField.waitFor({ state: "visible" });
|
||||
}
|
||||
|
||||
await this.$passwordField.fill(password);
|
||||
|
||||
await this.$submitButton.click();
|
||||
|
||||
const expectedPathname = typeof to === "string" ? to : to.pathname;
|
||||
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { createLocatorProxy, DeepLocatorProxy } from "#e2e/elements/proxy";
|
||||
/**
|
||||
* @file Playwright e2e test helpers.
|
||||
*/
|
||||
|
||||
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>;
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
|
||||
interface E2EFixturesTestScope {
|
||||
/**
|
||||
* A proxy to retrieve elements by test ID.
|
||||
*
|
||||
* ```ts
|
||||
* const $button = $.button;
|
||||
* ```
|
||||
*/
|
||||
$: TestIDLocatorProxy;
|
||||
session: SessionFixture;
|
||||
pointer: PointerFixture;
|
||||
form: FormFixture;
|
||||
@@ -30,18 +23,6 @@ interface E2EWorkerScope {
|
||||
}
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/* 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
2680
web/package-lock.json
generated
2680
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -94,10 +94,10 @@
|
||||
"@codemirror/legacy-modes": "^6.5.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@floating-ui/dom": "^1.7.2",
|
||||
"@floating-ui/dom": "^1.7.3",
|
||||
"@formatjs/intl-listformat": "^7.7.11",
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"@goauthentik/api": "^2025.6.4-1753714826",
|
||||
"@goauthentik/api": "^2025.6.4-1754491498",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
@@ -117,12 +117,12 @@
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.54.1",
|
||||
"@sentry/browser": "^9.42.1",
|
||||
"@sentry/browser": "^10.0.0",
|
||||
"@spotlightjs/spotlight": "^3.0.1",
|
||||
"@storybook/addon-docs": "^9.0.18",
|
||||
"@storybook/addon-links": "^9.0.18",
|
||||
"@storybook/web-components": "^9.0.18",
|
||||
"@storybook/web-components-vite": "^9.0.18",
|
||||
"@storybook/addon-docs": "^9.1.0",
|
||||
"@storybook/addon-links": "^9.1.0",
|
||||
"@storybook/web-components": "^9.1.0",
|
||||
"@storybook/web-components-vite": "^9.1.0",
|
||||
"@types/codemirror": "^5.60.16",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.3",
|
||||
@@ -133,16 +133,11 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@wdio/browser-runner": "^9.18.4",
|
||||
"@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.5.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chromedriver": "^138.0.4",
|
||||
"codemirror": "^6.0.2",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.44.0",
|
||||
@@ -203,9 +198,13 @@
|
||||
"@esbuild/darwin-arm64": "^0.25.4",
|
||||
"@esbuild/linux-arm64": "^0.25.4",
|
||||
"@esbuild/linux-x64": "^0.25.4",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.1"
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "^4.46.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.46.2",
|
||||
"@wdio/browser-runner": "^9.18.4",
|
||||
"@wdio/cli": "^9.18.4",
|
||||
"@wdio/spec-reporter": "^9.18.0",
|
||||
"@web/test-runner": "^0.20.2"
|
||||
},
|
||||
"wireit": {
|
||||
"build": {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"jquery": "^3.7.1",
|
||||
"prettier": "^3.5.3",
|
||||
"rollup": "^4.46.1",
|
||||
"rollup": "^4.46.2",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"weakmap-polyfill": "^2.0.4"
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
import { CapabilitiesEnum, WithCapabilitiesConfig } from "#elements/mixins/capabilities";
|
||||
import { navigate } from "#elements/router/RouterOutlet";
|
||||
|
||||
import { iconHelperText } from "#admin/helperText";
|
||||
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
|
||||
@@ -33,83 +34,90 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-application-form")
|
||||
export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
|
||||
constructor() {
|
||||
super();
|
||||
this.handleConfirmBackchannelProviders = this.handleConfirmBackchannelProviders.bind(this);
|
||||
this.makeRemoveBackchannelProviderHandler =
|
||||
this.makeRemoveBackchannelProviderHandler.bind(this);
|
||||
}
|
||||
#api = new CoreApi(DEFAULT_CONFIG);
|
||||
|
||||
async loadInstance(pk: string): Promise<Application> {
|
||||
const app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsRetrieve({
|
||||
protected override async loadInstance(pk: string): Promise<Application> {
|
||||
const app = await this.#api.coreApplicationsRetrieve({
|
||||
slug: pk,
|
||||
});
|
||||
|
||||
this.clearIcon = false;
|
||||
this.backchannelProviders = app.backchannelProvidersObj || [];
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
provider?: number;
|
||||
public provider?: number;
|
||||
|
||||
@state()
|
||||
backchannelProviders: Provider[] = [];
|
||||
protected backchannelProviders: Provider[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
clearIcon = false;
|
||||
public clearIcon = false;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
protected override getSuccessMessage(): string {
|
||||
return this.instance
|
||||
? msg("Successfully updated application.")
|
||||
: msg("Successfully created application.");
|
||||
}
|
||||
|
||||
async send(data: Application): Promise<Application | void> {
|
||||
let app: Application;
|
||||
data.backchannelProviders = this.backchannelProviders.map((p) => p.pk);
|
||||
if (this.instance) {
|
||||
app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsUpdate({
|
||||
slug: this.instance.slug,
|
||||
applicationRequest: data,
|
||||
});
|
||||
} else {
|
||||
app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({
|
||||
applicationRequest: data,
|
||||
});
|
||||
}
|
||||
public override async send(applicationRequest: Application): Promise<Application | void> {
|
||||
applicationRequest.backchannelProviders = this.backchannelProviders.map((p) => p.pk);
|
||||
|
||||
const currentSlug = this.instance?.slug;
|
||||
|
||||
const app = await (currentSlug
|
||||
? this.#api.coreApplicationsUpdate({
|
||||
applicationRequest,
|
||||
slug: currentSlug,
|
||||
})
|
||||
: this.#api.coreApplicationsCreate({ applicationRequest }));
|
||||
|
||||
const nextSlug = app.slug;
|
||||
|
||||
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
|
||||
const icon = this.files().get("metaIcon");
|
||||
|
||||
if (icon || this.clearIcon) {
|
||||
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
|
||||
slug: app.slug,
|
||||
await this.#api.coreApplicationsSetIconCreate({
|
||||
slug: nextSlug,
|
||||
file: icon,
|
||||
clear: this.clearIcon,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconUrlCreate({
|
||||
slug: app.slug,
|
||||
await this.#api.coreApplicationsSetIconUrlCreate({
|
||||
slug: nextSlug,
|
||||
filePathRequest: {
|
||||
url: data.metaIcon || "",
|
||||
url: applicationRequest.metaIcon || "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (currentSlug && currentSlug !== nextSlug) {
|
||||
// TODO: This needs refining.
|
||||
this.instancePk = nextSlug;
|
||||
navigate(`/core/applications/${nextSlug}`);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
handleConfirmBackchannelProviders(items: Provider[]) {
|
||||
#handleConfirmBackchannelProviders = (items: Provider[]) => {
|
||||
this.backchannelProviders = items;
|
||||
this.requestUpdate();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
makeRemoveBackchannelProviderHandler(provider: Provider) {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
#makeRemoveBackchannelProviderHandler = (provider: Provider) => {
|
||||
return () => {
|
||||
const idx = this.backchannelProviders.indexOf(provider);
|
||||
this.backchannelProviders.splice(idx, 1);
|
||||
this.requestUpdate();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
handleClearIcon(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
@@ -119,22 +127,25 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
this.clearIcon = !!(ev.target as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
public override renderForm(): TemplateResult {
|
||||
const alertMsg = msg(
|
||||
"Using this form will only create an Application. In order to authenticate with the application, you will have to manually pair it with a Provider.",
|
||||
);
|
||||
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
return html`
|
||||
${this.instance ? nothing : html`<ak-alert level="pf-m-info">${alertMsg}</ak-alert>`}
|
||||
<ak-text-input
|
||||
name="name"
|
||||
autocomplete="off"
|
||||
placeholder=${msg("Application name")}
|
||||
value=${ifDefined(this.instance?.name)}
|
||||
label=${msg("Name")}
|
||||
required
|
||||
help=${msg("Application's display Name.")}
|
||||
help=${msg("The name displayed in the application library.")}
|
||||
></ak-text-input>
|
||||
<ak-slug-input
|
||||
name="slug"
|
||||
autocomplete="off"
|
||||
value=${ifDefined(this.instance?.slug)}
|
||||
label=${msg("Slug")}
|
||||
required
|
||||
@@ -145,6 +156,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
name="group"
|
||||
value=${ifDefined(this.instance?.group)}
|
||||
label=${msg("Group")}
|
||||
placeholder=${msg("e.g. Collaboration, Communication, Internal, etc.")}
|
||||
help=${msg(
|
||||
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
|
||||
)}
|
||||
@@ -164,8 +176,8 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
"Select backchannel providers which augment the functionality of the main provider.",
|
||||
)}
|
||||
.providers=${this.backchannelProviders}
|
||||
.confirm=${this.handleConfirmBackchannelProviders}
|
||||
.remover=${this.makeRemoveBackchannelProviderHandler}
|
||||
.confirm=${this.#handleConfirmBackchannelProviders}
|
||||
.remover=${this.#makeRemoveBackchannelProviderHandler}
|
||||
.tooltip=${html`<pf-tooltip
|
||||
position="top"
|
||||
content=${msg("Add provider")}
|
||||
@@ -184,6 +196,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
<ak-text-input
|
||||
name="metaLaunchUrl"
|
||||
label=${msg("Launch URL")}
|
||||
placeholder="https://..."
|
||||
value=${ifDefined(this.instance?.metaLaunchUrl)}
|
||||
help=${msg(
|
||||
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
|
||||
@@ -235,7 +248,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
></ak-textarea-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import "#elements/buttons/SpinnerButton/ak-spinner-button";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
import { APIError, parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, html, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@@ -40,15 +41,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
@customElement("ak-application-view")
|
||||
export class ApplicationViewPage extends AKElement {
|
||||
@property({ type: String })
|
||||
applicationSlug?: string;
|
||||
|
||||
@state()
|
||||
application?: Application;
|
||||
|
||||
@state()
|
||||
missingOutpost = false;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFList,
|
||||
@@ -61,7 +53,28 @@ export class ApplicationViewPage extends AKElement {
|
||||
PFCard,
|
||||
];
|
||||
|
||||
fetchIsMissingOutpost(providersByPk: Array<number>) {
|
||||
//#region Properties
|
||||
|
||||
@property({ type: String })
|
||||
public applicationSlug?: string;
|
||||
//#endregion
|
||||
|
||||
//#region State
|
||||
|
||||
@state()
|
||||
protected application?: Application;
|
||||
|
||||
@state()
|
||||
protected error?: APIError;
|
||||
|
||||
@state()
|
||||
protected missingOutpost = false;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
protected fetchIsMissingOutpost(providersByPk: Array<number>) {
|
||||
new OutpostsApi(DEFAULT_CONFIG)
|
||||
.outpostsInstancesList({
|
||||
providersByPk,
|
||||
@@ -74,27 +87,34 @@ export class ApplicationViewPage extends AKElement {
|
||||
});
|
||||
}
|
||||
|
||||
fetchApplication(slug: string) {
|
||||
new CoreApi(DEFAULT_CONFIG).coreApplicationsRetrieve({ slug }).then((app) => {
|
||||
this.application = app;
|
||||
if (
|
||||
app.providerObj &&
|
||||
[
|
||||
RbacPermissionsAssignedByUsersListModelEnum.AuthentikProvidersProxyProxyprovider.toString(),
|
||||
RbacPermissionsAssignedByUsersListModelEnum.AuthentikProvidersLdapLdapprovider.toString(),
|
||||
].includes(app.providerObj.metaModelName)
|
||||
) {
|
||||
this.fetchIsMissingOutpost([app.provider || 0]);
|
||||
}
|
||||
});
|
||||
protected fetchApplication(slug: string) {
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreApplicationsRetrieve({ slug })
|
||||
.then((app) => {
|
||||
this.application = app;
|
||||
if (
|
||||
app.providerObj &&
|
||||
[
|
||||
RbacPermissionsAssignedByUsersListModelEnum.AuthentikProvidersProxyProxyprovider.toString(),
|
||||
RbacPermissionsAssignedByUsersListModelEnum.AuthentikProvidersLdapLdapprovider.toString(),
|
||||
].includes(app.providerObj.metaModelName)
|
||||
) {
|
||||
this.fetchIsMissingOutpost([app.provider || 0]);
|
||||
}
|
||||
})
|
||||
.catch(async (error) => {
|
||||
this.error = await parseAPIResponseError(error);
|
||||
});
|
||||
}
|
||||
|
||||
willUpdate(changedProperties: PropertyValues<this>) {
|
||||
public override willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("applicationSlug") && this.applicationSlug) {
|
||||
this.fetchApplication(this.applicationSlug);
|
||||
}
|
||||
}
|
||||
|
||||
//#region Render
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<ak-page-header
|
||||
header=${this.application?.name || msg("Loading")}
|
||||
@@ -111,9 +131,17 @@ export class ApplicationViewPage extends AKElement {
|
||||
}
|
||||
|
||||
renderApp(): TemplateResult {
|
||||
if (this.error) {
|
||||
return html`<ak-empty-state icon="fa-ban"
|
||||
><span>${msg(str`Failed to fetch application "${this.applicationSlug}".`)}</span>
|
||||
<div slot="body">${pluckErrorDetail(this.error)}</div>
|
||||
</ak-empty-state>`;
|
||||
}
|
||||
|
||||
if (!this.application) {
|
||||
return html`<ak-empty-state default-label></ak-empty-state>`;
|
||||
}
|
||||
|
||||
return html`<ak-tabs>
|
||||
${this.missingOutpost
|
||||
? html`<div slot="header" class="pf-c-banner pf-m-warning">
|
||||
@@ -188,7 +216,7 @@ export class ApplicationViewPage extends AKElement {
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<div class="pf-c-description-list__text pf-m-monospace">
|
||||
${this.application.policyEngineMode?.toUpperCase()}
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
@@ -114,12 +114,14 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
<form id="applicationform" class="pf-c-form pf-m-horizontal" slot="form">
|
||||
<ak-text-input
|
||||
name="name"
|
||||
autocomplete="off"
|
||||
placeholder=${msg("Application name")}
|
||||
value=${ifDefined(app.name)}
|
||||
label=${msg("Name")}
|
||||
required
|
||||
?invalid=${this.errors.has("name")}
|
||||
.errorMessages=${errors.name ?? this.errorMessages("name")}
|
||||
help=${msg("Application's display Name.")}
|
||||
help=${msg("The name displayed in the application library.")}
|
||||
></ak-text-input>
|
||||
<ak-slug-input
|
||||
name="slug"
|
||||
@@ -154,6 +156,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
|
||||
<ak-text-input
|
||||
name="metaLaunchUrl"
|
||||
label=${msg("Launch URL")}
|
||||
placeholder="https://..."
|
||||
value=${ifDefined(app.metaLaunchUrl)}
|
||||
?invalid=${this.errors.has("metaLaunchUrl")}
|
||||
.errorMessages=${errors.metaLaunchUrl ??
|
||||
|
||||
@@ -50,12 +50,13 @@ export class CoreGroupSearch extends CustomListenerElement(AKElement) {
|
||||
search!: SearchSelect<Group>;
|
||||
|
||||
@property({ type: String })
|
||||
public name?: string | null;
|
||||
name: string | null | undefined;
|
||||
|
||||
selectedGroup?: Group;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
|
||||
}
|
||||
|
||||
@@ -82,9 +83,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`
|
||||
|
||||
@@ -40,13 +40,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
|
||||
search!: SearchSelect<CertificateKeyPair>;
|
||||
|
||||
@property({ type: String })
|
||||
public name?: string | null;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string | undefined;
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder?: string | undefined;
|
||||
name: string | null | undefined;
|
||||
|
||||
/**
|
||||
* Set to `true` to allow certificates without private key to show up. When set to `false`,
|
||||
@@ -54,7 +48,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "nokey" })
|
||||
public noKey = false;
|
||||
noKey = false;
|
||||
|
||||
/**
|
||||
* Set this to true if, should there be only one certificate available, you want the system to
|
||||
@@ -63,12 +57,16 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean, attribute: "singleton" })
|
||||
public singleton = false;
|
||||
singleton = false;
|
||||
|
||||
/**
|
||||
* @todo Document this.
|
||||
*/
|
||||
public selectedKeypair?: CertificateKeyPair;
|
||||
selectedKeypair?: CertificateKeyPair;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
this.fetchObjects = this.fetchObjects.bind(this);
|
||||
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.selectedKeypair ? renderValue(this.selectedKeypair) : null;
|
||||
@@ -87,13 +85,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 }));
|
||||
};
|
||||
}
|
||||
|
||||
fetchObjects = async (query?: string): Promise<CertificateKeyPair[]> => {
|
||||
async fetchObjects(query?: string): Promise<CertificateKeyPair[]> {
|
||||
const args: CryptoCertificatekeypairsListRequest = {
|
||||
ordering: "name",
|
||||
hasKey: !this.noKey,
|
||||
@@ -106,21 +104,19 @@ 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)}
|
||||
.fetchObjects=${this.fetchObjects}
|
||||
.renderElement=${renderElement}
|
||||
.value=${renderValue}
|
||||
|
||||
@@ -3,7 +3,6 @@ import "#elements/forms/SearchSelect/index";
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import type { HorizontalFormElement } from "#elements/forms/HorizontalFormElement";
|
||||
import { SearchSelect } from "#elements/forms/SearchSelect/index";
|
||||
import { CustomListenerElement } from "#elements/utils/eventEmitter";
|
||||
|
||||
@@ -12,7 +11,6 @@ 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";
|
||||
@@ -35,17 +33,17 @@ export function getFlowValue(flow: Flow | undefined): string | undefined {
|
||||
* A wrapper around SearchSelect that understands the basic semantics of querying about Flows. This
|
||||
* code eliminates the long blocks of unreadable invocation that were embedded in every provider, as well as in
|
||||
* sources, brands, and applications.
|
||||
*
|
||||
*/
|
||||
export abstract class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement) {
|
||||
//#region Properties
|
||||
|
||||
export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement) {
|
||||
/**
|
||||
* The type of flow we're looking for.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
public flowType?: FlowsInstancesListDesignationEnum;
|
||||
flowType?: FlowsInstancesListDesignationEnum;
|
||||
|
||||
/**
|
||||
* The id of the current flow, if any. For stages where the flow is already defined.
|
||||
@@ -53,7 +51,7 @@ export abstract class FlowSearch<T extends Flow> extends CustomListenerElement(A
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
public currentFlow?: string | undefined;
|
||||
currentFlow?: string | undefined;
|
||||
|
||||
/**
|
||||
* If true, it is not valid to leave the flow blank.
|
||||
@@ -61,7 +59,10 @@ export abstract class FlowSearch<T extends Flow> extends CustomListenerElement(A
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
public required?: boolean = false;
|
||||
required?: boolean = false;
|
||||
|
||||
@query("ak-search-select")
|
||||
search!: SearchSelect<T>;
|
||||
|
||||
/**
|
||||
* When specified and the object instance does not have a flow selected, auto-select the flow with the given slug.
|
||||
@@ -69,81 +70,60 @@ export abstract class FlowSearch<T extends Flow> extends CustomListenerElement(A
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
public defaultFlowSlug?: string;
|
||||
defaultFlowSlug?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public name?: string;
|
||||
name: string | null | undefined;
|
||||
|
||||
/**
|
||||
* 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 = msg("Select a flow...");
|
||||
|
||||
@query("ak-search-select")
|
||||
protected search!: SearchSelect<T>;
|
||||
|
||||
protected selectedFlow?: T;
|
||||
selectedFlow?: T;
|
||||
|
||||
get value() {
|
||||
return this.selectedFlow ? getFlowValue(this.selectedFlow) : null;
|
||||
}
|
||||
|
||||
protected searchUpdateListener = (event: CustomEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
this.selectedFlow = event.detail.value;
|
||||
constructor() {
|
||||
super();
|
||||
this.fetchObjects = this.fetchObjects.bind(this);
|
||||
this.selected = this.selected.bind(this);
|
||||
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
|
||||
}
|
||||
|
||||
handleSearchUpdate(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this.selectedFlow = ev.detail.value;
|
||||
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
|
||||
};
|
||||
}
|
||||
|
||||
protected fetchObjects = (query?: string): Promise<Flow[]> => {
|
||||
async fetchObjects(query?: string): Promise<Flow[]> {
|
||||
const args: FlowsInstancesListRequest = {
|
||||
ordering: "slug",
|
||||
designation: this.flowType,
|
||||
...(query ? { search: query } : {}),
|
||||
...(query !== undefined ? { search: query } : {}),
|
||||
};
|
||||
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args).then((flows) => flows.results);
|
||||
};
|
||||
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
|
||||
* returning false.
|
||||
*/
|
||||
protected selected(flow: Flow): boolean {
|
||||
selected(flow: Flow): boolean {
|
||||
let selected = this.currentFlow === flow.pk;
|
||||
if (!this.currentFlow && this.defaultFlowSlug && flow.slug === this.defaultFlowSlug) {
|
||||
return true;
|
||||
selected = true;
|
||||
}
|
||||
|
||||
return this.currentFlow === flow.pk;
|
||||
return selected;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
const horizontalContainer = this.closest<HorizontalFormElement>(
|
||||
"ak-form-element-horizontal[name]",
|
||||
);
|
||||
|
||||
const horizontalContainer = this.closest("ak-form-element-horizontal[name]");
|
||||
if (!horizontalContainer) {
|
||||
throw new Error("This search can only be used in a named ak-form-element-horizontal");
|
||||
}
|
||||
|
||||
const name = horizontalContainer.getAttribute("name");
|
||||
const myName = this.getAttribute("name");
|
||||
|
||||
if (name !== null && name !== myName) {
|
||||
this.setAttribute("name", name);
|
||||
}
|
||||
@@ -157,10 +137,8 @@ export abstract class FlowSearch<T extends Flow> extends CustomListenerElement(A
|
||||
.renderElement=${renderElement}
|
||||
.renderDescription=${renderDescription}
|
||||
.value=${getFlowValue}
|
||||
placeholder=${ifDefined(this.placeholder)}
|
||||
label=${ifDefined(this.label)}
|
||||
name=${ifDefined(this.name)}
|
||||
@ak-change=${this.searchUpdateListener}
|
||||
name=${ifDefined(this.name ?? undefined)}
|
||||
@ak-change=${this.handleSearchUpdate}
|
||||
?blankable=${!this.required}
|
||||
>
|
||||
</ak-search-select>
|
||||
|
||||
@@ -19,9 +19,14 @@ export class AkBrandedFlowSearch<T extends Flow> extends FlowSearch<T> {
|
||||
* @attr
|
||||
*/
|
||||
@property({ attribute: false, type: String })
|
||||
public brandFlow?: string;
|
||||
brandFlow?: string;
|
||||
|
||||
protected override selected(flow: Flow): boolean {
|
||||
constructor() {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
}
|
||||
|
||||
selected(flow: Flow): boolean {
|
||||
return super.selected(flow) || flow.pk === this.brandFlow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export class AkFlowSearchNoDefault<T extends Flow> extends FlowSearch<T> {
|
||||
.renderElement=${renderElement}
|
||||
.renderDescription=${renderDescription}
|
||||
.value=${getFlowValue}
|
||||
@ak-change=${this.searchUpdateListener}
|
||||
@ak-change=${this.handleSearchUpdate}
|
||||
?blankable=${!this.required}
|
||||
>
|
||||
</ak-search-select>
|
||||
|
||||
@@ -18,8 +18,9 @@ export class AkSourceFlowSearch<T extends Flow> extends FlowSearch<T> {
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
|
||||
@property({ type: String })
|
||||
public fallback?: string;
|
||||
fallback: string | undefined;
|
||||
|
||||
/**
|
||||
* The primary key of the Source (not the Flow). Mostly the instancePk itself, used to affirm
|
||||
@@ -28,11 +29,16 @@ export class AkSourceFlowSearch<T extends Flow> extends FlowSearch<T> {
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String })
|
||||
public instanceId?: 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.
|
||||
protected override selected(flow: Flow): boolean {
|
||||
selected(flow: Flow): boolean {
|
||||
return (
|
||||
(!this.instanceId && !this.currentFlow && flow.slug === this.fallback) ||
|
||||
super.selected(flow)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import { groupBy } from "#common/utils";
|
||||
|
||||
import { ModelForm } from "#elements/forms/ModelForm";
|
||||
|
||||
import { policyEngineModes } from "#admin/policies/PolicyEngineModes";
|
||||
|
||||
import {
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
FlowStageBinding,
|
||||
InvalidResponseActionEnum,
|
||||
PolicyEngineMode,
|
||||
Stage,
|
||||
StagesAllListRequest,
|
||||
StagesApi,
|
||||
@@ -202,22 +203,7 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
|
||||
required
|
||||
name="policyEngineMode"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: "any",
|
||||
value: PolicyEngineMode.Any,
|
||||
default: true,
|
||||
description: html`${msg("Any policy must match to grant access")}`,
|
||||
},
|
||||
{
|
||||
label: "all",
|
||||
value: PolicyEngineMode.All,
|
||||
description: html`${msg("All policies must match to grant access")}`,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.policyEngineMode}
|
||||
>
|
||||
<ak-radio .options=${policyEngineModes} .value=${this.instance?.policyEngineMode}>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import type { RadioOption } from "#elements/forms/Radio";
|
||||
|
||||
import { PolicyEngineMode } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
|
||||
export const policyEngineModes = [
|
||||
export const policyEngineModes: RadioOption<PolicyEngineMode>[] = [
|
||||
{
|
||||
label: "any",
|
||||
label: "ANY",
|
||||
className: "pf-m-monospace",
|
||||
value: PolicyEngineMode.Any,
|
||||
default: true,
|
||||
description: html`${msg("Any policy must match to grant access")}`,
|
||||
},
|
||||
{
|
||||
label: "all",
|
||||
label: "ALL",
|
||||
className: "pf-m-monospace",
|
||||
value: PolicyEngineMode.All,
|
||||
description: html`${msg("All policies must match to grant access")}`,
|
||||
},
|
||||
|
||||
@@ -140,8 +140,6 @@ 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"
|
||||
>
|
||||
|
||||
@@ -113,6 +113,19 @@ export const redirectUriHelp = html`${redirectUriHelpMessages.map(
|
||||
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
|
||||
)}`;
|
||||
|
||||
const backchannelLogoutUriHelpMessages = [
|
||||
msg(
|
||||
"URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.",
|
||||
),
|
||||
msg(
|
||||
"These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.",
|
||||
),
|
||||
];
|
||||
|
||||
export const backchannelLogoutUriHelp = html`${backchannelLogoutUriHelpMessages.map(
|
||||
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
|
||||
)}`;
|
||||
|
||||
type ShowClientSecret = (show: boolean) => void;
|
||||
const defaultShowClientSecret: ShowClientSecret = (_show) => undefined;
|
||||
|
||||
@@ -124,7 +137,6 @@ export function renderForm(
|
||||
) {
|
||||
return html` <ak-text-input
|
||||
name="name"
|
||||
placeholder=${msg("Provider name")}
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
required
|
||||
@@ -136,8 +148,6 @@ export function renderForm(
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
label=${msg("Authorization flow")}
|
||||
placeholder=${msg("Select an authorization flow...")}
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
@@ -196,12 +206,21 @@ export function renderForm(
|
||||
</ak-array-input>
|
||||
${redirectUriHelp}
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
flow-direction="row"
|
||||
label=${msg("Back-Channel Logout URI")}
|
||||
>
|
||||
<ak-text-input
|
||||
name="backchannelLogoutUri"
|
||||
value="${provider?.backchannelLogoutUri ?? ""}"
|
||||
placeholder=${msg("URL")}
|
||||
></ak-text-input>
|
||||
${backchannelLogoutUriHelp}
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<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>
|
||||
@@ -210,8 +229,6 @@ 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>
|
||||
@@ -226,8 +243,6 @@ export function renderForm(
|
||||
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>
|
||||
@@ -243,8 +258,6 @@ 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"
|
||||
|
||||
@@ -4,6 +4,7 @@ import "#components/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/EmptyState";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/tasks/TaskList";
|
||||
import "#elements/ak-mdx/index";
|
||||
import "#elements/buttons/ModalButton";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
ClientTypeEnum,
|
||||
CoreApi,
|
||||
CoreUsersListRequest,
|
||||
ModelEnum,
|
||||
OAuth2Provider,
|
||||
OAuth2ProviderSetupURLs,
|
||||
PropertyMappingPreview,
|
||||
@@ -26,7 +28,6 @@ import {
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
User,
|
||||
} from "@goauthentik/api";
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
|
||||
import MDProviderOAuth2 from "~docs/add-secure-apps/providers/oauth2/index.mdx";
|
||||
|
||||
@@ -171,6 +172,7 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
if (!this.provider) {
|
||||
return html``;
|
||||
}
|
||||
const [appLabel, modelName] = ModelEnum.AuthentikProvidersOauth2Oauth2provider.split(".");
|
||||
return html` ${this.provider?.assignedApplicationName
|
||||
? html``
|
||||
: html`<div slot="header" class="pf-c-banner pf-m-warning">
|
||||
@@ -247,6 +249,18 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Back-Channel Logout URI")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text pf-m-monospace">
|
||||
${this.provider.backchannelLogoutUri}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
@@ -268,16 +282,12 @@ 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"
|
||||
for="${IDGenerator.elementID("providerInfo")}"
|
||||
>
|
||||
<label class="pf-c-form__label">
|
||||
<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"
|
||||
@@ -285,16 +295,12 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
/>
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label
|
||||
class="pf-c-form__label"
|
||||
for="${IDGenerator.elementID("issuer")}"
|
||||
>
|
||||
<label class="pf-c-form__label">
|
||||
<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"
|
||||
@@ -303,16 +309,12 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<hr class="pf-c-divider" />
|
||||
<div class="pf-c-form__group">
|
||||
<label
|
||||
class="pf-c-form__label"
|
||||
for="${IDGenerator.elementID("authorize")}"
|
||||
>
|
||||
<label class="pf-c-form__label">
|
||||
<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"
|
||||
@@ -320,14 +322,10 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
/>
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label
|
||||
class="pf-c-form__label"
|
||||
for="${IDGenerator.elementID("token")}"
|
||||
>
|
||||
<label class="pf-c-form__label">
|
||||
<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"
|
||||
@@ -335,16 +333,12 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
/>
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label
|
||||
class="pf-c-form__label"
|
||||
for="${IDGenerator.elementID("userInfo")}"
|
||||
>
|
||||
<label class="pf-c-form__label">
|
||||
<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"
|
||||
@@ -352,14 +346,10 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
/>
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label
|
||||
class="pf-c-form__label"
|
||||
for="${IDGenerator.elementID("logout")}"
|
||||
>
|
||||
<label class="pf-c-form__label">
|
||||
<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"
|
||||
@@ -367,14 +357,10 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
/>
|
||||
</div>
|
||||
<div class="pf-c-form__group">
|
||||
<label
|
||||
class="pf-c-form__label"
|
||||
for="${IDGenerator.elementID("jwks")}"
|
||||
>
|
||||
<label class="pf-c-form__label">
|
||||
<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"
|
||||
@@ -384,6 +370,18 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col-on-2xl">
|
||||
<div class="pf-c-card__title">${msg("Tasks")}</div>
|
||||
<ak-task-list
|
||||
.relObjAppLabel=${appLabel}
|
||||
.relObjModel=${modelName}
|
||||
.relObjId="${this.provider.pk}"
|
||||
></ak-task-list>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
|
||||
>
|
||||
@@ -420,12 +418,9 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
${renderDescriptionList(
|
||||
[
|
||||
[
|
||||
html`<label for="${IDGenerator.elementID("preview-user")}"
|
||||
>${msg("Preview for user")}</label
|
||||
>`,
|
||||
msg("Preview for user"),
|
||||
html`
|
||||
<ak-search-select
|
||||
id="${IDGenerator.elementID("preview-user")}"
|
||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||
const args: CoreUsersListRequest = {
|
||||
ordering: "username",
|
||||
|
||||
@@ -45,7 +45,6 @@ export function renderForm(
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
placeholder=${msg("Provider name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
@@ -59,8 +58,6 @@ 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}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PropertymappingsApi,
|
||||
PropertymappingsProviderSamlListRequest,
|
||||
SAMLNameIDPolicyEnum,
|
||||
SAMLPropertyMapping,
|
||||
SAMLProvider,
|
||||
SpBindingEnum,
|
||||
@@ -314,6 +315,54 @@ export function renderForm(
|
||||
"When using IDP-initiated logins, the relay state will be set to this value.",
|
||||
)}
|
||||
></ak-text-input>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Default NameID Policy")}
|
||||
required
|
||||
name="defaultNameIdPolicy"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatPersistent}
|
||||
?selected=${provider?.defaultNameIdPolicy ===
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatPersistent}
|
||||
>
|
||||
${msg("Persistent")}
|
||||
</option>
|
||||
<option
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatEmailAddress}
|
||||
?selected=${provider?.defaultNameIdPolicy ===
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatEmailAddress}
|
||||
>
|
||||
${msg("Email address")}
|
||||
</option>
|
||||
<option
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatWindowsDomainQualifiedName}
|
||||
?selected=${provider?.defaultNameIdPolicy ===
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatWindowsDomainQualifiedName}
|
||||
>
|
||||
${msg("Windows")}
|
||||
</option>
|
||||
<option
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatX509SubjectName}
|
||||
?selected=${provider?.defaultNameIdPolicy ===
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatX509SubjectName}
|
||||
>
|
||||
${msg("X509 Subject")}
|
||||
</option>
|
||||
<option
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatTransient}
|
||||
?selected=${provider?.defaultNameIdPolicy ===
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatTransient}
|
||||
>
|
||||
${msg("Transient")}
|
||||
</option>
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure the default NameID Policy used by IDP-initiated logins and when an incoming assertion doesn't specify a NameID Policy (also applies when using a custom NameID Mapping).",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-radio-input
|
||||
name="digestAlgorithm"
|
||||
|
||||
@@ -74,8 +74,8 @@ export class InitialPermissionsForm extends ModelForm<InitialPermissions, string
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const users = await new RbacApi(DEFAULT_CONFIG).rbacRolesList(args);
|
||||
return users.results;
|
||||
const roles = await new RbacApi(DEFAULT_CONFIG).rbacRolesList(args);
|
||||
return roles.results;
|
||||
}}
|
||||
.renderElement=${(role: Role): string => {
|
||||
return role.name;
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
DigestAlgorithmEnum,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
GroupMatchingModeEnum,
|
||||
NameIdPolicyEnum,
|
||||
SAMLNameIDPolicyEnum,
|
||||
SAMLSource,
|
||||
SignatureAlgorithmEnum,
|
||||
SourcesApi,
|
||||
@@ -351,37 +351,37 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm<SAMLSo
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
<option
|
||||
value=${NameIdPolicyEnum.UrnOasisNamesTcSaml20NameidFormatPersistent}
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatPersistent}
|
||||
?selected=${this.instance?.nameIdPolicy ===
|
||||
NameIdPolicyEnum.UrnOasisNamesTcSaml20NameidFormatPersistent}
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatPersistent}
|
||||
>
|
||||
${msg("Persistent")}
|
||||
</option>
|
||||
<option
|
||||
value=${NameIdPolicyEnum.UrnOasisNamesTcSaml11NameidFormatEmailAddress}
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatEmailAddress}
|
||||
?selected=${this.instance?.nameIdPolicy ===
|
||||
NameIdPolicyEnum.UrnOasisNamesTcSaml11NameidFormatEmailAddress}
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatEmailAddress}
|
||||
>
|
||||
${msg("Email address")}
|
||||
</option>
|
||||
<option
|
||||
value=${NameIdPolicyEnum.UrnOasisNamesTcSaml20NameidFormatWindowsDomainQualifiedName}
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatWindowsDomainQualifiedName}
|
||||
?selected=${this.instance?.nameIdPolicy ===
|
||||
NameIdPolicyEnum.UrnOasisNamesTcSaml20NameidFormatWindowsDomainQualifiedName}
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatWindowsDomainQualifiedName}
|
||||
>
|
||||
${msg("Windows")}
|
||||
</option>
|
||||
<option
|
||||
value=${NameIdPolicyEnum.UrnOasisNamesTcSaml11NameidFormatX509SubjectName}
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatX509SubjectName}
|
||||
?selected=${this.instance?.nameIdPolicy ===
|
||||
NameIdPolicyEnum.UrnOasisNamesTcSaml11NameidFormatX509SubjectName}
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml11NameidFormatX509SubjectName}
|
||||
>
|
||||
${msg("X509 Subject")}
|
||||
</option>
|
||||
<option
|
||||
value=${NameIdPolicyEnum.UrnOasisNamesTcSaml20NameidFormatTransient}
|
||||
value=${SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatTransient}
|
||||
?selected=${this.instance?.nameIdPolicy ===
|
||||
NameIdPolicyEnum.UrnOasisNamesTcSaml20NameidFormatTransient}
|
||||
SAMLNameIDPolicyEnum.UrnOasisNamesTcSaml20NameidFormatTransient}
|
||||
>
|
||||
${msg("Transient")}
|
||||
</option>
|
||||
|
||||
@@ -232,6 +232,36 @@ export class EmailStageForm extends BaseStageForm<EmailStage> {
|
||||
})}
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Account Recovery Max Attempts")}
|
||||
required
|
||||
name="recoveryMaxAttempts"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${this.instance?.recoveryMaxAttempts ?? 5}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Account Recovery Cache Timeout")}
|
||||
required
|
||||
name="recoveryCacheTimeout"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.recoveryCacheTimeout || "minutes=5")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"The time window used to count recent account recovery attempts.",
|
||||
)}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
${this.renderConnectionSettings()}`;
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
--ak-navbar--height: 7rem;
|
||||
}
|
||||
|
||||
.pf-c-form__group {
|
||||
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: minmax(max-content, 9.375rem);
|
||||
column-gap: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { html, nothing, TemplateResult } from "lit";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { map } from "lit/directives/map.js";
|
||||
|
||||
export type DescriptionPair = [
|
||||
term: SlottedTemplateResult,
|
||||
desc: SlottedTemplateResult | undefined,
|
||||
];
|
||||
export type DescriptionRecord = { term: string; desc: SlottedTemplateResult | undefined };
|
||||
export type DescriptionDesc = string | TemplateResult | undefined | typeof nothing;
|
||||
export type DescriptionPair = [string, DescriptionDesc];
|
||||
export type DescriptionRecord = { term: string; desc: DescriptionDesc };
|
||||
|
||||
interface DescriptionConfig {
|
||||
horizontal?: boolean;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user