mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 23:52:38 +02:00
Compare commits
4 Commits
playwright
...
docs-remov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1cb339f3a | ||
|
|
e124e21119 | ||
|
|
dc0c7a858a | ||
|
|
3fddbb918e |
@@ -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,authentik/server \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||
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,authentik/server \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||
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@v5
|
||||
- uses: actions/download-artifact@v4
|
||||
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,authentik/server
|
||||
image_name: ghcr.io/goauthentik/server,beryju/authentik
|
||||
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_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_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_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},authentik/${{ matrix.type }}
|
||||
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/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_CORP_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_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_CORP_USERNAME }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/server
|
||||
- name: Get static files from docker image
|
||||
|
||||
@@ -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.5 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.4 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -301,7 +301,6 @@ 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
|
||||
|
||||
@@ -70,7 +70,6 @@ class OAuth2ProviderSerializer(ProviderSerializer):
|
||||
"signing_key",
|
||||
"encryption_key",
|
||||
"redirect_uris",
|
||||
"backchannel_logout_uri",
|
||||
"sub_mode",
|
||||
"property_mappings",
|
||||
"issuer_mode",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""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
|
||||
@@ -54,23 +51,3 @@ 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,8 +4,10 @@ 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
|
||||
@@ -16,7 +18,6 @@ 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
|
||||
|
||||
@@ -29,6 +30,26 @@ 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
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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 TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
@@ -42,14 +42,11 @@ 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 DomainlessURLValidator, SerializerModel
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
from authentik.providers.oauth2.constants import SubModes
|
||||
from authentik.providers.oauth2.id_token import IDToken, SubModes
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.providers.oauth2.id_token import IDToken
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@@ -196,14 +193,9 @@ class OAuth2Provider(WebfingerProvider, Provider):
|
||||
default=generate_client_secret,
|
||||
)
|
||||
_redirect_uris = models.JSONField(
|
||||
default=list,
|
||||
default=dict,
|
||||
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,
|
||||
@@ -488,15 +480,13 @@ 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))
|
||||
|
||||
@@ -541,15 +531,13 @@ 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,34 +1,17 @@
|
||||
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_backchannel_logout_and_tokens_removal(
|
||||
sender, instance: AuthenticatedSession, **_
|
||||
):
|
||||
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
|
||||
"""Revoke tokens upon user logout"""
|
||||
LOGGER.debug("Sending back-channel logout notifications signal!", session=instance)
|
||||
|
||||
access_tokens = AccessToken.objects.filter(
|
||||
AccessToken.objects.filter(
|
||||
user=instance.user,
|
||||
session__session__session_key=instance.session.session_key,
|
||||
)
|
||||
|
||||
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()
|
||||
).delete()
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
"""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,46 +81,4 @@ 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")
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
"""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,10 +1,8 @@
|
||||
"""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
|
||||
|
||||
@@ -16,7 +14,6 @@ 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()
|
||||
@@ -214,36 +211,3 @@ 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,8 +9,7 @@ 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.id_token import IDToken
|
||||
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, RefreshToken
|
||||
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
|
||||
from authentik.providers.oauth2.utils import TokenResponse, authenticate_provider
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -72,8 +72,6 @@ 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,
|
||||
|
||||
@@ -44,8 +44,6 @@ class EmailStageSerializer(StageSerializer):
|
||||
"subject",
|
||||
"template",
|
||||
"activate_user_on_success",
|
||||
"recovery_max_attempts",
|
||||
"recovery_cache_timeout",
|
||||
]
|
||||
extra_kwargs = {"password": {"write_only": True}}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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,8 +16,6 @@ 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()
|
||||
|
||||
|
||||
@@ -72,17 +70,6 @@ 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,12 +1,9 @@
|
||||
"""authentik multi-stage authentication engine"""
|
||||
|
||||
import math
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from hashlib import sha256
|
||||
from datetime import timedelta
|
||||
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
|
||||
@@ -30,8 +27,6 @@ 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"
|
||||
|
||||
@@ -175,66 +170,10 @@ 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,9 +1,7 @@
|
||||
"""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
|
||||
@@ -11,7 +9,6 @@ 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
|
||||
@@ -20,7 +17,6 @@ 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
|
||||
@@ -295,173 +291,3 @@ 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,8 +61,6 @@ 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
|
||||
|
||||
@@ -8473,10 +8473,6 @@
|
||||
},
|
||||
"title": "Redirect uris"
|
||||
},
|
||||
"backchannel_logout_uri": {
|
||||
"type": "string",
|
||||
"title": "Back-Channel Logout URI"
|
||||
},
|
||||
"sub_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -14336,18 +14332,6 @@
|
||||
"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": []
|
||||
|
||||
2
go.mod
2
go.mod
@@ -29,7 +29,7 @@ require (
|
||||
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.6
|
||||
goauthentik.io/api/v3 v3.2025064.3
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.16.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -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.6 h1:s9DaQ8x93T9IjDBqSX69VTuB5kBH3nHyI3/2Mlhlf08=
|
||||
goauthentik.io/api/v3 v3.2025064.6/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
goauthentik.io/api/v3 v3.2025064.3 h1:REfDBEjswP2id2WRRDUajRxX+6u+XZ7e/smYq7jw5Z0=
|
||||
goauthentik.io/api/v3 v3.2025064.3/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=
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-06 00:11+0000\n"
|
||||
"POT-Creation-Date: 2025-07-28 16:09+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/constants.py
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
msgid "Based on the Hashed User ID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
msgid "Based on user ID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
msgid "Based on user UUID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
msgid "Based on the username"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
msgid "Based on the User's Email. This is recommended over the UPN method."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/constants.py
|
||||
#: authentik/providers/oauth2/id_token.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,10 +1617,6 @@ 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 ""
|
||||
@@ -1736,14 +1732,6 @@ 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-07-28 16:09+0000\n"
|
||||
"POT-Creation-Date: 2025-06-04 00:12+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,10 +33,6 @@ 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!"
|
||||
@@ -92,25 +88,10 @@ msgstr "Instances du plan"
|
||||
msgid "authentik Export - {date}"
|
||||
msgstr "Export authentik - {date}"
|
||||
|
||||
#: 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/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/brands/models.py
|
||||
msgid ""
|
||||
@@ -148,6 +129,10 @@ 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."
|
||||
@@ -394,10 +379,6 @@ 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é"
|
||||
@@ -453,14 +434,6 @@ 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"
|
||||
@@ -513,12 +486,6 @@ 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."
|
||||
@@ -571,18 +538,6 @@ 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é."
|
||||
@@ -631,42 +586,6 @@ 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"
|
||||
@@ -695,42 +614,6 @@ 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"
|
||||
@@ -769,12 +652,8 @@ msgid "SSF Stream Events"
|
||||
msgstr "Évènements du flux SSF"
|
||||
|
||||
#: authentik/enterprise/providers/ssf/tasks.py
|
||||
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."
|
||||
msgid "Failed to send request"
|
||||
msgstr "Échec de l'envoi de la requête"
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||
@@ -846,9 +725,10 @@ msgstr "Étape Source"
|
||||
msgid "Source Stages"
|
||||
msgstr "Étapes Source"
|
||||
|
||||
#: authentik/enterprise/tasks.py
|
||||
msgid "Update enterprise license status."
|
||||
msgstr "Mettre à jour le statut de licence entreprise."
|
||||
#: 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/events/models.py
|
||||
msgid "Event"
|
||||
@@ -960,15 +840,6 @@ 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"
|
||||
@@ -985,6 +856,10 @@ 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"
|
||||
@@ -993,31 +868,9 @@ msgstr "Tâches du système"
|
||||
msgid "System Tasks"
|
||||
msgstr "Tâches du système"
|
||||
|
||||
#: 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/events/system_tasks.py
|
||||
msgid "Task has not been run yet."
|
||||
msgstr "Tâche pas encore exécutée."
|
||||
|
||||
#: authentik/flows/api/flows.py
|
||||
#, python-brace-format
|
||||
@@ -1198,6 +1051,32 @@ 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'."
|
||||
@@ -1304,32 +1183,6 @@ 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é"
|
||||
@@ -2048,10 +1901,6 @@ 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 "
|
||||
@@ -2396,35 +2245,6 @@ 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"
|
||||
@@ -2579,14 +2399,6 @@ 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"
|
||||
@@ -2754,18 +2566,6 @@ 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."
|
||||
@@ -2915,14 +2715,6 @@ 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"
|
||||
@@ -2979,14 +2771,6 @@ 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}"
|
||||
@@ -3045,10 +2829,6 @@ 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"
|
||||
@@ -3493,13 +3273,6 @@ 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."
|
||||
@@ -3626,10 +3399,6 @@ 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!"
|
||||
@@ -4098,16 +3867,6 @@ 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"
|
||||
@@ -4159,38 +3918,6 @@ 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 "
|
||||
@@ -4283,76 +4010,3 @@ 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"
|
||||
|
||||
41
schema.yml
41
schema.yml
@@ -44845,15 +44845,6 @@ 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
|
||||
@@ -44914,16 +44905,6 @@ 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:
|
||||
@@ -49522,10 +49503,6 @@ 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'
|
||||
@@ -49633,10 +49610,6 @@ 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'
|
||||
@@ -53413,16 +53386,6 @@ 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
|
||||
@@ -54556,10 +54519,6 @@ 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'
|
||||
|
||||
31
uv.lock
generated
31
uv.lock
generated
@@ -86,15 +86,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.10.0"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -557,30 +557,30 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.40.2"
|
||||
version = "1.40.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/4d/70d209fdebf0377db233f80dfdf26ca2bc25d2b2e89d4882e0edccd2227f/boto3-1.40.1.tar.gz", hash = "sha256:985ed4bf64729807f870eadbc46ad98baf93096917f7194ec39d743ff75b3f1d", size = 111817, upload-time = "2025-08-01T19:24:18.017Z" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/0e/f0cb4f71c40ba07e6ed5b47699a737a080d3c4f4b7b26657d5671de48621/boto3-1.40.1-py3-none-any.whl", hash = "sha256:7c007d5c8ee549e9fcad0927536502da199b27891006ef515330f429aca9671f", size = 139880, upload-time = "2025-08-01T19:24:16.581Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.40.2"
|
||||
version = "1.40.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/d2/d914999f4a128f0f840f2a9cc8327cd98aa661d6b33b331a81a8111ab970/botocore-1.40.1.tar.gz", hash = "sha256:bdf30e2c0e8cdb939d81fc243182a6d1dd39c416694b406c5f2ea079b1c2f3f5", size = 14280398, upload-time = "2025-08-01T19:24:08.599Z" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/c1/aa7922c9bf74b6d6594d2430af6f854d234faff23187e269aaba89c326c8/botocore-1.40.1-py3-none-any.whl", hash = "sha256:e039774b55fbd6fe59f0f4fea51d156a2433bd4d8faa64fc1b87aee9a03f415d", size = 13940950, upload-time = "2025-08-01T19:24:03.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -603,15 +603,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cattrs"
|
||||
version = "25.1.1"
|
||||
version = "24.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -632,11 +631,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
version = "2025.7.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
2
web/.gitignore
vendored
2
web/.gitignore
vendored
@@ -25,8 +25,6 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
playwright-report
|
||||
test-results
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
import { PageFixture } from "#e2e/fixtures/PageFixture";
|
||||
import type { LocatorContext } from "#e2e/selectors/types";
|
||||
|
||||
import { expect, Page } from "@playwright/test";
|
||||
|
||||
export class FormFixture extends PageFixture {
|
||||
static fixtureName = "Form";
|
||||
|
||||
//#region Selector Methods
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Field Methods
|
||||
|
||||
/**
|
||||
* Set the value of a text input.
|
||||
*
|
||||
* @param fieldName The name of the form element.
|
||||
* @param value the value to set.
|
||||
*/
|
||||
public fill = async (
|
||||
fieldName: string,
|
||||
value: string,
|
||||
parent: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
const control = parent
|
||||
.getByRole("textbox", {
|
||||
name: fieldName,
|
||||
})
|
||||
.or(
|
||||
parent.getByRole("spinbutton", {
|
||||
name: fieldName,
|
||||
}),
|
||||
)
|
||||
.first();
|
||||
|
||||
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
|
||||
|
||||
await control.fill(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a radio or checkbox input.
|
||||
*
|
||||
* @param fieldName The name of the form element.
|
||||
* @param value the value to set.
|
||||
*/
|
||||
public setInputCheck = async (
|
||||
fieldName: string,
|
||||
value: boolean = true,
|
||||
parent: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
const control = parent.locator("ak-switch-input", {
|
||||
hasText: fieldName,
|
||||
});
|
||||
|
||||
await control.scrollIntoViewIfNeeded();
|
||||
|
||||
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
|
||||
|
||||
const currentChecked = await control
|
||||
.getAttribute("checked")
|
||||
.then((value) => value !== null);
|
||||
|
||||
if (currentChecked === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await control.click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a radio or checkbox input.
|
||||
*
|
||||
* @param fieldName The name of the form element.
|
||||
* @param pattern the value to set.
|
||||
*/
|
||||
public setRadio = async (
|
||||
groupName: string,
|
||||
fieldName: string,
|
||||
parent: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
const group = parent.getByRole("group", { name: groupName });
|
||||
|
||||
await expect(group, `Field "${groupName}" should be visible`).toBeVisible();
|
||||
const control = parent.getByRole("radio", { name: fieldName });
|
||||
|
||||
await control.setChecked(true, {
|
||||
force: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value of a search select input.
|
||||
*
|
||||
* @param fieldLabel The name of the search select element.
|
||||
* @param pattern The text to match against the search select entry.
|
||||
*/
|
||||
public selectSearchValue = async (
|
||||
fieldLabel: string,
|
||||
pattern: string | RegExp,
|
||||
parent: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
const control = parent.getByRole("textbox", { name: fieldLabel });
|
||||
|
||||
await expect(
|
||||
control,
|
||||
`Search select control (${fieldLabel}) should be visible`,
|
||||
).toBeVisible();
|
||||
|
||||
const fieldName = await control.getAttribute("name");
|
||||
|
||||
if (!fieldName) {
|
||||
throw new Error(`Unable to find name attribute on search select (${fieldLabel})`);
|
||||
}
|
||||
|
||||
// Find the search select input control and activate it.
|
||||
await control.click();
|
||||
|
||||
const button = this.page
|
||||
// ---
|
||||
.locator(`div[data-managed-for*="${fieldName}"] button`, {
|
||||
hasText: pattern,
|
||||
});
|
||||
|
||||
if (!button) {
|
||||
throw new Error(
|
||||
`Unable to find an ak-search-select entry matching ${fieldLabel}:${pattern.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
await button.click();
|
||||
await this.page.keyboard.press("Tab");
|
||||
await control.blur();
|
||||
};
|
||||
|
||||
public setFormGroup = async (
|
||||
pattern: string | RegExp,
|
||||
value: boolean = true,
|
||||
parent: LocatorContext = this.page,
|
||||
) => {
|
||||
const control = parent
|
||||
.locator("ak-form-group", {
|
||||
hasText: pattern,
|
||||
})
|
||||
.first();
|
||||
|
||||
const currentOpen = await control.getAttribute("open").then((value) => value !== null);
|
||||
|
||||
if (currentOpen === value) {
|
||||
this.logger.debug(`Form group ${pattern} is already ${value ? "open" : "closed"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`Toggling form group ${pattern} to ${value ? "open" : "closed"}`);
|
||||
|
||||
await control.click();
|
||||
|
||||
if (value) {
|
||||
await expect(control).toHaveAttribute("open");
|
||||
} else {
|
||||
await expect(control).not.toHaveAttribute("open");
|
||||
}
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
constructor(page: Page, testName: string) {
|
||||
super({ page, testName });
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { ConsoleLogger, FixtureLogger } from "#logger/node";
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export interface PageFixtureOptions {
|
||||
page: Page;
|
||||
testName: string;
|
||||
}
|
||||
|
||||
export abstract class PageFixture {
|
||||
/**
|
||||
* The name of the fixture.
|
||||
*
|
||||
* Used for logging.
|
||||
*/
|
||||
static fixtureName: string;
|
||||
|
||||
protected readonly logger: FixtureLogger;
|
||||
protected readonly page: Page;
|
||||
protected readonly testName: string;
|
||||
|
||||
constructor({ page, testName }: PageFixtureOptions) {
|
||||
this.page = page;
|
||||
this.testName = testName;
|
||||
|
||||
const Constructor = this.constructor as typeof PageFixture;
|
||||
|
||||
this.logger = ConsoleLogger.fixture(Constructor.fixtureName, this.testName);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { PageFixture } from "#e2e/fixtures/PageFixture";
|
||||
import type { LocatorContext } from "#e2e/selectors/types";
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export type GetByRoleParameters = Parameters<Page["getByRole"]>;
|
||||
export type ARIARole = GetByRoleParameters[0];
|
||||
export type ARIAOptions = GetByRoleParameters[1];
|
||||
|
||||
export type ClickByName = (name: string) => Promise<void>;
|
||||
export type ClickByRole = (
|
||||
role: ARIARole,
|
||||
options?: ARIAOptions,
|
||||
context?: LocatorContext,
|
||||
) => Promise<void>;
|
||||
|
||||
export class PointerFixture extends PageFixture {
|
||||
public static fixtureName = "Pointer";
|
||||
|
||||
public click = (
|
||||
name: string,
|
||||
optionsOrRole?: ARIAOptions | ARIARole,
|
||||
context: LocatorContext = this.page,
|
||||
): Promise<void> => {
|
||||
if (typeof optionsOrRole === "string") {
|
||||
return context.getByRole(optionsOrRole, { name }).click();
|
||||
}
|
||||
|
||||
const options = {
|
||||
...optionsOrRole,
|
||||
name,
|
||||
};
|
||||
|
||||
return (
|
||||
context
|
||||
// ---
|
||||
.getByRole("button", options)
|
||||
.or(context.getByRole("link", options))
|
||||
.click()
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { PageFixture } from "#e2e/fixtures/PageFixture";
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
export const GOOD_USERNAME = "test-admin@goauthentik.io";
|
||||
export const GOOD_PASSWORD = "test-runner";
|
||||
|
||||
export const BAD_USERNAME = "bad-username@bad-login.io";
|
||||
export const BAD_PASSWORD = "-this-is-a-bad-password-";
|
||||
|
||||
export interface LoginInit {
|
||||
username?: string;
|
||||
password?: string;
|
||||
to?: URL | string;
|
||||
}
|
||||
|
||||
export class SessionFixture extends PageFixture {
|
||||
static fixtureName = "Session";
|
||||
|
||||
public static readonly pathname = "/if/flow/default-authentication-flow/";
|
||||
|
||||
//#region Selectors
|
||||
|
||||
public $identificationStage = this.page.locator("ak-stage-identification");
|
||||
|
||||
/**
|
||||
* The username field on the login page.
|
||||
*/
|
||||
public $usernameField = this.page.getByLabel("Username");
|
||||
|
||||
public $passwordStage = this.page.locator("ak-stage-password");
|
||||
public $passwordField = this.page.getByLabel("Password");
|
||||
|
||||
/**
|
||||
* The button to submit the the login flow,
|
||||
* typically redirecting to the authenticated interface.
|
||||
*/
|
||||
public $submitButton = this.page.locator('button[type="submit"]');
|
||||
|
||||
/**
|
||||
* A possible authentication failure message.
|
||||
*/
|
||||
public $authFailureMessage = this.page.locator(".pf-m-error");
|
||||
|
||||
//#endregion
|
||||
|
||||
constructor(page: Page, testName: string) {
|
||||
super({ page, testName });
|
||||
}
|
||||
|
||||
//#region Specific interactions
|
||||
|
||||
public checkAuthenticated = async (): Promise<boolean> => {
|
||||
// TODO: Check if the user is authenticated via API
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Log into the application.
|
||||
*/
|
||||
public async login({
|
||||
username = GOOD_USERNAME,
|
||||
password = GOOD_PASSWORD,
|
||||
to = SessionFixture.pathname,
|
||||
}: LoginInit = {}) {
|
||||
this.logger.info("Logging in...");
|
||||
|
||||
const initialURL = new URL(this.page.url());
|
||||
|
||||
if (initialURL.pathname === SessionFixture.pathname) {
|
||||
this.logger.info("Skipping navigation because we're already in a authentication flow");
|
||||
} else {
|
||||
await this.page.goto(to.toString());
|
||||
}
|
||||
|
||||
await this.$usernameField.fill(username);
|
||||
|
||||
const passwordFieldVisible = await this.$passwordField.isVisible();
|
||||
|
||||
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;
|
||||
|
||||
await this.page.waitForURL(`**${expectedPathname}`);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Navigation
|
||||
|
||||
public async toLoginPage() {
|
||||
await this.page.goto(SessionFixture.pathname);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* @file Playwright e2e test helpers.
|
||||
*/
|
||||
|
||||
import { FormFixture } from "#e2e/fixtures/FormFixture";
|
||||
import { PointerFixture } from "#e2e/fixtures/PointerFixture";
|
||||
import { SessionFixture } from "#e2e/fixtures/SessionFixture";
|
||||
|
||||
import { test as base } from "@playwright/test";
|
||||
|
||||
export { expect } from "@playwright/test";
|
||||
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
|
||||
interface E2EFixturesTestScope {
|
||||
session: SessionFixture;
|
||||
pointer: PointerFixture;
|
||||
form: FormFixture;
|
||||
}
|
||||
|
||||
interface E2EWorkerScope {
|
||||
selectorRegistration: void;
|
||||
}
|
||||
|
||||
export const test = base.extend<E2EFixturesTestScope, E2EWorkerScope>({
|
||||
session: async ({ page }, use, { title }) => {
|
||||
await use(new SessionFixture(page, title));
|
||||
},
|
||||
|
||||
form: async ({ page }, use, { title }) => {
|
||||
await use(new FormFixture(page, title));
|
||||
},
|
||||
|
||||
pointer: async ({ page }, use, { title }) => {
|
||||
await use(new PointerFixture({ page, testName: title }));
|
||||
},
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { Locator } from "@playwright/test";
|
||||
|
||||
export type LocatorContext = Pick<
|
||||
Locator,
|
||||
| "locator"
|
||||
| "getByRole"
|
||||
| "getByTestId"
|
||||
| "getByText"
|
||||
| "getByLabel"
|
||||
| "getByAltText"
|
||||
| "getByTitle"
|
||||
| "getByPlaceholder"
|
||||
>;
|
||||
@@ -1,60 +0,0 @@
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
|
||||
import {
|
||||
adjectives,
|
||||
colors,
|
||||
Config as NameConfig,
|
||||
uniqueNamesGenerator,
|
||||
} from "unique-names-generator";
|
||||
|
||||
/**
|
||||
* Given a dictionary of words, slice the dictionary to only include words that start with the given letter.
|
||||
*/
|
||||
export function alliterate(dictionary: string[], letter: string): string[] {
|
||||
let firstIndex = 0;
|
||||
|
||||
for (let i = 0; i < dictionary.length; i++) {
|
||||
if (dictionary[i][0] === letter) {
|
||||
firstIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let lastIndex = firstIndex;
|
||||
|
||||
for (let i = firstIndex; i < dictionary.length; i++) {
|
||||
if (dictionary[i][0] !== letter) {
|
||||
lastIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dictionary.slice(firstIndex, lastIndex);
|
||||
}
|
||||
|
||||
export function createRandomName({
|
||||
seed = IDGenerator.randomID(),
|
||||
...config
|
||||
}: Partial<NameConfig> = {}) {
|
||||
const randomLetterIndex =
|
||||
typeof seed === "number"
|
||||
? seed
|
||||
: Array.from(seed).reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
|
||||
const letter = adjectives[randomLetterIndex % adjectives.length][0];
|
||||
|
||||
const availableAdjectives = alliterate(adjectives, letter);
|
||||
|
||||
const availableColors = alliterate(colors, letter);
|
||||
|
||||
const name = uniqueNamesGenerator({
|
||||
dictionaries: [availableAdjectives, availableAdjectives, availableColors],
|
||||
style: "capital",
|
||||
separator: " ",
|
||||
length: 3,
|
||||
seed,
|
||||
...config,
|
||||
});
|
||||
|
||||
return name;
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* Application logger.
|
||||
*
|
||||
* @import { LoggerOptions, Logger, Level, ChildLoggerOptions } from "pino"
|
||||
* @import { PrettyOptions } from "pino-pretty"
|
||||
*/
|
||||
|
||||
import { pino } from "pino";
|
||||
|
||||
//#region Constants
|
||||
|
||||
/**
|
||||
* Default options for creating a Pino logger.
|
||||
*
|
||||
* @category Logger
|
||||
* @satisfies {LoggerOptions<never, false>}
|
||||
*/
|
||||
export const DEFAULT_PINO_LOGGER_OPTIONS = {
|
||||
enabled: true,
|
||||
level: "info",
|
||||
transport: {
|
||||
target: "./transport.js",
|
||||
options: /** @satisfies {PrettyOptions} */ ({
|
||||
colorize: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Functions
|
||||
|
||||
/**
|
||||
* Read the log level from the environment.
|
||||
* @return {Level}
|
||||
*/
|
||||
export function readLogLevel() {
|
||||
return process.env.AK_LOG_LEVEL || DEFAULT_PINO_LOGGER_OPTIONS.level;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Logger} FixtureLogger
|
||||
*/
|
||||
|
||||
/**
|
||||
* @this {Logger}
|
||||
* @param {string} fixtureName
|
||||
* @param {string} [testName]
|
||||
* @param {ChildLoggerOptions} [options]
|
||||
* @returns {FixtureLogger}
|
||||
*/
|
||||
function createFixtureLogger(fixtureName, testName, options) {
|
||||
return this.child(
|
||||
{ name: fixtureName },
|
||||
{
|
||||
msgPrefix: `[${testName}] `,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} CustomLoggerMethods
|
||||
* @property {typeof createFixtureLogger} fixture
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Logger & CustomLoggerMethods} ConsoleLogger
|
||||
*/
|
||||
|
||||
/**
|
||||
* A singleton logger instance for Node.js.
|
||||
*
|
||||
* ```js
|
||||
* import { ConsoleLogger } from "#logger/node";
|
||||
*
|
||||
* ConsoleLogger.info("Hello, world!");
|
||||
* ```
|
||||
*
|
||||
* @runtime node
|
||||
* @type {ConsoleLogger}
|
||||
*/
|
||||
export const ConsoleLogger = Object.assign(
|
||||
pino({
|
||||
...DEFAULT_PINO_LOGGER_OPTIONS,
|
||||
level: readLogLevel(),
|
||||
}),
|
||||
{ fixture: createFixtureLogger },
|
||||
);
|
||||
|
||||
/**
|
||||
* @typedef {ReturnType<ConsoleLogger['child']>} ChildConsoleLogger
|
||||
*/
|
||||
|
||||
//#region Aliases
|
||||
|
||||
export const info = ConsoleLogger.info.bind(ConsoleLogger);
|
||||
export const debug = ConsoleLogger.debug.bind(ConsoleLogger);
|
||||
export const warn = ConsoleLogger.warn.bind(ConsoleLogger);
|
||||
export const error = ConsoleLogger.error.bind(ConsoleLogger);
|
||||
|
||||
//#endregion
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* @file Pretty transport for Pino
|
||||
*
|
||||
* @import { PrettyOptions } from "pino-pretty"
|
||||
*/
|
||||
|
||||
import PinoPretty from "pino-pretty";
|
||||
|
||||
/**
|
||||
* @param {PrettyOptions} options
|
||||
*/
|
||||
function prettyTransporter(options) {
|
||||
const pretty = PinoPretty({
|
||||
...options,
|
||||
ignore: "pid,hostname",
|
||||
translateTime: "SYS:HH:MM:ss",
|
||||
});
|
||||
|
||||
return pretty;
|
||||
}
|
||||
|
||||
export default prettyTransporter;
|
||||
1877
web/package-lock.json
generated
1877
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,8 @@
|
||||
"pseudolocalize": "node ./scripts/pseudolocalize.mjs",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "wireit",
|
||||
"test": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test": "wireit",
|
||||
"test:e2e": "wireit",
|
||||
"test:e2e:watch": "wireit",
|
||||
"test:watch": "wireit",
|
||||
"tsc": "wireit",
|
||||
@@ -69,9 +69,6 @@
|
||||
"#flow/*": "./src/flow/*.js",
|
||||
"#locales/*": "./src/locales/*.js",
|
||||
"#stories/*": "./src/stories/*.js",
|
||||
"#tests/*": "./tests/*.js",
|
||||
"#e2e": "./e2e/index.ts",
|
||||
"#e2e/*": "./e2e/*.ts",
|
||||
"#*/browser": {
|
||||
"types": "./out/*/browser.d.ts",
|
||||
"import": "./*/browser.js"
|
||||
@@ -97,7 +94,7 @@
|
||||
"@floating-ui/dom": "^1.7.3",
|
||||
"@formatjs/intl-listformat": "^7.7.11",
|
||||
"@fortawesome/fontawesome-free": "^7.0.0",
|
||||
"@goauthentik/api": "^2025.6.4-1754491498",
|
||||
"@goauthentik/api": "^2025.6.4-1754241870",
|
||||
"@goauthentik/core": "^1.0.0",
|
||||
"@goauthentik/esbuild-plugin-live-reload": "^1.1.0",
|
||||
"@goauthentik/eslint-config": "^1.0.5",
|
||||
@@ -116,7 +113,6 @@
|
||||
"@openlayers-elements/maps": "^0.4.0",
|
||||
"@patternfly/elements": "^4.1.0",
|
||||
"@patternfly/patternfly": "^4.224.2",
|
||||
"@playwright/test": "^1.54.1",
|
||||
"@sentry/browser": "^10.0.0",
|
||||
"@spotlightjs/spotlight": "^3.0.1",
|
||||
"@storybook/addon-docs": "^9.1.0",
|
||||
@@ -132,7 +128,6 @@
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"change-case": "^5.4.4",
|
||||
@@ -163,9 +158,6 @@
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^11.9.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pino": "^9.7.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"playwright": "^1.54.1",
|
||||
"prettier": "^3.6.2",
|
||||
"pseudolocale": "^2.1.0",
|
||||
"rapidoc": "^9.3.8",
|
||||
@@ -186,10 +178,7 @@
|
||||
"turnstile-types": "^1.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite": "^7.0.6",
|
||||
"vitest": "^3.2.4",
|
||||
"webcomponent-qr-code": "^1.3.0",
|
||||
"wireit": "^0.14.12",
|
||||
"yaml": "^2.8.0"
|
||||
@@ -277,7 +266,7 @@
|
||||
"command": "lit-analyzer src"
|
||||
},
|
||||
"lint:types:tests": {
|
||||
"command": "tsc --noEmit -p tsconfig.test.json"
|
||||
"command": "tsc --noEmit -p ./tests"
|
||||
},
|
||||
"lint:types": {
|
||||
"command": "tsc -p .",
|
||||
@@ -326,7 +315,7 @@
|
||||
],
|
||||
"env": {
|
||||
"CI": "true",
|
||||
"TS_NODE_PROJECT": "tsconfig.test.json"
|
||||
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
|
||||
}
|
||||
},
|
||||
"test:e2e:watch": {
|
||||
@@ -335,7 +324,7 @@
|
||||
"build"
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "tsconfig.test.json"
|
||||
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
|
||||
}
|
||||
},
|
||||
"test:watch": {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* @file Playwright configuration.
|
||||
*
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*
|
||||
* @import { LogFn, Logger } from "pino"
|
||||
*/
|
||||
|
||||
import { ConsoleLogger } from "#logger/node";
|
||||
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const CI = !!process.env.CI;
|
||||
|
||||
/**
|
||||
* @type {Map<string, Logger>}
|
||||
*/
|
||||
const LoggerCache = new Map();
|
||||
|
||||
const baseURL = process.env.AK_TEST_RUNNER_PAGE_URL ?? "http://localhost:9000";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./test/browser",
|
||||
fullyParallel: true,
|
||||
forbidOnly: CI,
|
||||
retries: CI ? 2 : 0,
|
||||
workers: CI ? 1 : undefined,
|
||||
reporter: CI
|
||||
? "github"
|
||||
: [
|
||||
// ---
|
||||
["list", { printSteps: true }],
|
||||
["html", { open: "never" }],
|
||||
],
|
||||
use: {
|
||||
testIdAttribute: "data-test-id",
|
||||
baseURL,
|
||||
trace: "on-first-retry",
|
||||
launchOptions: {
|
||||
logger: {
|
||||
isEnabled() {
|
||||
return true;
|
||||
},
|
||||
log: (name, severity, message, args) => {
|
||||
let logger = LoggerCache.get(name);
|
||||
|
||||
if (!logger) {
|
||||
logger = ConsoleLogger.child({
|
||||
name: `Playwright ${name.toUpperCase()}`,
|
||||
});
|
||||
LoggerCache.set(name, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {LogFn}
|
||||
*/
|
||||
let log;
|
||||
|
||||
switch (severity) {
|
||||
case "verbose":
|
||||
log = logger.debug;
|
||||
break;
|
||||
case "warning":
|
||||
log = logger.warn;
|
||||
break;
|
||||
case "error":
|
||||
log = logger.error;
|
||||
break;
|
||||
default:
|
||||
log = logger.info;
|
||||
break;
|
||||
}
|
||||
|
||||
if (name === "api") {
|
||||
log = logger.debug;
|
||||
}
|
||||
|
||||
log.call(logger, message.toString(), args);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -20,7 +20,6 @@ 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";
|
||||
@@ -34,90 +33,83 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-application-form")
|
||||
export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
|
||||
#api = new CoreApi(DEFAULT_CONFIG);
|
||||
constructor() {
|
||||
super();
|
||||
this.handleConfirmBackchannelProviders = this.handleConfirmBackchannelProviders.bind(this);
|
||||
this.makeRemoveBackchannelProviderHandler =
|
||||
this.makeRemoveBackchannelProviderHandler.bind(this);
|
||||
}
|
||||
|
||||
protected override async loadInstance(pk: string): Promise<Application> {
|
||||
const app = await this.#api.coreApplicationsRetrieve({
|
||||
async loadInstance(pk: string): Promise<Application> {
|
||||
const app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsRetrieve({
|
||||
slug: pk,
|
||||
});
|
||||
|
||||
this.clearIcon = false;
|
||||
this.backchannelProviders = app.backchannelProvidersObj || [];
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
public provider?: number;
|
||||
provider?: number;
|
||||
|
||||
@state()
|
||||
protected backchannelProviders: Provider[] = [];
|
||||
backchannelProviders: Provider[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public clearIcon = false;
|
||||
clearIcon = false;
|
||||
|
||||
protected override getSuccessMessage(): string {
|
||||
getSuccessMessage(): string {
|
||||
return this.instance
|
||||
? msg("Successfully updated application.")
|
||||
: msg("Successfully created application.");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
if (this.can(CapabilitiesEnum.CanSaveMedia)) {
|
||||
const icon = this.files().get("metaIcon");
|
||||
|
||||
if (icon || this.clearIcon) {
|
||||
await this.#api.coreApplicationsSetIconCreate({
|
||||
slug: nextSlug,
|
||||
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconCreate({
|
||||
slug: app.slug,
|
||||
file: icon,
|
||||
clear: this.clearIcon,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.#api.coreApplicationsSetIconUrlCreate({
|
||||
slug: nextSlug,
|
||||
await new CoreApi(DEFAULT_CONFIG).coreApplicationsSetIconUrlCreate({
|
||||
slug: app.slug,
|
||||
filePathRequest: {
|
||||
url: applicationRequest.metaIcon || "",
|
||||
url: data.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) => {
|
||||
makeRemoveBackchannelProviderHandler(provider: Provider) {
|
||||
return () => {
|
||||
const idx = this.backchannelProviders.indexOf(provider);
|
||||
this.backchannelProviders.splice(idx, 1);
|
||||
this.requestUpdate();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
handleClearIcon(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
@@ -127,25 +119,22 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
this.clearIcon = !!(ev.target as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
public override renderForm(): TemplateResult {
|
||||
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`
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
${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("The name displayed in the application library.")}
|
||||
help=${msg("Application's display Name.")}
|
||||
></ak-text-input>
|
||||
<ak-slug-input
|
||||
name="slug"
|
||||
autocomplete="off"
|
||||
value=${ifDefined(this.instance?.slug)}
|
||||
label=${msg("Slug")}
|
||||
required
|
||||
@@ -156,7 +145,6 @@ 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.",
|
||||
)}
|
||||
@@ -176,8 +164,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")}
|
||||
@@ -196,7 +184,6 @@ 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.",
|
||||
@@ -248,7 +235,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Applicatio
|
||||
></ak-textarea-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ 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";
|
||||
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { msg } 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";
|
||||
@@ -41,6 +40,15 @@ 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,
|
||||
@@ -53,28 +61,7 @@ export class ApplicationViewPage extends AKElement {
|
||||
PFCard,
|
||||
];
|
||||
|
||||
//#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>) {
|
||||
fetchIsMissingOutpost(providersByPk: Array<number>) {
|
||||
new OutpostsApi(DEFAULT_CONFIG)
|
||||
.outpostsInstancesList({
|
||||
providersByPk,
|
||||
@@ -87,34 +74,27 @@ export class ApplicationViewPage extends AKElement {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override willUpdate(changedProperties: PropertyValues<this>) {
|
||||
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")}
|
||||
@@ -131,17 +111,9 @@ 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">
|
||||
@@ -216,7 +188,7 @@ export class ApplicationViewPage extends AKElement {
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text pf-m-monospace">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.application.policyEngineMode?.toUpperCase()}
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
@@ -114,14 +114,12 @@ 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("The name displayed in the application library.")}
|
||||
help=${msg("Application's display Name.")}
|
||||
></ak-text-input>
|
||||
<ak-slug-input
|
||||
name="slug"
|
||||
@@ -156,7 +154,6 @@ 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 ??
|
||||
|
||||
@@ -7,13 +7,12 @@ 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,
|
||||
@@ -203,7 +202,22 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
|
||||
required
|
||||
name="policyEngineMode"
|
||||
>
|
||||
<ak-radio .options=${policyEngineModes} .value=${this.instance?.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>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import type { RadioOption } from "#elements/forms/Radio";
|
||||
|
||||
import { PolicyEngineMode } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
|
||||
export const policyEngineModes: RadioOption<PolicyEngineMode>[] = [
|
||||
export const policyEngineModes = [
|
||||
{
|
||||
label: "ANY",
|
||||
className: "pf-m-monospace",
|
||||
label: "any",
|
||||
value: PolicyEngineMode.Any,
|
||||
default: true,
|
||||
description: html`${msg("Any policy must match to grant access")}`,
|
||||
},
|
||||
{
|
||||
label: "ALL",
|
||||
className: "pf-m-monospace",
|
||||
label: "all",
|
||||
value: PolicyEngineMode.All,
|
||||
description: html`${msg("All policies must match to grant access")}`,
|
||||
},
|
||||
|
||||
@@ -113,19 +113,6 @@ 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;
|
||||
|
||||
@@ -206,17 +193,6 @@ 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 -->
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
ClientTypeEnum,
|
||||
CoreApi,
|
||||
CoreUsersListRequest,
|
||||
ModelEnum,
|
||||
OAuth2Provider,
|
||||
OAuth2ProviderSetupURLs,
|
||||
PropertyMappingPreview,
|
||||
@@ -172,7 +170,6 @@ 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">
|
||||
@@ -249,18 +246,6 @@ 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">
|
||||
@@ -370,18 +355,6 @@ 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"
|
||||
>
|
||||
|
||||
@@ -232,36 +232,6 @@ 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()}`;
|
||||
|
||||
@@ -292,7 +292,7 @@ export function applyDocumentTheme(hint: CSSColorSchemeValue | UIThemeHint = "au
|
||||
* @todo Can this be handled with a Lit Mixin?
|
||||
*/
|
||||
export function rootInterface<T extends HTMLElement = HTMLElement>(): T {
|
||||
const element = document.body.querySelector<T>("[data-test-id=interface-root]");
|
||||
const element = document.body.querySelector<T>("[data-ak-interface-root]");
|
||||
|
||||
if (!element) {
|
||||
throw new Error(
|
||||
|
||||
@@ -35,7 +35,7 @@ export class AkToggleGroup extends CustomEmitterElement(AKElement) {
|
||||
`,
|
||||
];
|
||||
|
||||
/**
|
||||
/*
|
||||
* The value (causes highlighting, value is returned)
|
||||
*
|
||||
* @attr
|
||||
|
||||
@@ -28,6 +28,6 @@ export abstract class Interface extends AKElement {
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.dataset.testId = "interface-root";
|
||||
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ type ContentValue = SlottedTemplateResult | undefined;
|
||||
*/
|
||||
export function akLoadingOverlay(
|
||||
properties: ILoadingOverlay = {},
|
||||
content: string | ILoadingOverlayContent = {},
|
||||
content: ILoadingOverlayContent = {},
|
||||
) {
|
||||
// `heading` here is an Object.key of ILoadingOverlayContent, not the obsolete
|
||||
// slot-name.
|
||||
|
||||
@@ -46,7 +46,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
|
||||
|
||||
/* The array of key/value pairs this pane is currently showing */
|
||||
@property({ type: Array })
|
||||
public readonly options?: DualSelectPair[];
|
||||
readonly options: DualSelectPair[] = [];
|
||||
|
||||
/**
|
||||
* A set (set being easy for lookups) of keys with all the pairs selected,
|
||||
@@ -54,7 +54,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
|
||||
* can be marked and their clicks ignored.
|
||||
*/
|
||||
@property({ type: Object })
|
||||
public readonly selected: Set<string> = new Set();
|
||||
readonly selected: Set<string> = new Set();
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -75,17 +75,11 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
|
||||
|
||||
//#region Refs
|
||||
|
||||
#listRef = createRef<HTMLDivElement>();
|
||||
|
||||
#scrollAnimationFrame = -1;
|
||||
|
||||
#scrollIntoView = (): void => {
|
||||
this.#listRef.value?.scrollTo(0, 0);
|
||||
};
|
||||
protected listRef = createRef<HTMLDivElement>();
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
public overrideconnectedCallback() {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
for (const [attr, value] of hostAttributes) {
|
||||
@@ -95,11 +89,9 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("options") && this.options?.length) {
|
||||
cancelAnimationFrame(this.#scrollAnimationFrame);
|
||||
|
||||
this.#scrollAnimationFrame = requestAnimationFrame(this.#scrollIntoView);
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("options")) {
|
||||
this.listRef.value?.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +118,10 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
|
||||
this.toMove.add(key);
|
||||
}
|
||||
|
||||
const moved = [...this.toMove].sort();
|
||||
|
||||
this.dispatchCustomEvent(DualSelectEventType.MoveChanged, moved);
|
||||
this.dispatchCustomEvent(
|
||||
DualSelectEventType.MoveChanged,
|
||||
Array.from(this.toMove.values()).sort(),
|
||||
);
|
||||
|
||||
this.dispatchCustomEvent(DualSelectEventType.Move);
|
||||
|
||||
@@ -152,7 +145,7 @@ export class AkDualSelectAvailablePane extends CustomEmitterElement<DualSelectEv
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div ${ref(this.#listRef)} class="pf-c-dual-list-selector__menu">
|
||||
<div ${ref(this.listRef)} class="pf-c-dual-list-selector__menu">
|
||||
<ul class="pf-c-dual-list-selector__list">
|
||||
${map(this.options, ([key, label]) => {
|
||||
const selected = classMap({
|
||||
|
||||
@@ -100,11 +100,9 @@ export const globalVariables = css`
|
||||
--pf-c-dual-list-selector__list-item-row--BackgroundColor: var(
|
||||
--ak-dark-background-light-ish
|
||||
);
|
||||
|
||||
--pf-c-dual-list-selector__list-item-row--focus-within--BackgroundColor: var(
|
||||
--ak-dark-background-darker
|
||||
--pf-c-dual-list-selector__list-item-row--hover--BackgroundColor: var(
|
||||
--ak-dark-background-lighter;
|
||||
);
|
||||
|
||||
--pf-c-dual-list-selector__list-item-row--hover--BackgroundColor: var(
|
||||
--pf-global--BackgroundColor--400
|
||||
);
|
||||
|
||||
@@ -220,10 +220,7 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
return rect.x + rect.y + rect.width + rect.height !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for returning a success message after a successful submission.
|
||||
*/
|
||||
protected getSuccessMessage(): string {
|
||||
public getSuccessMessage(): string {
|
||||
return this.successMessage;
|
||||
}
|
||||
|
||||
@@ -368,9 +365,6 @@ export abstract class Form<T = Record<string, unknown>> extends AKElement {
|
||||
</form>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* An overridable method for rendering the form content.
|
||||
*/
|
||||
public renderForm(): SlottedTemplateResult | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { css, CSSResult, html, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
@@ -20,6 +20,15 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
*/
|
||||
@customElement("ak-form-group")
|
||||
export class AKFormGroup extends AKElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
public label = msg("Details");
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
public description?: string;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
PFForm,
|
||||
@@ -37,6 +46,27 @@ export class AKFormGroup extends AKElement {
|
||||
}
|
||||
|
||||
details {
|
||||
&::details-content {
|
||||
height: 0;
|
||||
overflow: clip;
|
||||
transition-behavior: normal, allow-discrete;
|
||||
transition-duration: var(--pf-global--TransitionDuration);
|
||||
transition-timing-function: var(--pf-global--TimingFunction);
|
||||
transition-property: height, content-visibility;
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
transition-duration: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (interpolate-size: allow-keywords) {
|
||||
interpolate-size: allow-keywords;
|
||||
|
||||
&[open]::details-content {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&::details-content {
|
||||
padding-inline-start: var(
|
||||
--pf-c-form__field-group--GridTemplateColumns--toggle
|
||||
@@ -72,39 +102,12 @@ export class AKFormGroup extends AKElement {
|
||||
`,
|
||||
];
|
||||
|
||||
//region Properties
|
||||
formRef = createRef<HTMLFormElement>();
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
scrollAnimationFrame = -1;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
public label = msg("Details");
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
public description?: string;
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
public override updated(changedProperties: PropertyValues<this>): void {
|
||||
const previousOpen = changedProperties.get("open");
|
||||
|
||||
if (typeof previousOpen !== "boolean") return;
|
||||
|
||||
if (this.open && this.open !== previousOpen) {
|
||||
cancelAnimationFrame(this.#scrollAnimationFrame);
|
||||
|
||||
this.#scrollAnimationFrame = requestAnimationFrame(this.#scrollIntoView);
|
||||
}
|
||||
}
|
||||
|
||||
#detailsRef = createRef<HTMLDetailsElement>();
|
||||
|
||||
#scrollAnimationFrame = -1;
|
||||
|
||||
#scrollIntoView = (): void => {
|
||||
this.#detailsRef.value?.scrollIntoView({
|
||||
scrollIntoView = (): void => {
|
||||
this.formRef.value?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
@@ -114,16 +117,19 @@ export class AKFormGroup extends AKElement {
|
||||
*/
|
||||
public toggle = (event: Event): void => {
|
||||
event.preventDefault();
|
||||
cancelAnimationFrame(this.scrollAnimationFrame);
|
||||
|
||||
this.open = !this.open;
|
||||
};
|
||||
|
||||
//#region Render
|
||||
if (this.open) {
|
||||
this.scrollAnimationFrame = requestAnimationFrame(this.scrollIntoView);
|
||||
}
|
||||
};
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<details
|
||||
${ref(this.#detailsRef)}
|
||||
${ref(this.formRef)}
|
||||
?open=${this.open}
|
||||
?aria-expanded="${this.open}"
|
||||
role="group"
|
||||
@@ -161,8 +167,6 @@ export class AKFormGroup extends AKElement {
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -9,24 +9,15 @@ import { html, TemplateResult } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
/**
|
||||
* Model form
|
||||
*
|
||||
* A base form that automatically tracks the server-side object (instance)
|
||||
* that we're interested in. Handles loading and tracking of the instance.
|
||||
*/
|
||||
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
|
||||
/**
|
||||
* An overridable method for loading an instance.
|
||||
*
|
||||
* @param pk The primary key of the instance to load.
|
||||
* @returns A promise that resolves to the loaded instance.
|
||||
*/
|
||||
protected abstract loadInstance(pk: PKT): Promise<T>;
|
||||
|
||||
/**
|
||||
* An overridable method for loading any data, beyond the instance.
|
||||
*
|
||||
* @see {@linkcode loadInstance}
|
||||
* @returns A promise that resolves when the data has been loaded.
|
||||
*/
|
||||
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
|
||||
abstract loadInstance(pk: PKT): Promise<T>;
|
||||
|
||||
async load(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
export interface RadioOption<T> {
|
||||
label: string;
|
||||
description?: TemplateResult;
|
||||
className?: string;
|
||||
default?: boolean;
|
||||
value: T;
|
||||
disabled?: boolean;
|
||||
@@ -101,9 +100,7 @@ export class Radio<T> extends CustomEmitterElement(AKElement) {
|
||||
.checked=${option.value === this.value}
|
||||
.disabled=${option.disabled}
|
||||
/>
|
||||
<label class="pf-c-radio__label ${option.className ?? ""}" for=${id}
|
||||
>${option.label}</label
|
||||
>
|
||||
<label class="pf-c-radio__label" for=${id}>${option.label}</label>
|
||||
${option.description
|
||||
? html`<span class="pf-c-radio__description">${option.description}</span>`
|
||||
: nothing}
|
||||
|
||||
@@ -623,7 +623,7 @@ export abstract class Table<T extends object>
|
||||
<ak-table-pagination
|
||||
class="pf-c-toolbar__item pf-m-pagination"
|
||||
.pages=${this.data?.pagination}
|
||||
.onPageChange=${handler}
|
||||
.pageChangeHandler=${handler}
|
||||
>
|
||||
</ak-table-pagination>
|
||||
`;
|
||||
|
||||
@@ -15,13 +15,13 @@ export type TablePageChangeListener = (page: number) => void;
|
||||
@customElement("ak-table-pagination")
|
||||
export class TablePagination extends AKElement {
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
label?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public pages?: Pagination;
|
||||
pages?: Pagination;
|
||||
|
||||
@property({ attribute: false })
|
||||
public onPageChange?: TablePageChangeListener;
|
||||
onPageChange?: TablePageChangeListener;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFBase,
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { expect, test } from "#e2e";
|
||||
import {
|
||||
BAD_PASSWORD,
|
||||
BAD_USERNAME,
|
||||
GOOD_PASSWORD,
|
||||
GOOD_USERNAME,
|
||||
} from "#e2e/fixtures/SessionFixture";
|
||||
|
||||
test.beforeEach(async ({ session }) => {
|
||||
await session.toLoginPage();
|
||||
});
|
||||
|
||||
test.describe("Session management", () => {
|
||||
test("Login with valid credentials", async ({ session, page }) => {
|
||||
await session.login({ username: GOOD_USERNAME, password: GOOD_PASSWORD });
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
level: 1,
|
||||
}),
|
||||
).toHaveText("My applications");
|
||||
});
|
||||
|
||||
test("Reject bad username", async ({ session }) => {
|
||||
await session.login({ username: BAD_USERNAME, password: GOOD_PASSWORD });
|
||||
|
||||
await expect(session.$authFailureMessage).toBeVisible();
|
||||
await expect(session.$authFailureMessage).toHaveText("Invalid password");
|
||||
});
|
||||
|
||||
test("Reject bad password", async ({ session }) => {
|
||||
await session.login({ username: GOOD_USERNAME, password: BAD_PASSWORD });
|
||||
|
||||
await expect(session.$authFailureMessage).toBeVisible();
|
||||
await expect(session.$authFailureMessage).toHaveText("Invalid password");
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* @file Vitest browser utilities for Lit.
|
||||
*
|
||||
* @import { LocatorSelectors } from '@vitest/browser/context'
|
||||
* @import { PrettyDOMOptions } from '@vitest/browser/utils'
|
||||
* @import { RenderOptions as LitRenderOptions } from 'lit'
|
||||
*/
|
||||
|
||||
import { debug, getElementLocatorSelectors } from "@vitest/browser/utils";
|
||||
|
||||
import { render as renderLit } from "lit";
|
||||
|
||||
/**
|
||||
* @implements {Disposable}
|
||||
*/
|
||||
export class LitViteContext {
|
||||
/**
|
||||
* @type {Set<Disposable>}
|
||||
*/
|
||||
static #resources = new Set();
|
||||
|
||||
/**
|
||||
* @param {unknown} template
|
||||
* @param {HTMLElement} [container]
|
||||
* @param {LitRenderOptions} [options]
|
||||
*
|
||||
* @returns {LitViteContext}
|
||||
*/
|
||||
static render = (template, container = document.createElement("div"), options) => {
|
||||
const context = new LitViteContext(container);
|
||||
context.render(template, options);
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
static [Symbol.dispose] = () => {
|
||||
this.#resources.forEach((resource) => resource[Symbol.dispose]());
|
||||
this.#resources.clear();
|
||||
};
|
||||
|
||||
static cleanup = () => {
|
||||
return this[Symbol.dispose]();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {unknown} template
|
||||
* @param {LitRenderOptions} [options]
|
||||
*/
|
||||
render(template, options) {
|
||||
return renderLit(template, this.container, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {HTMLElement} container
|
||||
*/
|
||||
container;
|
||||
|
||||
/**
|
||||
* @type {LocatorSelectors}
|
||||
*/
|
||||
$;
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} container
|
||||
*/
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.$ = getElementLocatorSelectors(container);
|
||||
}
|
||||
|
||||
toFragment() {
|
||||
return document.createRange().createContextualFragment(this.container.innerHTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} [maxLength]
|
||||
* @param {PrettyDOMOptions} [options]
|
||||
*/
|
||||
debug(maxLength, options) {
|
||||
return debug(this.container, maxLength, options);
|
||||
}
|
||||
|
||||
[Symbol.dispose] = () => {
|
||||
this.container.remove();
|
||||
LitViteContext.#resources.delete(this);
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { LitViteContext } from "./rendering.js";
|
||||
|
||||
import { page } from "@vitest/browser/context";
|
||||
import { beforeEach } from "vitest";
|
||||
|
||||
page.extend({
|
||||
// @ts-ignore
|
||||
renderLit: LitViteContext.render,
|
||||
[Symbol.for("vitest:component-cleanup")]: LitViteContext.cleanup,
|
||||
});
|
||||
|
||||
beforeEach(() => LitViteContext.cleanup());
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"moduleResolution": "node",
|
||||
"module": "ESNext",
|
||||
|
||||
@@ -2,11 +2,5 @@
|
||||
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": [
|
||||
// ---
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.comp.ts",
|
||||
"./**/*.stories.ts",
|
||||
"./tests"
|
||||
]
|
||||
"exclude": ["src/**/*.test.ts", "./tests"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
// @file TSConfig used by the web package during build.
|
||||
|
||||
// @file TSConfig used during tests.
|
||||
{
|
||||
"extends": "./tsconfig.json"
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"types": ["node", "webdriverio/async", "@wdio/cucumber-framework", "expect-webdriverio"],
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [
|
||||
"ES5",
|
||||
"ES2015",
|
||||
"ES2016",
|
||||
"ES2017",
|
||||
"ES2018",
|
||||
"ES2019",
|
||||
"ES2020",
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"WebWorker"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
23
web/types/node.d.ts
vendored
23
web/types/node.d.ts
vendored
@@ -14,13 +14,12 @@ declare module "module" {
|
||||
* const relativeDirname = dirname(fileURLToPath(import.meta.url));
|
||||
* ```
|
||||
*/
|
||||
|
||||
var __dirname: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "process" {
|
||||
import { Level } from "pino";
|
||||
|
||||
global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
@@ -31,26 +30,6 @@ declare module "process" {
|
||||
* @see {@link https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production | The difference between development and production}
|
||||
*/
|
||||
readonly NODE_ENV?: "development" | "production";
|
||||
|
||||
/**
|
||||
* Whether or not we are running on a CI server.
|
||||
*/
|
||||
readonly CI?: string;
|
||||
|
||||
/**
|
||||
* The application log level.
|
||||
*/
|
||||
readonly AK_LOG_LEVEL?: Level;
|
||||
|
||||
/**
|
||||
* The base URL of web server to run the tests against.
|
||||
*
|
||||
* Typically this is `http://localhost:9000`.
|
||||
*
|
||||
* @format url
|
||||
*/
|
||||
readonly AK_TEST_RUNNER_PAGE_URL?: string;
|
||||
|
||||
/**
|
||||
* @todo Determine where this is used and if it is needed,
|
||||
* give it a better name.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/// <reference types="vitest/config" />
|
||||
|
||||
import { createBundleDefinitions } from "#bundler/utils/node";
|
||||
import { inlineCSSPlugin } from "#bundler/vite-plugin-lit-css/node";
|
||||
|
||||
@@ -11,41 +9,4 @@ export default defineConfig({
|
||||
// ---
|
||||
inlineCSSPlugin(),
|
||||
],
|
||||
test: {
|
||||
dir: "./test",
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/out/**",
|
||||
"**/.{idea,git,cache,output,temp}/**",
|
||||
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*",
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
include: ["./unit/**/*.{test,spec}.ts", "**/*.unit.{test,spec}.ts"],
|
||||
name: "unit",
|
||||
environment: "node",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
setupFiles: ["./test/lit/setup.js"],
|
||||
|
||||
include: ["./browser/**/*.{test,spec}.ts", "**/*.browser.{test,spec}.ts"],
|
||||
name: "browser",
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: "playwright",
|
||||
|
||||
instances: [
|
||||
{
|
||||
browser: "chromium",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1711,6 +1711,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Anwendung erfolgreich erstellt.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Anzeigename der Anwendung</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -10009,27 +10014,6 @@ Bindings zu Gruppen/Benutzern werden mit dem Benutzer des Ereignisses abgegliche
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1374,6 +1374,10 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Successfully created application.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Application's display Name.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
<target>Slug</target>
|
||||
@@ -7888,27 +7892,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1711,6 +1711,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Aplicación creada correctamente.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Nombre para mostrar de la aplicación.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -10057,27 +10062,6 @@ El valor de este campo se compara con el atributo de pertenencia del usuario.</t
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1711,6 +1711,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Application créée avec succès</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Nom d'affichage de l'application</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -9904,192 +9909,129 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
</trans-unit>
|
||||
<trans-unit id="s334b3924d2bd5d55">
|
||||
<source>Kerberos Source</source>
|
||||
<target>Source Kerberos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbcdf61483337948a">
|
||||
<source>Successfully updated schedule.</source>
|
||||
<target>Planification mise à jour avec succès.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd372fe5b712f6b30">
|
||||
<source>Crontab</source>
|
||||
<target>Crontab</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s95bc8722b2708f8b">
|
||||
<source>Paused</source>
|
||||
<target>Mis en pause</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se41af830054194bc">
|
||||
<source>Pause this schedule</source>
|
||||
<target>Mettre cette planification en pause</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd091e43d5e99dea4">
|
||||
<source>Waiting to run</source>
|
||||
<target>En attente de lancement</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85994a70cd39166c">
|
||||
<source>Running</source>
|
||||
<target>En cours d'exécution</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc7637275f670c938">
|
||||
<source>Queue</source>
|
||||
<target>File</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb91d1be10eb2c7da">
|
||||
<source>Last updated</source>
|
||||
<target>Mis à jour pour la dernière fois</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s911c2c952e64b223">
|
||||
<source>Show only standalone tasks</source>
|
||||
<target>Afficher uniquement les tâches non liées</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s14053a4609676b8b">
|
||||
<source>Exclude successful tasks</source>
|
||||
<target>Exclure les tâches réussies</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8ff646d0515aab2a">
|
||||
<source>Retry task</source>
|
||||
<target>Réessayer la tâche</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9b8dccb514a0e34c">
|
||||
<source>Schedule</source>
|
||||
<target>Planification</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf92789320708efed">
|
||||
<source>Next run</source>
|
||||
<target>Prochaine exécution</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1aecc6c0d6cbf4ed">
|
||||
<source>Last status</source>
|
||||
<target>Dernier état</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6089c283e28012fb">
|
||||
<source>Show only standalone schedules</source>
|
||||
<target>Afficher uniquement les tâches non liées</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s48006fb6e0b1860a">
|
||||
<source>Run scheduled task now</source>
|
||||
<target>Exécuter la tâche planifiée maintenant</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6492593d534108e1">
|
||||
<source>Update Schedule</source>
|
||||
<target>Mettre à jour la planification</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf2d616b20d62240d">
|
||||
<source>Schedules</source>
|
||||
<target>Planifications</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc797fd9076cc136d">
|
||||
<source>Tasks</source>
|
||||
<target>Tâches</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3f63421908094590">
|
||||
<source>Current status</source>
|
||||
<target>État actuel</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4412853e1655ebb3">
|
||||
<source>Sync is currently running.</source>
|
||||
<target>La synchronisation est en cours d'exécution.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s08661004803c26b0">
|
||||
<source>Sync is not currently running.</source>
|
||||
<target>La synchronisation n'est pas en cours d'exécution.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s81d16aa9ec62262c">
|
||||
<source>Last successful sync</source>
|
||||
<target>Dernière synchronisation réussie</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd1eb8f8acdbcd1d3">
|
||||
<source>No successful sync found.</source>
|
||||
<target>Pas de synchronisation réussie trouvée.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b0eeb3de1789c6e">
|
||||
<source>Last sync status</source>
|
||||
<target>Dernier état de synchronisation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sac44b0f4dc14c227">
|
||||
<source>Current execution logs</source>
|
||||
<target>Journaux d'exécution courant</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5e13dff03b580216">
|
||||
<source>Previous executions logs</source>
|
||||
<target>Journaux d'exécution précédents</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6abb1cd87fe0114e">
|
||||
<source>Home</source>
|
||||
<target>Accueil</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se58e6ed983bf34b0">
|
||||
<source>Collapse navigation</source>
|
||||
<target>Réduire la navigation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc6ef25894ed00175">
|
||||
<source>Expand navigation</source>
|
||||
<target>Développer la navigation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s148b5e365440a7c1">
|
||||
<source>Table pagination</source>
|
||||
<target>Pagination du tableau</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5d929ff1619ac0c9">
|
||||
<source>Search</source>
|
||||
<target>Rechercher</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd2c2366d13599d8c">
|
||||
<source>Table actions</source>
|
||||
<target>Actions du tableau</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3d195621e562d805">
|
||||
<source>Select row</source>
|
||||
<target>Sélectionner la ligne</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s572d21b6a41e24fa">
|
||||
<source>Table of <x id="0" equiv-text="${this.label}"/></source>
|
||||
<target>Tableau de <x id="0" equiv-text="${this.label}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa25b60b4fac481aa">
|
||||
<source>Table content</source>
|
||||
<target>Contenu du tableau</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5eba8fa19126f70a">
|
||||
<source>Learn more about the enterprise license.</source>
|
||||
<target>En apprendre plus sur les licences entreprise.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9db1679f3b234d4e">
|
||||
<source>Search for providers…</source>
|
||||
<target>Rechercher des fournisseurs…</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s76790480b7b28ad2">
|
||||
<source>Edit provider</source>
|
||||
<target>Modifier le fournisseur</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9839619155ed2cf6">
|
||||
<source>Default NameID Policy</source>
|
||||
<target>Politique NameID par défaut</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
<target>Configure la politique NameID par défaut utilisée pour les connexions initiées par l'IDP et lorsqu'une assertion entrante ne spécifie pas de politique NameID (s'applique également lors de l'utilisation d'un mappage NameID personnalisé).</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1711,6 +1711,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Applicazione creata con successo.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Nome visualizzato dell'applicazione.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -10013,27 +10018,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1682,6 +1682,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>애플리케이션을 성공적으로 만들었습니다.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>애플리케이션의 표시 이름입니다.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -9335,27 +9340,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1692,6 +1692,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Applicatie succesvol aangemaakt.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Weergavenaam van de applicatie.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -9239,27 +9244,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1711,6 +1711,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Pomyślnie utworzono aplikacje.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Wyświetlana nazwa aplikacji.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -9656,27 +9661,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1694,6 +1694,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Śũććēśśƒũĺĺŷ ćŕēàţēď àƥƥĺĩćàţĩōń.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Àƥƥĺĩćàţĩōń'ś ďĩśƥĺàŷ Ńàḿē.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -9665,25 +9670,4 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
||||
@@ -1711,6 +1711,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Приложение успешно создано.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Отображаемое имя приложения.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -9747,27 +9752,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1693,6 +1693,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>Uygulama başarıyla oluşturuldu.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>Uygulamanın görünen Adı.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -9720,27 +9725,6 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -533,6 +533,9 @@
|
||||
<trans-unit id="s9222ca30ae7786e4">
|
||||
<source>Successfully created application.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
</trans-unit>
|
||||
@@ -6514,27 +6517,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -1711,6 +1711,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>已成功创建应用程序。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>应用的显示名称。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -10014,27 +10019,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1303,6 +1303,10 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>已成功创建应用程序。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>应用的显示名称。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
<target>Slug</target>
|
||||
@@ -7594,27 +7598,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -1681,6 +1681,11 @@
|
||||
<source>Successfully created application.</source>
|
||||
<target>成功建立應用程式。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s03907d7a66c6164e">
|
||||
<source>Application's display Name.</source>
|
||||
<target>應用程式的顯示名稱。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s91f70424f5d5d23e">
|
||||
<source>Slug</source>
|
||||
@@ -9315,27 +9320,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0c48c5f754275796">
|
||||
<source>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).</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6c973fe9014080fc">
|
||||
<source>Application name</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s704091f8e3dbd721">
|
||||
<source>The name displayed in the application library.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s418c59028ef6bc2a">
|
||||
<source>URIs to send back-channel logout notifications to when users log out. Required for OpenID Connect Back-Channel Logout functionality.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa091b3064afa00f5">
|
||||
<source>These URIs are called server-to-server when a user logs out to notify OAuth2/OpenID clients about the logout event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d364031a996b540">
|
||||
<source>Back-Channel Logout URI</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc22c7703fd074f5f">
|
||||
<source>e.g. Collaboration, Communication, Internal, etc.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb6fcdabf769208a1">
|
||||
<source>Failed to fetch application "<x id="0" equiv-text="${this.applicationSlug}"/>".</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@@ -43,20 +43,11 @@ See https://docs.hcaptcha.com/switch
|
||||
|
||||
### Cloudflare Turnstile
|
||||
|
||||
See https://developers.cloudflare.com/turnstile/get-started/migrating-from-recaptcha.
|
||||
See https://developers.cloudflare.com/turnstile/get-started/migrating-from-recaptcha
|
||||
|
||||
#### Configuration options
|
||||
|
||||
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||
2. Navigate to **Flows and Stages** > **Stages** and click **Create**.
|
||||
3. Select **Captcha Stage** and click **Next**.
|
||||
4. Provide a descriptive name for the stage (e.g. `authentication-captcha`) and configure the following required settings based on the values of your [Cloudflare Turnstile Widget](https://developers.cloudflare.com/turnstile/concepts/widget/):
|
||||
- Under **Stage-specific settings**:
|
||||
- **Public Key**: set to the **Turnstile Site Key** value from the widget.
|
||||
- **Private Key**: set to the **Turnstile Secret Key** value from the widget.
|
||||
- **Enable Interactive**: Enable this option if the Turnstile instance is configured as **Invisible** or **Managed**.
|
||||
- Leave both score thresholds at their default, as they are not supported for Turnstile.
|
||||
|
||||
- Interactive: Enabled if the Turnstile instance is configured as visible or managed
|
||||
- JS URL: `https://challenges.cloudflare.com/turnstile/v0/api.js`
|
||||
- API URL: `https://challenges.cloudflare.com/turnstile/v0/siteverify`
|
||||
|
||||
|
||||
@@ -5,6 +5,6 @@ slug: /branding
|
||||
|
||||
You can configure several differently "branded" options depending on the associated domain, even though objects such as applications, providers, etc, are still global. This can be handy to use the same authentik instance, but branded differently for different domains.
|
||||
|
||||
The main settings that control your instance's appearance and behaviour are the [_branding settings_](../sys-mgmt/brands.md#branding-settings) and the the [_default flows_](../sys-mgmt/brands.md#default-flows). Review our tips for using images and icons in the [Image optimization](../sys-mgmt/brands.md#image-optimization) section.
|
||||
The main settings that control your instance's appearance and behaviour are the _default flows_ and the _branding settings_.
|
||||
|
||||
To create or modify a brand, open the Admin interface and navigate to **System** > **Brands**. For complete instructions refer to our [Brands documentation](../sys-mgmt/brands.md).
|
||||
|
||||
@@ -22,8 +22,6 @@ To simplify translation you can use https://www.transifex.com/authentik/authenti
|
||||
- Make (again, any recent version should work)
|
||||
- Docker
|
||||
|
||||
### Frontend
|
||||
|
||||
Run `npm i` in the `/web` folder to install all dependencies.
|
||||
|
||||
Ensure the language code is in the `lit-localize.json` file in `web/`:
|
||||
@@ -44,17 +42,3 @@ Afterwards, run `make web-i18n-extract` to generate a base .xlf file.
|
||||
The .xlf files can be edited by any text editor, or using a tool such as [POEdit](https://poedit.net/).
|
||||
|
||||
To see the change, run `make web-watch` in the root directory of the repository.
|
||||
|
||||
### Backend
|
||||
|
||||
Backend translations are handled by `core-i18n-extract`.
|
||||
|
||||
Use Django's translation utility to declare the string, e.g.:
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
_("New text to be translated.")
|
||||
```
|
||||
|
||||
Afterwards, run `make core-i18n-extract` to generate the updated translation files.
|
||||
|
||||
@@ -100,7 +100,7 @@ See [Configuration](../configuration/configuration.mdx) to change the internal p
|
||||
## Startup
|
||||
|
||||
:::warning
|
||||
All internal operations use UTC. Times displayed in the UI are automatically localized for the user. Do not update or mount `/etc/timezone` or `/etc/localtime` in the authentik containers; it will cause problems with OAuth and SAML authentication, as seen this [GitHub issue](https://github.com/goauthentik/authentik/issues/3005).
|
||||
The server assumes to have local timezone as UTC. All internal operations use UTC; When displaying times in the UI, they are automatically localized for the user. Do not update or mount `/etc/timezone` or `/etc/localtime` in the authentik containers; it will cause problems with OAuth and SAML authentication, as seen this [GitHub issue](https://github.com/goauthentik/authentik/issues/3005).
|
||||
:::
|
||||
|
||||
Afterward, run these commands to finish:
|
||||
|
||||
@@ -54,21 +54,6 @@ Instead, the following metrics are now available:
|
||||
- `authentik_tasks_delayed_in_progress`
|
||||
- `authentik_tasks_duration_milliseconds`
|
||||
|
||||
### Docker image deprecation notice for `beryju/authentik` and `beryju/authentik-*`
|
||||
|
||||
The `beryju/authentik` and `beryju/authentik-*` Docker images are no longer being updated. Users are now encouraged to use the following images:
|
||||
|
||||
- **Server image:**
|
||||
- `ghcr.io/goauthentik/server` or `authentik/server`
|
||||
|
||||
- **Outpost images:**
|
||||
- `ghcr.io/goauthentik/ldap` or `authentik/ldap`
|
||||
- `ghcr.io/goauthentik/proxy` or `authentik/proxy`
|
||||
- `ghcr.io/goauthentik/rac` or `authentik/rac`
|
||||
- `ghcr.io/goauthentik/radius` or `authentik/radius`
|
||||
|
||||
We recommend updating your Docker Compose files or other container configurations to use these new image paths.
|
||||
|
||||
## New features
|
||||
|
||||
## Upgrading
|
||||
|
||||
@@ -551,7 +551,7 @@ const items = [
|
||||
},
|
||||
items: [
|
||||
"users-sources/sources/social-logins/apple/index",
|
||||
"users-sources/sources/social-logins/entra-id/index",
|
||||
"users-sources/sources/social-logins/azure-ad/index",
|
||||
"users-sources/sources/social-logins/discord/index",
|
||||
"users-sources/sources/social-logins/facebook/index",
|
||||
"users-sources/sources/social-logins/github/index",
|
||||
|
||||
@@ -72,23 +72,3 @@ When using the [Mutual TLS Stage](../add-secure-apps/flows-stages/stages/mtls/in
|
||||
#### Attributes
|
||||
|
||||
Attributes such as locale, theme settings (light/dark mode), and custom attributes can be set to a per-brand default value here. Any custom attributes can be retrieved via [`group_attributes()`](../users-sources/user/user_ref.mdx#object-properties).
|
||||
|
||||
## Image optimization
|
||||
|
||||
When you use images and icons for a brand's logo, favicon, etc., be aware of the following optimization tips:
|
||||
|
||||
- Use an SVG version of the image.
|
||||
|
||||
- Trim excess whitespace from around the logo. You can use an SVG editor such as Inkscape, Sketch, or Adobe Illustrator.
|
||||
|
||||
- Adjust the viewBox: Ensure the SVG’s `viewBox` attribute tightly wraps the actual logo content. This helps in scaling the logo appropriately.
|
||||
|
||||
- Remove fixed dimensions: delete any fixed width and height attributes from the SVG. This allows the logo to scale responsively within its container.
|
||||
|
||||
- Check if your SVG needs `preserveAspectRatio` to retain its shape when resized.
|
||||
|
||||
- Wordmark logos: aim for an aspect ratio of approximately 7:1 (width to height).
|
||||
|
||||
- Icon logos: use a 1:1 aspect ratio, ensuring the icon fills the entire viewBox and is centered.
|
||||
|
||||
- The SVG tool [SVGOMG](https://svgomg.net/) is useful for trimming any excess metadata that might affect how the browser rasterizes the image.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user