Merge branch 'main' into web/update-provider-forms-for-invalidation
* main: (44 commits) web/admin: add strict dompurify config for diagram (#11783) core: bump cryptography from 43.0.1 to 43.0.3 (#11750) web: bump API Client version (#11781) sources: add Kerberos (#10815) root: rework CSRF middleware to set secure flag (#11753) web/admin: improve invalidation flow default & field grouping (#11769) providers/scim: add comparison with existing group on update and delta update users (#11414) website: bump mermaid from 10.6.0 to 10.9.3 in /website (#11766) web/flows: use dompurify for footer links (#11773) core, web: update translations (#11775) core: bump goauthentik.io/api/v3 from 3.2024083.10 to 3.2024083.11 (#11776) website: bump @types/react from 18.3.11 to 18.3.12 in /website (#11777) website: bump http-proxy-middleware from 2.0.6 to 2.0.7 in /website (#11771) web: bump API Client version (#11770) stages: authenticator_endpoint_gdtc (#10477) core: add prompt_data to auth flow (#11702) tests/e2e: fix dex tests failing (#11761) web/rac: disable DPI scaling (#11757) web/admin: update flow background (#11758) website/docs: fix some broken links (#11742) ...
This commit is contained in:
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@ -14,7 +14,7 @@ runs:
|
||||
run: |
|
||||
pipx install poetry || true
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
|
||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
||||
- name: Setup python and restore poetry
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
|
@ -110,7 +110,7 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloa
|
||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||
apt-get update && \
|
||||
# Required for installing pip packages
|
||||
apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev
|
||||
apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev libkrb5-dev
|
||||
|
||||
RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
|
||||
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
|
||||
@ -141,7 +141,7 @@ WORKDIR /
|
||||
# We cannot cache this layer otherwise we'll end up with a bigger image
|
||||
RUN apt-get update && \
|
||||
# Required for runtime
|
||||
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates && \
|
||||
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 && \
|
||||
# Required for bootstrap & healtcheck
|
||||
apt-get install -y --no-install-recommends runit && \
|
||||
apt-get clean && \
|
||||
@ -161,6 +161,7 @@ COPY ./tests /tests
|
||||
COPY ./manage.py /
|
||||
COPY ./blueprints /blueprints
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
||||
COPY --from=go-builder /go/authentik /bin/authentik
|
||||
COPY --from=python-deps /ak-root/venv /ak-root/venv
|
||||
COPY --from=web-builder /work/web/dist/ /web/dist/
|
||||
|
@ -51,6 +51,10 @@ from authentik.enterprise.providers.microsoft_entra.models import (
|
||||
MicrosoftEntraProviderUser,
|
||||
)
|
||||
from authentik.enterprise.providers.rac.models import ConnectionToken
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
||||
EndpointDevice,
|
||||
EndpointDeviceConnection,
|
||||
)
|
||||
from authentik.events.logs import LogEvent, capture_logs
|
||||
from authentik.events.models import SystemTask
|
||||
from authentik.events.utils import cleanse_dict
|
||||
@ -119,6 +123,8 @@ def excluded_models() -> list[type[Model]]:
|
||||
GoogleWorkspaceProviderGroup,
|
||||
MicrosoftEntraProviderUser,
|
||||
MicrosoftEntraProviderGroup,
|
||||
EndpointDevice,
|
||||
EndpointDeviceConnection,
|
||||
)
|
||||
|
||||
|
||||
|
@ -6,15 +6,16 @@ from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
DateTimeField,
|
||||
IntegerField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.stages.authenticator import device_classes, devices_for_user
|
||||
from authentik.stages.authenticator.models import Device
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
@ -23,7 +24,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
class DeviceSerializer(MetaNameSerializer):
|
||||
"""Serializer for Duo authenticator devices"""
|
||||
|
||||
pk = IntegerField()
|
||||
pk = CharField()
|
||||
name = CharField()
|
||||
type = SerializerMethodField()
|
||||
confirmed = BooleanField()
|
||||
@ -40,6 +41,8 @@ class DeviceSerializer(MetaNameSerializer):
|
||||
"""Get extra description"""
|
||||
if isinstance(instance, WebAuthnDevice):
|
||||
return instance.device_type.description
|
||||
if isinstance(instance, EndpointDevice):
|
||||
return instance.data.get("deviceSignals", {}).get("deviceModel")
|
||||
return ""
|
||||
|
||||
|
||||
@ -60,7 +63,7 @@ class AdminDeviceViewSet(ViewSet):
|
||||
"""Viewset for authenticator devices"""
|
||||
|
||||
serializer_class = DeviceSerializer
|
||||
permission_classes = [IsAdminUser]
|
||||
permission_classes = []
|
||||
|
||||
def get_devices(self, **kwargs):
|
||||
"""Get all devices in all child classes"""
|
||||
@ -78,6 +81,10 @@ class AdminDeviceViewSet(ViewSet):
|
||||
],
|
||||
responses={200: DeviceSerializer(many=True)},
|
||||
)
|
||||
@permission_required(
|
||||
None,
|
||||
[f"{model._meta.app_label}.view_{model._meta.model_name}" for model in device_classes()],
|
||||
)
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Get all devices for current user"""
|
||||
kwargs = {}
|
||||
|
@ -4,6 +4,7 @@ import code
|
||||
import platform
|
||||
import sys
|
||||
import traceback
|
||||
from pprint import pprint
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand
|
||||
@ -34,7 +35,9 @@ class Command(BaseCommand):
|
||||
|
||||
def get_namespace(self):
|
||||
"""Prepare namespace with all models"""
|
||||
namespace = {}
|
||||
namespace = {
|
||||
"pprint": pprint,
|
||||
}
|
||||
|
||||
# Gather Django models and constants from each app
|
||||
for app in apps.get_app_configs():
|
||||
|
@ -330,11 +330,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
||||
"""superuser == staff user"""
|
||||
return self.is_superuser # type: ignore
|
||||
|
||||
def set_password(self, raw_password, signal=True):
|
||||
def set_password(self, raw_password, signal=True, sender=None):
|
||||
if self.pk and signal:
|
||||
from authentik.core.signals import password_changed
|
||||
|
||||
password_changed.send(sender=self, user=self, password=raw_password)
|
||||
if not sender:
|
||||
sender = self
|
||||
password_changed.send(sender=sender, user=self, password=raw_password)
|
||||
self.password_change_date = now()
|
||||
return super().set_password(raw_password)
|
||||
|
||||
|
@ -272,7 +272,6 @@ class SourceFlowManager:
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""Login user and redirect."""
|
||||
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user}
|
||||
return self._prepare_flow(
|
||||
self.source.authentication_flow,
|
||||
connection,
|
||||
@ -286,7 +285,11 @@ class SourceFlowManager:
|
||||
),
|
||||
)
|
||||
],
|
||||
**flow_kwargs,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: connection.user,
|
||||
PLAN_CONTEXT_PROMPT: delete_none_values(self.user_properties),
|
||||
PLAN_CONTEXT_USER_PATH: self.source.get_user_path(),
|
||||
},
|
||||
)
|
||||
|
||||
def handle_existing_link(
|
||||
|
59
authentik/core/tests/test_devices_api.py
Normal file
59
authentik/core/tests/test_devices_api.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""Test Devices API"""
|
||||
|
||||
from json import loads
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
||||
|
||||
|
||||
class TestDevicesAPI(APITestCase):
|
||||
"""Test applications API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.admin = create_test_admin_user()
|
||||
self.user1 = create_test_user()
|
||||
self.device1 = self.user1.staticdevice_set.create()
|
||||
self.user2 = create_test_user()
|
||||
self.device2 = self.user2.staticdevice_set.create()
|
||||
|
||||
def test_user_api(self):
|
||||
"""Test user API"""
|
||||
self.client.force_login(self.user1)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:device-list",
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(len(body), 1)
|
||||
self.assertEqual(body[0]["pk"], str(self.device1.pk))
|
||||
|
||||
def test_user_api_as_admin(self):
|
||||
"""Test user API"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:device-list",
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(len(body), 0)
|
||||
|
||||
def test_admin_api(self):
|
||||
"""Test admin API"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:admin-device-list",
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content.decode())
|
||||
self.assertEqual(len(body), 2)
|
||||
self.assertEqual(
|
||||
{body[0]["pk"], body[1]["pk"]}, {str(self.device1.pk), str(self.device2.pk)}
|
||||
)
|
@ -5,7 +5,6 @@ from channels.sessions import CookieMiddleware
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from authentik.core.api.applications import ApplicationViewSet
|
||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||
@ -44,19 +43,19 @@ urlpatterns = [
|
||||
# Interfaces
|
||||
path(
|
||||
"if/admin/",
|
||||
ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/admin.html")),
|
||||
BrandDefaultRedirectView.as_view(template_name="if/admin.html"),
|
||||
name="if-admin",
|
||||
),
|
||||
path(
|
||||
"if/user/",
|
||||
ensure_csrf_cookie(BrandDefaultRedirectView.as_view(template_name="if/user.html")),
|
||||
BrandDefaultRedirectView.as_view(template_name="if/user.html"),
|
||||
name="if-user",
|
||||
),
|
||||
path(
|
||||
"if/flow/<slug:flow_slug>/",
|
||||
# FIXME: move this url to the flows app...also will cause all
|
||||
# of the reverse calls to be adjusted
|
||||
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
||||
FlowInterfaceView.as_view(),
|
||||
name="if-flow",
|
||||
),
|
||||
# Fallback for WS
|
||||
|
@ -3,7 +3,6 @@
|
||||
from channels.auth import AuthMiddleware
|
||||
from channels.sessions import CookieMiddleware
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from authentik.enterprise.providers.rac.api.connection_tokens import ConnectionTokenViewSet
|
||||
from authentik.enterprise.providers.rac.api.endpoints import EndpointViewSet
|
||||
@ -19,12 +18,12 @@ from authentik.root.middleware import ChannelsLoggingMiddleware
|
||||
urlpatterns = [
|
||||
path(
|
||||
"application/rac/<slug:app>/<uuid:endpoint>/",
|
||||
ensure_csrf_cookie(RACStartView.as_view()),
|
||||
RACStartView.as_view(),
|
||||
name="start",
|
||||
),
|
||||
path(
|
||||
"if/rac/<str:token>/",
|
||||
ensure_csrf_cookie(RACInterface.as_view()),
|
||||
RACInterface.as_view(),
|
||||
name="if-rac",
|
||||
),
|
||||
]
|
||||
|
@ -17,6 +17,7 @@ TENANT_APPS = [
|
||||
"authentik.enterprise.providers.google_workspace",
|
||||
"authentik.enterprise.providers.microsoft_entra",
|
||||
"authentik.enterprise.providers.rac",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
"authentik.enterprise.stages.source",
|
||||
]
|
||||
|
||||
|
@ -0,0 +1,82 @@
|
||||
"""AuthenticatorEndpointGDTCStage API Views"""
|
||||
|
||||
from django_filters.rest_framework.backends import DjangoFilterBackend
|
||||
from rest_framework import mixins
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
||||
AuthenticatorEndpointGDTCStage,
|
||||
EndpointDevice,
|
||||
)
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class AuthenticatorEndpointGDTCStageSerializer(EnterpriseRequiredMixin, StageSerializer):
|
||||
"""AuthenticatorEndpointGDTCStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = AuthenticatorEndpointGDTCStage
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"configure_flow",
|
||||
"friendly_name",
|
||||
"credentials",
|
||||
]
|
||||
|
||||
|
||||
class AuthenticatorEndpointGDTCStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticatorEndpointGDTCStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorEndpointGDTCStage.objects.all()
|
||||
serializer_class = AuthenticatorEndpointGDTCStageSerializer
|
||||
filterset_fields = [
|
||||
"name",
|
||||
"configure_flow",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
class EndpointDeviceSerializer(ModelSerializer):
|
||||
"""Serializer for Endpoint authenticator devices"""
|
||||
|
||||
class Meta:
|
||||
model = EndpointDevice
|
||||
fields = ["pk", "name"]
|
||||
depth = 2
|
||||
|
||||
|
||||
class EndpointDeviceViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
UsedByMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
"""Viewset for Endpoint authenticator devices"""
|
||||
|
||||
queryset = EndpointDevice.objects.all()
|
||||
serializer_class = EndpointDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
ordering = ["name"]
|
||||
permission_classes = [OwnerPermissions]
|
||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
|
||||
|
||||
class EndpointAdminDeviceViewSet(ModelViewSet):
|
||||
"""Viewset for Endpoint authenticator devices (for admins)"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
queryset = EndpointDevice.objects.all()
|
||||
serializer_class = EndpointDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
ordering = ["name"]
|
@ -0,0 +1,13 @@
|
||||
"""authentik Endpoint app config"""
|
||||
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
|
||||
|
||||
class AuthentikStageAuthenticatorEndpointConfig(EnterpriseConfig):
|
||||
"""authentik endpoint config"""
|
||||
|
||||
name = "authentik.enterprise.stages.authenticator_endpoint_gdtc"
|
||||
label = "authentik_stages_authenticator_endpoint_gdtc"
|
||||
verbose_name = "authentik Enterprise.Stages.Authenticator.Endpoint GDTC"
|
||||
default = True
|
||||
mountpoint = "endpoint/gdtc/"
|
@ -0,0 +1,115 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-22 11:40
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0027_auto_20231028_1424"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="AuthenticatorEndpointGDTCStage",
|
||||
fields=[
|
||||
(
|
||||
"stage_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_flows.stage",
|
||||
),
|
||||
),
|
||||
("friendly_name", models.TextField(null=True)),
|
||||
("credentials", models.JSONField()),
|
||||
(
|
||||
"configure_flow",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Endpoint Authenticator Google Device Trust Connector Stage",
|
||||
"verbose_name_plural": "Endpoint Authenticator Google Device Trust Connector Stages",
|
||||
},
|
||||
bases=("authentik_flows.stage", models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EndpointDevice",
|
||||
fields=[
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("last_updated", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="The human-readable name of this device.", max_length=64
|
||||
),
|
||||
),
|
||||
(
|
||||
"confirmed",
|
||||
models.BooleanField(default=True, help_text="Is this device ready for use?"),
|
||||
),
|
||||
("last_used", models.DateTimeField(null=True)),
|
||||
("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
(
|
||||
"host_identifier",
|
||||
models.TextField(
|
||||
help_text="A unique identifier for the endpoint device, usually the device serial number",
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("data", models.JSONField()),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Endpoint Device",
|
||||
"verbose_name_plural": "Endpoint Devices",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EndpointDeviceConnection",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("attributes", models.JSONField()),
|
||||
(
|
||||
"device",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_stages_authenticator_endpoint_gdtc.endpointdevice",
|
||||
),
|
||||
),
|
||||
(
|
||||
"stage",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
@ -0,0 +1,101 @@
|
||||
"""Endpoint stage"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from google.oauth2.service_account import Credentials
|
||||
from rest_framework.serializers import BaseSerializer, Serializer
|
||||
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.stages.authenticator.models import Device
|
||||
|
||||
|
||||
class AuthenticatorEndpointGDTCStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Setup Google Chrome Device-trust connection"""
|
||||
|
||||
credentials = models.JSONField()
|
||||
|
||||
def google_credentials(self):
|
||||
return {
|
||||
"credentials": Credentials.from_service_account_info(
|
||||
self.credentials, scopes=["https://www.googleapis.com/auth/verifiedaccess"]
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.api import (
|
||||
AuthenticatorEndpointGDTCStageSerializer,
|
||||
)
|
||||
|
||||
return AuthenticatorEndpointGDTCStageSerializer
|
||||
|
||||
@property
|
||||
def view(self) -> type[StageView]:
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.stage import (
|
||||
AuthenticatorEndpointStageView,
|
||||
)
|
||||
|
||||
return AuthenticatorEndpointStageView
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-stage-authenticator-endpoint-gdtc-form"
|
||||
|
||||
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.friendly_name or str(self._meta.verbose_name),
|
||||
"component": "ak-user-settings-authenticator-endpoint",
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Endpoint Authenticator Google Device Trust Connector Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Endpoint Authenticator Google Device Trust Connector Stage")
|
||||
verbose_name_plural = _("Endpoint Authenticator Google Device Trust Connector Stages")
|
||||
|
||||
|
||||
class EndpointDevice(SerializerModel, Device):
|
||||
"""Endpoint Device for a single user"""
|
||||
|
||||
uuid = models.UUIDField(primary_key=True, default=uuid4)
|
||||
host_identifier = models.TextField(
|
||||
unique=True,
|
||||
help_text="A unique identifier for the endpoint device, usually the device serial number",
|
||||
)
|
||||
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
data = models.JSONField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> Serializer:
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.api import (
|
||||
EndpointDeviceSerializer,
|
||||
)
|
||||
|
||||
return EndpointDeviceSerializer
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name) or str(self.user_id)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Endpoint Device")
|
||||
verbose_name_plural = _("Endpoint Devices")
|
||||
|
||||
|
||||
class EndpointDeviceConnection(models.Model):
|
||||
device = models.ForeignKey(EndpointDevice, on_delete=models.CASCADE)
|
||||
stage = models.ForeignKey(AuthenticatorEndpointGDTCStage, on_delete=models.CASCADE)
|
||||
|
||||
attributes = models.JSONField()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Endpoint device connection {self.device_id} to {self.stage_id}"
|
@ -0,0 +1,32 @@
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.flows.challenge import (
|
||||
Challenge,
|
||||
ChallengeResponse,
|
||||
FrameChallenge,
|
||||
FrameChallengeResponse,
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
|
||||
|
||||
class AuthenticatorEndpointStageView(ChallengeStageView):
|
||||
"""Endpoint stage"""
|
||||
|
||||
response_class = FrameChallengeResponse
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
return FrameChallenge(
|
||||
data={
|
||||
"component": "xak-flow-frame",
|
||||
"url": self.request.build_absolute_uri(
|
||||
reverse("authentik_stages_authenticator_endpoint_gdtc:chrome")
|
||||
),
|
||||
"loading_overlay": True,
|
||||
"loading_text": _("Verifying your browser..."),
|
||||
}
|
||||
)
|
||||
|
||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||
return self.executor.stage_ok()
|
@ -0,0 +1,9 @@
|
||||
<html>
|
||||
<script>
|
||||
window.parent.postMessage({
|
||||
message: "submit",
|
||||
source: "goauthentik.io",
|
||||
context: "flow-executor"
|
||||
});
|
||||
</script>
|
||||
</html>
|
@ -0,0 +1,26 @@
|
||||
"""API URLs"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.api import (
|
||||
AuthenticatorEndpointGDTCStageViewSet,
|
||||
EndpointAdminDeviceViewSet,
|
||||
EndpointDeviceViewSet,
|
||||
)
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.views.dtc import (
|
||||
GoogleChromeDeviceTrustConnector,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("chrome/", GoogleChromeDeviceTrustConnector.as_view(), name="chrome"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("authenticators/endpoint", EndpointDeviceViewSet),
|
||||
(
|
||||
"authenticators/admin/endpoint",
|
||||
EndpointAdminDeviceViewSet,
|
||||
"admin-endpointdevice",
|
||||
),
|
||||
("stages/authenticator/endpoint_gdtc", AuthenticatorEndpointGDTCStageViewSet),
|
||||
]
|
@ -0,0 +1,84 @@
|
||||
from json import dumps, loads
|
||||
from typing import Any
|
||||
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
||||
AuthenticatorEndpointGDTCStage,
|
||||
EndpointDevice,
|
||||
EndpointDeviceConnection,
|
||||
)
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
# Header we get from chrome that initiates verified access
|
||||
HEADER_DEVICE_TRUST = "X-Device-Trust"
|
||||
# Header we send to the client with the challenge
|
||||
HEADER_ACCESS_CHALLENGE = "X-Verified-Access-Challenge"
|
||||
# Header we get back from the client that we verify with google
|
||||
HEADER_ACCESS_CHALLENGE_RESPONSE = "X-Verified-Access-Challenge-Response"
|
||||
# Header value for x-device-trust that initiates the flow
|
||||
DEVICE_TRUST_VERIFIED_ACCESS = "VerifiedAccess"
|
||||
|
||||
|
||||
class GoogleChromeDeviceTrustConnector(View):
|
||||
"""Google Chrome Device-trust connector based endpoint authenticator"""
|
||||
|
||||
def get_flow_plan(self) -> FlowPlan:
|
||||
flow_plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
||||
return flow_plan
|
||||
|
||||
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
||||
super().setup(request, *args, **kwargs)
|
||||
stage: AuthenticatorEndpointGDTCStage = self.get_flow_plan().bindings[0].stage
|
||||
self.google_client = build(
|
||||
"verifiedaccess",
|
||||
"v2",
|
||||
cache_discovery=False,
|
||||
**stage.google_credentials(),
|
||||
)
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
x_device_trust = request.headers.get(HEADER_DEVICE_TRUST)
|
||||
x_access_challenge_response = request.headers.get(HEADER_ACCESS_CHALLENGE_RESPONSE)
|
||||
if x_device_trust == "VerifiedAccess" and x_access_challenge_response is None:
|
||||
challenge = self.google_client.challenge().generate().execute()
|
||||
res = HttpResponseRedirect(
|
||||
self.request.build_absolute_uri(
|
||||
reverse("authentik_stages_authenticator_endpoint_gdtc:chrome")
|
||||
)
|
||||
)
|
||||
res[HEADER_ACCESS_CHALLENGE] = dumps(challenge)
|
||||
return res
|
||||
if x_access_challenge_response:
|
||||
response = (
|
||||
self.google_client.challenge()
|
||||
.verify(body=loads(x_access_challenge_response))
|
||||
.execute()
|
||||
)
|
||||
# Remove deprecated string representation of deviceSignals
|
||||
response.pop("deviceSignal", None)
|
||||
flow_plan: FlowPlan = self.get_flow_plan()
|
||||
device, _ = EndpointDevice.objects.update_or_create(
|
||||
host_identifier=response["deviceSignals"]["serialNumber"],
|
||||
user=flow_plan.context.get(PLAN_CONTEXT_PENDING_USER),
|
||||
defaults={"name": response["deviceSignals"]["hostname"], "data": response},
|
||||
)
|
||||
EndpointDeviceConnection.objects.update_or_create(
|
||||
device=device,
|
||||
stage=flow_plan.bindings[0].stage,
|
||||
defaults={
|
||||
"attributes": response,
|
||||
},
|
||||
)
|
||||
flow_plan.context.setdefault(PLAN_CONTEXT_METHOD, "trusted_endpoint")
|
||||
flow_plan.context.setdefault(PLAN_CONTEXT_METHOD_ARGS, {})
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS].setdefault("endpoints", [])
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS]["endpoints"].append(response)
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
return TemplateResponse(request, "stages/authenticator_endpoint/google_chrome_dtc.html")
|
@ -8,7 +8,7 @@ from uuid import UUID
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.http import JsonResponse
|
||||
from rest_framework.fields import CharField, ChoiceField, DictField
|
||||
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField
|
||||
from rest_framework.request import Request
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
@ -160,6 +160,20 @@ class AutoSubmitChallengeResponse(ChallengeResponse):
|
||||
component = CharField(default="ak-stage-autosubmit")
|
||||
|
||||
|
||||
class FrameChallenge(Challenge):
|
||||
"""Challenge type to render a frame"""
|
||||
|
||||
component = CharField(default="xak-flow-frame")
|
||||
url = CharField()
|
||||
loading_overlay = BooleanField(default=False)
|
||||
loading_text = CharField()
|
||||
|
||||
|
||||
class FrameChallengeResponse(ChallengeResponse):
|
||||
|
||||
component = CharField(default="xak-flow-frame")
|
||||
|
||||
|
||||
class DataclassEncoder(DjangoJSONEncoder):
|
||||
"""Convert any dataclass to json"""
|
||||
|
||||
|
@ -105,6 +105,10 @@ ldap:
|
||||
tls:
|
||||
ciphers: null
|
||||
|
||||
sources:
|
||||
kerberos:
|
||||
task_timeout_hours: 2
|
||||
|
||||
reputation:
|
||||
expiry: 86400
|
||||
|
||||
|
@ -21,7 +21,14 @@ class DebugSession(Session):
|
||||
|
||||
def send(self, req: PreparedRequest, *args, **kwargs):
|
||||
request_id = str(uuid4())
|
||||
LOGGER.debug("HTTP request sent", uid=request_id, path=req.path_url, headers=req.headers)
|
||||
LOGGER.debug(
|
||||
"HTTP request sent",
|
||||
uid=request_id,
|
||||
url=req.url,
|
||||
method=req.method,
|
||||
headers=req.headers,
|
||||
body=req.body,
|
||||
)
|
||||
resp = super().send(req, *args, **kwargs)
|
||||
LOGGER.debug(
|
||||
"HTTP response received",
|
||||
|
@ -108,7 +108,7 @@ class EventMatcherPolicy(Policy):
|
||||
result=result,
|
||||
)
|
||||
matches.append(result)
|
||||
passing = any(x.passing for x in matches)
|
||||
passing = all(x.passing for x in matches)
|
||||
messages = chain(*[x.messages for x in matches])
|
||||
result = PolicyResult(passing, *messages)
|
||||
result.source_results = matches
|
||||
|
@ -77,11 +77,24 @@ class TestEventMatcherPolicy(TestCase):
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["event"] = event
|
||||
policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
|
||||
client_ip="1.2.3.5", app="bar"
|
||||
client_ip="1.2.3.5", app="foo"
|
||||
)
|
||||
response = policy.passes(request)
|
||||
self.assertFalse(response.passing)
|
||||
|
||||
def test_multiple(self):
|
||||
"""Test multiple"""
|
||||
event = Event.new(EventAction.LOGIN)
|
||||
event.app = "foo"
|
||||
event.client_ip = "1.2.3.4"
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["event"] = event
|
||||
policy: EventMatcherPolicy = EventMatcherPolicy.objects.create(
|
||||
client_ip="1.2.3.4", app="foo"
|
||||
)
|
||||
response = policy.passes(request)
|
||||
self.assertTrue(response.passing)
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test passing event"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
|
@ -439,15 +439,14 @@ class TokenParams:
|
||||
# (22 chars being the length of the "template")
|
||||
username=f"ak-{self.provider.name[:150-22]}-client_credentials",
|
||||
defaults={
|
||||
"attributes": {
|
||||
USER_ATTRIBUTE_GENERATED: True,
|
||||
},
|
||||
"last_login": timezone.now(),
|
||||
"name": f"Autogenerated user from application {app.name} (client credentials)",
|
||||
"path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
|
||||
"type": UserTypes.SERVICE_ACCOUNT,
|
||||
},
|
||||
)
|
||||
self.user.attributes[USER_ATTRIBUTE_GENERATED] = True
|
||||
self.user.save()
|
||||
self.__check_policy_access(app, request)
|
||||
|
||||
Event.new(
|
||||
@ -471,9 +470,6 @@ class TokenParams:
|
||||
self.user, created = User.objects.update_or_create(
|
||||
username=f"{self.provider.name}-{token.get('sub')}",
|
||||
defaults={
|
||||
"attributes": {
|
||||
USER_ATTRIBUTE_GENERATED: True,
|
||||
},
|
||||
"last_login": timezone.now(),
|
||||
"name": (
|
||||
f"Autogenerated user from application {app.name} (client credentials JWT)"
|
||||
@ -482,6 +478,8 @@ class TokenParams:
|
||||
"type": UserTypes.SERVICE_ACCOUNT,
|
||||
},
|
||||
)
|
||||
self.user.attributes[USER_ATTRIBUTE_GENERATED] = True
|
||||
self.user.save()
|
||||
exp = token.get("exp")
|
||||
if created and exp:
|
||||
self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
from itertools import batched
|
||||
|
||||
from django.db import transaction
|
||||
from pydantic import ValidationError
|
||||
from pydanticscim.group import GroupMember
|
||||
from pydanticscim.responses import PatchOp, PatchOperation
|
||||
from pydanticscim.responses import PatchOp
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
@ -19,7 +20,7 @@ from authentik.providers.scim.clients.base import SCIMClient
|
||||
from authentik.providers.scim.clients.exceptions import (
|
||||
SCIMRequestException,
|
||||
)
|
||||
from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchRequest
|
||||
from authentik.providers.scim.clients.schema import SCIM_GROUP_SCHEMA, PatchOperation, PatchRequest
|
||||
from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema
|
||||
from authentik.providers.scim.models import (
|
||||
SCIMMapping,
|
||||
@ -104,13 +105,47 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
provider=self.provider, group=group, scim_id=scim_id
|
||||
)
|
||||
users = list(group.users.order_by("id").values_list("id", flat=True))
|
||||
self._patch_add_users(group, users)
|
||||
self._patch_add_users(connection, users)
|
||||
return connection
|
||||
|
||||
def update(self, group: Group, connection: SCIMProviderGroup):
|
||||
"""Update existing group"""
|
||||
scim_group = self.to_schema(group, connection)
|
||||
scim_group.id = connection.scim_id
|
||||
try:
|
||||
if self._config.patch.supported:
|
||||
return self._update_patch(group, scim_group, connection)
|
||||
return self._update_put(group, scim_group, connection)
|
||||
except NotFoundSyncException:
|
||||
# Resource missing is handled by self.write, which will re-create the group
|
||||
raise
|
||||
|
||||
def _update_patch(
|
||||
self, group: Group, scim_group: SCIMGroupSchema, connection: SCIMProviderGroup
|
||||
):
|
||||
"""Update a group via PATCH request"""
|
||||
# Patch group's attributes instead of replacing it and re-adding users if we can
|
||||
self._request(
|
||||
"PATCH",
|
||||
f"/Groups/{connection.scim_id}",
|
||||
json=PatchRequest(
|
||||
Operations=[
|
||||
PatchOperation(
|
||||
op=PatchOp.replace,
|
||||
path=None,
|
||||
value=scim_group.model_dump(mode="json", exclude_unset=True),
|
||||
)
|
||||
]
|
||||
).model_dump(
|
||||
mode="json",
|
||||
exclude_unset=True,
|
||||
exclude_none=True,
|
||||
),
|
||||
)
|
||||
return self.patch_compare_users(group)
|
||||
|
||||
def _update_put(self, group: Group, scim_group: SCIMGroupSchema, connection: SCIMProviderGroup):
|
||||
"""Update a group via PUT request"""
|
||||
try:
|
||||
self._request(
|
||||
"PUT",
|
||||
@ -120,33 +155,25 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
exclude_unset=True,
|
||||
),
|
||||
)
|
||||
users = list(group.users.order_by("id").values_list("id", flat=True))
|
||||
return self._patch_add_users(group, users)
|
||||
except NotFoundSyncException:
|
||||
# Resource missing is handled by self.write, which will re-create the group
|
||||
raise
|
||||
return self.patch_compare_users(group)
|
||||
except (SCIMRequestException, ObjectExistsSyncException):
|
||||
# Some providers don't support PUT on groups, so this is mainly a fix for the initial
|
||||
# sync, send patch add requests for all the users the group currently has
|
||||
users = list(group.users.order_by("id").values_list("id", flat=True))
|
||||
self._patch_add_users(group, users)
|
||||
# Also update the group name
|
||||
return self._patch(
|
||||
scim_group.id,
|
||||
PatchOperation(
|
||||
op=PatchOp.replace,
|
||||
path="displayName",
|
||||
value=scim_group.displayName,
|
||||
),
|
||||
)
|
||||
return self._update_patch(group, scim_group, connection)
|
||||
|
||||
def update_group(self, group: Group, action: Direction, users_set: set[int]):
|
||||
"""Update a group, either using PUT to replace it or PATCH if supported"""
|
||||
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first()
|
||||
if not scim_group:
|
||||
self.logger.warning(
|
||||
"could not sync group membership, group does not exist", group=group
|
||||
)
|
||||
return
|
||||
if self._config.patch.supported:
|
||||
if action == Direction.add:
|
||||
return self._patch_add_users(group, users_set)
|
||||
return self._patch_add_users(scim_group, users_set)
|
||||
if action == Direction.remove:
|
||||
return self._patch_remove_users(group, users_set)
|
||||
return self._patch_remove_users(scim_group, users_set)
|
||||
try:
|
||||
return self.write(group)
|
||||
except SCIMRequestException as exc:
|
||||
@ -154,16 +181,19 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
# Assume that provider does not support PUT and also doesn't support
|
||||
# ServiceProviderConfig, so try PATCH as a fallback
|
||||
if action == Direction.add:
|
||||
return self._patch_add_users(group, users_set)
|
||||
return self._patch_add_users(scim_group, users_set)
|
||||
if action == Direction.remove:
|
||||
return self._patch_remove_users(group, users_set)
|
||||
return self._patch_remove_users(scim_group, users_set)
|
||||
raise exc
|
||||
|
||||
def _patch(
|
||||
def _patch_chunked(
|
||||
self,
|
||||
group_id: str,
|
||||
*ops: PatchOperation,
|
||||
):
|
||||
"""Helper function that chunks patch requests based on the maxOperations attribute.
|
||||
This is not strictly according to specs but there's nothing in the schema that allows the
|
||||
us to know what the maximum patch operations per request should be."""
|
||||
chunk_size = self._config.bulk.maxOperations
|
||||
if chunk_size < 1:
|
||||
chunk_size = len(ops)
|
||||
@ -177,16 +207,67 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
),
|
||||
)
|
||||
|
||||
def _patch_add_users(self, group: Group, users_set: set[int]):
|
||||
"""Add users in users_set to group"""
|
||||
if len(users_set) < 1:
|
||||
return
|
||||
@transaction.atomic
|
||||
def patch_compare_users(self, group: Group):
|
||||
"""Compare users with a SCIM group and add/remove any differences"""
|
||||
# Get scim group first
|
||||
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first()
|
||||
if not scim_group:
|
||||
self.logger.warning(
|
||||
"could not sync group membership, group does not exist", group=group
|
||||
)
|
||||
return
|
||||
# Get a list of all users in the authentik group
|
||||
raw_users_should = list(group.users.order_by("id").values_list("id", flat=True))
|
||||
# Lookup the SCIM IDs of the users
|
||||
users_should: list[str] = list(
|
||||
SCIMProviderUser.objects.filter(
|
||||
user__pk__in=raw_users_should, provider=self.provider
|
||||
).values_list("scim_id", flat=True)
|
||||
)
|
||||
if len(raw_users_should) != len(users_should):
|
||||
self.logger.warning(
|
||||
"User count mismatch, not all users in the group are synced to SCIM yet.",
|
||||
group=group,
|
||||
)
|
||||
# Get current group status
|
||||
current_group = SCIMGroupSchema.model_validate(
|
||||
self._request("GET", f"/Groups/{scim_group.scim_id}")
|
||||
)
|
||||
users_to_add = []
|
||||
users_to_remove = []
|
||||
# Check users currently in group and if they shouldn't be in the group and remove them
|
||||
for user in current_group.members:
|
||||
if user.value not in users_should:
|
||||
users_to_remove.append(user.value)
|
||||
# Check users that should be in the group and add them
|
||||
for user in users_should:
|
||||
if len([x for x in current_group.members if x.value == user]) < 1:
|
||||
users_to_add.append(user)
|
||||
return self._patch_chunked(
|
||||
scim_group.scim_id,
|
||||
*[
|
||||
PatchOperation(
|
||||
op=PatchOp.add,
|
||||
path="members",
|
||||
value=[{"value": x}],
|
||||
)
|
||||
for x in users_to_add
|
||||
],
|
||||
*[
|
||||
PatchOperation(
|
||||
op=PatchOp.remove,
|
||||
path="members",
|
||||
value=[{"value": x}],
|
||||
)
|
||||
for x in users_to_remove
|
||||
],
|
||||
)
|
||||
|
||||
def _patch_add_users(self, scim_group: SCIMProviderGroup, users_set: set[int]):
|
||||
"""Add users in users_set to group"""
|
||||
if len(users_set) < 1:
|
||||
return
|
||||
user_ids = list(
|
||||
SCIMProviderUser.objects.filter(
|
||||
user__pk__in=users_set, provider=self.provider
|
||||
@ -194,7 +275,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
)
|
||||
if len(user_ids) < 1:
|
||||
return
|
||||
self._patch(
|
||||
self._patch_chunked(
|
||||
scim_group.scim_id,
|
||||
*[
|
||||
PatchOperation(
|
||||
@ -206,16 +287,10 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
],
|
||||
)
|
||||
|
||||
def _patch_remove_users(self, group: Group, users_set: set[int]):
|
||||
def _patch_remove_users(self, scim_group: SCIMProviderGroup, users_set: set[int]):
|
||||
"""Remove users in users_set from group"""
|
||||
if len(users_set) < 1:
|
||||
return
|
||||
scim_group = SCIMProviderGroup.objects.filter(provider=self.provider, group=group).first()
|
||||
if not scim_group:
|
||||
self.logger.warning(
|
||||
"could not sync group membership, group does not exist", group=group
|
||||
)
|
||||
return
|
||||
user_ids = list(
|
||||
SCIMProviderUser.objects.filter(
|
||||
user__pk__in=users_set, provider=self.provider
|
||||
@ -223,7 +298,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
|
||||
)
|
||||
if len(user_ids) < 1:
|
||||
return
|
||||
self._patch(
|
||||
self._patch_chunked(
|
||||
scim_group.scim_id,
|
||||
*[
|
||||
PatchOperation(
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from pydantic import Field
|
||||
from pydanticscim.group import Group as BaseGroup
|
||||
from pydanticscim.responses import PatchOperation as BasePatchOperation
|
||||
from pydanticscim.responses import PatchRequest as BasePatchRequest
|
||||
from pydanticscim.responses import SCIMError as BaseSCIMError
|
||||
from pydanticscim.service_provider import Bulk as BaseBulk
|
||||
@ -68,6 +69,12 @@ class PatchRequest(BasePatchRequest):
|
||||
schemas: tuple[str] = ("urn:ietf:params:scim:api:messages:2.0:PatchOp",)
|
||||
|
||||
|
||||
class PatchOperation(BasePatchOperation):
|
||||
"""PatchOperation with optional path"""
|
||||
|
||||
path: str | None
|
||||
|
||||
|
||||
class SCIMError(BaseSCIMError):
|
||||
"""SCIM error with optional status code"""
|
||||
|
||||
|
@ -252,3 +252,118 @@ class SCIMMembershipTests(TestCase):
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
def test_member_add_save(self):
|
||||
"""Test member add + save"""
|
||||
config = ServiceProviderConfiguration.default()
|
||||
|
||||
config.patch.supported = True
|
||||
user_scim_id = generate_id()
|
||||
group_scim_id = generate_id()
|
||||
uid = generate_id()
|
||||
group = Group.objects.create(
|
||||
name=uid,
|
||||
)
|
||||
|
||||
user = User.objects.create(username=generate_id())
|
||||
|
||||
# Test initial sync of group creation
|
||||
with Mocker() as mocker:
|
||||
mocker.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json=config.model_dump(),
|
||||
)
|
||||
mocker.post(
|
||||
"https://localhost/Users",
|
||||
json={
|
||||
"id": user_scim_id,
|
||||
},
|
||||
)
|
||||
mocker.post(
|
||||
"https://localhost/Groups",
|
||||
json={
|
||||
"id": group_scim_id,
|
||||
},
|
||||
)
|
||||
|
||||
self.configure()
|
||||
sync_tasks.trigger_single_task(self.provider, scim_sync).get()
|
||||
|
||||
self.assertEqual(mocker.call_count, 6)
|
||||
self.assertEqual(mocker.request_history[0].method, "GET")
|
||||
self.assertEqual(mocker.request_history[1].method, "GET")
|
||||
self.assertEqual(mocker.request_history[2].method, "GET")
|
||||
self.assertEqual(mocker.request_history[3].method, "POST")
|
||||
self.assertEqual(mocker.request_history[4].method, "GET")
|
||||
self.assertEqual(mocker.request_history[5].method, "POST")
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[3].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
||||
"emails": [],
|
||||
"active": True,
|
||||
"externalId": user.uid,
|
||||
"name": {"familyName": " ", "formatted": " ", "givenName": ""},
|
||||
"displayName": "",
|
||||
"userName": user.username,
|
||||
},
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[5].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
"displayName": group.name,
|
||||
},
|
||||
)
|
||||
|
||||
with Mocker() as mocker:
|
||||
mocker.get(
|
||||
"https://localhost/ServiceProviderConfig",
|
||||
json=config.model_dump(),
|
||||
)
|
||||
mocker.get(
|
||||
f"https://localhost/Groups/{group_scim_id}",
|
||||
json={},
|
||||
)
|
||||
mocker.patch(
|
||||
f"https://localhost/Groups/{group_scim_id}",
|
||||
json={},
|
||||
)
|
||||
group.users.add(user)
|
||||
group.save()
|
||||
self.assertEqual(mocker.call_count, 5)
|
||||
self.assertEqual(mocker.request_history[0].method, "GET")
|
||||
self.assertEqual(mocker.request_history[1].method, "PATCH")
|
||||
self.assertEqual(mocker.request_history[2].method, "GET")
|
||||
self.assertEqual(mocker.request_history[3].method, "PATCH")
|
||||
self.assertEqual(mocker.request_history[4].method, "GET")
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "members",
|
||||
"value": [{"value": user_scim_id}],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
mocker.request_history[3].body,
|
||||
{
|
||||
"Operations": [
|
||||
{
|
||||
"op": "replace",
|
||||
"value": {
|
||||
"id": group_scim_id,
|
||||
"displayName": group.name,
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
|
||||
"externalId": str(group.pk),
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
@ -41,7 +41,9 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
||||
# Since go does not consider localhost with http a secure origin
|
||||
# we can't set the secure flag.
|
||||
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
||||
if user_agent.startswith("goauthentik.io/outpost/") or "safari" in user_agent.lower():
|
||||
if user_agent.startswith("goauthentik.io/outpost/") or (
|
||||
"safari" in user_agent.lower() and "chrome" not in user_agent.lower()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
@ -38,6 +38,7 @@ LANGUAGE_COOKIE_NAME = "authentik_language"
|
||||
SESSION_COOKIE_NAME = "authentik_session"
|
||||
SESSION_COOKIE_DOMAIN = CONFIG.get("cookie_domain", None)
|
||||
APPEND_SLASH = False
|
||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
@ -90,6 +91,7 @@ TENANT_APPS = [
|
||||
"authentik.providers.scim",
|
||||
"authentik.rbac",
|
||||
"authentik.recovery",
|
||||
"authentik.sources.kerberos",
|
||||
"authentik.sources.ldap",
|
||||
"authentik.sources.oauth",
|
||||
"authentik.sources.plex",
|
||||
|
0
authentik/sources/kerberos/__init__.py
Normal file
0
authentik/sources/kerberos/__init__.py
Normal file
0
authentik/sources/kerberos/api/__init__.py
Normal file
0
authentik/sources/kerberos/api/__init__.py
Normal file
31
authentik/sources/kerberos/api/property_mappings.py
Normal file
31
authentik/sources/kerberos/api/property_mappings.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Kerberos Property Mapping API"""
|
||||
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.kerberos.models import KerberosSourcePropertyMapping
|
||||
|
||||
|
||||
class KerberosSourcePropertyMappingSerializer(PropertyMappingSerializer):
|
||||
"""Kerberos PropertyMapping Serializer"""
|
||||
|
||||
class Meta(PropertyMappingSerializer.Meta):
|
||||
model = KerberosSourcePropertyMapping
|
||||
|
||||
|
||||
class KerberosSourcePropertyMappingFilter(PropertyMappingFilterSet):
|
||||
"""Filter for KerberosSourcePropertyMapping"""
|
||||
|
||||
class Meta(PropertyMappingFilterSet.Meta):
|
||||
model = KerberosSourcePropertyMapping
|
||||
|
||||
|
||||
class KerberosSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""KerberosSource PropertyMapping Viewset"""
|
||||
|
||||
queryset = KerberosSourcePropertyMapping.objects.all()
|
||||
serializer_class = KerberosSourcePropertyMappingSerializer
|
||||
filterset_class = KerberosSourcePropertyMappingFilter
|
||||
search_fields = ["name"]
|
||||
ordering = ["name"]
|
114
authentik/sources/kerberos/api/source.py
Normal file
114
authentik/sources/kerberos/api/source.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""Source API Views"""
|
||||
|
||||
from django.core.cache import cache
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.api.tasks import SystemTaskSerializer
|
||||
from authentik.sources.kerberos.models import KerberosSource
|
||||
from authentik.sources.kerberos.tasks import CACHE_KEY_STATUS
|
||||
|
||||
|
||||
class KerberosSourceSerializer(SourceSerializer):
|
||||
"""Kerberos Source Serializer"""
|
||||
|
||||
connectivity = SerializerMethodField()
|
||||
|
||||
def get_connectivity(self, source: KerberosSource) -> dict[str, str] | None:
|
||||
"""Get cached source connectivity"""
|
||||
return cache.get(CACHE_KEY_STATUS + source.slug, None)
|
||||
|
||||
class Meta:
|
||||
model = KerberosSource
|
||||
fields = SourceSerializer.Meta.fields + [
|
||||
"group_matching_mode",
|
||||
"realm",
|
||||
"krb5_conf",
|
||||
"sync_users",
|
||||
"sync_users_password",
|
||||
"sync_principal",
|
||||
"sync_password",
|
||||
"sync_keytab",
|
||||
"sync_ccache",
|
||||
"connectivity",
|
||||
"spnego_server_name",
|
||||
"spnego_keytab",
|
||||
"spnego_ccache",
|
||||
"password_login_update_internal_password",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"sync_password": {"write_only": True},
|
||||
"sync_keytab": {"write_only": True},
|
||||
"spnego_keytab": {"write_only": True},
|
||||
}
|
||||
|
||||
|
||||
class KerberosSyncStatusSerializer(PassiveSerializer):
|
||||
"""Kerberos Source sync status"""
|
||||
|
||||
is_running = BooleanField(read_only=True)
|
||||
tasks = SystemTaskSerializer(many=True, read_only=True)
|
||||
|
||||
|
||||
class KerberosSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Kerberos Source Viewset"""
|
||||
|
||||
queryset = KerberosSource.objects.all()
|
||||
serializer_class = KerberosSourceSerializer
|
||||
lookup_field = "slug"
|
||||
filterset_fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"enabled",
|
||||
"realm",
|
||||
"sync_users",
|
||||
"sync_users_password",
|
||||
"sync_principal",
|
||||
"spnego_server_name",
|
||||
"password_login_update_internal_password",
|
||||
]
|
||||
search_fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"realm",
|
||||
"krb5_conf",
|
||||
"sync_principal",
|
||||
"spnego_server_name",
|
||||
]
|
||||
ordering = ["name"]
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: KerberosSyncStatusSerializer(),
|
||||
}
|
||||
)
|
||||
@action(
|
||||
methods=["GET"],
|
||||
detail=True,
|
||||
pagination_class=None,
|
||||
url_path="sync/status",
|
||||
filter_backends=[],
|
||||
)
|
||||
def sync_status(self, request: Request, slug: str) -> Response:
|
||||
"""Get source's sync status"""
|
||||
source: KerberosSource = self.get_object()
|
||||
tasks = list(
|
||||
get_objects_for_user(request.user, "authentik_events.view_systemtask").filter(
|
||||
name="kerberos_sync",
|
||||
uid__startswith=source.slug,
|
||||
)
|
||||
)
|
||||
with source.sync_lock as lock_acquired:
|
||||
status = {
|
||||
"tasks": tasks,
|
||||
"is_running": not lock_acquired,
|
||||
}
|
||||
return Response(KerberosSyncStatusSerializer(status).data)
|
51
authentik/sources/kerberos/api/source_connection.py
Normal file
51
authentik/sources/kerberos/api/source_connection.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""Kerberos Source Serializer"""
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
|
||||
from authentik.core.api.sources import (
|
||||
GroupSourceConnectionSerializer,
|
||||
GroupSourceConnectionViewSet,
|
||||
UserSourceConnectionSerializer,
|
||||
)
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.kerberos.models import (
|
||||
GroupKerberosSourceConnection,
|
||||
UserKerberosSourceConnection,
|
||||
)
|
||||
|
||||
|
||||
class UserKerberosSourceConnectionSerializer(UserSourceConnectionSerializer):
|
||||
"""Kerberos Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = UserKerberosSourceConnection
|
||||
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
|
||||
|
||||
|
||||
class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = UserKerberosSourceConnection.objects.all()
|
||||
serializer_class = UserKerberosSourceConnectionSerializer
|
||||
filterset_fields = ["source__slug"]
|
||||
search_fields = ["source__slug"]
|
||||
permission_classes = [OwnerSuperuserPermissions]
|
||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
ordering = ["source__slug"]
|
||||
|
||||
|
||||
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
|
||||
"""OAuth Group-Source connection Serializer"""
|
||||
|
||||
class Meta(GroupSourceConnectionSerializer.Meta):
|
||||
model = GroupKerberosSourceConnection
|
||||
|
||||
|
||||
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet):
|
||||
"""Group-source connection Viewset"""
|
||||
|
||||
queryset = GroupKerberosSourceConnection.objects.all()
|
||||
serializer_class = GroupKerberosSourceConnectionSerializer
|
13
authentik/sources/kerberos/apps.py
Normal file
13
authentik/sources/kerberos/apps.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""authentik kerberos source config"""
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
|
||||
|
||||
class AuthentikSourceKerberosConfig(ManagedAppConfig):
|
||||
"""Authentik source kerberos app config"""
|
||||
|
||||
name = "authentik.sources.kerberos"
|
||||
label = "authentik_sources_kerberos"
|
||||
verbose_name = "authentik Sources.Kerberos"
|
||||
mountpoint = "source/kerberos/"
|
||||
default = True
|
116
authentik/sources/kerberos/auth.py
Normal file
116
authentik/sources/kerberos/auth.py
Normal file
@ -0,0 +1,116 @@
|
||||
"""authentik Kerberos Authentication Backend"""
|
||||
|
||||
import gssapi
|
||||
from django.http import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.auth import InbuiltBackend
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.kerberos.models import (
|
||||
KerberosSource,
|
||||
Krb5ConfContext,
|
||||
UserKerberosSourceConnection,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class KerberosBackend(InbuiltBackend):
|
||||
"""Authenticate users against Kerberos realm"""
|
||||
|
||||
def authenticate(self, request: HttpRequest, **kwargs):
|
||||
"""Try to authenticate a user via kerberos"""
|
||||
if "password" not in kwargs or "username" not in kwargs:
|
||||
return None
|
||||
username = kwargs.pop("username")
|
||||
realm = None
|
||||
if "@" in username:
|
||||
username, realm = username.rsplit("@", 1)
|
||||
|
||||
user, source = self.auth_user(username, realm, **kwargs)
|
||||
if user:
|
||||
self.set_method("kerberos", request, source=source)
|
||||
return user
|
||||
return None
|
||||
|
||||
def auth_user(
|
||||
self, username: str, realm: str | None, password: str, **filters
|
||||
) -> tuple[User | None, KerberosSource | None]:
|
||||
sources = KerberosSource.objects.filter(enabled=True)
|
||||
user = User.objects.filter(usersourceconnection__source__in=sources, **filters).first()
|
||||
|
||||
if user is not None:
|
||||
# User found, let's get its connections for the sources that are available
|
||||
user_source_connections = UserKerberosSourceConnection.objects.filter(
|
||||
user=user, source__in=sources
|
||||
)
|
||||
elif realm is not None:
|
||||
user_source_connections = UserKerberosSourceConnection.objects.filter(
|
||||
source__in=sources, identifier=f"{username}@{realm}"
|
||||
)
|
||||
# no realm specified, we can't do anything
|
||||
else:
|
||||
user_source_connections = UserKerberosSourceConnection.objects.none()
|
||||
|
||||
if not user_source_connections.exists():
|
||||
LOGGER.debug("no kerberos source found for user", username=username)
|
||||
return None, None
|
||||
|
||||
for user_source_connection in user_source_connections.prefetch_related().select_related(
|
||||
"source__kerberossource"
|
||||
):
|
||||
# User either has an unusable password,
|
||||
# or has a password, but couldn't be authenticated by ModelBackend
|
||||
# This means we check with a kinit to see if the Kerberos password has changed
|
||||
if self.auth_user_by_kinit(user_source_connection, password):
|
||||
# Password was successful in kinit to Kerberos, so we save it in database
|
||||
if (
|
||||
user_source_connection.source.kerberossource.password_login_update_internal_password
|
||||
):
|
||||
LOGGER.debug(
|
||||
"Updating user's password in DB",
|
||||
source=user_source_connection.source,
|
||||
user=user_source_connection.user,
|
||||
)
|
||||
user_source_connection.user.set_password(
|
||||
password, sender=user_source_connection.source
|
||||
)
|
||||
user_source_connection.user.save()
|
||||
return user, user_source_connection.source
|
||||
# Password doesn't match, onto next source
|
||||
LOGGER.debug(
|
||||
"failed to kinit, password invalid",
|
||||
source=user_source_connection.source,
|
||||
user=user_source_connection.user,
|
||||
)
|
||||
# No source with valid password found
|
||||
LOGGER.debug("no valid kerberos source found for user", user=user)
|
||||
return None, None
|
||||
|
||||
def auth_user_by_kinit(
|
||||
self, user_source_connection: UserKerberosSourceConnection, password: str
|
||||
) -> bool:
|
||||
"""Attempt authentication by kinit to the source."""
|
||||
LOGGER.debug(
|
||||
"Attempting to kinit as user",
|
||||
user=user_source_connection.user,
|
||||
source=user_source_connection.source,
|
||||
principal=user_source_connection.identifier,
|
||||
)
|
||||
|
||||
with Krb5ConfContext(user_source_connection.source.kerberossource):
|
||||
name = gssapi.raw.import_name(
|
||||
user_source_connection.identifier.encode(), gssapi.raw.NameType.kerberos_principal
|
||||
)
|
||||
try:
|
||||
# Use a temporary credentials cache to not interfere with whatever is defined
|
||||
# elsewhere
|
||||
gssapi.raw.ext_krb5.krb5_ccache_name(f"MEMORY:{generate_id(12)}".encode())
|
||||
gssapi.raw.ext_password.acquire_cred_with_password(name, password.encode())
|
||||
# Restore the credentials cache to what it was before
|
||||
gssapi.raw.ext_krb5.krb5_ccache_name(None)
|
||||
return True
|
||||
except gssapi.exceptions.GSSError as exc:
|
||||
LOGGER.warning("failed to kinit", exc=exc)
|
||||
return False
|
4
authentik/sources/kerberos/krb5.conf
Normal file
4
authentik/sources/kerberos/krb5.conf
Normal file
@ -0,0 +1,4 @@
|
||||
[libdefaults]
|
||||
dns_canonicalize_hostname = false
|
||||
dns_fallback = true
|
||||
rnds = false
|
0
authentik/sources/kerberos/management/__init__.py
Normal file
0
authentik/sources/kerberos/management/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""Kerberos Connection check"""
|
||||
|
||||
from json import dumps
|
||||
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.sources.kerberos.models import KerberosSource
|
||||
from authentik.tenants.management import TenantCommand
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class Command(TenantCommand):
|
||||
"""Check connectivity to Kerberos servers for a source"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("source_slugs", nargs="?", type=str)
|
||||
|
||||
def handle_per_tenant(self, **options):
|
||||
sources = KerberosSource.objects.filter(enabled=True)
|
||||
if options["source_slugs"]:
|
||||
sources = KerberosSource.objects.filter(slug__in=options["source_slugs"])
|
||||
for source in sources.order_by("slug"):
|
||||
status = source.check_connection()
|
||||
self.stdout.write(dumps(status, indent=4))
|
@ -0,0 +1,25 @@
|
||||
"""Kerberos Sync"""
|
||||
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.sources.kerberos.models import KerberosSource
|
||||
from authentik.sources.kerberos.sync import KerberosSync
|
||||
from authentik.tenants.management import TenantCommand
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class Command(TenantCommand):
|
||||
"""Run sync for an Kerberos Source"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("source_slugs", nargs="+", type=str)
|
||||
|
||||
def handle_per_tenant(self, **options):
|
||||
for source_slug in options["source_slugs"]:
|
||||
source = KerberosSource.objects.filter(slug=source_slug).first()
|
||||
if not source:
|
||||
LOGGER.warning("Source does not exist", slug=source_slug)
|
||||
continue
|
||||
user_count = KerberosSync(source).sync()
|
||||
LOGGER.info(f"Synced {user_count} users", slug=source_slug)
|
179
authentik/sources/kerberos/migrations/0001_initial.py
Normal file
179
authentik/sources/kerberos/migrations/0001_initial.py
Normal file
@ -0,0 +1,179 @@
|
||||
# Generated by Django 5.0.9 on 2024-09-23 11:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GroupKerberosSourceConnection",
|
||||
fields=[
|
||||
(
|
||||
"groupsourceconnection_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.groupsourceconnection",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Group Kerberos Source Connection",
|
||||
"verbose_name_plural": "Group Kerberos Source Connections",
|
||||
},
|
||||
bases=("authentik_core.groupsourceconnection",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="KerberosSource",
|
||||
fields=[
|
||||
(
|
||||
"source_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.source",
|
||||
),
|
||||
),
|
||||
("realm", models.TextField(help_text="Kerberos realm", unique=True)),
|
||||
(
|
||||
"krb5_conf",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Custom krb5.conf to use. Uses the system one by default",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sync_users",
|
||||
models.BooleanField(
|
||||
db_index=True,
|
||||
default=False,
|
||||
help_text="Sync users from Kerberos into authentik",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sync_users_password",
|
||||
models.BooleanField(
|
||||
db_index=True,
|
||||
default=True,
|
||||
help_text="When a user changes their password, sync it back to Kerberos",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sync_principal",
|
||||
models.TextField(
|
||||
blank=True, help_text="Principal to authenticate to kadmin for sync."
|
||||
),
|
||||
),
|
||||
(
|
||||
"sync_password",
|
||||
models.TextField(
|
||||
blank=True, help_text="Password to authenticate to kadmin for sync"
|
||||
),
|
||||
),
|
||||
(
|
||||
"sync_keytab",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the form TYPE:residual",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sync_ccache",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Credentials cache to authenticate to kadmin for sync. Must be in the form TYPE:residual",
|
||||
),
|
||||
),
|
||||
(
|
||||
"spnego_server_name",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Force the use of a specific server name for SPNEGO. Must be in the form HTTP@hostname",
|
||||
),
|
||||
),
|
||||
(
|
||||
"spnego_keytab",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="SPNEGO keytab base64-encoded or path to keytab in the form FILE:path",
|
||||
),
|
||||
),
|
||||
(
|
||||
"spnego_ccache",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Credential cache to use for SPNEGO in form type:residual",
|
||||
),
|
||||
),
|
||||
(
|
||||
"password_login_update_internal_password",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="If enabled, the authentik-stored password will be updated upon login with the Kerberos password backend",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Kerberos Source",
|
||||
"verbose_name_plural": "Kerberos Sources",
|
||||
},
|
||||
bases=("authentik_core.source",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="KerberosSourcePropertyMapping",
|
||||
fields=[
|
||||
(
|
||||
"propertymapping_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.propertymapping",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Kerberos Source Property Mapping",
|
||||
"verbose_name_plural": "Kerberos Source Property Mappings",
|
||||
},
|
||||
bases=("authentik_core.propertymapping",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserKerberosSourceConnection",
|
||||
fields=[
|
||||
(
|
||||
"usersourceconnection_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.usersourceconnection",
|
||||
),
|
||||
),
|
||||
("identifier", models.TextField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User Kerberos Source Connection",
|
||||
"verbose_name_plural": "User Kerberos Source Connections",
|
||||
},
|
||||
bases=("authentik_core.usersourceconnection",),
|
||||
),
|
||||
]
|
0
authentik/sources/kerberos/migrations/__init__.py
Normal file
0
authentik/sources/kerberos/migrations/__init__.py
Normal file
376
authentik/sources/kerberos/models.py
Normal file
376
authentik/sources/kerberos/models.py
Normal file
@ -0,0 +1,376 @@
|
||||
"""authentik Kerberos Source Models"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from typing import Any
|
||||
|
||||
import gssapi
|
||||
import kadmin
|
||||
import pglock
|
||||
from django.db import connection, models
|
||||
from django.db.models.fields import b64decode
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import reverse
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import (
|
||||
GroupSourceConnection,
|
||||
PropertyMapping,
|
||||
Source,
|
||||
UserSourceConnection,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.flows.challenge import RedirectChallenge
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
# python-kadmin leaks file descriptors. As such, this global is used to reuse
|
||||
# existing kadmin connections instead of creating new ones, which results in less to no file
|
||||
# descriptors leaks
|
||||
_kadmin_connections: dict[str, Any] = {}
|
||||
|
||||
|
||||
class KerberosSource(Source):
|
||||
"""Federate Kerberos realm with authentik"""
|
||||
|
||||
realm = models.TextField(help_text=_("Kerberos realm"), unique=True)
|
||||
krb5_conf = models.TextField(
|
||||
blank=True,
|
||||
help_text=_("Custom krb5.conf to use. Uses the system one by default"),
|
||||
)
|
||||
|
||||
sync_users = models.BooleanField(
|
||||
default=False, help_text=_("Sync users from Kerberos into authentik"), db_index=True
|
||||
)
|
||||
sync_users_password = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_("When a user changes their password, sync it back to Kerberos"),
|
||||
db_index=True,
|
||||
)
|
||||
sync_principal = models.TextField(
|
||||
help_text=_("Principal to authenticate to kadmin for sync."), blank=True
|
||||
)
|
||||
sync_password = models.TextField(
|
||||
help_text=_("Password to authenticate to kadmin for sync"), blank=True
|
||||
)
|
||||
sync_keytab = models.TextField(
|
||||
help_text=_(
|
||||
"Keytab to authenticate to kadmin for sync. "
|
||||
"Must be base64-encoded or in the form TYPE:residual"
|
||||
),
|
||||
blank=True,
|
||||
)
|
||||
sync_ccache = models.TextField(
|
||||
help_text=_(
|
||||
"Credentials cache to authenticate to kadmin for sync. "
|
||||
"Must be in the form TYPE:residual"
|
||||
),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
spnego_server_name = models.TextField(
|
||||
help_text=_(
|
||||
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@hostname"
|
||||
),
|
||||
blank=True,
|
||||
)
|
||||
spnego_keytab = models.TextField(
|
||||
help_text=_("SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"),
|
||||
blank=True,
|
||||
)
|
||||
spnego_ccache = models.TextField(
|
||||
help_text=_("Credential cache to use for SPNEGO in form type:residual"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
password_login_update_internal_password = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"If enabled, the authentik-stored password will be updated upon "
|
||||
"login with the Kerberos password backend"
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Kerberos Source")
|
||||
verbose_name_plural = _("Kerberos Sources")
|
||||
|
||||
def __str__(self):
|
||||
return f"Kerberos Source {self.name}"
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-source-kerberos-form"
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.kerberos.api.source import KerberosSourceSerializer
|
||||
|
||||
return KerberosSourceSerializer
|
||||
|
||||
@property
|
||||
def property_mapping_type(self) -> type[PropertyMapping]:
|
||||
return KerberosSourcePropertyMapping
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str:
|
||||
icon = super().icon_url
|
||||
if not icon:
|
||||
return static("authentik/sources/kerberos.png")
|
||||
return icon
|
||||
|
||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
challenge=RedirectChallenge(
|
||||
data={
|
||||
"to": reverse(
|
||||
"authentik_sources_kerberos:spnego-login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
}
|
||||
),
|
||||
name=self.name,
|
||||
icon_url=self.icon_url,
|
||||
)
|
||||
|
||||
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.name,
|
||||
"component": "ak-user-settings-source-kerberos",
|
||||
"configure_url": reverse(
|
||||
"authentik_sources_kerberos:spnego-login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
"icon_url": self.icon_url,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def sync_lock(self) -> pglock.advisory:
|
||||
"""Redis lock for syncing Kerberos to prevent multiple parallel syncs happening"""
|
||||
return pglock.advisory(
|
||||
lock_id=f"goauthentik.io/{connection.schema_name}/sources/kerberos/sync/{self.slug}",
|
||||
timeout=0,
|
||||
side_effect=pglock.Return,
|
||||
)
|
||||
|
||||
def get_base_user_properties(self, principal: str, **kwargs):
|
||||
localpart, _ = principal.rsplit("@", 1)
|
||||
|
||||
return {
|
||||
"username": localpart,
|
||||
"type": UserTypes.INTERNAL,
|
||||
"path": self.get_user_path(),
|
||||
}
|
||||
|
||||
def get_base_group_properties(self, group_id: str, **kwargs):
|
||||
return {
|
||||
"name": group_id,
|
||||
}
|
||||
|
||||
@property
|
||||
def tempdir(self) -> Path:
|
||||
"""Get temporary storage for Kerberos files"""
|
||||
path = (
|
||||
Path(gettempdir())
|
||||
/ "authentik"
|
||||
/ connection.schema_name
|
||||
/ "sources"
|
||||
/ "kerberos"
|
||||
/ str(self.pk)
|
||||
)
|
||||
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
@property
|
||||
def krb5_conf_path(self) -> str | None:
|
||||
"""Get krb5.conf path"""
|
||||
if not self.krb5_conf:
|
||||
return None
|
||||
conf_path = self.tempdir / "krb5.conf"
|
||||
conf_path.write_text(self.krb5_conf)
|
||||
return str(conf_path)
|
||||
|
||||
def _kadmin_init(self) -> "kadmin.KAdmin | None":
|
||||
# kadmin doesn't use a ccache for its connection
|
||||
# as such, we don't need to create a separate ccache for each source
|
||||
if not self.sync_principal:
|
||||
return None
|
||||
if self.sync_password:
|
||||
return kadmin.init_with_password(
|
||||
self.sync_principal,
|
||||
self.sync_password,
|
||||
)
|
||||
if self.sync_keytab:
|
||||
keytab = self.sync_keytab
|
||||
if ":" not in keytab:
|
||||
keytab_path = self.tempdir / "kadmin_keytab"
|
||||
keytab_path.touch(mode=0o600)
|
||||
keytab_path.write_bytes(b64decode(self.sync_keytab))
|
||||
keytab = f"FILE:{keytab_path}"
|
||||
return kadmin.init_with_keytab(
|
||||
self.sync_principal,
|
||||
keytab,
|
||||
)
|
||||
if self.sync_ccache:
|
||||
return kadmin.init_with_ccache(
|
||||
self.sync_principal,
|
||||
self.sync_ccache,
|
||||
)
|
||||
return None
|
||||
|
||||
def connection(self) -> "kadmin.KAdmin | None":
|
||||
"""Get kadmin connection"""
|
||||
if str(self.pk) not in _kadmin_connections:
|
||||
kadm = self._kadmin_init()
|
||||
if kadm is not None:
|
||||
_kadmin_connections[str(self.pk)] = self._kadmin_init()
|
||||
return _kadmin_connections.get(str(self.pk), None)
|
||||
|
||||
def check_connection(self) -> dict[str, str]:
|
||||
"""Check Kerberos Connection"""
|
||||
status = {"status": "ok"}
|
||||
if not self.sync_users:
|
||||
return status
|
||||
with Krb5ConfContext(self):
|
||||
try:
|
||||
kadm = self.connection()
|
||||
if kadm is None:
|
||||
status["status"] = "no connection"
|
||||
return status
|
||||
status["principal_exists"] = kadm.principal_exists(self.sync_principal)
|
||||
except kadmin.KAdminError as exc:
|
||||
status["status"] = str(exc)
|
||||
return status
|
||||
|
||||
def get_gssapi_store(self) -> dict[str, str]:
|
||||
"""Get GSSAPI credentials store for this source"""
|
||||
ccache = self.spnego_ccache
|
||||
keytab = None
|
||||
|
||||
if not ccache:
|
||||
ccache_path = self.tempdir / "spnego_ccache"
|
||||
ccache_path.touch(mode=0o600)
|
||||
ccache = f"FILE:{ccache_path}"
|
||||
|
||||
if self.spnego_keytab:
|
||||
# Keytab is of the form type:residual, use as-is
|
||||
if ":" in self.spnego_keytab:
|
||||
keytab = self.spnego_keytab
|
||||
# Parse the keytab and write it in the file
|
||||
else:
|
||||
keytab_path = self.tempdir / "spnego_keytab"
|
||||
keytab_path.touch(mode=0o600)
|
||||
keytab_path.write_bytes(b64decode(self.spnego_keytab))
|
||||
keytab = f"FILE:{keytab_path}"
|
||||
|
||||
store = {"ccache": ccache}
|
||||
if keytab is not None:
|
||||
store["keytab"] = keytab
|
||||
return store
|
||||
|
||||
def get_gssapi_creds(self) -> gssapi.creds.Credentials | None:
|
||||
"""Get GSSAPI credentials for this source"""
|
||||
try:
|
||||
name = None
|
||||
if self.spnego_server_name:
|
||||
# pylint: disable=c-extension-no-member
|
||||
name = gssapi.names.Name(
|
||||
base=self.spnego_server_name,
|
||||
name_type=gssapi.raw.types.NameType.hostbased_service,
|
||||
)
|
||||
return gssapi.creds.Credentials(
|
||||
usage="accept", name=name, store=self.get_gssapi_store()
|
||||
)
|
||||
except gssapi.exceptions.GSSError as exc:
|
||||
LOGGER.warn("GSSAPI credentials failure", exc=exc)
|
||||
return None
|
||||
|
||||
|
||||
class Krb5ConfContext:
|
||||
"""
|
||||
Context manager to set the path to the krb5.conf config file.
|
||||
"""
|
||||
|
||||
def __init__(self, source: KerberosSource):
|
||||
self._source = source
|
||||
self._path = self._source.krb5_conf_path
|
||||
self._previous = None
|
||||
|
||||
def __enter__(self):
|
||||
if not self._path:
|
||||
return
|
||||
self._previous = os.environ.get("KRB5_CONFIG", None)
|
||||
os.environ["KRB5_CONFIG"] = self._path
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
if not self._path:
|
||||
return
|
||||
if self._previous:
|
||||
os.environ["KRB5_CONFIG"] = self._previous
|
||||
else:
|
||||
del os.environ["KRB5_CONFIG"]
|
||||
|
||||
|
||||
class KerberosSourcePropertyMapping(PropertyMapping):
|
||||
"""Map Kerberos Property to User object attribute"""
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-property-mapping-source-kerberos-form"
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.kerberos.api.property_mappings import (
|
||||
KerberosSourcePropertyMappingSerializer,
|
||||
)
|
||||
|
||||
return KerberosSourcePropertyMappingSerializer
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Kerberos Source Property Mapping")
|
||||
verbose_name_plural = _("Kerberos Source Property Mappings")
|
||||
|
||||
|
||||
class UserKerberosSourceConnection(UserSourceConnection):
|
||||
"""Connection to configured Kerberos Sources."""
|
||||
|
||||
identifier = models.TextField()
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.kerberos.api.source_connection import (
|
||||
UserKerberosSourceConnectionSerializer,
|
||||
)
|
||||
|
||||
return UserKerberosSourceConnectionSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User Kerberos Source Connection")
|
||||
verbose_name_plural = _("User Kerberos Source Connections")
|
||||
|
||||
|
||||
class GroupKerberosSourceConnection(GroupSourceConnection):
|
||||
"""Connection to configured Kerberos Sources."""
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.sources.kerberos.api.source_connection import (
|
||||
GroupKerberosSourceConnectionSerializer,
|
||||
)
|
||||
|
||||
return GroupKerberosSourceConnectionSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Group Kerberos Source Connection")
|
||||
verbose_name_plural = _("Group Kerberos Source Connections")
|
18
authentik/sources/kerberos/settings.py
Normal file
18
authentik/sources/kerberos/settings.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""LDAP Settings"""
|
||||
|
||||
from celery.schedules import crontab
|
||||
|
||||
from authentik.lib.utils.time import fqdn_rand
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"sources_kerberos_sync": {
|
||||
"task": "authentik.sources.kerberos.tasks.kerberos_sync_all",
|
||||
"schedule": crontab(minute=fqdn_rand("sources_kerberos_sync"), hour="*/2"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
"sources_kerberos_connectivity_check": {
|
||||
"task": "authentik.sources.kerberos.tasks.kerberos_connectivity_check",
|
||||
"schedule": crontab(minute=fqdn_rand("sources_kerberos_connectivity_check"), hour="*"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
}
|
61
authentik/sources/kerberos/signals.py
Normal file
61
authentik/sources/kerberos/signals.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""authentik kerberos source signals"""
|
||||
|
||||
import kadmin
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from rest_framework.serializers import ValidationError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.signals import password_changed
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.sources.kerberos.models import (
|
||||
KerberosSource,
|
||||
Krb5ConfContext,
|
||||
UserKerberosSourceConnection,
|
||||
)
|
||||
from authentik.sources.kerberos.tasks import kerberos_connectivity_check, kerberos_sync_single
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@receiver(post_save, sender=KerberosSource)
|
||||
def sync_kerberos_source_on_save(sender, instance: KerberosSource, **_):
|
||||
"""Ensure that source is synced on save (if enabled)"""
|
||||
if not instance.enabled or not instance.sync_users:
|
||||
return
|
||||
kerberos_sync_single.delay(instance.pk)
|
||||
kerberos_connectivity_check.delay(instance.pk)
|
||||
|
||||
|
||||
@receiver(password_changed)
|
||||
def kerberos_sync_password(sender, user: User, password: str, **_):
|
||||
"""Connect to kerberos and update password."""
|
||||
user_source_connections = UserKerberosSourceConnection.objects.select_related(
|
||||
"source__kerberossource"
|
||||
).filter(
|
||||
user=user,
|
||||
source__enabled=True,
|
||||
source__kerberossource__sync_users=True,
|
||||
source__kerberossource__sync_users_password=True,
|
||||
)
|
||||
for user_source_connection in user_source_connections:
|
||||
source = user_source_connection.source.kerberossource
|
||||
if source.pk == getattr(sender, "pk", None):
|
||||
continue
|
||||
with Krb5ConfContext(source):
|
||||
try:
|
||||
source.connection().getprinc(user_source_connection.identifier).change_password(
|
||||
password
|
||||
)
|
||||
except kadmin.KAdminError as exc:
|
||||
LOGGER.warning("failed to set Kerberos password", exc=exc, source=source)
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(
|
||||
"Failed to change password in Kerberos source due to remote error: "
|
||||
f"{exc}"
|
||||
),
|
||||
source=source,
|
||||
).set_user(user).save()
|
||||
raise ValidationError("Failed to set password") from exc
|
167
authentik/sources/kerberos/sync.py
Normal file
167
authentik/sources/kerberos/sync.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""Sync Kerberos users into authentik"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import kadmin
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db import IntegrityError, transaction
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.expression.exceptions import (
|
||||
PropertyMappingExpressionException,
|
||||
SkipObjectException,
|
||||
)
|
||||
from authentik.core.models import Group, User, UserTypes
|
||||
from authentik.core.sources.mapper import SourceMapper
|
||||
from authentik.core.sources.matcher import Action, SourceMatcher
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.sources.kerberos.models import (
|
||||
GroupKerberosSourceConnection,
|
||||
KerberosSource,
|
||||
Krb5ConfContext,
|
||||
UserKerberosSourceConnection,
|
||||
)
|
||||
|
||||
|
||||
class KerberosSync:
|
||||
"""Sync Kerberos users into authentik"""
|
||||
|
||||
_source: KerberosSource
|
||||
_logger: BoundLogger
|
||||
_connection: "kadmin.KAdmin"
|
||||
mapper: SourceMapper
|
||||
user_manager: PropertyMappingManager
|
||||
group_manager: PropertyMappingManager
|
||||
matcher: SourceMatcher
|
||||
|
||||
def __init__(self, source: KerberosSource):
|
||||
self._source = source
|
||||
with Krb5ConfContext(self._source):
|
||||
self._connection = self._source.connection()
|
||||
self._messages = []
|
||||
self._logger = get_logger().bind(source=self._source, syncer=self.__class__.__name__)
|
||||
self.mapper = SourceMapper(self._source)
|
||||
self.user_manager = self.mapper.get_manager(User, ["principal"])
|
||||
self.group_manager = self.mapper.get_manager(Group, ["group_id", "principal"])
|
||||
self.matcher = SourceMatcher(
|
||||
self._source, UserKerberosSourceConnection, GroupKerberosSourceConnection
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
"""UI name for the type of object this class synchronizes"""
|
||||
return "users"
|
||||
|
||||
@property
|
||||
def messages(self) -> list[str]:
|
||||
"""Get all UI messages"""
|
||||
return self._messages
|
||||
|
||||
def message(self, *args, **kwargs):
|
||||
"""Add message that is later added to the System Task and shown to the user"""
|
||||
formatted_message = " ".join(args)
|
||||
self._messages.append(formatted_message)
|
||||
self._logger.warning(*args, **kwargs)
|
||||
|
||||
def _handle_principal(self, principal: str) -> bool:
|
||||
try:
|
||||
defaults = self.mapper.build_object_properties(
|
||||
object_type=User,
|
||||
manager=self.user_manager,
|
||||
user=None,
|
||||
request=None,
|
||||
principal=principal,
|
||||
)
|
||||
self._logger.debug("Writing user with attributes", **defaults)
|
||||
if "username" not in defaults:
|
||||
raise IntegrityError("Username was not set by propertymappings")
|
||||
|
||||
action, connection = self.matcher.get_user_action(principal, defaults)
|
||||
self._logger.debug("Action returned", action=action, connection=connection)
|
||||
if action == Action.DENY:
|
||||
return False
|
||||
|
||||
group_properties = {
|
||||
group_id: self.mapper.build_object_properties(
|
||||
object_type=Group,
|
||||
manager=self.group_manager,
|
||||
user=None,
|
||||
request=None,
|
||||
group_id=group_id,
|
||||
principal=principal,
|
||||
)
|
||||
for group_id in defaults.pop("groups", [])
|
||||
}
|
||||
|
||||
if action == Action.ENROLL:
|
||||
user = User.objects.create(**defaults)
|
||||
if user.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
connection.user = user
|
||||
connection.save()
|
||||
elif action in (Action.AUTH, Action.LINK):
|
||||
user = connection.user
|
||||
user.update_attributes(defaults)
|
||||
else:
|
||||
return False
|
||||
|
||||
groups: list[Group] = []
|
||||
for group_id, properties in group_properties.items():
|
||||
group = self._handle_group(group_id, properties)
|
||||
if group:
|
||||
groups.append(group)
|
||||
|
||||
with transaction.atomic():
|
||||
user.ak_groups.remove(
|
||||
*user.ak_groups.filter(groupsourceconnection__source=self._source)
|
||||
)
|
||||
user.ak_groups.add(*groups)
|
||||
|
||||
except PropertyMappingExpressionException as exc:
|
||||
raise StopSync(exc, None, exc.mapping) from exc
|
||||
except SkipObjectException:
|
||||
return False
|
||||
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=(f"Failed to create user: {str(exc)} "),
|
||||
source=self._source,
|
||||
principal=principal,
|
||||
).save()
|
||||
return False
|
||||
self._logger.debug("Synced User", user=user.username)
|
||||
return True
|
||||
|
||||
def _handle_group(
|
||||
self, group_id: str, defaults: dict[str, Any | dict[str, Any]]
|
||||
) -> Group | None:
|
||||
action, connection = self.matcher.get_group_action(group_id, defaults)
|
||||
if action == Action.DENY:
|
||||
return None
|
||||
if action == Action.ENROLL:
|
||||
group = Group.objects.create(**defaults)
|
||||
connection.group = group
|
||||
connection.save()
|
||||
return group
|
||||
if action in (Action.AUTH, Action.LINK):
|
||||
group = connection.group
|
||||
group.update_attributes(defaults)
|
||||
connection.save()
|
||||
return group
|
||||
return None
|
||||
|
||||
def sync(self) -> int:
|
||||
"""Iterate over all Kerberos users and create authentik_core.User instances"""
|
||||
if not self._source.enabled or not self._source.sync_users:
|
||||
self.message("Source is disabled or user syncing is disabled for this Source")
|
||||
return -1
|
||||
|
||||
user_count = 0
|
||||
with Krb5ConfContext(self._source):
|
||||
for principal in self._connection.principals():
|
||||
if self._handle_principal(principal):
|
||||
user_count += 1
|
||||
return user_count
|
68
authentik/sources/kerberos/tasks.py
Normal file
68
authentik/sources/kerberos/tasks.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""Kerberos Sync tasks"""
|
||||
|
||||
from django.core.cache import cache
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import SystemTask as DBSystemTask
|
||||
from authentik.events.models import TaskStatus
|
||||
from authentik.events.system_tasks import SystemTask
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.sources.kerberos.models import KerberosSource
|
||||
from authentik.sources.kerberos.sync import KerberosSync
|
||||
|
||||
LOGGER = get_logger()
|
||||
CACHE_KEY_STATUS = "goauthentik.io/sources/kerberos/status/"
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def kerberos_sync_all():
|
||||
"""Sync all sources"""
|
||||
for source in KerberosSource.objects.filter(enabled=True, sync_users=True):
|
||||
kerberos_sync_single.delay(str(source.pk))
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def kerberos_connectivity_check(pk: str | None = None):
|
||||
"""Check connectivity for Kerberos Sources"""
|
||||
# 2 hour timeout, this task should run every hour
|
||||
timeout = 60 * 60 * 2
|
||||
sources = KerberosSource.objects.filter(enabled=True)
|
||||
if pk:
|
||||
sources = sources.filter(pk=pk)
|
||||
for source in sources:
|
||||
status = source.check_connection()
|
||||
cache.set(CACHE_KEY_STATUS + source.slug, status, timeout=timeout)
|
||||
|
||||
|
||||
@CELERY_APP.task(
|
||||
bind=True,
|
||||
base=SystemTask,
|
||||
# We take the configured hours timeout time by 2.5 as we run user and
|
||||
# group in parallel and then membership, so 2x is to cover the serial tasks,
|
||||
# and 0.5x on top of that to give some more leeway
|
||||
soft_time_limit=(60 * 60 * CONFIG.get_int("sources.kerberos.task_timeout_hours")) * 2.5,
|
||||
task_time_limit=(60 * 60 * CONFIG.get_int("sources.kerberos.task_timeout_hours")) * 2.5,
|
||||
)
|
||||
def kerberos_sync_single(self, source_pk: str):
|
||||
"""Sync a single source"""
|
||||
source: KerberosSource = KerberosSource.objects.filter(pk=source_pk).first()
|
||||
if not source or not source.enabled:
|
||||
return
|
||||
try:
|
||||
with source.sync_lock as lock_acquired:
|
||||
if not lock_acquired:
|
||||
LOGGER.debug(
|
||||
"Failed to acquire lock for Kerberos sync, skipping task", source=source.slug
|
||||
)
|
||||
return
|
||||
# Delete all sync tasks from the cache
|
||||
DBSystemTask.objects.filter(name="kerberos_sync", uid__startswith=source.slug).delete()
|
||||
syncer = KerberosSync(source)
|
||||
syncer.sync()
|
||||
self.set_status(TaskStatus.SUCCESSFUL, *syncer.messages)
|
||||
except StopSync as exc:
|
||||
LOGGER.warning(exception_to_string(exc))
|
||||
self.set_error(exc)
|
0
authentik/sources/kerberos/tests/__init__.py
Normal file
0
authentik/sources/kerberos/tests/__init__.py
Normal file
57
authentik/sources/kerberos/tests/test_auth.py
Normal file
57
authentik/sources/kerberos/tests/test_auth.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Kerberos Source Auth tests"""
|
||||
|
||||
from django.contrib.auth.hashers import is_password_usable
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.kerberos.auth import KerberosBackend
|
||||
from authentik.sources.kerberos.models import KerberosSource, UserKerberosSourceConnection
|
||||
from authentik.sources.kerberos.tests.utils import KerberosTestCase
|
||||
|
||||
|
||||
class TestKerberosAuth(KerberosTestCase):
|
||||
"""Kerberos Auth tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.source = KerberosSource.objects.create(
|
||||
name="kerberos",
|
||||
slug="kerberos",
|
||||
realm=self.realm.realm,
|
||||
sync_users=False,
|
||||
sync_users_password=False,
|
||||
password_login_update_internal_password=True,
|
||||
)
|
||||
self.user = User.objects.create(username=generate_id())
|
||||
self.user.set_unusable_password()
|
||||
UserKerberosSourceConnection.objects.create(
|
||||
source=self.source, user=self.user, identifier=self.realm.user_princ
|
||||
)
|
||||
|
||||
def test_auth_username(self):
|
||||
"""Test auth username"""
|
||||
backend = KerberosBackend()
|
||||
self.assertEqual(
|
||||
backend.authenticate(
|
||||
None, username=self.user.username, password=self.realm.password("user")
|
||||
),
|
||||
self.user,
|
||||
)
|
||||
|
||||
def test_auth_principal(self):
|
||||
"""Test auth principal"""
|
||||
backend = KerberosBackend()
|
||||
self.assertEqual(
|
||||
backend.authenticate(
|
||||
None, username=self.realm.user_princ, password=self.realm.password("user")
|
||||
),
|
||||
self.user,
|
||||
)
|
||||
|
||||
def test_internal_password_update(self):
|
||||
"""Test internal password update"""
|
||||
backend = KerberosBackend()
|
||||
backend.authenticate(
|
||||
None, username=self.realm.user_princ, password=self.realm.password("user")
|
||||
)
|
||||
self.user.refresh_from_db()
|
||||
self.assertTrue(is_password_usable(self.user.password))
|
78
authentik/sources/kerberos/tests/test_spnego.py
Normal file
78
authentik/sources/kerberos/tests/test_spnego.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Kerberos Source SPNEGO tests"""
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
from pathlib import Path
|
||||
|
||||
import gssapi
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.sources.kerberos.models import KerberosSource
|
||||
from authentik.sources.kerberos.tests.utils import KerberosTestCase
|
||||
|
||||
|
||||
class TestSPNEGOSource(KerberosTestCase):
|
||||
"""Kerberos Source SPNEGO tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.source = KerberosSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
spnego_keytab=b64encode(Path(self.realm.http_keytab).read_bytes()).decode(),
|
||||
)
|
||||
# Force store creation early
|
||||
self.source.get_gssapi_store()
|
||||
|
||||
def test_api_read(self):
|
||||
"""Test reading a source"""
|
||||
self.client.force_login(create_test_admin_user())
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:kerberossource-detail",
|
||||
kwargs={
|
||||
"slug": self.source.slug,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_source_login(self):
|
||||
"""test login view"""
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_sources_kerberos:spnego-login",
|
||||
kwargs={"source_slug": self.source.slug},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
endpoint = response.headers["Location"]
|
||||
|
||||
response = self.client.get(endpoint)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertEqual(response.headers["WWW-Authenticate"], "Negotiate")
|
||||
|
||||
server_name = gssapi.names.Name("HTTP/testserver@")
|
||||
client_creds = gssapi.creds.Credentials(
|
||||
usage="initiate", store={"ccache": self.realm.ccache}
|
||||
)
|
||||
client_ctx = gssapi.sec_contexts.SecurityContext(
|
||||
name=server_name, usage="initiate", creds=client_creds
|
||||
)
|
||||
|
||||
status = 401
|
||||
server_token = None
|
||||
while status == 401 and not client_ctx.complete: # noqa: PLR2004
|
||||
client_token = client_ctx.step(server_token)
|
||||
if not client_token:
|
||||
break
|
||||
response = self.client.get(
|
||||
endpoint,
|
||||
headers={"Authorization": f"Negotiate {b64encode(client_token).decode('ascii')}"},
|
||||
)
|
||||
status = response.status_code
|
||||
if status == 401: # noqa: PLR2004
|
||||
server_token = b64decode(response.headers["WWW-Authenticate"][9:].strip())
|
||||
|
||||
# 400 because no enroll flow
|
||||
self.assertEqual(status, 400)
|
75
authentik/sources/kerberos/tests/test_sync.py
Normal file
75
authentik/sources/kerberos/tests/test_sync.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""Kerberos Source sync tests"""
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.kerberos.models import KerberosSource, KerberosSourcePropertyMapping
|
||||
from authentik.sources.kerberos.sync import KerberosSync
|
||||
from authentik.sources.kerberos.tasks import kerberos_sync_all
|
||||
from authentik.sources.kerberos.tests.utils import KerberosTestCase
|
||||
|
||||
|
||||
class TestKerberosSync(KerberosTestCase):
|
||||
"""Kerberos Sync tests"""
|
||||
|
||||
@apply_blueprint("system/sources-kerberos.yaml")
|
||||
def setUp(self):
|
||||
self.source: KerberosSource = KerberosSource.objects.create(
|
||||
name="kerberos",
|
||||
slug="kerberos",
|
||||
realm=self.realm.realm,
|
||||
sync_users=True,
|
||||
sync_users_password=True,
|
||||
sync_principal=self.realm.admin_princ,
|
||||
sync_password=self.realm.password("admin"),
|
||||
)
|
||||
self.source.user_property_mappings.set(
|
||||
KerberosSourcePropertyMapping.objects.filter(
|
||||
managed__startswith="goauthentik.io/sources/kerberos/user/default/"
|
||||
)
|
||||
)
|
||||
|
||||
def test_default_mappings(self):
|
||||
"""Test default mappings"""
|
||||
KerberosSync(self.source).sync()
|
||||
|
||||
self.assertTrue(
|
||||
User.objects.filter(username=self.realm.user_princ.rsplit("@", 1)[0]).exists()
|
||||
)
|
||||
self.assertFalse(
|
||||
User.objects.filter(username=self.realm.nfs_princ.rsplit("@", 1)[0]).exists()
|
||||
)
|
||||
|
||||
def test_sync_mapping(self):
|
||||
"""Test property mappings"""
|
||||
noop = KerberosSourcePropertyMapping.objects.create(
|
||||
name=generate_id(), expression="return {}"
|
||||
)
|
||||
email = KerberosSourcePropertyMapping.objects.create(
|
||||
name=generate_id(), expression='return {"email": principal.lower()}'
|
||||
)
|
||||
dont_sync_service = KerberosSourcePropertyMapping.objects.create(
|
||||
name=generate_id(),
|
||||
expression='if "/" in principal:\n return {"username": None}\nreturn {}',
|
||||
)
|
||||
self.source.user_property_mappings.set([noop, email, dont_sync_service])
|
||||
|
||||
KerberosSync(self.source).sync()
|
||||
|
||||
self.assertTrue(
|
||||
User.objects.filter(username=self.realm.user_princ.rsplit("@", 1)[0]).exists()
|
||||
)
|
||||
self.assertEqual(
|
||||
User.objects.get(username=self.realm.user_princ.rsplit("@", 1)[0]).email,
|
||||
self.realm.user_princ.lower(),
|
||||
)
|
||||
self.assertFalse(
|
||||
User.objects.filter(username=self.realm.nfs_princ.rsplit("@", 1)[0]).exists()
|
||||
)
|
||||
|
||||
def test_tasks(self):
|
||||
"""Test Scheduled tasks"""
|
||||
kerberos_sync_all.delay().get()
|
||||
self.assertTrue(
|
||||
User.objects.filter(username=self.realm.user_princ.rsplit("@", 1)[0]).exists()
|
||||
)
|
40
authentik/sources/kerberos/tests/utils.py
Normal file
40
authentik/sources/kerberos/tests/utils.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""Kerberos Source test utils"""
|
||||
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from time import sleep
|
||||
|
||||
from k5test import realm
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
|
||||
class KerberosTestCase(APITestCase):
|
||||
"""Kerberos Test Case"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.realm = realm.K5Realm(start_kadmind=True)
|
||||
|
||||
cls.realm.http_princ = f"HTTP/testserver@{cls.realm.realm}"
|
||||
cls.realm.http_keytab = os.path.join(cls.realm.tmpdir, "http_keytab")
|
||||
cls.realm.addprinc(cls.realm.http_princ)
|
||||
cls.realm.extract_keytab(cls.realm.http_princ, cls.realm.http_keytab)
|
||||
|
||||
cls._saved_env = deepcopy(os.environ)
|
||||
for k, v in cls.realm.env.items():
|
||||
os.environ[k] = v
|
||||
# Wait for everything to start correctly
|
||||
# Otherwise leads to flaky tests
|
||||
sleep(5)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.realm.stop()
|
||||
del cls.realm
|
||||
|
||||
for k in deepcopy(os.environ):
|
||||
if k in cls._saved_env:
|
||||
os.environ[k] = cls._saved_env[k]
|
||||
else:
|
||||
del os.environ[k]
|
||||
cls._saved_env = None
|
22
authentik/sources/kerberos/urls.py
Normal file
22
authentik/sources/kerberos/urls.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Kerberos Source urls"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from authentik.sources.kerberos.api.property_mappings import KerberosSourcePropertyMappingViewSet
|
||||
from authentik.sources.kerberos.api.source import KerberosSourceViewSet
|
||||
from authentik.sources.kerberos.api.source_connection import (
|
||||
GroupKerberosSourceConnectionViewSet,
|
||||
UserKerberosSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.sources.kerberos.views import SPNEGOView
|
||||
|
||||
urlpatterns = [
|
||||
path("<slug:source_slug>/", SPNEGOView.as_view(), name="spnego-login"),
|
||||
]
|
||||
|
||||
api_urlpatterns = [
|
||||
("propertymappings/source/kerberos", KerberosSourcePropertyMappingViewSet),
|
||||
("sources/user_connections/kerberos", UserKerberosSourceConnectionViewSet),
|
||||
("sources/group_connections/kerberos", GroupKerberosSourceConnectionViewSet),
|
||||
("sources/kerberos", KerberosSourceViewSet),
|
||||
]
|
181
authentik/sources/kerberos/views.py
Normal file
181
authentik/sources/kerberos/views.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""Kerberos source SPNEGO views"""
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
import gssapi
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.sources.kerberos.models import (
|
||||
GroupKerberosSourceConnection,
|
||||
KerberosSource,
|
||||
Krb5ConfContext,
|
||||
UserKerberosSourceConnection,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
SPNEGO_REQUEST_STATUS = 401
|
||||
WWW_AUTHENTICATE = "WWW-Authenticate"
|
||||
HTTP_AUTHORIZATION = "Authorization"
|
||||
NEGOTIATE = "Negotiate"
|
||||
|
||||
SPNEGO_STATE_CACHE_PREFIX = "goauthentik.io/sources/spnego"
|
||||
SPNEGO_STATE_CACHE_TIMEOUT = 60 * 5 # 5 minutes
|
||||
|
||||
|
||||
def add_negotiate_to_response(
|
||||
response: HttpResponse, token: str | bytes | None = None
|
||||
) -> HttpResponse:
|
||||
if isinstance(token, str):
|
||||
token = token.encode()
|
||||
response[WWW_AUTHENTICATE] = (
|
||||
NEGOTIATE if token is None else f"{NEGOTIATE} {b64encode(token).decode('ascii')}"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class SPNEGOView(View):
|
||||
"""SPNEGO login"""
|
||||
|
||||
source: KerberosSource
|
||||
|
||||
def challenge(self, request, token: str | bytes | None = None) -> HttpResponse:
|
||||
"""Get SNPEGO challenge response"""
|
||||
response = render(
|
||||
request,
|
||||
"if/error.html",
|
||||
context={
|
||||
"title": _("SPNEGO authentication required"),
|
||||
"message": _(
|
||||
"""
|
||||
Make sure you have valid tickets (obtainable via kinit)
|
||||
and configured the browser correctly.
|
||||
Please contact your administrator.
|
||||
"""
|
||||
),
|
||||
},
|
||||
status=401,
|
||||
)
|
||||
return add_negotiate_to_response(response, token)
|
||||
|
||||
def get_authstr(self, request) -> str | None:
|
||||
"""Get SPNEGO authentication string from headers"""
|
||||
authorization_header = request.headers.get(HTTP_AUTHORIZATION, "")
|
||||
if NEGOTIATE.lower() not in authorization_header.lower():
|
||||
return None
|
||||
|
||||
auth_tuple = authorization_header.split(" ", 1)
|
||||
if not auth_tuple or auth_tuple[0].lower() != NEGOTIATE.lower():
|
||||
return None
|
||||
if len(auth_tuple) != 2: # noqa: PLR2004
|
||||
raise SuspiciousOperation("Malformed authorization header")
|
||||
return auth_tuple[1]
|
||||
|
||||
def new_state(self) -> str:
|
||||
"""Generate request state"""
|
||||
return get_random_string(32)
|
||||
|
||||
def get_server_ctx(self, key: str) -> gssapi.sec_contexts.SecurityContext | None:
|
||||
"""Get GSSAPI server context from cache or create it"""
|
||||
server_creds = self.source.get_gssapi_creds()
|
||||
if server_creds is None:
|
||||
return None
|
||||
|
||||
state = cache.get(f"{SPNEGO_STATE_CACHE_PREFIX}/{key}", None)
|
||||
|
||||
if state:
|
||||
# pylint: disable=c-extension-no-member
|
||||
return gssapi.sec_contexts.SecurityContext(
|
||||
base=gssapi.raw.sec_contexts.import_sec_context(state),
|
||||
)
|
||||
|
||||
return gssapi.sec_contexts.SecurityContext(creds=server_creds, usage="accept")
|
||||
|
||||
def set_server_ctx(self, key: str, ctx: gssapi.sec_contexts.SecurityContext):
|
||||
"""Store the GSSAPI server context in cache"""
|
||||
cache.set(f"{SPNEGO_STATE_CACHE_PREFIX}/{key}", ctx.export(), SPNEGO_STATE_CACHE_TIMEOUT)
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def dispatch(self, request, *args, **kwargs) -> HttpResponse:
|
||||
"""Process SPNEGO request"""
|
||||
self.source: KerberosSource = get_object_or_404(
|
||||
KerberosSource,
|
||||
slug=kwargs.get("source_slug", ""),
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
qstring = request.GET if request.method == "GET" else request.POST
|
||||
state = qstring.get("state", None)
|
||||
if not state:
|
||||
return redirect(
|
||||
reverse(
|
||||
"authentik_sources_kerberos:spnego-login",
|
||||
kwargs={"source_slug": self.source.slug},
|
||||
)
|
||||
+ f"?state={self.new_state()}"
|
||||
)
|
||||
|
||||
authstr = self.get_authstr(request)
|
||||
if not authstr:
|
||||
LOGGER.debug("authstr not present, sending challenge")
|
||||
return self.challenge(request)
|
||||
|
||||
try:
|
||||
in_token = b64decode(authstr)
|
||||
except (TypeError, ValueError):
|
||||
return self.challenge(request)
|
||||
|
||||
with Krb5ConfContext(self.source):
|
||||
server_ctx = self.get_server_ctx(state)
|
||||
if not server_ctx:
|
||||
return self.challenge(request)
|
||||
|
||||
try:
|
||||
out_token = server_ctx.step(in_token)
|
||||
except gssapi.exceptions.GSSError as exc:
|
||||
LOGGER.debug("GSSAPI security context failure", exc=exc)
|
||||
return self.challenge(request)
|
||||
|
||||
if not server_ctx.complete or server_ctx.initiator_name is None:
|
||||
self.set_server_ctx(state, server_ctx)
|
||||
return self.challenge(request, out_token)
|
||||
|
||||
def name_to_str(n: gssapi.names.Name) -> str:
|
||||
return n.display_as(n.name_type)
|
||||
|
||||
identifier = name_to_str(server_ctx.initiator_name)
|
||||
context = {
|
||||
"spnego_info": {
|
||||
"initiator_name": name_to_str(server_ctx.initiator_name),
|
||||
"target_name": name_to_str(server_ctx.target_name),
|
||||
"mech": str(server_ctx.mech),
|
||||
"actual_flags": server_ctx.actual_flags,
|
||||
},
|
||||
}
|
||||
|
||||
response = SPNEGOSourceFlowManager(
|
||||
source=self.source,
|
||||
request=request,
|
||||
identifier=identifier,
|
||||
user_info={
|
||||
"principal": identifier,
|
||||
**context,
|
||||
},
|
||||
policy_context=context,
|
||||
).get_flow()
|
||||
return add_negotiate_to_response(response, out_token)
|
||||
|
||||
|
||||
class SPNEGOSourceFlowManager(SourceFlowManager):
|
||||
"""Flow manager for Kerberos SPNEGO sources"""
|
||||
|
||||
user_connection_type = UserKerberosSourceConnection
|
||||
group_connection_type = GroupKerberosSourceConnection
|
@ -43,7 +43,7 @@ class LDAPBackend(InbuiltBackend):
|
||||
if source.password_login_update_internal_password:
|
||||
# Password given successfully binds to LDAP, so we save it in our Database
|
||||
LOGGER.debug("Updating user's password in DB", user=user)
|
||||
user.set_password(password, signal=False)
|
||||
user.set_password(password, sender=source)
|
||||
user.save()
|
||||
return user
|
||||
# Password doesn't match
|
||||
|
@ -62,6 +62,8 @@ def ldap_sync_password(sender, user: User, password: str, **_):
|
||||
if not sources.exists():
|
||||
return
|
||||
source = sources.first()
|
||||
if source.pk == getattr(sender, "pk", None):
|
||||
return
|
||||
if not LDAPPasswordChanger.should_check_user(user):
|
||||
return
|
||||
try:
|
||||
|
@ -9,7 +9,6 @@ from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, ChoiceField, IntegerField
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
@ -197,7 +196,6 @@ class DuoDeviceViewSet(
|
||||
class DuoAdminDeviceViewSet(ModelViewSet):
|
||||
"""Viewset for Duo authenticator devices (for admins)"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
queryset = DuoDevice.objects.all()
|
||||
serializer_class = DuoDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
|
@ -3,7 +3,6 @@
|
||||
from django_filters.rest_framework.backends import DjangoFilterBackend
|
||||
from rest_framework import mixins
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
@ -76,7 +75,6 @@ class SMSDeviceViewSet(
|
||||
class SMSAdminDeviceViewSet(ModelViewSet):
|
||||
"""Viewset for sms authenticator devices (for admins)"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
queryset = SMSDevice.objects.all()
|
||||
serializer_class = SMSDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
|
@ -3,7 +3,6 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import mixins
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
@ -80,7 +79,6 @@ class StaticDeviceViewSet(
|
||||
class StaticAdminDeviceViewSet(ModelViewSet):
|
||||
"""Viewset for static authenticator devices (for admins)"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
queryset = StaticDevice.objects.all()
|
||||
serializer_class = StaticDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
|
@ -4,7 +4,6 @@ from django_filters.rest_framework.backends import DjangoFilterBackend
|
||||
from rest_framework import mixins
|
||||
from rest_framework.fields import ChoiceField
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
@ -72,7 +71,6 @@ class TOTPDeviceViewSet(
|
||||
class TOTPAdminDeviceViewSet(ModelViewSet):
|
||||
"""Viewset for totp authenticator devices (for admins)"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
queryset = TOTPDevice.objects.all()
|
||||
serializer_class = TOTPDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
|
@ -3,7 +3,6 @@
|
||||
from django_filters.rest_framework.backends import DjangoFilterBackend
|
||||
from rest_framework import mixins
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
@ -48,7 +47,6 @@ class WebAuthnDeviceViewSet(
|
||||
class WebAuthnAdminDeviceViewSet(ModelViewSet):
|
||||
"""Viewset for WebAuthn authenticator devices (for admins)"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
queryset = WebAuthnDevice.objects.all()
|
||||
serializer_class = WebAuthnDeviceSerializer
|
||||
search_fields = ["name"]
|
||||
|
@ -126,5 +126,20 @@
|
||||
"name": "iPasswords",
|
||||
"icon_dark":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEwODAgMTA4MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjI7Ij4KICAgIDxnIHRyYW5zZm9ybT0ibWF0cml4KDAuOTgzODI3LDAsMCwwLjk4MzgyNywxMy42NjEsLTAuMjg0Njk5KSI+CiAgICAgICAgPHBhdGggZD0iTTcyMy4yNTMsNDkzLjcwNEM3NTYuMjg4LDQ5NS4yMiA3ODIuNjQ0LDUyMi41MiA3ODIuNjQ0LDU1NS45MjdMNzgyLjY0NCw4MzQuMzA0Qzc4Mi42NDQsODY4LjY4MyA3NTQuNzMzLDg5Ni41OTMgNzIwLjM1NSw4OTYuNTkzTDM1MS4zOTMsODk2LjU5M0MzMTcuMDE1LDg5Ni41OTMgMjg5LjEwNCw4NjguNjgzIDI4OS4xMDQsODM0LjMwNEwyODkuMTA0LDU1NS45MjdDMjg5LjEwNCw1MjMuMTE3IDMxNC41MjYsNDk2LjE5OCAzNDYuNzMsNDkzLjgxTDM0Ni43MywzOTAuMTE5QzM0Ni43MywyODYuMjE1IDQzMS4wODgsMjAxLjg1OCA1MzQuOTkyLDIwMS44NThDNjM4Ljg5NiwyMDEuODU4IDcyMy4yNTMsMjg2LjIxNSA3MjMuMjUzLDM5MC4xMTlMNzIzLjI1Myw0OTMuNzA0Wk00MzMuMzI4LDQ5MC45NjZMNjM2LjY4Niw0OTIuMTE2QzYzNi42ODYsNDkyLjExNiA2MzcuMTQyLDM4NS4xMzIgNjM3LjA4NiwzODQuNDg5QzYzMS44NDksMzI0LjE2MyA1NzkuMTE4LDI4OC4wNzkgNTM0Ljk5MiwyODguNTM1QzQ5My4wMTcsMjg4Ljk2OSA0MzUuOTIsMzE4LjEgNDMzLjY1NiwzODMuNzk3QzQzMy40NzYsMzg5LjAxNyA0MzMuMzI4LDQ5MC45NjYgNDMzLjMyOCw0OTAuOTY2Wk01MDMuMjk2LDcxNC4zODJMNDkyLjQzMyw3ODQuNDU1QzQ5Mi40MzMsNzg0LjQ1NSA0OTAuMzc2LDc5OC4yODQgNDkyLjQxNiw4MDIuNjY0QzQ5Ni43OCw4MTIuMDMxIDUwMy44MjIsODExLjExMSA1MDMuODIyLDgxMS4xMTFMNTY1LjYsODEwLjgzOEM1NjUuNiw4MTAuODM4IDU3Mi44NjMsODExLjQ3MiA1NzcuNjM3LDgwMi4xNTVDNTc5Ljg4LDc5Ny43NzUgNTc3LjMyMyw3ODMuNzQgNTc3LjMyMyw3ODMuNzRMNTY2LjY0OSw3MTQuNDAxQzU5MC43NDMsNzAyLjY0OSA2MDcuMzU5LDY3Ny45MDggNjA3LjM1OSw2NDkuMzE4QzYwNy4zNTksNjA5LjM3NyA1NzQuOTMyLDU3Ni45NSA1MzQuOTkyLDU3Ni45NUM0OTUuMDUxLDU3Ni45NSA0NjIuNjI0LDYwOS4zNzcgNDYyLjYyNCw2NDkuMzE4QzQ2Mi42MjQsNjc3Ljg5MyA0NzkuMjIzLDcwMi42MjIgNTAzLjI5Niw3MTQuMzgyWiIgc3R5bGU9ImZpbGw6cmdiKDAsMTUyLDI0OCk7Ii8+CiAgICA8L2c+Cjwvc3ZnPgo=",
|
||||
"icon_light":"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEwODAgMTA4MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjI7Ij4KICAgIDxnIHRyYW5zZm9ybT0ibWF0cml4KDAuOTgzODI3LDAsMCwwLjk4MzgyNywxMy42NjEsLTAuMjg0Njk5KSI+CiAgICAgICAgPHBhdGggZD0iTTcyMy4yNTMsNDkzLjcwNEM3NTYuMjg4LDQ5NS4yMiA3ODIuNjQ0LDUyMi41MiA3ODIuNjQ0LDU1NS45MjdMNzgyLjY0NCw4MzQuMzA0Qzc4Mi42NDQsODY4LjY4MyA3NTQuNzMzLDg5Ni41OTMgNzIwLjM1NSw4OTYuNTkzTDM1MS4zOTMsODk2LjU5M0MzMTcuMDE1LDg5Ni41OTMgMjg5LjEwNCw4NjguNjgzIDI4OS4xMDQsODM0LjMwNEwyODkuMTA0LDU1NS45MjdDMjg5LjEwNCw1MjMuMTE3IDMxNC41MjYsNDk2LjE5OCAzNDYuNzMsNDkzLjgxTDM0Ni43MywzOTAuMTE5QzM0Ni43MywyODYuMjE1IDQzMS4wODgsMjAxLjg1OCA1MzQuOTkyLDIwMS44NThDNjM4Ljg5NiwyMDEuODU4IDcyMy4yNTMsMjg2LjIxNSA3MjMuMjUzLDM5MC4xMTlMNzIzLjI1Myw0OTMuNzA0Wk00MzMuMzI4LDQ5MC45NjZMNjM2LjY4Niw0OTIuMTE2QzYzNi42ODYsNDkyLjExNiA2MzcuMTQyLDM4NS4xMzIgNjM3LjA4NiwzODQuNDg5QzYzMS44NDksMzI0LjE2MyA1NzkuMTE4LDI4OC4wNzkgNTM0Ljk5MiwyODguNTM1QzQ5My4wMTcsMjg4Ljk2OSA0MzUuOTIsMzE4LjEgNDMzLjY1NiwzODMuNzk3QzQzMy40NzYsMzg5LjAxNyA0MzMuMzI4LDQ5MC45NjYgNDMzLjMyOCw0OTAuOTY2Wk01MDMuMjk2LDcxNC4zODJMNDkyLjQzMyw3ODQuNDU1QzQ5Mi40MzMsNzg0LjQ1NSA0OTAuMzc2LDc5OC4yODQgNDkyLjQxNiw4MDIuNjY0QzQ5Ni43OCw4MTIuMDMxIDUwMy44MjIsODExLjExMSA1MDMuODIyLDgxMS4xMTFMNTY1LjYsODEwLjgzOEM1NjUuNiw4MTAuODM4IDU3Mi44NjMsODExLjQ3MiA1NzcuNjM3LDgwMi4xNTVDNTc5Ljg4LDc5Ny43NzUgNTc3LjMyMyw3ODMuNzQgNTc3LjMyMyw3ODMuNzRMNTY2LjY0OSw3MTQuNDAxQzU5MC43NDMsNzAyLjY0OSA2MDcuMzU5LDY3Ny45MDggNjA3LjM1OSw2NDkuMzE4QzYwNy4zNTksNjA5LjM3NyA1NzQuOTMyLDU3Ni45NSA1MzQuOTkyLDU3Ni45NUM0OTUuMDUxLDU3Ni45NSA0NjIuNjI0LDYwOS4zNzcgNDYyLjYyNCw2NDkuMzE4QzQ2Mi42MjQsNjc3Ljg5MyA0NzkuMjIzLDcwMi42MjIgNTAzLjI5Niw3MTQuMzgyWiIgc3R5bGU9ImZpbGw6cmdiKDAsMTUyLDI0OCk7Ii8+CiAgICA8L2c+Cjwvc3ZnPgo="
|
||||
}
|
||||
},
|
||||
"b35a26b2-8f6e-4697-ab1d-d44db4da28c6":{
|
||||
"name": "Zoho Vault",
|
||||
"icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMzIgMzIiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICMyMjZlYjM7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQoKICAgICAgLmNscy0zIHsKICAgICAgICBvcGFjaXR5OiAuOTsKICAgICAgfQoKICAgICAgLmNscy00IHsKICAgICAgICBmaWxsOiAjZTQyNTI4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yNi4xLDMuNjJINS45Yy0xLjI0LDAtMi4yNSwxLjAxLTIuMjUsMi4yNXYxNi45NGMwLDEuMjQsMS4wMSwyLjI1LDIuMjUsMi4yNWguMWMuMy4wNy42Ny4yOS42Ny45MXYyLjExYzAsLjE1LjEyLjI3LjI3LjI3aDIuNzVjLjE1LDAsLjI3LS4xMi4yNy0uMjd2LTEuMjNzLjA3LTEuNTYsMS43NS0xLjc5aDguNThjMS42OC4yMywxLjc1LDEuNzksMS43NSwxLjc5djEuMjNjMCwuMTUuMTIuMjcuMjcuMjdoMi43NWMuMTUsMCwuMjctLjEyLjI3LS4yN3YtMi4xMWMwLS42Mi4zNy0uODMuNjctLjkxaC4wOWMxLjI0LDAsMi4yNS0xLjAxLDIuMjUtMi4yNVY1Ljg3YzAtMS4yNC0xLjAxLTIuMjUtMi4yNS0yLjI1WiIvPgogIDxnPgogICAgPGcgY2xhc3M9ImNscy0zIj4KICAgICAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjUuMDYsMzBoLTIuNzVjLTEuMDYsMC0xLjkyLS44Ni0xLjkyLTEuOTJ2LTEuMTNjMC0uMTUtLjEyLS4yNy0uMjctLjI3aC04LjI0Yy0uMTUsMC0uMjcuMTItLjI3LjI3djEuMTNjMCwxLjA2LS44NiwxLjkyLTEuOTIsMS45MmgtMi43NWMtMS4wNiwwLTEuOTItLjg2LTEuOTItMS45MnYtMS40OWMtMS43Mi0uMzgtMy4wMi0xLjkyLTMuMDItMy43NlY1Ljg0YzAtMi4xMiwxLjcyLTMuODQsMy44NC0zLjg0aDIwLjMxYzIuMTIsMCwzLjg0LDEuNzIsMy44NCwzLjg0djE2Ljk5YzAsMS44NC0xLjMsMy4zOC0zLjAyLDMuNzZ2MS40OWMwLDEuMDYtLjg2LDEuOTItMS45MiwxLjkyWk0xMS44OCwyNS4wM2g4LjI0YzEuMDYsMCwxLjkyLjg2LDEuOTIsMS45MnYxLjEzYzAsLjE1LjEyLjI3LjI3LjI3aDIuNzVjLjE1LDAsLjI3LS4xMi4yNy0uMjd2LTIuMjJjMC0uNDYuMzctLjgyLjgyLS44MiwxLjIxLDAsMi4yLS45OSwyLjItMi4yVjUuODRjMC0xLjIxLS45OS0yLjItMi4yLTIuMkg1Ljg0Yy0xLjIxLDAtMi4yLjk5LTIuMiwyLjJ2MTYuOTljMCwxLjIxLjk5LDIuMiwyLjIsMi4yLjQ2LDAsLjgyLjM3LjgyLjgydjIuMjJjMCwuMTUuMTIuMjcuMjcuMjdoMi43NWMuMTUsMCwuMjctLjEyLjI3LS4yN3YtMS4xM2MwLTEuMDYuODYtMS45MiwxLjkyLTEuOTJaIi8+CiAgICA8L2c+CiAgICA8ZyBjbGFzcz0iY2xzLTMiPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTQiIGQ9Ik0xMC43NywxOS4yNWMtLjE3LDAtLjM0LS4wNS0uNDgtLjE2LS4zNy0uMjctLjQ1LS43OC0uMTgtMS4xNWwyLjY3LTMuNjhjLjI3LS4zNy43OC0uNDUsMS4xNS0uMTguMzcuMjcuNDUuNzguMTgsMS4xNWwtMi42NywzLjY4Yy0uMTYuMjItLjQxLjM0LS42Ny4zNFoiLz4KICAgIDwvZz4KICAgIDxnIGNsYXNzPSJjbHMtMyI+CiAgICAgIDxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTE2LjEyLDE5LjI1Yy0uMjYsMC0uNTEtLjEyLS42Ny0uMzRsLTIuNjctMy42OGMtLjI3LS4zNy0uMTktLjg4LjE4LTEuMTUuMzctLjI3Ljg4LS4xOSwxLjE1LjE4bDIuNjcsMy42OGMuMjcuMzcuMTkuODgtLjE4LDEuMTUtLjE1LjExLS4zMi4xNi0uNDguMTZaIi8+CiAgICA8L2c+CiAgICA8ZyBjbGFzcz0iY2xzLTMiPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTQiIGQ9Ik0xMy40NCwxNS41N2MtLjQ2LDAtLjgyLS4zNy0uODItLjgydi00LjUxYzAtLjQ2LjM3LS44Mi44Mi0uODJzLjgyLjM3LjgyLjgydjQuNTFjMCwuNDYtLjM3LjgyLS44Mi44MloiLz4KICAgIDwvZz4KICAgIDxnIGNsYXNzPSJjbHMtMyI+CiAgICAgIDxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTEzLjQ0LDE1LjU4Yy0uMzUsMC0uNjctLjIyLS43OC0uNTctLjE0LS40My4xLS45LjUzLTEuMDRsNC4zMi0xLjM5Yy40My0uMTQuOS4xLDEuMDQuNTMuMTQuNDMtLjEuOS0uNTMsMS4wNGwtNC4zMiwxLjM5Yy0uMDkuMDMtLjE3LjA0LS4yNi4wNFoiLz4KICAgIDwvZz4KICAgIDxnIGNsYXNzPSJjbHMtMyI+CiAgICAgIDxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTEzLjQ0LDE1LjU4Yy0uMDksMC0uMTctLjAxLS4yNS0uMDRsLTQuMzItMS4zOWMtLjQzLS4xNC0uNjctLjYtLjUzLTEuMDQuMTQtLjQzLjYtLjY3LDEuMDQtLjUzbDQuMzIsMS4zOWMuNDMuMTQuNjcuNi41MywxLjA0LS4xMS4zNS0uNDMuNTctLjc4LjU3WiIvPgogICAgPC9nPgogICAgPGcgY2xhc3M9ImNscy0zIj4KICAgICAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjIuODUsMTkuMjNjLS40NiwwLS44Mi0uMzctLjgyLS44MnYtOC4xNWMwLS40Ni4zNy0uODIuODItLjgycy44Mi4zNy44Mi44MnY4LjE1YzAsLjQ2LS4zNy44Mi0uODIuODJaIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4=",
|
||||
"icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMzIgMzIiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICMyMjZlYjM7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQoKICAgICAgLmNscy0zIHsKICAgICAgICBvcGFjaXR5OiAuOTsKICAgICAgfQoKICAgICAgLmNscy00IHsKICAgICAgICBmaWxsOiAjZTQyNTI4OwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yNi4xLDMuNjJINS45Yy0xLjI0LDAtMi4yNSwxLjAxLTIuMjUsMi4yNXYxNi45NGMwLDEuMjQsMS4wMSwyLjI1LDIuMjUsMi4yNWguMWMuMy4wNy42Ny4yOS42Ny45MXYyLjExYzAsLjE1LjEyLjI3LjI3LjI3aDIuNzVjLjE1LDAsLjI3LS4xMi4yNy0uMjd2LTEuMjNzLjA3LTEuNTYsMS43NS0xLjc5aDguNThjMS42OC4yMywxLjc1LDEuNzksMS43NSwxLjc5djEuMjNjMCwuMTUuMTIuMjcuMjcuMjdoMi43NWMuMTUsMCwuMjctLjEyLjI3LS4yN3YtMi4xMWMwLS42Mi4zNy0uODMuNjctLjkxaC4wOWMxLjI0LDAsMi4yNS0xLjAxLDIuMjUtMi4yNVY1Ljg3YzAtMS4yNC0xLjAxLTIuMjUtMi4yNS0yLjI1WiIvPgogIDxnPgogICAgPGcgY2xhc3M9ImNscy0zIj4KICAgICAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjUuMDYsMzBoLTIuNzVjLTEuMDYsMC0xLjkyLS44Ni0xLjkyLTEuOTJ2LTEuMTNjMC0uMTUtLjEyLS4yNy0uMjctLjI3aC04LjI0Yy0uMTUsMC0uMjcuMTItLjI3LjI3djEuMTNjMCwxLjA2LS44NiwxLjkyLTEuOTIsMS45MmgtMi43NWMtMS4wNiwwLTEuOTItLjg2LTEuOTItMS45MnYtMS40OWMtMS43Mi0uMzgtMy4wMi0xLjkyLTMuMDItMy43NlY1Ljg0YzAtMi4xMiwxLjcyLTMuODQsMy44NC0zLjg0aDIwLjMxYzIuMTIsMCwzLjg0LDEuNzIsMy44NCwzLjg0djE2Ljk5YzAsMS44NC0xLjMsMy4zOC0zLjAyLDMuNzZ2MS40OWMwLDEuMDYtLjg2LDEuOTItMS45MiwxLjkyWk0xMS44OCwyNS4wM2g4LjI0YzEuMDYsMCwxLjkyLjg2LDEuOTIsMS45MnYxLjEzYzAsLjE1LjEyLjI3LjI3LjI3aDIuNzVjLjE1LDAsLjI3LS4xMi4yNy0uMjd2LTIuMjJjMC0uNDYuMzctLjgyLjgyLS44MiwxLjIxLDAsMi4yLS45OSwyLjItMi4yVjUuODRjMC0xLjIxLS45OS0yLjItMi4yLTIuMkg1Ljg0Yy0xLjIxLDAtMi4yLjk5LTIuMiwyLjJ2MTYuOTljMCwxLjIxLjk5LDIuMiwyLjIsMi4yLjQ2LDAsLjgyLjM3LjgyLjgydjIuMjJjMCwuMTUuMTIuMjcuMjcuMjdoMi43NWMuMTUsMCwuMjctLjEyLjI3LS4yN3YtMS4xM2MwLTEuMDYuODYtMS45MiwxLjkyLTEuOTJaIi8+CiAgICA8L2c+CiAgICA8ZyBjbGFzcz0iY2xzLTMiPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTQiIGQ9Ik0xMC43NywxOS4yNWMtLjE3LDAtLjM0LS4wNS0uNDgtLjE2LS4zNy0uMjctLjQ1LS43OC0uMTgtMS4xNWwyLjY3LTMuNjhjLjI3LS4zNy43OC0uNDUsMS4xNS0uMTguMzcuMjcuNDUuNzguMTgsMS4xNWwtMi42NywzLjY4Yy0uMTYuMjItLjQxLjM0LS42Ny4zNFoiLz4KICAgIDwvZz4KICAgIDxnIGNsYXNzPSJjbHMtMyI+CiAgICAgIDxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTE2LjEyLDE5LjI1Yy0uMjYsMC0uNTEtLjEyLS42Ny0uMzRsLTIuNjctMy42OGMtLjI3LS4zNy0uMTktLjg4LjE4LTEuMTUuMzctLjI3Ljg4LS4xOSwxLjE1LjE4bDIuNjcsMy42OGMuMjcuMzcuMTkuODgtLjE4LDEuMTUtLjE1LjExLS4zMi4xNi0uNDguMTZaIi8+CiAgICA8L2c+CiAgICA8ZyBjbGFzcz0iY2xzLTMiPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTQiIGQ9Ik0xMy40NCwxNS41N2MtLjQ2LDAtLjgyLS4zNy0uODItLjgydi00LjUxYzAtLjQ2LjM3LS44Mi44Mi0uODJzLjgyLjM3LjgyLjgydjQuNTFjMCwuNDYtLjM3LjgyLS44Mi44MloiLz4KICAgIDwvZz4KICAgIDxnIGNsYXNzPSJjbHMtMyI+CiAgICAgIDxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTEzLjQ0LDE1LjU4Yy0uMzUsMC0uNjctLjIyLS43OC0uNTctLjE0LS40My4xLS45LjUzLTEuMDRsNC4zMi0xLjM5Yy40My0uMTQuOS4xLDEuMDQuNTMuMTQuNDMtLjEuOS0uNTMsMS4wNGwtNC4zMiwxLjM5Yy0uMDkuMDMtLjE3LjA0LS4yNi4wNFoiLz4KICAgIDwvZz4KICAgIDxnIGNsYXNzPSJjbHMtMyI+CiAgICAgIDxwYXRoIGNsYXNzPSJjbHMtNCIgZD0iTTEzLjQ0LDE1LjU4Yy0uMDksMC0uMTctLjAxLS4yNS0uMDRsLTQuMzItMS4zOWMtLjQzLS4xNC0uNjctLjYtLjUzLTEuMDQuMTQtLjQzLjYtLjY3LDEuMDQtLjUzbDQuMzIsMS4zOWMuNDMuMTQuNjcuNi41MywxLjA0LS4xMS4zNS0uNDMuNTctLjc4LjU3WiIvPgogICAgPC9nPgogICAgPGcgY2xhc3M9ImNscy0zIj4KICAgICAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjIuODUsMTkuMjNjLS40NiwwLS44Mi0uMzctLjgyLS44MnYtOC4xNWMwLS40Ni4zNy0uODIuODItLjgycy44Mi4zNy44Mi44MnY4LjE1YzAsLjQ2LS4zNy44Mi0uODIuODJaIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4="
|
||||
},
|
||||
"b78a0a55-6ef8-d246-a042-ba0f6d55050c": {
|
||||
"name": "LastPass",
|
||||
"icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIHZpZXdCb3g9IjAgMCA1MCA1MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjUwIiBoZWlnaHQ9IjUwIiByeD0iNy44NDc4MiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzI4NF81NzQzKSIvPgo8cGF0aCBkPSJNMzkuMzUxOCAxNy42MzgzQzM4LjkwNSAxNy42MzgzIDM4LjU2NjggMTcuOTc0NyAzOC41NjY4IDE4LjQyODdWMzEuNTYwNEMzOC41NjY4IDMyLjAxMDggMzguOTAxNCAzMi4zNTA5IDM5LjM1MTggMzIuMzUwOUMzOS44MDIyIDMyLjM1MDkgNDAuMTM2OCAzMi4wMTQ0IDQwLjEzNjggMzEuNTYwNFYxOC40Mjg3QzQwLjEzNjggMTcuOTc4NCAzOS44MDIyIDE3LjYzODMgMzkuMzUxOCAxNy42MzgzWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIxLjg5MTggMjIuNTQ0OUMyMC4zODE0IDIyLjU0NDkgMTkuMDk1NCAyMy43ODU3IDE5LjA5NTQgMjUuMzA2OUMxOS4wOTU0IDI2LjgyODEgMjAuMzI3MiAyOC4xMjMyIDIxLjgzNzUgMjguMTIzMkMyMy4zNDc4IDI4LjEyMzIgMjQuNjMzOSAyNi44ODI0IDI0LjYzMzkgMjUuMzYxMkMyNC42MzM5IDIzLjg0IDIzLjQwMjEgMjIuNTQ0OSAyMS44OTE4IDIyLjU0NDlaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMzEuMDY5NCAyMi41NDQ5QzI5LjU1OTEgMjIuNTQ0OSAyOC4yNzMxIDIzLjc4NTcgMjguMjczMSAyNS4zMDY5QzI4LjI3MzEgMjYuODI4MSAyOS41MDQ4IDI4LjEyMzIgMzEuMDE1MiAyOC4xMjMyQzMyLjUyNTUgMjguMTIzMiAzMy44MTE1IDI2Ljg4MjQgMzMuODExNSAyNS4zNjEyQzMzLjg2NTggMjMuODQgMzIuNjM0IDIyLjU0NDkgMzEuMDY5NCAyMi41NDQ5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEyLjY1OTYgMjIuNTQ0OUMxMS4xNDkzIDIyLjU0NDkgOS44NjMyOCAyMy43ODU3IDkuODYzMjggMjUuMzA2OUM5Ljg2MzI4IDI2LjgyODEgMTEuMDk1MSAyOC4xMjMyIDEyLjYwNTQgMjguMTIzMkMxNC4xMTU3IDI4LjEyMzIgMTUuNDAxNyAyNi44ODI0IDE1LjQwMTcgMjUuMzYxMkMxNS40MDE3IDIzLjg0IDE0LjE3IDIyLjU0NDkgMTIuNjU5NiAyMi41NDQ5WiIgZmlsbD0id2hpdGUiLz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl8yODRfNTc0MyIgeDE9IjI1IiB5MT0iMCIgeDI9IjI1IiB5Mj0iNTAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzNDM0QzRCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyNjI2MjYiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K",
|
||||
"icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIHZpZXdCb3g9IjAgMCA1MCA1MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjUwIiBoZWlnaHQ9IjUwIiByeD0iNy44NDc4MiIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzI4NF81Nzc0KSIvPgo8cGF0aCBkPSJNMzkuMzUxNyAxNy42Mzg0QzM4LjkwNDkgMTcuNjM4NCAzOC41NjY3IDE3Ljk3NDkgMzguNTY2NyAxOC40Mjg5VjMxLjU2MDVDMzguNTY2NyAzMi4wMTA4IDM4LjkwMTMgMzIuMzUwOSAzOS4zNTE3IDMyLjM1MDlDMzkuODAyMSAzMi4zNTA5IDQwLjEzNjcgMzIuMDE0NSA0MC4xMzY3IDMxLjU2MDVWMTguNDI4OUM0MC4xMzY3IDE3Ljk3ODUgMzkuODAyMSAxNy42Mzg0IDM5LjM1MTcgMTcuNjM4NFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMS44OTE4IDIyLjU0NUMyMC4zODE0IDIyLjU0NSAxOS4wOTU0IDIzLjc4NTkgMTkuMDk1NCAyNS4zMDdDMTkuMDk1NCAyNi44MjgyIDIwLjMyNzIgMjguMTIzMyAyMS44Mzc1IDI4LjEyMzNDMjMuMzQ3OCAyOC4xMjMzIDI0LjYzMzggMjYuODgyNSAyNC42MzM4IDI1LjM2MTNDMjQuNjMzOCAyMy44NDAxIDIzLjQwMjEgMjIuNTQ1IDIxLjg5MTggMjIuNTQ1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTMxLjA2OTQgMjIuNTQ1QzI5LjU1OTEgMjIuNTQ1IDI4LjI3MyAyMy43ODU5IDI4LjI3MyAyNS4zMDdDMjguMjczIDI2LjgyODIgMjkuNTA0OCAyOC4xMjMzIDMxLjAxNTEgMjguMTIzM0MzMi41MjU0IDI4LjEyMzMgMzMuODExNSAyNi44ODI1IDMzLjgxMTUgMjUuMzYxM0MzMy44NjU3IDIzLjg0MDEgMzIuNjM0IDIyLjU0NSAzMS4wNjk0IDIyLjU0NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMi42NTk3IDIyLjU0NUMxMS4xNDk0IDIyLjU0NSA5Ljg2MzM5IDIzLjc4NTkgOS44NjMzOSAyNS4zMDdDOS44NjMzOSAyNi44MjgyIDExLjA5NTIgMjguMTIzMyAxMi42MDU1IDI4LjEyMzNDMTQuMTE1OCAyOC4xMjMzIDE1LjQwMTggMjYuODgyNSAxNS40MDE4IDI1LjM2MTNDMTUuNDAxOCAyMy44NDAxIDE0LjE3IDIyLjU0NSAxMi42NTk3IDIyLjU0NVoiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMjg0XzU3NzQiIHgxPSIyNSIgeTE9IjAiIHgyPSIyNSIgeTI9IjUwIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNFRDE5NEEiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjQzMwODMzIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg=="
|
||||
},
|
||||
"de503f9c-21a4-4f76-b4b7-558eb55c6f89": {
|
||||
"name": "Devolutions",
|
||||
"icon_dark": "data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNzJweCIgaGVpZ2h0PSI3MnB4IiB2aWV3Qm94PSIwIDAgNzIgNzIiPgk8ZGVmcz4KICAgICAgICA8ZmlsdGVyIGlkPSJhIiB3aWR0aD0iMjAwJSIgaGVpZ2h0PSIyMDAlIj4KICAgICAgICAgICAgPGZlT2Zmc2V0IHJlc3VsdD0ib2ZmT3V0IiBpbj0iU291cmNlQWxwaGEiIGR5PSIyLjIiLz4KICAgICAgICAgICAgPGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iYmx1ck91dCIgaW49Im9mZk91dCIgc3RkRGV2aWF0aW9uPSIxLjUiLz4KICAgICAgICAgICAgPGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjQgMCIvPgogICAgICAgICAgICA8ZmVNZXJnZT4KICAgICAgICAgICAgICAgIDxmZU1lcmdlTm9kZS8+CiAgICAgICAgICAgICAgICA8ZmVNZXJnZU5vZGUgaW49IlNvdXJjZUdyYXBoaWMiLz4KICAgICAgICAgICAgPC9mZU1lcmdlPgogICAgICAgIDwvZmlsdGVyPgogICAgPC9kZWZzPgo8cGF0aCBmaWxsPSIjZmZmZmZmIiBmaWx0ZXI9InVybCgjYSkiIGQ9Ik0zMS4wNTksNS4zOTVjMTYuODktMi43MjcsMzIuODE3LDguNzcyLDM1LjU0NSwyNS42NjJjMi43MjcsMTYuODktOC43NzIsMzIuODE3LTI1LjY2MiwzNS41NDUKCUMyNC4wNTEsNjkuMzI5LDguMTI0LDU3LjgzLDUuMzk3LDQwLjk0QzIuNjcsMjQuMDQ5LDE0LjE2OSw4LjEyMiwzMS4wNTksNS4zOTV6Ii8+CjxwYXRoIGZpbGw9IiMwMDY4YzMiIGQ9Ik01NS4zNjQsMTcuMjAyYy01LjA0Ny01LjE5Ny0xMS44MDItOC4xMDktMTkuMDItOC4yYy03LjE5MS0wLjA5LTEzLjk4NSwyLjY2OS0xOS4xNDksNy43NjgKCUMxMS45MSwyMS45ODksOSwyOC45NTUsOSwzNi4zOTJsMC4wNDQsMS4yNmMwLDEuMTgxLDAuOTYxLDIuMTQyLDIuMTQyLDIuMTQyczIuMTQxLTAuOTYsMi4xNDItMi4xNGwwLjAxLTAuOTEzCgljMC05Ljk0NSw4LjQ1My0yMC41OTMsMjEuMDM1LTIwLjU5M2MxMy4xMzIsMCwyMS4yNjEsMTAuNzEyLDIxLjI2MSwyMC42MzdjMCw1LjE3My0yLjA2Myw5LjkxOS01LjgwOCwxMy4zNjMKCUM0Ni4xNyw1My41MDksNDEuMjYsNTUuMzYsMzYsNTUuMzZjLTMuMTMsMC02LjIxOS0wLjc1NS04Ljk1OC0yLjE4NmwxOC44Ny04LjY4NmMxLjI2Ny0wLjU4MywyLjExNS0xLjgxLDIuMjEzLTMuMjAxCglzLTAuNTY5LTIuNzI1LTEuNzU0LTMuNDg3bC0xNS43MDYtOC40MTVjLTAuNTU0LTAuMzU3LTEuMjEzLTAuNDc3LTEuODU4LTAuMzM3Yy0wLjY0NCwwLjEzOS0xLjE5NSwwLjUyMS0xLjU1MiwxLjA3NQoJYy0wLjczNywxLjE0NC0wLjQwNiwyLjY3MywwLjcyMSwzLjM5N2w4LjQ1NCw2LjkyM2wtMTguNDE5LDguNDc4Yy0xLjQyNywwLjY1Ny0yLjI5OCwyLjEtMi4yMTgsMy42NzUKCWMwLjA0OCwwLjk0OSwwLjQ5MywxLjg4NCwxLjI1MywyLjYzNEMyMi4xNTgsNjAuMjY5LDI4Ljg0LDYzLDM1Ljk4NSw2M2MwLjQ0NSwwLDAuODkyLTAuMDExLDEuMzQxLTAuMDMyCgljMTQuMTU2LTAuNjczLDI1LjQzMi0xMi4zMTUsMjUuNjcxLTI2LjUwNUM2My4xMTgsMjkuMjM2LDYwLjQwNywyMi4zOTYsNTUuMzY0LDE3LjIwMnoiLz4KPC9zdmc+Cg==",
|
||||
"icon_light": "data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNzJweCIgaGVpZ2h0PSI3MnB4IiB2aWV3Qm94PSIwIDAgNzIgNzIiPgk8ZGVmcz4KICAgICAgICA8ZmlsdGVyIGlkPSJhIiB3aWR0aD0iMjAwJSIgaGVpZ2h0PSIyMDAlIj4KICAgICAgICAgICAgPGZlT2Zmc2V0IHJlc3VsdD0ib2ZmT3V0IiBpbj0iU291cmNlQWxwaGEiIGR5PSIyLjIiLz4KICAgICAgICAgICAgPGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iYmx1ck91dCIgaW49Im9mZk91dCIgc3RkRGV2aWF0aW9uPSIxLjUiLz4KICAgICAgICAgICAgPGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjQgMCIvPgogICAgICAgICAgICA8ZmVNZXJnZT4KICAgICAgICAgICAgICAgIDxmZU1lcmdlTm9kZS8+CiAgICAgICAgICAgICAgICA8ZmVNZXJnZU5vZGUgaW49IlNvdXJjZUdyYXBoaWMiLz4KICAgICAgICAgICAgPC9mZU1lcmdlPgogICAgICAgIDwvZmlsdGVyPgogICAgPC9kZWZzPgo8cGF0aCBmaWxsPSIjZmZmZmZmIiBmaWx0ZXI9InVybCgjYSkiIGQ9Ik0zMS4wNTksNS4zOTVjMTYuODktMi43MjcsMzIuODE3LDguNzcyLDM1LjU0NSwyNS42NjJjMi43MjcsMTYuODktOC43NzIsMzIuODE3LTI1LjY2MiwzNS41NDUKCUMyNC4wNTEsNjkuMzI5LDguMTI0LDU3LjgzLDUuMzk3LDQwLjk0QzIuNjcsMjQuMDQ5LDE0LjE2OSw4LjEyMiwzMS4wNTksNS4zOTV6Ii8+CjxwYXRoIGZpbGw9IiMwMDY4YzMiIGQ9Ik01NS4zNjQsMTcuMjAyYy01LjA0Ny01LjE5Ny0xMS44MDItOC4xMDktMTkuMDItOC4yYy03LjE5MS0wLjA5LTEzLjk4NSwyLjY2OS0xOS4xNDksNy43NjgKCUMxMS45MSwyMS45ODksOSwyOC45NTUsOSwzNi4zOTJsMC4wNDQsMS4yNmMwLDEuMTgxLDAuOTYxLDIuMTQyLDIuMTQyLDIuMTQyczIuMTQxLTAuOTYsMi4xNDItMi4xNGwwLjAxLTAuOTEzCgljMC05Ljk0NSw4LjQ1My0yMC41OTMsMjEuMDM1LTIwLjU5M2MxMy4xMzIsMCwyMS4yNjEsMTAuNzEyLDIxLjI2MSwyMC42MzdjMCw1LjE3My0yLjA2Myw5LjkxOS01LjgwOCwxMy4zNjMKCUM0Ni4xNyw1My41MDksNDEuMjYsNTUuMzYsMzYsNTUuMzZjLTMuMTMsMC02LjIxOS0wLjc1NS04Ljk1OC0yLjE4NmwxOC44Ny04LjY4NmMxLjI2Ny0wLjU4MywyLjExNS0xLjgxLDIuMjEzLTMuMjAxCglzLTAuNTY5LTIuNzI1LTEuNzU0LTMuNDg3bC0xNS43MDYtOC40MTVjLTAuNTU0LTAuMzU3LTEuMjEzLTAuNDc3LTEuODU4LTAuMzM3Yy0wLjY0NCwwLjEzOS0xLjE5NSwwLjUyMS0xLjU1MiwxLjA3NQoJYy0wLjczNywxLjE0NC0wLjQwNiwyLjY3MywwLjcyMSwzLjM5N2w4LjQ1NCw2LjkyM2wtMTguNDE5LDguNDc4Yy0xLjQyNywwLjY1Ny0yLjI5OCwyLjEtMi4yMTgsMy42NzUKCWMwLjA0OCwwLjk0OSwwLjQ5MywxLjg4NCwxLjI1MywyLjYzNEMyMi4xNTgsNjAuMjY5LDI4Ljg0LDYzLDM1Ljk4NSw2M2MwLjQ0NSwwLDAuODkyLTAuMDExLDEuMzQxLTAuMDMyCgljMTQuMTU2LTAuNjczLDI1LjQzMi0xMi4zMTUsMjUuNjcxLTI2LjUwNUM2My4xMTgsMjkuMjM2LDYwLjQwNywyMi4zOTYsNTUuMzY0LDE3LjIwMnoiLz4KPC9zdmc+Cg=="
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -68,7 +68,7 @@ class AuthenticatorAttachment(models.TextChoices):
|
||||
|
||||
|
||||
class AuthenticatorWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""WebAuthn stage"""
|
||||
"""Stage to enroll WebAuthn-based authenticators."""
|
||||
|
||||
user_verification = models.TextField(
|
||||
choices=UserVerification.choices,
|
||||
|
@ -3,3 +3,4 @@
|
||||
BACKEND_INBUILT = "authentik.core.auth.InbuiltBackend"
|
||||
BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend"
|
||||
BACKEND_APP_PASSWORD = "authentik.core.auth.TokenBackend" # nosec
|
||||
BACKEND_KERBEROS = "authentik.sources.kerberos.auth.KerberosBackend"
|
||||
|
@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.0.8 on 2024-08-07 22:17
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_password", "0009_passwordstage_allow_show_password"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="passwordstage",
|
||||
name="backends",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(
|
||||
choices=[
|
||||
("authentik.core.auth.InbuiltBackend", "User database + standard password"),
|
||||
("authentik.core.auth.TokenBackend", "User database + app passwords"),
|
||||
(
|
||||
"authentik.sources.ldap.auth.LDAPBackend",
|
||||
"User database + LDAP password",
|
||||
),
|
||||
(
|
||||
"authentik.sources.kerberos.auth.KerberosBackend",
|
||||
"User database + Kerberos password",
|
||||
),
|
||||
]
|
||||
),
|
||||
help_text="Selection of backends to test the password against.",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
@ -8,7 +8,12 @@ from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
from authentik.stages.password import (
|
||||
BACKEND_APP_PASSWORD,
|
||||
BACKEND_INBUILT,
|
||||
BACKEND_KERBEROS,
|
||||
BACKEND_LDAP,
|
||||
)
|
||||
|
||||
|
||||
def get_authentication_backends():
|
||||
@ -26,6 +31,10 @@ def get_authentication_backends():
|
||||
BACKEND_LDAP,
|
||||
_("User database + LDAP password"),
|
||||
),
|
||||
(
|
||||
BACKEND_KERBEROS,
|
||||
_("User database + Kerberos password"),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -19,6 +19,7 @@ entries:
|
||||
- attrs:
|
||||
backends:
|
||||
- authentik.core.auth.InbuiltBackend
|
||||
- authentik.sources.kerberos.auth.KerberosBackend
|
||||
- authentik.sources.ldap.auth.LDAPBackend
|
||||
- authentik.core.auth.TokenBackend
|
||||
configure_flow: !Find [authentik_flows.flow, [slug, default-password-change]]
|
||||
|
@ -1081,6 +1081,166 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_kerberos.kerberossource"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossource_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossource"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossource"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_kerberos.kerberossourcepropertymapping"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossourcepropertymapping_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossourcepropertymapping"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossourcepropertymapping"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_kerberos.userkerberossourceconnection"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.userkerberossourceconnection_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.userkerberossourceconnection"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.userkerberossourceconnection"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_sources_kerberos.groupkerberossourceconnection"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.groupkerberossourceconnection_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.groupkerberossourceconnection"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_sources_kerberos.groupkerberossourceconnection"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -3361,6 +3521,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"model",
|
||||
"identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"const": "authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"absent",
|
||||
"present",
|
||||
"created",
|
||||
"must_created"
|
||||
],
|
||||
"default": "present"
|
||||
},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage_permissions"
|
||||
},
|
||||
"attrs": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage"
|
||||
},
|
||||
"identifiers": {
|
||||
"$ref": "#/$defs/model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -4271,6 +4471,7 @@
|
||||
"authentik.providers.scim",
|
||||
"authentik.rbac",
|
||||
"authentik.recovery",
|
||||
"authentik.sources.kerberos",
|
||||
"authentik.sources.ldap",
|
||||
"authentik.sources.oauth",
|
||||
"authentik.sources.plex",
|
||||
@ -4304,6 +4505,7 @@
|
||||
"authentik.enterprise.providers.google_workspace",
|
||||
"authentik.enterprise.providers.microsoft_entra",
|
||||
"authentik.enterprise.providers.rac",
|
||||
"authentik.enterprise.stages.authenticator_endpoint_gdtc",
|
||||
"authentik.enterprise.stages.source",
|
||||
"authentik.events"
|
||||
],
|
||||
@ -4343,6 +4545,10 @@
|
||||
"authentik_providers_scim.scimprovider",
|
||||
"authentik_providers_scim.scimmapping",
|
||||
"authentik_rbac.role",
|
||||
"authentik_sources_kerberos.kerberossource",
|
||||
"authentik_sources_kerberos.kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.userkerberossourceconnection",
|
||||
"authentik_sources_kerberos.groupkerberossourceconnection",
|
||||
"authentik_sources_ldap.ldapsource",
|
||||
"authentik_sources_ldap.ldapsourcepropertymapping",
|
||||
"authentik_sources_oauth.oauthsource",
|
||||
@ -4400,6 +4606,7 @@
|
||||
"authentik_providers_rac.racprovider",
|
||||
"authentik_providers_rac.endpoint",
|
||||
"authentik_providers_rac.racpropertymapping",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage",
|
||||
"authentik_stages_source.sourcestage",
|
||||
"authentik_events.event",
|
||||
"authentik_events.notificationtransport",
|
||||
@ -6371,6 +6578,22 @@
|
||||
"authentik_rbac.view_role",
|
||||
"authentik_rbac.view_system_info",
|
||||
"authentik_rbac.view_system_settings",
|
||||
"authentik_sources_kerberos.add_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.add_kerberossource",
|
||||
"authentik_sources_kerberos.add_kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.add_userkerberossourceconnection",
|
||||
"authentik_sources_kerberos.change_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.change_kerberossource",
|
||||
"authentik_sources_kerberos.change_kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.change_userkerberossourceconnection",
|
||||
"authentik_sources_kerberos.delete_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.delete_kerberossource",
|
||||
"authentik_sources_kerberos.delete_kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.delete_userkerberossourceconnection",
|
||||
"authentik_sources_kerberos.view_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.view_kerberossource",
|
||||
"authentik_sources_kerberos.view_kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.view_userkerberossourceconnection",
|
||||
"authentik_sources_ldap.add_ldapsource",
|
||||
"authentik_sources_ldap.add_ldapsourcepropertymapping",
|
||||
"authentik_sources_ldap.change_ldapsource",
|
||||
@ -6451,6 +6674,18 @@
|
||||
"authentik_stages_authenticator_duo.delete_duodevice",
|
||||
"authentik_stages_authenticator_duo.view_authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.view_duodevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_endpointdeviceconnection",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.change_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.change_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.change_endpointdeviceconnection",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.delete_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.delete_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.delete_endpointdeviceconnection",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.view_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.view_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.view_endpointdeviceconnection",
|
||||
"authentik_stages_authenticator_sms.add_authenticatorsmsstage",
|
||||
"authentik_stages_authenticator_sms.add_smsdevice",
|
||||
"authentik_stages_authenticator_sms.change_authenticatorsmsstage",
|
||||
@ -6606,6 +6841,319 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_kerberos.kerberossource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name",
|
||||
"description": "Source's display Name."
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 50,
|
||||
"minLength": 1,
|
||||
"pattern": "^[-a-zA-Z0-9_]+$",
|
||||
"title": "Slug",
|
||||
"description": "Internal source name, used in URLs."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"title": "Enabled"
|
||||
},
|
||||
"authentication_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Authentication flow",
|
||||
"description": "Flow to use when authenticating existing users."
|
||||
},
|
||||
"enrollment_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Enrollment flow",
|
||||
"description": "Flow to use when enrolling new users."
|
||||
},
|
||||
"user_property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "User property mappings"
|
||||
},
|
||||
"group_property_mappings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "Group property mappings"
|
||||
},
|
||||
"policy_engine_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"all",
|
||||
"any"
|
||||
],
|
||||
"title": "Policy engine mode"
|
||||
},
|
||||
"user_matching_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"identifier",
|
||||
"email_link",
|
||||
"email_deny",
|
||||
"username_link",
|
||||
"username_deny"
|
||||
],
|
||||
"title": "User matching mode",
|
||||
"description": "How the source determines if an existing user should be authenticated or a new user enrolled."
|
||||
},
|
||||
"user_path_template": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "User path template"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Icon"
|
||||
},
|
||||
"group_matching_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"identifier",
|
||||
"name_link",
|
||||
"name_deny"
|
||||
],
|
||||
"title": "Group matching mode",
|
||||
"description": "How the source determines if an existing group should be used or a new group created."
|
||||
},
|
||||
"realm": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Realm",
|
||||
"description": "Kerberos realm"
|
||||
},
|
||||
"krb5_conf": {
|
||||
"type": "string",
|
||||
"title": "Krb5 conf",
|
||||
"description": "Custom krb5.conf to use. Uses the system one by default"
|
||||
},
|
||||
"sync_users": {
|
||||
"type": "boolean",
|
||||
"title": "Sync users",
|
||||
"description": "Sync users from Kerberos into authentik"
|
||||
},
|
||||
"sync_users_password": {
|
||||
"type": "boolean",
|
||||
"title": "Sync users password",
|
||||
"description": "When a user changes their password, sync it back to Kerberos"
|
||||
},
|
||||
"sync_principal": {
|
||||
"type": "string",
|
||||
"title": "Sync principal",
|
||||
"description": "Principal to authenticate to kadmin for sync."
|
||||
},
|
||||
"sync_password": {
|
||||
"type": "string",
|
||||
"title": "Sync password",
|
||||
"description": "Password to authenticate to kadmin for sync"
|
||||
},
|
||||
"sync_keytab": {
|
||||
"type": "string",
|
||||
"title": "Sync keytab",
|
||||
"description": "Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the form TYPE:residual"
|
||||
},
|
||||
"sync_ccache": {
|
||||
"type": "string",
|
||||
"title": "Sync ccache",
|
||||
"description": "Credentials cache to authenticate to kadmin for sync. Must be in the form TYPE:residual"
|
||||
},
|
||||
"spnego_server_name": {
|
||||
"type": "string",
|
||||
"title": "Spnego server name",
|
||||
"description": "Force the use of a specific server name for SPNEGO"
|
||||
},
|
||||
"spnego_keytab": {
|
||||
"type": "string",
|
||||
"title": "Spnego keytab",
|
||||
"description": "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"
|
||||
},
|
||||
"spnego_ccache": {
|
||||
"type": "string",
|
||||
"title": "Spnego ccache",
|
||||
"description": "Credential cache to use for SPNEGO in form type:residual"
|
||||
},
|
||||
"password_login_update_internal_password": {
|
||||
"type": "boolean",
|
||||
"title": "Password login update internal password",
|
||||
"description": "If enabled, the authentik-stored password will be updated upon login with the Kerberos password backend"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_kerberos.kerberossource_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_kerberossource",
|
||||
"change_kerberossource",
|
||||
"delete_kerberossource",
|
||||
"view_kerberossource"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_kerberos.kerberossourcepropertymapping": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"managed": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"minLength": 1,
|
||||
"title": "Managed by authentik",
|
||||
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Expression"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_kerberos.kerberossourcepropertymapping_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_kerberossourcepropertymapping",
|
||||
"change_kerberossourcepropertymapping",
|
||||
"delete_kerberossourcepropertymapping",
|
||||
"view_kerberossourcepropertymapping"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_kerberos.userkerberossourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "integer",
|
||||
"title": "User"
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Identifier"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Icon"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_kerberos.userkerberossourceconnection_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_userkerberossourceconnection",
|
||||
"change_userkerberossourceconnection",
|
||||
"delete_userkerberossourceconnection",
|
||||
"view_userkerberossourceconnection"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_kerberos.groupkerberossourceconnection": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Icon"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_sources_kerberos.groupkerberossourceconnection_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_groupkerberossourceconnection",
|
||||
"change_groupkerberossourceconnection",
|
||||
"delete_groupkerberossourceconnection",
|
||||
"view_groupkerberossourceconnection"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_sources_ldap.ldapsource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -10490,7 +11038,8 @@
|
||||
"enum": [
|
||||
"authentik.core.auth.InbuiltBackend",
|
||||
"authentik.core.auth.TokenBackend",
|
||||
"authentik.sources.ldap.auth.LDAPBackend"
|
||||
"authentik.sources.ldap.auth.LDAPBackend",
|
||||
"authentik.sources.kerberos.auth.KerberosBackend"
|
||||
],
|
||||
"title": "Backends"
|
||||
},
|
||||
@ -12027,6 +12576,22 @@
|
||||
"authentik_rbac.view_role",
|
||||
"authentik_rbac.view_system_info",
|
||||
"authentik_rbac.view_system_settings",
|
||||
"authentik_sources_kerberos.add_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.add_kerberossource",
|
||||
"authentik_sources_kerberos.add_kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.add_userkerberossourceconnection",
|
||||
"authentik_sources_kerberos.change_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.change_kerberossource",
|
||||
"authentik_sources_kerberos.change_kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.change_userkerberossourceconnection",
|
||||
"authentik_sources_kerberos.delete_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.delete_kerberossource",
|
||||
"authentik_sources_kerberos.delete_kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.delete_userkerberossourceconnection",
|
||||
"authentik_sources_kerberos.view_groupkerberossourceconnection",
|
||||
"authentik_sources_kerberos.view_kerberossource",
|
||||
"authentik_sources_kerberos.view_kerberossourcepropertymapping",
|
||||
"authentik_sources_kerberos.view_userkerberossourceconnection",
|
||||
"authentik_sources_ldap.add_ldapsource",
|
||||
"authentik_sources_ldap.add_ldapsourcepropertymapping",
|
||||
"authentik_sources_ldap.change_ldapsource",
|
||||
@ -12107,6 +12672,18 @@
|
||||
"authentik_stages_authenticator_duo.delete_duodevice",
|
||||
"authentik_stages_authenticator_duo.view_authenticatorduostage",
|
||||
"authentik_stages_authenticator_duo.view_duodevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.add_endpointdeviceconnection",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.change_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.change_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.change_endpointdeviceconnection",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.delete_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.delete_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.delete_endpointdeviceconnection",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.view_authenticatorendpointgdtcstage",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.view_endpointdevice",
|
||||
"authentik_stages_authenticator_endpoint_gdtc.view_endpointdeviceconnection",
|
||||
"authentik_stages_authenticator_sms.add_authenticatorsmsstage",
|
||||
"authentik_stages_authenticator_sms.add_smsdevice",
|
||||
"authentik_stages_authenticator_sms.change_authenticatorsmsstage",
|
||||
@ -12997,6 +13574,144 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"flow_set": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Name"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 50,
|
||||
"minLength": 1,
|
||||
"pattern": "^[-a-zA-Z0-9_]+$",
|
||||
"title": "Slug",
|
||||
"description": "Visible in the URL."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"title": "Title",
|
||||
"description": "Shown as the Title in Flow pages."
|
||||
},
|
||||
"designation": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"authentication",
|
||||
"authorization",
|
||||
"invalidation",
|
||||
"enrollment",
|
||||
"unenrollment",
|
||||
"recovery",
|
||||
"stage_configuration"
|
||||
],
|
||||
"title": "Designation",
|
||||
"description": "Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik."
|
||||
},
|
||||
"policy_engine_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"all",
|
||||
"any"
|
||||
],
|
||||
"title": "Policy engine mode"
|
||||
},
|
||||
"compatibility_mode": {
|
||||
"type": "boolean",
|
||||
"title": "Compatibility mode",
|
||||
"description": "Enable compatibility mode, increases compatibility with password managers on mobile devices."
|
||||
},
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stacked",
|
||||
"content_left",
|
||||
"content_right",
|
||||
"sidebar_left",
|
||||
"sidebar_right"
|
||||
],
|
||||
"title": "Layout"
|
||||
},
|
||||
"denied_action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"message_continue",
|
||||
"message",
|
||||
"continue"
|
||||
],
|
||||
"title": "Denied action",
|
||||
"description": "Configure what should happen when a flow denies access to a user."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"slug",
|
||||
"title",
|
||||
"designation"
|
||||
]
|
||||
},
|
||||
"title": "Flow set"
|
||||
},
|
||||
"configure_flow": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Configure flow",
|
||||
"description": "Flow used by an authenticated user to configure this Stage. If empty, user will not be able to configure this stage."
|
||||
},
|
||||
"friendly_name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"minLength": 1,
|
||||
"title": "Friendly name"
|
||||
},
|
||||
"credentials": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"title": "Credentials"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"model_authentik_stages_authenticator_endpoint_gdtc.authenticatorendpointgdtcstage_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_authenticatorendpointgdtcstage",
|
||||
"change_authenticatorendpointgdtcstage",
|
||||
"delete_authenticatorendpointgdtcstage",
|
||||
"view_authenticatorendpointgdtcstage"
|
||||
]
|
||||
},
|
||||
"user": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model_authentik_stages_source.sourcestage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
55
blueprints/system/sources-kerberos.yaml
Normal file
55
blueprints/system/sources-kerberos.yaml
Normal file
@ -0,0 +1,55 @@
|
||||
version: 1
|
||||
metadata:
|
||||
labels:
|
||||
blueprints.goauthentik.io/system: "true"
|
||||
name: System - Kerberos Source - Mappings
|
||||
entries:
|
||||
- identifiers:
|
||||
managed: goauthentik.io/sources/kerberos/user/default/multipart-principals-as-service-accounts
|
||||
model: authentik_sources_kerberos.kerberossourcepropertymapping
|
||||
attrs:
|
||||
name: "authentik default Kerberos User Mapping: Multipart principals as service accounts"
|
||||
expression: |
|
||||
from authentik.core.models import USER_PATH_SERVICE_ACCOUNT, UserTypes
|
||||
|
||||
localpart, _ = principal.rsplit("@", 1)
|
||||
is_service_account = "/" in localpart
|
||||
attrs = {}
|
||||
if is_service_account:
|
||||
attrs = {
|
||||
"type": UserTypes.SERVICE_ACCOUNT,
|
||||
"path": USER_PATH_SERVICE_ACCOUNT,
|
||||
}
|
||||
return attrs
|
||||
- identifiers:
|
||||
managed: goauthentik.io/sources/kerberos/user/default/ignore-other-realms
|
||||
model: authentik_sources_kerberos.kerberossourcepropertymapping
|
||||
attrs:
|
||||
name: "authentik default Kerberos User Mapping: Ignore other realms"
|
||||
expression: |
|
||||
localpart, realm = principal.rsplit("@", 1)
|
||||
if realm.upper() != source.realm.upper():
|
||||
raise SkipObject
|
||||
return {}
|
||||
- identifiers:
|
||||
managed: goauthentik.io/sources/kerberos/user/default/ignore-system-principals
|
||||
model: authentik_sources_kerberos.kerberossourcepropertymapping
|
||||
attrs:
|
||||
name: "authentik default Kerberos User Mapping: Ignore system principals"
|
||||
expression: |
|
||||
localpart, realm = principal.rsplit("@", 1)
|
||||
denied_prefixes = ["kadmin/", "krbtgt/", "K/M", "WELLKNOWN/"]
|
||||
for prefix in denied_prefixes:
|
||||
if localpart.lower().startswith(prefix.lower()):
|
||||
raise SkipObject
|
||||
return {}
|
||||
- identifiers:
|
||||
managed: goauthentik.io/sources/kerberos/user/realm-as-group
|
||||
model: authentik_sources_kerberos.kerberossourcepropertymapping
|
||||
attrs:
|
||||
name: "authentik default Kerberos User Mapping: Add realm as group"
|
||||
expression: |
|
||||
localpart, realm = principal.rsplit("@", 1)
|
||||
return {
|
||||
"groups": [realm.upper()]
|
||||
}
|
4
go.mod
4
go.mod
@ -23,13 +23,13 @@ require (
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||
github.com/pires/go-proxyproto v0.8.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/redis/go-redis/v9 v9.6.2
|
||||
github.com/redis/go-redis/v9 v9.7.0
|
||||
github.com/sethvargo/go-envconfig v1.1.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2024083.8
|
||||
goauthentik.io/api/v3 v3.2024083.11
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.23.0
|
||||
golang.org/x/sync v0.8.0
|
||||
|
8
go.sum
8
go.sum
@ -248,8 +248,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.6.2 h1:w0uvkRbc9KpgD98zcvo5IrVUsn0lXpRMuhNgiHDJzdk=
|
||||
github.com/redis/go-redis/v9 v9.6.2/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
@ -299,8 +299,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.2024083.8 h1:KEKPkPxfM6Mt29cp0CRusdFu7OMZlUSAtNBLz+8sBBo=
|
||||
goauthentik.io/api/v3 v3.2024083.8/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2024083.11 h1:kF5WAnS0dB2cq9Uldqel8e8PDepJg/824JC3YFsQVHU=
|
||||
goauthentik.io/api/v3 v3.2024083.11/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -2,7 +2,7 @@
|
||||
MODE_FILE="${TMPDIR}/authentik-mode"
|
||||
|
||||
function log {
|
||||
printf '{"event": "%s", "level": "info", "logger": "bootstrap"}\n' "$@" > /dev/stderr
|
||||
printf '{"event": "%s", "level": "info", "logger": "bootstrap"}\n' "$@" >/dev/stderr
|
||||
}
|
||||
|
||||
function wait_for_db {
|
||||
@ -45,7 +45,7 @@ function run_authentik {
|
||||
}
|
||||
|
||||
function set_mode {
|
||||
echo $1 > $MODE_FILE
|
||||
echo $1 >$MODE_FILE
|
||||
trap cleanup EXIT
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ function cleanup {
|
||||
}
|
||||
|
||||
function prepare_debug {
|
||||
apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server
|
||||
VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction
|
||||
touch /unittest.xml
|
||||
chown authentik:authentik /unittest.xml
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 00:08+0000\n"
|
||||
"POT-Creation-Date: 2024-10-23 00:08+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"
|
||||
@ -18,6 +18,10 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
msgid "Version history"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/admin/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "New version {version} available!"
|
||||
@ -526,6 +530,26 @@ msgstr ""
|
||||
msgid "(You are already connected in another tab/window)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stage"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Authenticator Google Device Trust Connector Stages"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Device"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/models.py
|
||||
msgid "Endpoint Devices"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/authenticator_endpoint_gdtc/stage.py
|
||||
msgid "Verifying your browser..."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/enterprise/stages/source/models.py
|
||||
msgid ""
|
||||
"Amount of time a user can take to return from the source to continue the "
|
||||
@ -1373,10 +1397,18 @@ msgstr ""
|
||||
msgid "Signing Key"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Key used to sign the tokens."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Encryption Key"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
"Key used to sign the tokens. Only required when JWT Algorithm is set to "
|
||||
"RS256."
|
||||
"Key used to encrypt the tokens. When set, tokens will be encrypted and "
|
||||
"returned as JWEs."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
|
@ -6,12 +6,12 @@
|
||||
# Translators:
|
||||
# Bastien Germond, 2022
|
||||
# Phorcys, 2022
|
||||
# Titouan Petit, 2023
|
||||
# Kyllian Delaye-Maillot, 2023
|
||||
# Manuel Viens, 2023
|
||||
# Mordecai, 2023
|
||||
# Charles Leclerc, 2024
|
||||
# nerdinator <florian.dupret@gmail.com>, 2024
|
||||
# Titouan Petit, 2024
|
||||
# Marc Schmitt, 2024
|
||||
#
|
||||
#, fuzzy
|
||||
@ -19,7 +19,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-15 00:08+0000\n"
|
||||
"POT-Creation-Date: 2024-10-18 00:09+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Marc Schmitt, 2024\n"
|
||||
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
|
||||
@ -29,6 +29,10 @@ msgstr ""
|
||||
"Language: fr\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
msgid "Version history"
|
||||
msgstr "Historique des versions"
|
||||
|
||||
#: authentik/admin/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "New version {version} available!"
|
||||
@ -1527,13 +1531,21 @@ msgstr "Configure comment le champ émetteur du jeton ID sera rempli."
|
||||
msgid "Signing Key"
|
||||
msgstr "Clé de signature"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Key used to sign the tokens."
|
||||
msgstr "Clé utilisée pour signer les jetons."
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Encryption Key"
|
||||
msgstr "Clé de chiffrement"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
"Key used to sign the tokens. Only required when JWT Algorithm is set to "
|
||||
"RS256."
|
||||
"Key used to encrypt the tokens. When set, tokens will be encrypted and "
|
||||
"returned as JWEs."
|
||||
msgstr ""
|
||||
"Clé utilisée pour signer les jetons. Nécessaire uniquement lorsque "
|
||||
"l'algorithme JWT est réglé sur RS256."
|
||||
"Clé utilisée pour chiffrer les jetons. Si sélectionné, les jetons seront "
|
||||
"chiffrés et retournés sous forme de JWE."
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
|
Binary file not shown.
@ -9,16 +9,17 @@
|
||||
# Matteo Piccina <altermatte@gmail.com>, 2024
|
||||
# Enrico Campani, 2024
|
||||
# Marco Vitale, 2024
|
||||
# Kowalski Dragon <kowalski.7cc@gmail.com>, 2024
|
||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2024
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2024
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-06-05 00:07+0000\n"
|
||||
"POT-Creation-Date: 2024-10-18 00:09+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Kowalski Dragon <kowalski.7cc@gmail.com>, 2024\n"
|
||||
"Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2024\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -26,6 +27,15 @@ msgstr ""
|
||||
"Language: it\n"
|
||||
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
msgid "Version history"
|
||||
msgstr "Cronologia delle versioni"
|
||||
|
||||
#: authentik/admin/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "New version {version} available!"
|
||||
msgstr "Nuova versione {version} disponibile!"
|
||||
|
||||
#: authentik/api/schema.py
|
||||
msgid "Generic API Error"
|
||||
msgstr "Errore API generico"
|
||||
@ -39,9 +49,8 @@ msgid "Blueprint file does not exist"
|
||||
msgstr "File del progetto inesistente"
|
||||
|
||||
#: authentik/blueprints/api.py
|
||||
#, python-brace-format
|
||||
msgid "Failed to validate blueprint: {logs}"
|
||||
msgstr "Impossibile convalidare il progetto: {logs}"
|
||||
msgid "Failed to validate blueprint"
|
||||
msgstr "Impossibile convalidare il progetto"
|
||||
|
||||
#: authentik/blueprints/api.py
|
||||
msgid "Either path or content must be set."
|
||||
@ -90,6 +99,14 @@ msgstr ""
|
||||
"Dominio che attiva questo marchio. Può essere un superset, ad esempio `a.b` "
|
||||
"per `aa.b` e `ba.b`"
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid ""
|
||||
"When set, external users will be redirected to this application after "
|
||||
"authenticating."
|
||||
msgstr ""
|
||||
"Se impostata, gli utenti esterni verranno reindirizzati a questa "
|
||||
"applicazione dopo l'autenticazione."
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid "Web Certificate used by the authentik Core webserver."
|
||||
msgstr "Certificato Web utilizzato dal server Web authentik Core."
|
||||
@ -193,6 +210,10 @@ msgstr ""
|
||||
msgid "Flow used when authorizing this provider."
|
||||
msgstr "Flusso utilizzato durante l'autorizzazione di questo provider."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow used ending the session from a provider."
|
||||
msgstr "Flusso utilizzato per terminare la sessione da un provider."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"Accessed from applications; optional backchannel providers for protocols "
|
||||
@ -258,6 +279,19 @@ msgstr ""
|
||||
"Utilizza il nome utente dell'utente, ma nega l'iscrizione quando il nome "
|
||||
"utente esiste già."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"Link to a group with identical name. Can have security implications when a "
|
||||
"group name is used with another source."
|
||||
msgstr ""
|
||||
"Collegamento a un gruppo con nome identico. Può avere implicazioni di "
|
||||
"sicurezza quando un nome di gruppo viene utilizzato con un'altra fonte."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Use the group name, but deny enrollment when the name already exists."
|
||||
msgstr ""
|
||||
"Utilizza il nome del gruppo, ma nega l'iscrizione se il nome esiste già."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Source's display Name."
|
||||
msgstr "Nome visualizzato della sorgente."
|
||||
@ -282,6 +316,14 @@ msgstr ""
|
||||
"Modalità in cui la fonte determina se un utente esistente deve essere "
|
||||
"autenticato o un nuovo utente registrato."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"How the source determines if an existing group should be used or a new group"
|
||||
" created."
|
||||
msgstr ""
|
||||
"In che modo la fonte determina se utilizzare un gruppo esistente o crearne "
|
||||
"uno nuovo."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
@ -337,60 +379,12 @@ msgstr "{source} collegata correttamente!"
|
||||
msgid "Source is not configured for enrollment."
|
||||
msgstr "La sorgente non è configurata per la registrazione."
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
msgid "End session"
|
||||
msgstr "Fine sessione"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"You've logged out of %(application)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Sei disconnesso da %(application)s.\n"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" You've logged out of %(application)s. You can go back to the overview to launch another application, or log out of your %(branding_title)s account.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Hai effettuato il logout da %(application)s. Puoi tornare alla panoramica per avviare un'altra applicazione o effettuare il logout dal tuo account %(branding_title)s."
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
msgid "Go back to overview"
|
||||
msgstr "Torna alla panoramica"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Log out of %(branding_title)s\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Esci da %(branding_title)s\n"
|
||||
" "
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Log back into %(application)s\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Accedi di nuovo a %(application)s\n"
|
||||
" "
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
msgid "Go home"
|
||||
msgstr "Vai alla pagina iniziale"
|
||||
|
||||
#: authentik/core/templates/login/base_full.html
|
||||
#: authentik/flows/templates/if/flow-sfe.html
|
||||
msgid "Powered by authentik"
|
||||
msgstr "Gestito da authentik"
|
||||
|
||||
@ -401,6 +395,10 @@ msgstr "Gestito da authentik"
|
||||
msgid "You're about to sign into %(application)s."
|
||||
msgstr "Stai per accedere a %(application)s"
|
||||
|
||||
#: authentik/core/views/interface.py
|
||||
msgid "Interface can only be accessed by internal users."
|
||||
msgstr "L'interfaccia è accessibile solo agli utenti interni."
|
||||
|
||||
#: authentik/crypto/api.py
|
||||
msgid "Subject-alt name"
|
||||
msgstr "Nome alternativo del soggetto"
|
||||
@ -461,9 +459,25 @@ msgstr "Versione Enterprise richiesta per accedere a questa funzione"
|
||||
msgid "Feature only accessible for internal users."
|
||||
msgstr "Caratteristica accessibile solo agli utenti interni"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
msgid "Google Workspace Provider User"
|
||||
msgstr "Utente Google Workspace Provider"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
msgid "Google Workspace Provider Users"
|
||||
msgstr "Utenti Google Workspace Provider"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
msgid "Google Workspace Provider Group"
|
||||
msgstr "Gruppo Google Workspace Provider"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
msgid "Google Workspace Provider Groups"
|
||||
msgstr "Gruppi Google Workspace Provider"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
#: authentik/providers/scim/models.py authentik/sources/ldap/models.py
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "Property mappings used for group creation/updating."
|
||||
msgstr ""
|
||||
"Mapping delle proprietà utilizzate per la creazione/aggiornamento dei "
|
||||
@ -485,21 +499,17 @@ msgstr "Mappatura Google Workspace Provider"
|
||||
msgid "Google Workspace Provider Mappings"
|
||||
msgstr "Mappature Google Workspace Provider"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
msgid "Google Workspace Provider User"
|
||||
msgstr "Utente Google Workspace Provider"
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
msgid "Microsoft Entra Provider User"
|
||||
msgstr "Utente Microsoft Entra Provider"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
msgid "Google Workspace Provider Users"
|
||||
msgstr "Utenti Google Workspace Provider"
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
msgid "Microsoft Entra Provider Group"
|
||||
msgstr "Gruppo Microsoft Entra Provider"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
msgid "Google Workspace Provider Group"
|
||||
msgstr "Gruppo Google Workspace Provider"
|
||||
|
||||
#: authentik/enterprise/providers/google_workspace/models.py
|
||||
msgid "Google Workspace Provider Groups"
|
||||
msgstr "Gruppi Google Workspace Provider"
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
msgid "Microsoft Entra Provider Groups"
|
||||
msgstr "Gruppi Microsoft Entra Provider"
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
msgid "Microsoft Entra Provider"
|
||||
@ -517,18 +527,6 @@ msgstr "Mappatura Microsoft Entra Provider"
|
||||
msgid "Microsoft Entra Provider Mappings"
|
||||
msgstr "Mappature Microsoft Entra Provider"
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
msgid "Microsoft Entra Provider User"
|
||||
msgstr "Utente Microsoft Entra Provider"
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
msgid "Microsoft Entra Provider Group"
|
||||
msgstr "Gruppo Microsoft Entra Provider"
|
||||
|
||||
#: authentik/enterprise/providers/microsoft_entra/models.py
|
||||
msgid "Microsoft Entra Provider Groups"
|
||||
msgstr "Gruppi Microsoft Entra Provider"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid ""
|
||||
@ -562,12 +560,12 @@ msgid "RAC Endpoints"
|
||||
msgstr "Endpoints RAC"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Property Mapping"
|
||||
msgstr "Mappatura Proprietà RAC"
|
||||
msgid "RAC Provider Property Mapping"
|
||||
msgstr "Mappatura delle proprietà del provider RAC"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Property Mappings"
|
||||
msgstr "Mappature Proprietà RAC"
|
||||
msgid "RAC Provider Property Mappings"
|
||||
msgstr "Mappature proprietà del provider RAC"
|
||||
|
||||
#: authentik/enterprise/providers/rac/models.py
|
||||
msgid "RAC Connection token"
|
||||
@ -1111,6 +1109,30 @@ msgstr "Criterio di Espressione"
|
||||
msgid "Expression Policies"
|
||||
msgstr "Criteri di espressione"
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "GeoIP: client IP not found in ASN database."
|
||||
msgstr "GeoIP: IP client non trovato nel database ASN."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "Client IP is not part of an allowed autonomous system."
|
||||
msgstr "L'IP del client non fa parte di un sistema autonomo consentito."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "GeoIP: client IP address not found in City database."
|
||||
msgstr "GeoIP: indirizzo IP del client non trovato nel database della città."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "Client IP is not in an allowed country."
|
||||
msgstr "L'IP del client non si trova in un paese consentito."
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "GeoIP Policy"
|
||||
msgstr "Criterio GeoIP"
|
||||
|
||||
#: authentik/policies/geoip/models.py
|
||||
msgid "GeoIP Policies"
|
||||
msgstr "Criteri GeoIP"
|
||||
|
||||
#: authentik/policies/models.py
|
||||
msgid "all, all policies must pass"
|
||||
msgstr "tutte, tutti i criteri devono passare"
|
||||
@ -1274,14 +1296,6 @@ msgstr "Impossibile risolvere l'applicazione"
|
||||
msgid "DN under which objects are accessible."
|
||||
msgstr "DN sotto il quale gli oggetti sono accessibili."
|
||||
|
||||
#: authentik/providers/ldap/models.py
|
||||
msgid ""
|
||||
"Users in this group can do search queries. If not set, every user can "
|
||||
"execute search queries."
|
||||
msgstr ""
|
||||
"Gli utenti di questo gruppo possono scrivere query di ricerca. Se non "
|
||||
"specificato, qualsiasi utente può eseguirle."
|
||||
|
||||
#: authentik/providers/ldap/models.py
|
||||
msgid ""
|
||||
"The start for uidNumbers, this number is added to the user.pk to make sure "
|
||||
@ -1328,6 +1342,10 @@ msgstr "Provider LDAP"
|
||||
msgid "LDAP Providers"
|
||||
msgstr "Providers LDAP"
|
||||
|
||||
#: authentik/providers/ldap/models.py
|
||||
msgid "Search full LDAP directory"
|
||||
msgstr "Ricerca completa nella directory LDAP"
|
||||
|
||||
#: authentik/providers/oauth2/id_token.py
|
||||
msgid "Based on the Hashed User ID"
|
||||
msgstr "Basato sull'ID utente hashato"
|
||||
@ -1503,13 +1521,21 @@ msgstr ""
|
||||
msgid "Signing Key"
|
||||
msgstr "Chiave di firma"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Key used to sign the tokens."
|
||||
msgstr "Chiave utilizzata per firmare i token."
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Encryption Key"
|
||||
msgstr "Chiave di crittografia"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
"Key used to sign the tokens. Only required when JWT Algorithm is set to "
|
||||
"RS256."
|
||||
"Key used to encrypt the tokens. When set, tokens will be encrypted and "
|
||||
"returned as JWEs."
|
||||
msgstr ""
|
||||
"Chiave utilizzata per firmare i token. Richiesta solo quando l'algoritmo JWT"
|
||||
" è impostato su RS256."
|
||||
"Chiave utilizzata per crittografare i token. Quando impostata, i token "
|
||||
"saranno crittografati e restituiti come JWE."
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
@ -1714,6 +1740,22 @@ msgstr "Provider Radius"
|
||||
msgid "Radius Providers"
|
||||
msgstr "Providers Radius"
|
||||
|
||||
#: authentik/providers/radius/models.py
|
||||
msgid "Radius Provider Property Mapping"
|
||||
msgstr "Mappatura delle proprietà del provider Radius"
|
||||
|
||||
#: authentik/providers/radius/models.py
|
||||
msgid "Radius Provider Property Mappings"
|
||||
msgstr "Mappature delle proprietà del provider Radius"
|
||||
|
||||
#: authentik/providers/saml/api/providers.py
|
||||
msgid ""
|
||||
"With a signing keypair selected, at least one of 'Sign assertion' and 'Sign "
|
||||
"Response' must be selected."
|
||||
msgstr ""
|
||||
"Dopo aver selezionato una coppia di chiavi di firma, è necessario "
|
||||
"selezionare almeno una tra 'Firma asserzione' e 'Firma risposta'."
|
||||
|
||||
#: authentik/providers/saml/api/providers.py
|
||||
msgid "Invalid XML Syntax"
|
||||
msgstr "Sintassi XML non valida"
|
||||
@ -1862,6 +1904,20 @@ msgstr ""
|
||||
msgid "Signing Keypair"
|
||||
msgstr "Coppia di chiavi di firma"
|
||||
|
||||
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
|
||||
msgid ""
|
||||
"When selected, incoming assertions are encrypted by the IdP using the public"
|
||||
" key of the encryption keypair. The assertion is decrypted by the SP using "
|
||||
"the the private key."
|
||||
msgstr ""
|
||||
"Se selezionata, le asserzioni in arrivo vengono crittografate dall'IdP "
|
||||
"utilizzando la chiave pubblica della coppia di chiavi di crittografia. "
|
||||
"L'asserzione viene decrittografata dall'SP utilizzando la chiave privata."
|
||||
|
||||
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
|
||||
msgid "Encryption Keypair"
|
||||
msgstr "Coppia di chiavi di crittografia"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Default relay_state value for IDP-initiated logins"
|
||||
msgstr "Valore predefinito di relay_state per i login inizializzati da IDP"
|
||||
@ -1875,12 +1931,12 @@ msgid "SAML Providers"
|
||||
msgstr "Providers SAML"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SAML Property Mapping"
|
||||
msgstr "Mappatura Proprietà SAML"
|
||||
msgid "SAML Provider Property Mapping"
|
||||
msgstr "Mapping delle proprietà del provider SAML"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SAML Property Mappings"
|
||||
msgstr "Mappature Proprietà SAML"
|
||||
msgid "SAML Provider Property Mappings"
|
||||
msgstr "Mappature delle proprietà del provider SAML"
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SAML Provider from Metadata"
|
||||
@ -1907,12 +1963,12 @@ msgid "SCIM Providers"
|
||||
msgstr "Providers SCIM"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Mapping"
|
||||
msgstr "Mappatura SCIM"
|
||||
msgid "SCIM Provider Mapping"
|
||||
msgstr "Mappatura dei provider SCIM"
|
||||
|
||||
#: authentik/providers/scim/models.py
|
||||
msgid "SCIM Mappings"
|
||||
msgstr "Mappature SCIM"
|
||||
msgid "SCIM Provider Mappings"
|
||||
msgstr "Mappature dei provider SCIM"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Role"
|
||||
@ -1961,6 +2017,12 @@ msgstr ""
|
||||
msgid "Used recovery-link to authenticate."
|
||||
msgstr "Utilizzato il link di recupero per autenticarsi."
|
||||
|
||||
#: authentik/sources/ldap/api.py
|
||||
msgid "Only a single LDAP Source with password synchronization is allowed"
|
||||
msgstr ""
|
||||
"È consentita solo una singola sorgente LDAP con sincronizzazione della "
|
||||
"password"
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Server URI"
|
||||
msgstr "URI Server"
|
||||
@ -2050,12 +2112,12 @@ msgid "LDAP Sources"
|
||||
msgstr "Sorgenti LDAP"
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "LDAP Property Mapping"
|
||||
msgstr "Mappatura proprietà LDAP"
|
||||
msgid "LDAP Source Property Mapping"
|
||||
msgstr "Mappatura delle proprietà sorgente LDAP"
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "LDAP Property Mappings"
|
||||
msgstr "Mappatura proprietà LDAP"
|
||||
msgid "LDAP Source Property Mappings"
|
||||
msgstr "Mappature delle proprietà della sorgente LDAP"
|
||||
|
||||
#: authentik/sources/ldap/signals.py
|
||||
msgid "Password does not match Active Directory Complexity."
|
||||
@ -2225,6 +2287,14 @@ msgstr "Sorgente OAuth di Reddit"
|
||||
msgid "Reddit OAuth Sources"
|
||||
msgstr "Sorgenti OAuth di Reddit"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "OAuth Source Property Mapping"
|
||||
msgstr "Mapping delle proprietà sorgente OAuth"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "OAuth Source Property Mappings"
|
||||
msgstr "Mappature delle proprietà sorgente OAuth"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "User OAuth Source Connection"
|
||||
msgstr "Connessione origine OAuth utente"
|
||||
@ -2233,6 +2303,14 @@ msgstr "Connessione origine OAuth utente"
|
||||
msgid "User OAuth Source Connections"
|
||||
msgstr "Connessioni origine OAuth utente"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Group OAuth Source Connection"
|
||||
msgstr "Connessione sorgente OAuth di gruppo"
|
||||
|
||||
#: authentik/sources/oauth/models.py
|
||||
msgid "Group OAuth Source Connections"
|
||||
msgstr "Connessioni di origine OAuth di gruppo"
|
||||
|
||||
#: authentik/sources/oauth/views/callback.py
|
||||
#, python-brace-format
|
||||
msgid "Authentication failed: {reason}"
|
||||
@ -2267,6 +2345,14 @@ msgstr "Sorgente Plex"
|
||||
msgid "Plex Sources"
|
||||
msgstr "Sorgenti Plex"
|
||||
|
||||
#: authentik/sources/plex/models.py
|
||||
msgid "Plex Source Property Mapping"
|
||||
msgstr "Mappatura delle proprietà sorgente Plex"
|
||||
|
||||
#: authentik/sources/plex/models.py
|
||||
msgid "Plex Source Property Mappings"
|
||||
msgstr "Mappature delle proprietà sorgente Plex"
|
||||
|
||||
#: authentik/sources/plex/models.py
|
||||
msgid "User Plex Source Connection"
|
||||
msgstr "Connessione sorgente Plex utente"
|
||||
@ -2275,6 +2361,14 @@ msgstr "Connessione sorgente Plex utente"
|
||||
msgid "User Plex Source Connections"
|
||||
msgstr "Connessioni sorgente Plex utente"
|
||||
|
||||
#: authentik/sources/plex/models.py
|
||||
msgid "Group Plex Source Connection"
|
||||
msgstr "Connessione sorgente Plex di gruppo"
|
||||
|
||||
#: authentik/sources/plex/models.py
|
||||
msgid "Group Plex Source Connections"
|
||||
msgstr "Connessioni sorgente Plex di gruppo"
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "Redirect Binding"
|
||||
msgstr "Associazione reindirizzamento"
|
||||
@ -2362,6 +2456,14 @@ msgstr "Sorgente SAML"
|
||||
msgid "SAML Sources"
|
||||
msgstr "Sorgenti SAML"
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "SAML Source Property Mapping"
|
||||
msgstr "Mappatura delle proprietà sorgente SAML"
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "SAML Source Property Mappings"
|
||||
msgstr "Mappature delle proprietà sorgente SAML"
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "User SAML Source Connection"
|
||||
msgstr "User SAML Source Connection"
|
||||
@ -2370,6 +2472,14 @@ msgstr "User SAML Source Connection"
|
||||
msgid "User SAML Source Connections"
|
||||
msgstr "User SAML Source Connections"
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "Group SAML Source Connection"
|
||||
msgstr "Connessione sorgente SAML di gruppo"
|
||||
|
||||
#: authentik/sources/saml/models.py
|
||||
msgid "Group SAML Source Connections"
|
||||
msgstr "Connessioni sorgente SAML di gruppo"
|
||||
|
||||
#: authentik/sources/scim/models.py
|
||||
msgid "SCIM Source"
|
||||
msgstr "Sorgente SCIM"
|
||||
@ -2378,6 +2488,14 @@ msgstr "Sorgente SCIM"
|
||||
msgid "SCIM Sources"
|
||||
msgstr "Sorgenti SCIM"
|
||||
|
||||
#: authentik/sources/scim/models.py
|
||||
msgid "SCIM Source Property Mapping"
|
||||
msgstr "Mappatura delle proprietà della sorgente SCIM"
|
||||
|
||||
#: authentik/sources/scim/models.py
|
||||
msgid "SCIM Source Property Mappings"
|
||||
msgstr "Mappature delle proprietà sorgente SCIM"
|
||||
|
||||
#: authentik/stages/authenticator_duo/models.py
|
||||
msgid "Duo Authenticator Setup Stage"
|
||||
msgstr "Fase di configurazione dell'autenticatore Duo"
|
||||
@ -3009,6 +3127,14 @@ msgstr ""
|
||||
"escludere l'utente, utilizzare un criterio di reputazione e una fase "
|
||||
"user_write."
|
||||
|
||||
#: authentik/stages/password/models.py
|
||||
msgid ""
|
||||
"When enabled, provides a 'show password' button with the password input "
|
||||
"field."
|
||||
msgstr ""
|
||||
"Se abilitato, fornisce un pulsante \"mostra password\" insieme al campo di "
|
||||
"immissione della password."
|
||||
|
||||
#: authentik/stages/password/models.py
|
||||
msgid "Password Stage"
|
||||
msgstr "Fase della password"
|
||||
|
@ -6,8 +6,8 @@
|
||||
# Translators:
|
||||
# Chen Zhikai, 2022
|
||||
# 刘松, 2022
|
||||
# Jens L. <jens@goauthentik.io>, 2024
|
||||
# Tianhao Chai <cth451@gmail.com>, 2024
|
||||
# Jens L. <jens@goauthentik.io>, 2024
|
||||
# deluxghost, 2024
|
||||
#
|
||||
#, fuzzy
|
||||
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-12 00:08+0000\n"
|
||||
"POT-Creation-Date: 2024-10-18 00:09+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -25,6 +25,10 @@ msgstr ""
|
||||
"Language: zh-Hans\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
msgid "Version history"
|
||||
msgstr "版本历史"
|
||||
|
||||
#: authentik/admin/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "New version {version} available!"
|
||||
@ -192,6 +196,10 @@ msgstr "当关联应用程序被未验证身份的用户访问时,用于身份
|
||||
msgid "Flow used when authorizing this provider."
|
||||
msgstr "授权此提供程序时使用的流程。"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow used ending the session from a provider."
|
||||
msgstr "从提供程序结束会话使用的流程。"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"Accessed from applications; optional backchannel providers for protocols "
|
||||
@ -337,55 +345,6 @@ msgstr "成功链接 {source}!"
|
||||
msgid "Source is not configured for enrollment."
|
||||
msgstr "源未被配置用于注册。"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
msgid "End session"
|
||||
msgstr "结束会话"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"You've logged out of %(application)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"您已登出 %(application)s。\n"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" You've logged out of %(application)s. You can go back to the overview to launch another application, or log out of your %(branding_title)s account.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" 您已成功登出 %(application)s 。现在您可以返回总览页来启动其他应用,或者登出您的 %(branding_title)s 账户。"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
msgid "Go back to overview"
|
||||
msgstr "返回总览"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Log out of %(branding_title)s\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" 登出 %(branding_title)s\n"
|
||||
" "
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Log back into %(application)s\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" 重新登录 %(application)s\n"
|
||||
" "
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
msgid "Go home"
|
||||
msgstr "前往首页"
|
||||
@ -1441,11 +1400,19 @@ msgstr "配置如何填写 ID 令牌的颁发者字段。"
|
||||
msgid "Signing Key"
|
||||
msgstr "签名密钥"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Key used to sign the tokens."
|
||||
msgstr "用于签名令牌的密钥。"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Encryption Key"
|
||||
msgstr "加密密钥"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
"Key used to sign the tokens. Only required when JWT Algorithm is set to "
|
||||
"RS256."
|
||||
msgstr "用于签名令牌的密钥。仅当 JWT 算法设置为 RS256 时才需要。"
|
||||
"Key used to encrypt the tokens. When set, tokens will be encrypted and "
|
||||
"returned as JWEs."
|
||||
msgstr "用于加密令牌的密钥。如果设置,则令牌会被加密,并以 JWE 形式返回。"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
|
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-10-12 00:08+0000\n"
|
||||
"POT-Creation-Date: 2024-10-18 00:09+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -24,6 +24,10 @@ msgstr ""
|
||||
"Language: zh_CN\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: authentik/admin/models.py
|
||||
msgid "Version history"
|
||||
msgstr "版本历史"
|
||||
|
||||
#: authentik/admin/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "New version {version} available!"
|
||||
@ -191,6 +195,10 @@ msgstr "当关联应用程序被未验证身份的用户访问时,用于身份
|
||||
msgid "Flow used when authorizing this provider."
|
||||
msgstr "授权此提供程序时使用的流程。"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid "Flow used ending the session from a provider."
|
||||
msgstr "从提供程序结束会话使用的流程。"
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"Accessed from applications; optional backchannel providers for protocols "
|
||||
@ -336,55 +344,6 @@ msgstr "成功链接 {source}!"
|
||||
msgid "Source is not configured for enrollment."
|
||||
msgstr "源未被配置用于注册。"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
msgid "End session"
|
||||
msgstr "结束会话"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"You've logged out of %(application)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"您已登出 %(application)s。\n"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" You've logged out of %(application)s. You can go back to the overview to launch another application, or log out of your %(branding_title)s account.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" 您已成功登出 %(application)s 。现在您可以返回总览页来启动其他应用,或者登出您的 %(branding_title)s 账户。"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
msgid "Go back to overview"
|
||||
msgstr "返回总览"
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Log out of %(branding_title)s\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" 登出 %(branding_title)s\n"
|
||||
" "
|
||||
|
||||
#: authentik/core/templates/if/end_session.html
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Log back into %(application)s\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" 重新登录 %(application)s\n"
|
||||
" "
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
msgid "Go home"
|
||||
msgstr "前往首页"
|
||||
@ -1440,11 +1399,19 @@ msgstr "配置如何填写 ID 令牌的颁发者字段。"
|
||||
msgid "Signing Key"
|
||||
msgstr "签名密钥"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Key used to sign the tokens."
|
||||
msgstr "用于签名令牌的密钥。"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid "Encryption Key"
|
||||
msgstr "加密密钥"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
"Key used to sign the tokens. Only required when JWT Algorithm is set to "
|
||||
"RS256."
|
||||
msgstr "用于签名令牌的密钥。仅当 JWT 算法设置为 RS256 时才需要。"
|
||||
"Key used to encrypt the tokens. When set, tokens will be encrypted and "
|
||||
"returned as JWEs."
|
||||
msgstr "用于加密令牌的密钥。如果设置,则令牌会被加密,并以 JWE 形式返回。"
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
|
318
poetry.lock
generated
318
poetry.lock
generated
@ -969,73 +969,73 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.3"
|
||||
version = "7.6.4"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"},
|
||||
{file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"},
|
||||
{file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"},
|
||||
{file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"},
|
||||
{file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"},
|
||||
{file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"},
|
||||
{file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"},
|
||||
{file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"},
|
||||
{file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"},
|
||||
{file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"},
|
||||
{file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"},
|
||||
{file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"},
|
||||
{file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"},
|
||||
{file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"},
|
||||
{file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"},
|
||||
{file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"},
|
||||
{file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -1043,38 +1043,38 @@ toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "43.0.1"
|
||||
version = "43.0.3"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
|
||||
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
|
||||
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
|
||||
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
|
||||
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
|
||||
{file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
|
||||
{file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
|
||||
{file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
|
||||
{file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
|
||||
{file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
|
||||
{file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"},
|
||||
{file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"},
|
||||
{file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"},
|
||||
{file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"},
|
||||
{file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"},
|
||||
{file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"},
|
||||
{file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"},
|
||||
{file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"},
|
||||
{file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
|
||||
{file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
|
||||
{file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
|
||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
|
||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
|
||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
|
||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
|
||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
|
||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
|
||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
|
||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
|
||||
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1087,7 +1087,7 @@ nox = ["nox"]
|
||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
||||
sdist = ["build"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
@ -1157,6 +1157,17 @@ files = [
|
||||
{file = "debugpy-1.8.7.zip", hash = "sha256:18b8f731ed3e2e1df8e9cdaa23fb1fc9c24e570cd0081625308ec51c82efe42e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decorator"
|
||||
version = "5.1.1"
|
||||
description = "Decorators for Humans"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
|
||||
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deepmerge"
|
||||
version = "2.0"
|
||||
@ -1836,6 +1847,42 @@ protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4
|
||||
[package.extras]
|
||||
grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"]
|
||||
|
||||
[[package]]
|
||||
name = "gssapi"
|
||||
version = "1.8.3"
|
||||
description = "Python GSSAPI Wrapper"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "gssapi-1.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e4a83e9b275fe69b5d40be6d5479889866b80333a12c51a9243f2712d4f0554"},
|
||||
{file = "gssapi-1.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d57d67547e18f4e44a688bfb20abbf176d1b8df547da2b31c3f2df03cfdc269"},
|
||||
{file = "gssapi-1.8.3-cp310-cp310-win32.whl", hash = "sha256:3a3f63105f39c4af29ffc8f7b6542053d87fe9d63010c689dd9a9f5571facb8e"},
|
||||
{file = "gssapi-1.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:b031c0f186ab4275186da385b2c7470dd47c9b27522cb3b753757c9ac4bebf11"},
|
||||
{file = "gssapi-1.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b03d6b30f1fcd66d9a688b45a97e302e4dd3f1386d5c333442731aec73cdb409"},
|
||||
{file = "gssapi-1.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca6ceb17fc15eda2a69f2e8c6cf10d11e2edb32832255e5d4c65b21b6db4680a"},
|
||||
{file = "gssapi-1.8.3-cp311-cp311-win32.whl", hash = "sha256:edc8ef3a9e397dbe18bb6016f8e2209969677b534316d20bb139da2865a38efe"},
|
||||
{file = "gssapi-1.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:8fdb1ff130cee49bc865ec1624dee8cf445cd6c6e93b04bffef2c6f363a60cb9"},
|
||||
{file = "gssapi-1.8.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:19c373b3ba63ce19cd3163aa1495635e3d01b0de6cc4ff1126095eded1df6e01"},
|
||||
{file = "gssapi-1.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f1a8046d695f2c9b8d640a6e385780d3945c0741571ed6fee6f94c31e431dc"},
|
||||
{file = "gssapi-1.8.3-cp312-cp312-win32.whl", hash = "sha256:338db18612e3e6ed64e92b6d849242a535fdc98b365f21122992fb8cae737617"},
|
||||
{file = "gssapi-1.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5731c5b40ecc3116cfe7fb7e1d1e128583ec8b3df1e68bf8cd12073160793acd"},
|
||||
{file = "gssapi-1.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e556878da197ad115a566d36e46a8082d0079731d9c24d1ace795132d725ff2a"},
|
||||
{file = "gssapi-1.8.3-cp37-cp37m-win32.whl", hash = "sha256:e2bb081f2db2111377effe7d40ba23f9a87359b9d2f4881552b731e9da88b36b"},
|
||||
{file = "gssapi-1.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4d9ed83f2064cda60aad90e6840ae282096801b2c814b8cbd390bf0df4635aab"},
|
||||
{file = "gssapi-1.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7d91fe6e2a5c89b32102ea8e374b8ae13b9031d43d7b55f3abc1f194ddce820d"},
|
||||
{file = "gssapi-1.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5b28237afc0668046934792756dd4b6b7e957b0d95a608d02f296734a2819ad"},
|
||||
{file = "gssapi-1.8.3-cp38-cp38-win32.whl", hash = "sha256:791e44f7bea602b8e3da1ec56fbdb383b8ee3326fdeb736f904c2aa9af13a67d"},
|
||||
{file = "gssapi-1.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:5b4bf84d0a6d7779a4bf11dacfd3db57ae02dd53562e2aeadac4219a68eaee07"},
|
||||
{file = "gssapi-1.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e40efc88ccefefd6142f8c47b8af498731938958b808bad49990442a91f45160"},
|
||||
{file = "gssapi-1.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee74b9211c977b9181ff4652d886d7712c9a221560752a35393b58e5ea07887a"},
|
||||
{file = "gssapi-1.8.3-cp39-cp39-win32.whl", hash = "sha256:465c6788f2ac6ef7c738394ba8fde1ede6004e5721766f386add63891d8c90af"},
|
||||
{file = "gssapi-1.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:8fb8ee70458f47b51ed881a6881f30b187c987c02af16cc0fff0079255d4d465"},
|
||||
{file = "gssapi-1.8.3.tar.gz", hash = "sha256:aa3c8d0b1526f52559552bb2c9d2d6be013d76a8e5db00b39a1db5727e93b0b0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
decorator = "*"
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "23.0.0"
|
||||
@ -2228,6 +2275,20 @@ files = [
|
||||
cryptography = ">=3.4"
|
||||
typing-extensions = ">=4.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "k5test"
|
||||
version = "0.10.4"
|
||||
description = "A library for testing Python applications in self-contained Kerberos 5 environments"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "k5test-0.10.4-py2.py3-none-any.whl", hash = "sha256:33de7ff10bf99155fe8ee5d5976798ad1db6237214306dadf5a0ae9d6bb0ad03"},
|
||||
{file = "k5test-0.10.4.tar.gz", hash = "sha256:e152491e6602f6a93b3d533d387bd4590f2476093b6842170ff0b93de64bef30"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
extension-test = ["gssapi"]
|
||||
|
||||
[[package]]
|
||||
name = "kombu"
|
||||
version = "5.3.7"
|
||||
@ -2868,13 +2929,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"]
|
||||
|
||||
[[package]]
|
||||
name = "msgraph-sdk"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
description = "The Microsoft Graph Python SDK"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "msgraph_sdk-1.10.0-py3-none-any.whl", hash = "sha256:b346013f978d2e23255d044d38751e2715e1eed3159b1b1c3d7cbe831dd121e8"},
|
||||
{file = "msgraph_sdk-1.10.0.tar.gz", hash = "sha256:7b94646fea833d85ad2f793643ff72946de23bc2cc253cfdb694798ae7a60229"},
|
||||
{file = "msgraph_sdk-1.11.0-py3-none-any.whl", hash = "sha256:91e5243005298cec19c6c4712647dcb8bc66e54f02cd1d6b0c93541bd50ae0be"},
|
||||
{file = "msgraph_sdk-1.11.0.tar.gz", hash = "sha256:636d73e48e9cb5fddefe110669c17d47059228e4855db6a55cafad5ff02af8a2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -3888,6 +3949,21 @@ files = [
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-kadmin"
|
||||
version = "0.2.0"
|
||||
description = "Python module for kerberos admin (kadm5)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = []
|
||||
develop = false
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/authentik-community/python-kadmin.git"
|
||||
reference = "v0.2.0"
|
||||
resolved_reference = "6f9ce6ee2427e3488b403a900a9211166c7569e1"
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2024.1"
|
||||
@ -4216,29 +4292,29 @@ pyasn1 = ">=0.1.3"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.9"
|
||||
version = "0.7.0"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"},
|
||||
{file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"},
|
||||
{file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"},
|
||||
{file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"},
|
||||
{file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"},
|
||||
{file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"},
|
||||
{file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"},
|
||||
{file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"},
|
||||
{file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"},
|
||||
{file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"},
|
||||
{file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"},
|
||||
{file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"},
|
||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"},
|
||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"},
|
||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"},
|
||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"},
|
||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"},
|
||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"},
|
||||
{file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"},
|
||||
{file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"},
|
||||
{file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"},
|
||||
{file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"},
|
||||
{file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"},
|
||||
{file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"},
|
||||
{file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"},
|
||||
{file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"},
|
||||
{file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4296,13 +4372,13 @@ websocket-client = ">=1.8,<2.0"
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.16.0"
|
||||
version = "2.17.0"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "sentry_sdk-2.16.0-py2.py3-none-any.whl", hash = "sha256:49139c31ebcd398f4f6396b18910610a0c1602f6e67083240c33019d1f6aa30c"},
|
||||
{file = "sentry_sdk-2.16.0.tar.gz", hash = "sha256:90f733b32e15dfc1999e6b7aca67a38688a567329de4d6e184154a73f96c6892"},
|
||||
{file = "sentry_sdk-2.17.0-py2.py3-none-any.whl", hash = "sha256:625955884b862cc58748920f9e21efdfb8e0d4f98cca4ab0d3918576d5b606ad"},
|
||||
{file = "sentry_sdk-2.17.0.tar.gz", hash = "sha256:dd0a05352b78ffeacced73a94e86f38b32e2eae15fff5f30ca5abb568a72eacf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4674,13 +4750,13 @@ wsproto = ">=0.14"
|
||||
|
||||
[[package]]
|
||||
name = "twilio"
|
||||
version = "9.3.3"
|
||||
version = "9.3.4"
|
||||
description = "Twilio API client and TwiML generator"
|
||||
optional = false
|
||||
python-versions = ">=3.7.0"
|
||||
files = [
|
||||
{file = "twilio-9.3.3-py2.py3-none-any.whl", hash = "sha256:716a38a96867d4e233cf540ee9b79eb8b2f839ee72ccbec0331829d20beccdcd"},
|
||||
{file = "twilio-9.3.3.tar.gz", hash = "sha256:4750f7b512258fa1cf61f6666f3f93ddbf850449745cbbc3beec6ea59a813153"},
|
||||
{file = "twilio-9.3.4-py2.py3-none-any.whl", hash = "sha256:2cae99f0f7aecbd9da02fa59ad8f11b360db4a9281fc3fb3237ad50be21d8a9b"},
|
||||
{file = "twilio-9.3.4.tar.gz", hash = "sha256:38a6ab04752f44313dcf736eae45236a901528d3f53dfc21d3afd33539243c7f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -5488,4 +5564,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "~3.12"
|
||||
content-hash = "f3bd82b8ae975dbb660a97fe248f118f780e43687d082d49f37a2d53b450adda"
|
||||
content-hash = "10aa88f2f0e56cddd91adba8c39c52de92763429fb615a27c3dc218952cff808"
|
||||
|
@ -115,6 +115,7 @@ flower = "*"
|
||||
geoip2 = "*"
|
||||
google-api-python-client = "*"
|
||||
gunicorn = "*"
|
||||
gssapi = "*"
|
||||
jsonpatch = "*"
|
||||
jwcrypto = "*"
|
||||
kubernetes = "*"
|
||||
@ -130,6 +131,8 @@ pydantic-scim = "*"
|
||||
pyjwt = "*"
|
||||
pyrad = "*"
|
||||
python = "~3.12"
|
||||
# Fork of python-kadmin with compilation fixes as it's unmaintained
|
||||
python-kadmin = { git = "https://github.com/authentik-community/python-kadmin.git", tag = "v0.2.0" }
|
||||
pyyaml = "*"
|
||||
requests-oauthlib = "*"
|
||||
scim2-filter-parser = "*"
|
||||
@ -162,6 +165,7 @@ debugpy = "*"
|
||||
drf-jsonschema-serializer = "*"
|
||||
freezegun = "*"
|
||||
importlib-metadata = "*"
|
||||
k5test = "*"
|
||||
pdoc = "*"
|
||||
pytest = "*"
|
||||
pytest-django = "*"
|
||||
|
2517
schema.yml
2517
schema.yml
File diff suppressed because it is too large
Load Diff
@ -7,8 +7,8 @@ staticClients:
|
||||
- id: example-app
|
||||
name: Example App
|
||||
redirectURIs:
|
||||
- {{ .Env.AK_REDIRECT_URL }}
|
||||
secret: {{ .Env.AK_CLIENT_SECRET }}
|
||||
- "{{ .Env.AK_REDIRECT_URL }}"
|
||||
secret: "{{ .Env.AK_CLIENT_SECRET }}"
|
||||
staticPasswords:
|
||||
- email: admin@example.com
|
||||
# hash for 'password', for testing
|
||||
|
@ -13,7 +13,7 @@ from selenium.webdriver.support.wait import WebDriverWait
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
@ -23,7 +23,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
"""test OAuth Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.client_secret = generate_key()
|
||||
self.client_secret = generate_id()
|
||||
self.slug = generate_id()
|
||||
super().setUp()
|
||||
self.run_container(
|
||||
|
BIN
web/authentik/sources/kerberos.png
Normal file
BIN
web/authentik/sources/kerberos.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
83
web/package-lock.json
generated
83
web/package-lock.json
generated
@ -23,7 +23,8 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2024.8.3-1729166675",
|
||||
"@goauthentik/api": "^2024.8.3-1729699127",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
@ -41,6 +42,7 @@
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.38.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"dompurify": "^3.1.7",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.2.0",
|
||||
@ -69,6 +71,7 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.2",
|
||||
@ -1772,9 +1775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.8.3-1729166675",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729166675.tgz",
|
||||
"integrity": "sha512-mAcVaMHB2KGGGigUgu3aurSo05yBLarbfvEnvhUs2pvxaTSuX5Zezl34OlFh/gGWQEt0Z7St7GGaY256F3E74g=="
|
||||
"version": "2024.8.3-1729699127",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729699127.tgz",
|
||||
"integrity": "sha512-luo0SAASR6BTTtLszDgfdwofBejv4F3hCHgPxeSoTSFgE8/A2+zJD8EtWPZaa1udDkwPa9lbIeJSSmbgFke3jA=="
|
||||
},
|
||||
"node_modules/@goauthentik/web": {
|
||||
"resolved": "",
|
||||
@ -2459,11 +2462,50 @@
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lit-labs/ssr": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr/-/ssr-3.2.2.tgz",
|
||||
"integrity": "sha512-He5TzeNPM9ECmVpgXRYmVlz0UA5YnzHlT43kyLi2Lu6mUidskqJVonk9W5K699+2DKhoXp8Ra4EJmHR6KrcW1Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-client": "^1.1.7",
|
||||
"@lit-labs/ssr-dom-shim": "^1.2.0",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@parse5/tools": "^0.3.0",
|
||||
"@types/node": "^16.0.0",
|
||||
"enhanced-resolve": "^5.10.0",
|
||||
"lit": "^3.1.2",
|
||||
"lit-element": "^4.0.4",
|
||||
"lit-html": "^3.1.2",
|
||||
"node-fetch": "^3.2.8",
|
||||
"parse5": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=13.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lit-labs/ssr-client": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-client/-/ssr-client-1.1.7.tgz",
|
||||
"integrity": "sha512-VvqhY/iif3FHrlhkzEPsuX/7h/NqnfxLwVf0p8ghNIlKegRyRqgeaJevZ57s/u/LiFyKgqksRP5n+LmNvpxN+A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"lit": "^3.1.2",
|
||||
"lit-html": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz",
|
||||
"integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ=="
|
||||
},
|
||||
"node_modules/@lit-labs/ssr/node_modules/@types/node": {
|
||||
"version": "16.18.114",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.114.tgz",
|
||||
"integrity": "sha512-7oAtnxrgkMNzyzT443UDWwzkmYew81F1ZSPm3/lsITJfW/WludaSOpegTvUG+UdapcbrtWOtY/E4LyTkhPGJ5Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lit/context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@lit/context/-/context-1.1.2.tgz",
|
||||
@ -3027,7 +3069,6 @@
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@parse5/tools/-/tools-0.3.0.tgz",
|
||||
"integrity": "sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"parse5": "^7.0.0"
|
||||
}
|
||||
@ -5629,6 +5670,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dompurify": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
@ -9676,7 +9727,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
@ -10053,9 +10103,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
|
||||
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ=="
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
|
||||
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
@ -10278,7 +10329,6 @@
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
@ -10316,7 +10366,6 @@
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
@ -12518,8 +12567,7 @@
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||
},
|
||||
"node_modules/grapheme-splitter": {
|
||||
"version": "1.0.4",
|
||||
@ -14969,6 +15017,12 @@
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid/node_modules/dompurify": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz",
|
||||
"integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
@ -15649,7 +15703,6 @@
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
@ -16588,7 +16641,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"entities": "^4.4.0"
|
||||
},
|
||||
@ -19574,7 +19626,6 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
@ -11,7 +11,8 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2024.8.3-1729166675",
|
||||
"@goauthentik/api": "^2024.8.3-1729699127",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
@ -29,6 +30,7 @@
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.38.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"dompurify": "^3.1.7",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.2.0",
|
||||
@ -57,6 +59,7 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/grecaptcha": "^3.0.9",
|
||||
"@types/guacamole-common-js": "^1.5.2",
|
||||
|
@ -1,33 +1,14 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { renderForm } from "@goauthentik/admin/providers/ldap/LDAPProviderFormForm.js";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { html } from "lit";
|
||||
|
||||
import { FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||
import type { LDAPProvider } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
import {
|
||||
bindModeOptions,
|
||||
cryptoCertificateHelp,
|
||||
gidStartNumberHelp,
|
||||
mfaSupportHelp,
|
||||
searchModeOptions,
|
||||
tlsServerNameHelp,
|
||||
uidStartNumberHelp,
|
||||
} from "./LDAPOptionsAndHelp";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-ldap")
|
||||
export class ApplicationWizardApplicationDetails extends WithBrandConfig(BaseProviderPanel) {
|
||||
@ -37,115 +18,7 @@ export class ApplicationWizardApplicationDetails extends WithBrandConfig(BasePro
|
||||
|
||||
return html` <ak-wizard-title>${msg("Configure LDAP Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(provider?.name)}
|
||||
label=${msg("Name")}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
help=${msg("Method's display Name.")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Bind flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.brandFlow=${this.brand.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authenticate.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-radio-input
|
||||
label=${msg("Bind mode")}
|
||||
name="bindMode"
|
||||
.options=${bindModeOptions}
|
||||
.value=${provider?.bindMode}
|
||||
help=${msg("Configure how the outpost authenticates requests.")}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-radio-input
|
||||
label=${msg("Search mode")}
|
||||
name="searchMode"
|
||||
.options=${searchModeOptions}
|
||||
.value=${provider?.searchMode}
|
||||
help=${msg(
|
||||
"Configure how the outpost queries the core authentik server's users.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-switch-input
|
||||
name="openInNewTab"
|
||||
label=${msg("Code-based MFA Support")}
|
||||
?checked=${provider?.mfaSupport ?? true}
|
||||
help=${mfaSupportHelp}
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="baseDn"
|
||||
label=${msg("Base DN")}
|
||||
required
|
||||
value="${first(provider?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
|
||||
.errorMessages=${errors?.baseDn ?? []}
|
||||
help=${msg(
|
||||
"LDAP DN under which bind requests and search requests can be made.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Certificate")}
|
||||
name="certificate"
|
||||
.errorMessages=${errors?.certificate ?? []}
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.certificate ?? nothing)}
|
||||
name="certificate"
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
label=${msg("TLS Server name")}
|
||||
name="tlsServerName"
|
||||
value="${first(provider?.tlsServerName, "")}"
|
||||
.errorMessages=${errors?.tlsServerName ?? []}
|
||||
help=${tlsServerNameHelp}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-number-input
|
||||
label=${msg("UID start number")}
|
||||
required
|
||||
name="uidStartNumber"
|
||||
value="${first(provider?.uidStartNumber, 2000)}"
|
||||
.errorMessages=${errors?.uidStartNumber ?? []}
|
||||
help=${uidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
|
||||
<ak-number-input
|
||||
label=${msg("GID start number")}
|
||||
required
|
||||
name="gidStartNumber"
|
||||
value="${first(provider?.gidStartNumber, 4000)}"
|
||||
.errorMessages=${errors?.gidStartNumber ?? []}
|
||||
help=${gidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
${renderForm(provider, errors, this.brand)}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
@ -119,21 +119,6 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="invalidationFlow"
|
||||
label=${msg("Invalidation flow")}
|
||||
.errorMessages=${errors?.invalidationFlow ?? []}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
${this.renderProxyMode()}
|
||||
|
||||
@ -176,9 +161,11 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
|
||||
<ak-textarea-input
|
||||
name="skipPathRegex"
|
||||
label=${this.mode === ProxyMode.ForwardDomain
|
||||
? msg("Unauthenticated URLs")
|
||||
: msg("Unauthenticated Paths")}
|
||||
label=${
|
||||
this.mode === ProxyMode.ForwardDomain
|
||||
? msg("Unauthenticated URLs")
|
||||
: msg("Unauthenticated Paths")
|
||||
}
|
||||
value=${ifDefined(this.instance?.skipPathRegex)}
|
||||
.errorMessages=${errors?.skipPathRegex ?? []}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
@ -195,6 +182,39 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
</ak-textarea-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Authentication settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
|
@ -103,21 +103,6 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="invalidationFlow"
|
||||
label=${msg("Invalidation flow")}
|
||||
.errorMessages=${errors?.invalidationFlow ?? []}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
@ -160,6 +145,39 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
@ -181,52 +199,60 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.hasSigningKp
|
||||
? html` <ak-form-element-horizontal name="signAssertion">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(provider?.signAssertion, true)}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
${
|
||||
this.hasSigningKp
|
||||
? html` <ak-form-element-horizontal name="signAssertion">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(provider?.signAssertion, true)}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i
|
||||
class="fas fa-check"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Sign assertions")}</span
|
||||
>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When enabled, the assertion element of the SAML response will be signed.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="signResponse">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(provider?.signResponse, false)}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Sign assertions")}</span
|
||||
>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When enabled, the assertion element of the SAML response will be signed.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="signResponse">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(provider?.signResponse, false)}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
<i
|
||||
class="fas fa-check"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Sign responses")}</span
|
||||
>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When enabled, the assertion element of the SAML response will be signed.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>`
|
||||
: nothing}
|
||||
<span class="pf-c-switch__label"
|
||||
>${msg("Sign responses")}</span
|
||||
>
|
||||
</label>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When enabled, the assertion element of the SAML response will be signed.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Verification Certificate")}
|
||||
|
@ -61,6 +61,14 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
|
||||
@query("ak-search-select")
|
||||
search!: SearchSelect<T>;
|
||||
|
||||
/**
|
||||
* When specified and the object instance does not have a flow selected, auto-select the flow with the given slug.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property()
|
||||
defaultFlowSlug?: string;
|
||||
|
||||
@property({ type: String })
|
||||
name: string | null | undefined;
|
||||
|
||||
@ -97,9 +105,12 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
|
||||
* use this method, but several have more complex needs, such as relating to the brand, or just
|
||||
* returning false.
|
||||
*/
|
||||
|
||||
selected(flow: Flow): boolean {
|
||||
return this.currentFlow === flow.pk;
|
||||
let selected = this.currentFlow === flow.pk;
|
||||
if (!this.currentFlow && this.defaultFlowSlug && flow.slug === this.defaultFlowSlug) {
|
||||
selected = true;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
@ -6,6 +6,7 @@ import "@goauthentik/admin/property-mappings/PropertyMappingProviderRadiusForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingProviderSAMLForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingProviderSCIMForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingProviderScopeForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSourceKerberosForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSourceLDAPForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSourceOAuthForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSourcePlexForm";
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import { KerberosSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-property-mapping-source-kerberos-form")
|
||||
export class PropertyMappingSourceKerberosForm extends BasePropertyMappingForm<KerberosSourcePropertyMapping> {
|
||||
docLink(): string {
|
||||
return "/docs/sources/property-mappings/expressions?utm_source=authentik";
|
||||
}
|
||||
|
||||
loadInstance(pk: string): Promise<KerberosSourcePropertyMapping> {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceKerberosRetrieve({
|
||||
pmUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: KerberosSourcePropertyMapping): Promise<KerberosSourcePropertyMapping> {
|
||||
if (this.instance) {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceKerberosUpdate({
|
||||
pmUuid: this.instance.pk,
|
||||
kerberosSourcePropertyMappingRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceKerberosCreate({
|
||||
kerberosSourcePropertyMappingRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-property-mapping-source-kerberos-form": PropertyMappingSourceKerberosForm;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import "@goauthentik/admin/property-mappings/PropertyMappingProviderRadiusForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingProviderSAMLForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingProviderSCIMForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingProviderScopeForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSourceKerberosForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSourceLDAPForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSourceOAuthForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSourcePlexForm";
|
||||
|
@ -8,7 +8,12 @@ import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { GoogleWorkspaceProviderGroup, ProvidersApi, SyncObjectModelEnum } from "@goauthentik/api";
|
||||
import {
|
||||
GoogleWorkspaceProviderGroup,
|
||||
ProvidersApi,
|
||||
ProvidersGoogleWorkspaceSyncObjectCreateRequest,
|
||||
SyncObjectModelEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-google-workspace-groups-list")
|
||||
export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProviderGroup> {
|
||||
@ -31,8 +36,11 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
|
||||
<ak-sync-object-form
|
||||
.provider=${this.providerId}
|
||||
model=${SyncObjectModelEnum.Group}
|
||||
.sync=${new ProvidersApi(DEFAULT_CONFIG)
|
||||
.providersGoogleWorkspaceSyncObjectCreate}
|
||||
.sync=${(data: ProvidersGoogleWorkspaceSyncObjectCreateRequest) => {
|
||||
return new ProvidersApi(
|
||||
DEFAULT_CONFIG,
|
||||
).providersGoogleWorkspaceSyncObjectCreate(data);
|
||||
}}
|
||||
slot="form"
|
||||
>
|
||||
</ak-sync-object-form>
|
||||
|
@ -8,7 +8,12 @@ import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { GoogleWorkspaceProviderUser, ProvidersApi, SyncObjectModelEnum } from "@goauthentik/api";
|
||||
import {
|
||||
GoogleWorkspaceProviderUser,
|
||||
ProvidersApi,
|
||||
ProvidersGoogleWorkspaceSyncObjectCreateRequest,
|
||||
SyncObjectModelEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-google-workspace-users-list")
|
||||
export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProviderUser> {
|
||||
@ -31,8 +36,11 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
|
||||
<ak-sync-object-form
|
||||
.provider=${this.providerId}
|
||||
model=${SyncObjectModelEnum.User}
|
||||
.sync=${new ProvidersApi(DEFAULT_CONFIG)
|
||||
.providersGoogleWorkspaceSyncObjectCreate}
|
||||
.sync=${(data: ProvidersGoogleWorkspaceSyncObjectCreateRequest) => {
|
||||
return new ProvidersApi(
|
||||
DEFAULT_CONFIG,
|
||||
).providersGoogleWorkspaceSyncObjectCreate(data);
|
||||
}}
|
||||
slot="form"
|
||||
>
|
||||
</ak-sync-object-form>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user