Merge branch 'main' into dev
* main: (36 commits) ci: use container registry for container build cache (#9809) core: bump lxml from 5.2.1 to 5.2.2 (#9717) web: bump mermaid from 10.9.0 to 10.9.1 in /web (#9734) core: bump scim2-filter-parser from 0.5.0 to 0.5.1 (#9730) web: bump core-js from 3.37.0 to 3.37.1 in /web (#9733) stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#9729) translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9802) translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9803) core: bump sentry-sdk from 2.2.0 to 2.2.1 (#9807) web: bump the storybook group in /web with 7 updates (#9804) web: bump glob from 10.3.15 to 10.3.16 in /web (#9805) root: docker-compose: remove version top level element (#9631) core, web: update translations (#9790) web: bump API Client version (#9801) web/admin: rework initial wizard pages and add grid layout (#9668) website/integrations: discord: fix typo (#9800) website/integration/netbox: fix group custom pipeline example (#9738) root: add primary-replica db router (#9479) website/integrations: add three more policy-expressions to discord-docs (#5760) website/integrations: netbox: add missing scope configuration (#9491) ...
This commit is contained in:
2
.github/actions/setup/docker-compose.yml
vendored
2
.github/actions/setup/docker-compose.yml
vendored
@ -1,5 +1,3 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:${PSQL_TAG:-16}
|
||||
|
4
.github/workflows/ci-main.yml
vendored
4
.github/workflows/ci-main.yml
vendored
@ -252,8 +252,8 @@ jobs:
|
||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
pr-comment:
|
||||
needs:
|
||||
|
4
.github/workflows/ci-outpost.yml
vendored
4
.github/workflows/ci-outpost.yml
vendored
@ -105,8 +105,8 @@ jobs:
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache,mode=max
|
||||
build-binary:
|
||||
timeout-minutes: 120
|
||||
needs:
|
||||
|
@ -178,6 +178,14 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("include_users", bool, default=True),
|
||||
]
|
||||
)
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
@permission_required("authentik_core.add_user_to_group")
|
||||
@extend_schema(
|
||||
request=UserAccountSerializer,
|
||||
|
79
authentik/core/api/object_types.py
Normal file
79
authentik/core/api/object_types.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""API Utilities"""
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
)
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class TypeCreateSerializer(PassiveSerializer):
|
||||
"""Types of an object that can be created"""
|
||||
|
||||
name = CharField(required=True)
|
||||
description = CharField(required=True)
|
||||
component = CharField(required=True)
|
||||
model_name = CharField(required=True)
|
||||
|
||||
icon_url = CharField(required=False)
|
||||
requires_enterprise = BooleanField(default=False)
|
||||
|
||||
|
||||
class CreatableType:
|
||||
"""Class to inherit from to mark a model as creatable, even if the model itself is marked
|
||||
as abstract"""
|
||||
|
||||
|
||||
class NonCreatableType:
|
||||
"""Class to inherit from to mark a model as non-creatable even if it is not abstract"""
|
||||
|
||||
|
||||
class TypesMixin:
|
||||
"""Mixin which adds an API endpoint to list all possible types that can be created"""
|
||||
|
||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def types(self, request: Request, additional: list[dict] | None = None) -> Response:
|
||||
"""Get all creatable types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
instance = None
|
||||
if subclass._meta.abstract:
|
||||
if not issubclass(subclass, CreatableType):
|
||||
continue
|
||||
# Circumvent the django protection for not being able to instantiate
|
||||
# abstract models. We need a model instance to access .component
|
||||
# and further down .icon_url
|
||||
instance = subclass.__new__(subclass)
|
||||
# Django re-sets abstract = False so we need to override that
|
||||
instance.Meta.abstract = True
|
||||
else:
|
||||
if issubclass(subclass, NonCreatableType):
|
||||
continue
|
||||
instance = subclass()
|
||||
try:
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": instance.component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
"icon_url": getattr(instance, "icon_url", None),
|
||||
"requires_enterprise": isinstance(
|
||||
subclass._meta.app_config, EnterpriseConfig
|
||||
),
|
||||
}
|
||||
)
|
||||
except NotImplementedError:
|
||||
continue
|
||||
if additional:
|
||||
data.extend(additional)
|
||||
data = sorted(data, key=lambda x: x["name"])
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
@ -15,12 +15,15 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.blueprints.api import ManagedSerializer
|
||||
from authentik.core.api.object_types import TypesMixin
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
|
||||
from authentik.core.api.utils import (
|
||||
MetaNameSerializer,
|
||||
PassiveSerializer,
|
||||
)
|
||||
from authentik.core.expression.evaluator import PropertyMappingEvaluator
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.events.utils import sanitize_item
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
from authentik.policies.api.exec import PolicyTestSerializer
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
@ -64,6 +67,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
||||
|
||||
|
||||
class PropertyMappingViewSet(
|
||||
TypesMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
@ -83,23 +87,6 @@ class PropertyMappingViewSet(
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return PropertyMapping.objects.select_subclasses()
|
||||
|
||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable property-mapping types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
subclass: PropertyMapping
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@permission_required("authentik_core.view_propertymapping")
|
||||
@extend_schema(
|
||||
request=PolicyTestSerializer(),
|
||||
|
@ -5,20 +5,15 @@ from django.db.models.query import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_filters.filters import BooleanFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.core.api.object_types import TypesMixin
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
@ -86,6 +81,7 @@ class ProviderFilter(FilterSet):
|
||||
|
||||
|
||||
class ProviderViewSet(
|
||||
TypesMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
@ -104,31 +100,3 @@ class ProviderViewSet(
|
||||
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return Provider.objects.select_subclasses()
|
||||
|
||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable provider types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
subclass: Provider
|
||||
if subclass._meta.abstract:
|
||||
continue
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
"requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig),
|
||||
}
|
||||
)
|
||||
data.append(
|
||||
{
|
||||
"name": _("SAML Provider from Metadata"),
|
||||
"description": _("Create a SAML Provider by importing its Metadata."),
|
||||
"component": "ak-provider-saml-import-form",
|
||||
"model_name": "",
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
@ -17,8 +17,9 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.object_types import TypesMixin
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.models import Source, UserSourceConnection
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.lib.utils.file import (
|
||||
@ -27,7 +28,6 @@ from authentik.lib.utils.file import (
|
||||
set_file,
|
||||
set_file_url,
|
||||
)
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.rbac.decorators import permission_required
|
||||
|
||||
@ -74,6 +74,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
|
||||
|
||||
class SourceViewSet(
|
||||
TypesMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
@ -132,30 +133,6 @@ class SourceViewSet(
|
||||
source: Source = self.get_object()
|
||||
return set_file_url(request, source, "icon")
|
||||
|
||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable source types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
subclass: Source
|
||||
component = ""
|
||||
if len(subclass.__subclasses__()) > 0:
|
||||
continue
|
||||
if subclass._meta.abstract:
|
||||
component = subclass.__bases__[0]().component
|
||||
else:
|
||||
component = subclass().component
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@extend_schema(responses={200: UserSettingSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def user_settings(self, request: Request) -> Response:
|
||||
|
@ -6,8 +6,16 @@ from django.db.models import Model
|
||||
from drf_spectacular.extensions import OpenApiSerializerFieldExtension
|
||||
from drf_spectacular.plumbing import build_basic_type
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework.fields import BooleanField, CharField, IntegerField, JSONField
|
||||
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
|
||||
from rest_framework.fields import (
|
||||
CharField,
|
||||
IntegerField,
|
||||
JSONField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.serializers import (
|
||||
Serializer,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
def is_dict(value: Any):
|
||||
@ -68,16 +76,6 @@ class MetaNameSerializer(PassiveSerializer):
|
||||
return f"{obj._meta.app_label}.{obj._meta.model_name}"
|
||||
|
||||
|
||||
class TypeCreateSerializer(PassiveSerializer):
|
||||
"""Types of an object that can be created"""
|
||||
|
||||
name = CharField(required=True)
|
||||
description = CharField(required=True)
|
||||
component = CharField(required=True)
|
||||
model_name = CharField(required=True)
|
||||
requires_enterprise = BooleanField(default=False)
|
||||
|
||||
|
||||
class CacheSerializer(PassiveSerializer):
|
||||
"""Generic cache stats for an object"""
|
||||
|
||||
|
@ -31,8 +31,9 @@ class InbuiltBackend(ModelBackend):
|
||||
# Since we can't directly pass other variables to signals, and we want to log the method
|
||||
# and the token used, we assume we're running in a flow and set a variable in the context
|
||||
flow_plan: FlowPlan = request.session.get(SESSION_KEY_PLAN, FlowPlan(""))
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD] = method
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = cleanse_dict(sanitize_dict(kwargs))
|
||||
flow_plan.context.setdefault(PLAN_CONTEXT_METHOD, method)
|
||||
flow_plan.context.setdefault(PLAN_CONTEXT_METHOD_ARGS, {})
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS].update(cleanse_dict(sanitize_dict(kwargs)))
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Property Mapping Evaluator"""
|
||||
|
||||
from types import CodeType
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import Model
|
||||
@ -24,6 +25,8 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
||||
"""Custom Evaluator that adds some different context variables."""
|
||||
|
||||
dry_run: bool
|
||||
model: Model
|
||||
_compiled: CodeType | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -33,23 +36,32 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
||||
dry_run: bool | None = False,
|
||||
**kwargs,
|
||||
):
|
||||
self.model = model
|
||||
if hasattr(model, "name"):
|
||||
_filename = model.name
|
||||
else:
|
||||
_filename = str(model)
|
||||
super().__init__(filename=_filename)
|
||||
self.dry_run = dry_run
|
||||
self.set_context(user, request, **kwargs)
|
||||
|
||||
def set_context(
|
||||
self,
|
||||
user: User | None = None,
|
||||
request: HttpRequest | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
req = PolicyRequest(user=User())
|
||||
req.obj = model
|
||||
req.obj = self.model
|
||||
if user:
|
||||
req.user = user
|
||||
self._context["user"] = user
|
||||
if request:
|
||||
req.http_request = request
|
||||
self._context["request"] = req
|
||||
req.context.update(**kwargs)
|
||||
self._context["request"] = req
|
||||
self._context.update(**kwargs)
|
||||
self._globals["SkipObject"] = SkipObjectException
|
||||
self.dry_run = dry_run
|
||||
|
||||
def handle_error(self, exc: Exception, expression_source: str):
|
||||
"""Exception Handler"""
|
||||
@ -71,3 +83,9 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
||||
def evaluate(self, *args, **kwargs) -> Any:
|
||||
with PROPERTY_MAPPING_TIME.labels(mapping_name=self._filename).time():
|
||||
return super().evaluate(*args, **kwargs)
|
||||
|
||||
def compile(self, expression: str | None = None) -> Any:
|
||||
if not self._compiled:
|
||||
compiled = super().compile(expression or self.model.expression)
|
||||
self._compiled = compiled
|
||||
return self._compiled
|
||||
|
@ -6,6 +6,11 @@ from authentik.lib.sentry import SentryIgnoredException
|
||||
class PropertyMappingExpressionException(SentryIgnoredException):
|
||||
"""Error when a PropertyMapping Exception expression could not be parsed or evaluated."""
|
||||
|
||||
def __init__(self, exc: Exception, mapping) -> None:
|
||||
super().__init__()
|
||||
self.exc = exc
|
||||
self.mapping = mapping
|
||||
|
||||
|
||||
class SkipObjectException(PropertyMappingExpressionException):
|
||||
"""Exception which can be raised in a property mapping to skip syncing an object.
|
||||
|
@ -377,6 +377,10 @@ class Provider(SerializerModel):
|
||||
Can return None for providers that are not URL-based"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
"""Return component used to edit this object"""
|
||||
@ -768,7 +772,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
||||
try:
|
||||
return evaluator.evaluate(self.expression)
|
||||
except Exception as exc:
|
||||
raise PropertyMappingExpressionException(exc) from exc
|
||||
raise PropertyMappingExpressionException(self, exc) from exc
|
||||
|
||||
def __str__(self):
|
||||
return f"Property Mapping {self.name}"
|
||||
|
@ -23,6 +23,17 @@ class TestGroupsAPI(APITestCase):
|
||||
response = self.client.get(reverse("authentik_api:group-list"), {"include_users": "true"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_retrieve_with_users(self):
|
||||
"""Test retrieve with users"""
|
||||
admin = create_test_admin_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
self.client.force_login(admin)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
||||
{"include_users": "true"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_add_user(self):
|
||||
"""Test add_user"""
|
||||
group = Group.objects.create(name=generate_id())
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""authentik core models tests"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from time import sleep
|
||||
from datetime import timedelta
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.utils.timezone import now
|
||||
from freezegun import freeze_time
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.models import Provider, Source, Token
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
@ -17,18 +17,20 @@ class TestModels(TestCase):
|
||||
|
||||
def test_token_expire(self):
|
||||
"""Test token expiring"""
|
||||
token = Token.objects.create(expires=now(), user=get_anonymous_user())
|
||||
sleep(0.5)
|
||||
self.assertTrue(token.is_expired)
|
||||
with freeze_time() as freeze:
|
||||
token = Token.objects.create(expires=now(), user=get_anonymous_user())
|
||||
freeze.tick(timedelta(seconds=1))
|
||||
self.assertTrue(token.is_expired)
|
||||
|
||||
def test_token_expire_no_expire(self):
|
||||
"""Test token expiring with "expiring" set"""
|
||||
token = Token.objects.create(expires=now(), user=get_anonymous_user(), expiring=False)
|
||||
sleep(0.5)
|
||||
self.assertFalse(token.is_expired)
|
||||
with freeze_time() as freeze:
|
||||
token = Token.objects.create(expires=now(), user=get_anonymous_user(), expiring=False)
|
||||
freeze.tick(timedelta(seconds=1))
|
||||
self.assertFalse(token.is_expired)
|
||||
|
||||
|
||||
def source_tester_factory(test_model: type[Stage]) -> Callable:
|
||||
def source_tester_factory(test_model: type[Source]) -> Callable:
|
||||
"""Test source"""
|
||||
|
||||
factory = RequestFactory()
|
||||
@ -36,19 +38,19 @@ def source_tester_factory(test_model: type[Stage]) -> Callable:
|
||||
|
||||
def tester(self: TestModels):
|
||||
model_class = None
|
||||
if test_model._meta.abstract: # pragma: no cover
|
||||
model_class = test_model.__bases__[0]()
|
||||
if test_model._meta.abstract:
|
||||
model_class = [x for x in test_model.__bases__ if issubclass(x, Source)][0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
model_class.slug = "test"
|
||||
self.assertIsNotNone(model_class.component)
|
||||
_ = model_class.ui_login_button(request)
|
||||
_ = model_class.ui_user_settings()
|
||||
model_class.ui_login_button(request)
|
||||
model_class.ui_user_settings()
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
def provider_tester_factory(test_model: type[Stage]) -> Callable:
|
||||
def provider_tester_factory(test_model: type[Provider]) -> Callable:
|
||||
"""Test provider"""
|
||||
|
||||
def tester(self: TestModels):
|
||||
|
@ -1,28 +1,22 @@
|
||||
from deepmerge import always_merger
|
||||
from django.db import transaction
|
||||
from django.utils.text import slugify
|
||||
|
||||
from authentik.core.expression.exceptions import (
|
||||
PropertyMappingExpressionException,
|
||||
SkipObjectException,
|
||||
)
|
||||
from authentik.core.models import Group
|
||||
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
|
||||
from authentik.enterprise.providers.google_workspace.models import (
|
||||
GoogleWorkspaceProvider,
|
||||
GoogleWorkspaceProviderGroup,
|
||||
GoogleWorkspaceProviderMapping,
|
||||
GoogleWorkspaceProviderUser,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.base import Direction
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
NotFoundSyncException,
|
||||
ObjectExistsSyncException,
|
||||
StopSync,
|
||||
TransientSyncException,
|
||||
)
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
|
||||
class GoogleWorkspaceGroupClient(
|
||||
@ -34,41 +28,21 @@ class GoogleWorkspaceGroupClient(
|
||||
connection_type_query = "group"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: GoogleWorkspaceProvider) -> None:
|
||||
super().__init__(provider)
|
||||
self.mapper = PropertyMappingManager(
|
||||
self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
|
||||
GoogleWorkspaceProviderMapping,
|
||||
["group", "provider", "creating"],
|
||||
)
|
||||
|
||||
def to_schema(self, obj: Group, creating: bool) -> dict:
|
||||
"""Convert authentik group"""
|
||||
raw_google_group = {
|
||||
"email": f"{slugify(obj.name)}@{self.provider.default_group_email_domain}"
|
||||
}
|
||||
for mapping in (
|
||||
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
|
||||
):
|
||||
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
|
||||
continue
|
||||
try:
|
||||
value = mapping.evaluate(
|
||||
user=None,
|
||||
request=None,
|
||||
group=obj,
|
||||
provider=self.provider,
|
||||
creating=creating,
|
||||
)
|
||||
if value is None:
|
||||
continue
|
||||
always_merger.merge(raw_google_group, value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except (PropertyMappingExpressionException, ValueError) as exc:
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
mapping=mapping,
|
||||
).save()
|
||||
raise StopSync(exc, obj, mapping) from exc
|
||||
if not raw_google_group:
|
||||
raise StopSync(ValueError("No group mappings configured"), obj)
|
||||
|
||||
return raw_google_group
|
||||
return super().to_schema(
|
||||
obj,
|
||||
creating,
|
||||
email=f"{slugify(obj.name)}@{self.provider.default_group_email_domain}",
|
||||
)
|
||||
|
||||
def delete(self, obj: Group):
|
||||
"""Delete group"""
|
||||
|
@ -1,24 +1,18 @@
|
||||
from deepmerge import always_merger
|
||||
from django.db import transaction
|
||||
|
||||
from authentik.core.expression.exceptions import (
|
||||
PropertyMappingExpressionException,
|
||||
SkipObjectException,
|
||||
)
|
||||
from authentik.core.models import User
|
||||
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
|
||||
from authentik.enterprise.providers.google_workspace.models import (
|
||||
GoogleWorkspaceProvider,
|
||||
GoogleWorkspaceProviderMapping,
|
||||
GoogleWorkspaceProviderUser,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
ObjectExistsSyncException,
|
||||
StopSync,
|
||||
TransientSyncException,
|
||||
)
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.policies.utils import delete_none_values
|
||||
|
||||
|
||||
@ -29,34 +23,17 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
|
||||
connection_type_query = "user"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: GoogleWorkspaceProvider) -> None:
|
||||
super().__init__(provider)
|
||||
self.mapper = PropertyMappingManager(
|
||||
self.provider.property_mappings.all().order_by("name").select_subclasses(),
|
||||
GoogleWorkspaceProviderMapping,
|
||||
["provider", "creating"],
|
||||
)
|
||||
|
||||
def to_schema(self, obj: User, creating: bool) -> dict:
|
||||
"""Convert authentik user"""
|
||||
raw_google_user = {}
|
||||
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
|
||||
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
|
||||
continue
|
||||
try:
|
||||
value = mapping.evaluate(
|
||||
user=obj,
|
||||
request=None,
|
||||
provider=self.provider,
|
||||
creating=creating,
|
||||
)
|
||||
if value is None:
|
||||
continue
|
||||
always_merger.merge(raw_google_user, value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except (PropertyMappingExpressionException, ValueError) as exc:
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
mapping=mapping,
|
||||
).save()
|
||||
raise StopSync(exc, obj, mapping) from exc
|
||||
if not raw_google_user:
|
||||
raise StopSync(ValueError("No user mappings configured"), obj)
|
||||
raw_google_user = super().to_schema(obj, creating)
|
||||
if "primaryEmail" not in raw_google_user:
|
||||
raw_google_user["primaryEmail"] = str(obj.email)
|
||||
return delete_none_values(raw_google_user)
|
||||
|
@ -5,6 +5,7 @@ from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from google.oauth2.service_account import Credentials
|
||||
from rest_framework.serializers import Serializer
|
||||
@ -98,6 +99,10 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
).with_subject(self.delegated_subject),
|
||||
}
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/google.svg")
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-provider-google-workspace-form"
|
||||
|
@ -82,6 +82,27 @@ class GoogleWorkspaceGroupTests(TestCase):
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
self.assertEqual(len(http.requests()), 2)
|
||||
|
||||
def test_group_not_created(self):
|
||||
"""Test without group property mappings, no group is created"""
|
||||
self.provider.property_mappings_group.clear()
|
||||
uid = generate_id()
|
||||
http = MockHTTP()
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
|
||||
domains_list_v1_mock,
|
||||
)
|
||||
with patch(
|
||||
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
|
||||
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
|
||||
):
|
||||
group = Group.objects.create(name=uid)
|
||||
google_group = GoogleWorkspaceProviderGroup.objects.filter(
|
||||
provider=self.provider, group=group
|
||||
).first()
|
||||
self.assertIsNone(google_group)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
self.assertEqual(len(http.requests()), 1)
|
||||
|
||||
def test_group_create_update(self):
|
||||
"""Test group updating"""
|
||||
uid = generate_id()
|
||||
|
@ -86,6 +86,31 @@ class GoogleWorkspaceUserTests(TestCase):
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
self.assertEqual(len(http.requests()), 2)
|
||||
|
||||
def test_user_not_created(self):
|
||||
"""Test without property mappings, no group is created"""
|
||||
self.provider.property_mappings.clear()
|
||||
uid = generate_id()
|
||||
http = MockHTTP()
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
|
||||
domains_list_v1_mock,
|
||||
)
|
||||
with patch(
|
||||
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
|
||||
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
|
||||
):
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
google_user = GoogleWorkspaceProviderUser.objects.filter(
|
||||
provider=self.provider, user=user
|
||||
).first()
|
||||
self.assertIsNone(google_user)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
self.assertEqual(len(http.requests()), 1)
|
||||
|
||||
def test_user_create_update(self):
|
||||
"""Test user updating"""
|
||||
uid = generate_id()
|
||||
|
@ -1,21 +1,17 @@
|
||||
from deepmerge import always_merger
|
||||
from django.db import transaction
|
||||
from msgraph.generated.groups.groups_request_builder import GroupsRequestBuilder
|
||||
from msgraph.generated.models.group import Group as MSGroup
|
||||
from msgraph.generated.models.reference_create import ReferenceCreate
|
||||
|
||||
from authentik.core.expression.exceptions import (
|
||||
PropertyMappingExpressionException,
|
||||
SkipObjectException,
|
||||
)
|
||||
from authentik.core.models import Group
|
||||
from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient
|
||||
from authentik.enterprise.providers.microsoft_entra.models import (
|
||||
MicrosoftEntraProvider,
|
||||
MicrosoftEntraProviderGroup,
|
||||
MicrosoftEntraProviderMapping,
|
||||
MicrosoftEntraProviderUser,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.base import Direction
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
NotFoundSyncException,
|
||||
@ -24,7 +20,6 @@ from authentik.lib.sync.outgoing.exceptions import (
|
||||
TransientSyncException,
|
||||
)
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
|
||||
class MicrosoftEntraGroupClient(
|
||||
@ -36,37 +31,17 @@ class MicrosoftEntraGroupClient(
|
||||
connection_type_query = "group"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: MicrosoftEntraProvider) -> None:
|
||||
super().__init__(provider)
|
||||
self.mapper = PropertyMappingManager(
|
||||
self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
|
||||
MicrosoftEntraProviderMapping,
|
||||
["group", "provider", "creating"],
|
||||
)
|
||||
|
||||
def to_schema(self, obj: Group, creating: bool) -> MSGroup:
|
||||
"""Convert authentik group"""
|
||||
raw_microsoft_group = {}
|
||||
for mapping in (
|
||||
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
|
||||
):
|
||||
if not isinstance(mapping, MicrosoftEntraProviderMapping):
|
||||
continue
|
||||
try:
|
||||
value = mapping.evaluate(
|
||||
user=None,
|
||||
request=None,
|
||||
group=obj,
|
||||
provider=self.provider,
|
||||
creating=creating,
|
||||
)
|
||||
if value is None:
|
||||
continue
|
||||
always_merger.merge(raw_microsoft_group, value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except (PropertyMappingExpressionException, ValueError) as exc:
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
mapping=mapping,
|
||||
).save()
|
||||
raise StopSync(exc, obj, mapping) from exc
|
||||
if not raw_microsoft_group:
|
||||
raise StopSync(ValueError("No group mappings configured"), obj)
|
||||
raw_microsoft_group = super().to_schema(obj, creating)
|
||||
try:
|
||||
return MSGroup(**raw_microsoft_group)
|
||||
except TypeError as exc:
|
||||
|
@ -1,26 +1,21 @@
|
||||
from deepmerge import always_merger
|
||||
from django.db import transaction
|
||||
from msgraph.generated.models.user import User as MSUser
|
||||
from msgraph.generated.users.users_request_builder import UsersRequestBuilder
|
||||
|
||||
from authentik.core.expression.exceptions import (
|
||||
PropertyMappingExpressionException,
|
||||
SkipObjectException,
|
||||
)
|
||||
from authentik.core.models import User
|
||||
from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient
|
||||
from authentik.enterprise.providers.microsoft_entra.models import (
|
||||
MicrosoftEntraProvider,
|
||||
MicrosoftEntraProviderMapping,
|
||||
MicrosoftEntraProviderUser,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
ObjectExistsSyncException,
|
||||
StopSync,
|
||||
TransientSyncException,
|
||||
)
|
||||
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.policies.utils import delete_none_values
|
||||
|
||||
|
||||
@ -31,34 +26,17 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
|
||||
connection_type_query = "user"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: MicrosoftEntraProvider) -> None:
|
||||
super().__init__(provider)
|
||||
self.mapper = PropertyMappingManager(
|
||||
self.provider.property_mappings.all().order_by("name").select_subclasses(),
|
||||
MicrosoftEntraProviderMapping,
|
||||
["provider", "creating"],
|
||||
)
|
||||
|
||||
def to_schema(self, obj: User, creating: bool) -> MSUser:
|
||||
"""Convert authentik user"""
|
||||
raw_microsoft_user = {}
|
||||
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
|
||||
if not isinstance(mapping, MicrosoftEntraProviderMapping):
|
||||
continue
|
||||
try:
|
||||
value = mapping.evaluate(
|
||||
user=obj,
|
||||
request=None,
|
||||
provider=self.provider,
|
||||
creating=creating,
|
||||
)
|
||||
if value is None:
|
||||
continue
|
||||
always_merger.merge(raw_microsoft_user, value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except (PropertyMappingExpressionException, ValueError) as exc:
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
mapping=mapping,
|
||||
).save()
|
||||
raise StopSync(exc, obj, mapping) from exc
|
||||
if not raw_microsoft_user:
|
||||
raise StopSync(ValueError("No user mappings configured"), obj)
|
||||
raw_microsoft_user = super().to_schema(obj, creating)
|
||||
try:
|
||||
return MSUser(**delete_none_values(raw_microsoft_user))
|
||||
except TypeError as exc:
|
||||
|
@ -6,6 +6,7 @@ from uuid import uuid4
|
||||
from azure.identity.aio import ClientSecretCredential
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
@ -87,6 +88,10 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
)
|
||||
}
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/azuread.svg")
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-provider-microsoft-entra-form"
|
||||
|
@ -93,6 +93,38 @@ class MicrosoftEntraGroupTests(TestCase):
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
group_create.assert_called_once()
|
||||
|
||||
def test_group_not_created(self):
|
||||
"""Test without group property mappings, no group is created"""
|
||||
self.provider.property_mappings_group.clear()
|
||||
uid = generate_id()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
|
||||
AsyncMock(return_value=MSGroup(id=generate_id())),
|
||||
) as group_create,
|
||||
):
|
||||
group = Group.objects.create(name=uid)
|
||||
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
|
||||
provider=self.provider, group=group
|
||||
).first()
|
||||
self.assertIsNone(microsoft_group)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
group_create.assert_not_called()
|
||||
|
||||
def test_group_create_update(self):
|
||||
"""Test group updating"""
|
||||
uid = generate_id()
|
||||
|
@ -94,6 +94,42 @@ class MicrosoftEntraUserTests(TestCase):
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
user_create.assert_called_once()
|
||||
|
||||
def test_user_not_created(self):
|
||||
"""Test without property mappings, no group is created"""
|
||||
self.provider.property_mappings.clear()
|
||||
uid = generate_id()
|
||||
with (
|
||||
patch(
|
||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
||||
MagicMock(return_value={"credentials": self.creds}),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
||||
AsyncMock(
|
||||
return_value=OrganizationCollectionResponse(
|
||||
value=[
|
||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
||||
]
|
||||
)
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
|
||||
AsyncMock(return_value=MSUser(id=generate_id())),
|
||||
) as user_create,
|
||||
):
|
||||
user = User.objects.create(
|
||||
username=uid,
|
||||
name=f"{uid} {uid}",
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
|
||||
provider=self.provider, user=user
|
||||
).first()
|
||||
self.assertIsNone(microsoft_user)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
user_create.assert_not_called()
|
||||
|
||||
def test_user_create_update(self):
|
||||
"""Test user updating"""
|
||||
uid = generate_id()
|
||||
|
@ -7,6 +7,7 @@ from deepmerge import always_merger
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
@ -63,6 +64,10 @@ class RACProvider(Provider):
|
||||
Can return None for providers that are not URL-based"""
|
||||
return "goauthentik.io://providers/rac/launch"
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/rac.svg")
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-provider-rac-form"
|
||||
|
@ -19,7 +19,8 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.admin.api.metrics import CoordinateSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
|
||||
from authentik.core.api.object_types import TypeCreateSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
||||
|
@ -10,10 +10,10 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.object_types import TypesMixin
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.enterprise.apps import EnterpriseConfig
|
||||
from authentik.flows.api.flows import FlowSetSerializer
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
@ -47,6 +47,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
|
||||
|
||||
|
||||
class StageViewSet(
|
||||
TypesMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
@ -63,25 +64,6 @@ class StageViewSet(
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return Stage.objects.select_subclasses().prefetch_related("flow_set")
|
||||
|
||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable stage types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model, False):
|
||||
subclass: Stage
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
"requires_enterprise": isinstance(subclass._meta.app_config, EnterpriseConfig),
|
||||
}
|
||||
)
|
||||
data = sorted(data, key=lambda x: x["name"])
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@extend_schema(responses={200: UserSettingSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def user_settings(self, request: Request) -> Response:
|
||||
|
@ -304,6 +304,12 @@ class ConfigLoader:
|
||||
"""Wrapper for get that converts value into boolean"""
|
||||
return str(self.get(path, default)).lower() == "true"
|
||||
|
||||
def get_keys(self, path: str, sep=".") -> list[str]:
|
||||
"""List attribute keys by using yaml path"""
|
||||
root = self.raw
|
||||
attr: Attr = get_path_from_dict(root, path, sep=sep, default=Attr({}))
|
||||
return attr.keys()
|
||||
|
||||
def get_dict_from_b64_json(self, path: str, default=None) -> dict:
|
||||
"""Wrapper for get that converts value from Base64 encoded string into dictionary"""
|
||||
config_value = self.get(path)
|
||||
|
@ -10,6 +10,10 @@ postgresql:
|
||||
use_pgpool: false
|
||||
test:
|
||||
name: test_authentik
|
||||
read_replicas: {}
|
||||
# For example
|
||||
# 0:
|
||||
# host: replica1.example.com
|
||||
|
||||
listen:
|
||||
listen_http: 0.0.0.0:9000
|
||||
|
@ -5,6 +5,7 @@ import socket
|
||||
from collections.abc import Iterable
|
||||
from ipaddress import ip_address, ip_network
|
||||
from textwrap import indent
|
||||
from types import CodeType
|
||||
from typing import Any
|
||||
|
||||
from cachetools import TLRUCache, cached
|
||||
@ -184,7 +185,7 @@ class BaseEvaluator:
|
||||
full_expression += f"\nresult = handler({handler_signature})"
|
||||
return full_expression
|
||||
|
||||
def compile(self, expression: str) -> Any:
|
||||
def compile(self, expression: str) -> CodeType:
|
||||
"""Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
|
||||
param_keys = self._context.keys()
|
||||
return compile(self.wrap_expression(expression, param_keys), self._filename, "exec")
|
||||
|
67
authentik/lib/sync/mapper.py
Normal file
67
authentik/lib/sync/mapper.py
Normal file
@ -0,0 +1,67 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpRequest
|
||||
|
||||
from authentik.core.expression.evaluator import PropertyMappingEvaluator
|
||||
from authentik.core.expression.exceptions import PropertyMappingExpressionException
|
||||
from authentik.core.models import PropertyMapping, User
|
||||
|
||||
|
||||
class PropertyMappingManager:
|
||||
"""Pre-compile and cache property mappings when an identical
|
||||
set is used multiple times"""
|
||||
|
||||
query_set: QuerySet[PropertyMapping]
|
||||
mapping_subclass: type[PropertyMapping]
|
||||
|
||||
_evaluators: list[PropertyMappingEvaluator]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
qs: QuerySet[PropertyMapping],
|
||||
# Expected subclass of PropertyMappings, any objects in the queryset
|
||||
# that are not an instance of this class will be discarded
|
||||
mapping_subclass: type[PropertyMapping],
|
||||
# As they keys of parameters are part of the compilation,
|
||||
# we need a list of all parameter names that will be used during evaluation
|
||||
context_keys: list[str],
|
||||
) -> None:
|
||||
self.query_set = qs
|
||||
self.mapping_subclass = mapping_subclass
|
||||
self.context_keys = context_keys
|
||||
self.compile()
|
||||
|
||||
def compile(self):
|
||||
self._evaluators = []
|
||||
for mapping in self.query_set:
|
||||
if not isinstance(mapping, self.mapping_subclass):
|
||||
continue
|
||||
evaluator = PropertyMappingEvaluator(
|
||||
mapping, **{key: None for key in self.context_keys}
|
||||
)
|
||||
# Compile and cache expression
|
||||
evaluator.compile()
|
||||
self._evaluators.append(evaluator)
|
||||
|
||||
def iter_eval(
|
||||
self,
|
||||
user: User | None,
|
||||
request: HttpRequest | None,
|
||||
return_mapping: bool = False,
|
||||
**kwargs,
|
||||
) -> Generator[tuple[dict, PropertyMapping], None]:
|
||||
"""Iterate over all mappings that were pre-compiled and
|
||||
execute all of them with the given context"""
|
||||
for mapping in self._evaluators:
|
||||
mapping.set_context(user, request, **kwargs)
|
||||
try:
|
||||
value = mapping.evaluate(mapping.model.expression)
|
||||
except Exception as exc:
|
||||
raise PropertyMappingExpressionException(exc, mapping.model) from exc
|
||||
if value is None:
|
||||
continue
|
||||
if return_mapping:
|
||||
yield value, mapping.model
|
||||
else:
|
||||
yield value
|
@ -3,10 +3,18 @@
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deepmerge import always_merger
|
||||
from django.db import DatabaseError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException
|
||||
from authentik.core.expression.exceptions import (
|
||||
PropertyMappingExpressionException,
|
||||
SkipObjectException,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import Model
|
||||
@ -28,6 +36,7 @@ class BaseOutgoingSyncClient[
|
||||
provider: TProvider
|
||||
connection_type: type[TConnection]
|
||||
connection_type_query: str
|
||||
mapper: PropertyMappingManager
|
||||
|
||||
can_discover = False
|
||||
|
||||
@ -70,9 +79,35 @@ class BaseOutgoingSyncClient[
|
||||
"""Delete object from destination"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def to_schema(self, obj: TModel, creating: bool) -> TSchema:
|
||||
def to_schema(self, obj: TModel, creating: bool, **defaults) -> TSchema:
|
||||
"""Convert object to destination schema"""
|
||||
raise NotImplementedError()
|
||||
raw_final_object = {}
|
||||
try:
|
||||
eval_kwargs = {
|
||||
"request": None,
|
||||
"provider": self.provider,
|
||||
"creating": creating,
|
||||
obj._meta.model_name: obj,
|
||||
}
|
||||
eval_kwargs.setdefault("user", None)
|
||||
for value in self.mapper.iter_eval(**eval_kwargs):
|
||||
try:
|
||||
always_merger.merge(raw_final_object, value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except PropertyMappingExpressionException as exc:
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
mapping=exc.mapping,
|
||||
).save()
|
||||
raise StopSync(exc, obj, exc.mapping) from exc
|
||||
if not raw_final_object:
|
||||
raise StopSync(ValueError("No user mappings configured"), obj)
|
||||
for key, value in defaults.items():
|
||||
raw_final_object.setdefault(key, value)
|
||||
return raw_final_object
|
||||
|
||||
def discover(self):
|
||||
"""Optional method. Can be used to implement a "discovery" where
|
||||
|
@ -169,3 +169,9 @@ class TestConfig(TestCase):
|
||||
self.assertEqual(config.get("cache.timeout_flows"), "32m")
|
||||
self.assertEqual(config.get("cache.timeout_policies"), "3920ns")
|
||||
self.assertEqual(config.get("cache.timeout_reputation"), "298382us")
|
||||
|
||||
def test_get_keys(self):
|
||||
"""Test get_keys"""
|
||||
config = ConfigLoader()
|
||||
config.set("foo.bar", "baz")
|
||||
self.assertEqual(list(config.get_keys("foo")), ["bar"])
|
||||
|
@ -12,7 +12,7 @@ from authentik.lib.config import CONFIG
|
||||
SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
|
||||
|
||||
|
||||
def all_subclasses(cls, sort=True):
|
||||
def all_subclasses[T](cls: T, sort=True) -> list[T] | set[T]:
|
||||
"""Recursively return all subclassess of cls"""
|
||||
classes = set(cls.__subclasses__()).union(
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c, sort=sort)]
|
||||
|
@ -15,9 +15,12 @@ from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.core.api.object_types import TypesMixin
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
from authentik.core.api.utils import (
|
||||
MetaNameSerializer,
|
||||
PassiveSerializer,
|
||||
)
|
||||
from authentik.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
@ -57,6 +60,7 @@ class ServiceConnectionStateSerializer(PassiveSerializer):
|
||||
|
||||
|
||||
class ServiceConnectionViewSet(
|
||||
TypesMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
@ -70,23 +74,6 @@ class ServiceConnectionViewSet(
|
||||
search_fields = ["name"]
|
||||
filterset_fields = ["name"]
|
||||
|
||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable service connection types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
subclass: OutpostServiceConnection
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
|
||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||
def state(self, request: Request, pk: str) -> Response:
|
||||
|
@ -13,10 +13,13 @@ from rest_framework.viewsets import GenericViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.applications import user_app_cache_key
|
||||
from authentik.core.api.object_types import TypesMixin
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import CacheSerializer, MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.api.utils import (
|
||||
CacheSerializer,
|
||||
MetaNameSerializer,
|
||||
)
|
||||
from authentik.events.logs import LogEventSerializer, capture_logs
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer
|
||||
from authentik.policies.models import Policy, PolicyBinding
|
||||
from authentik.policies.process import PolicyProcess
|
||||
@ -69,6 +72,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
|
||||
|
||||
|
||||
class PolicyViewSet(
|
||||
TypesMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
@ -89,23 +93,6 @@ class PolicyViewSet(
|
||||
def get_queryset(self): # pragma: no cover
|
||||
return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set")
|
||||
|
||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
def types(self, request: Request) -> Response:
|
||||
"""Get all creatable policy types"""
|
||||
data = []
|
||||
for subclass in all_subclasses(self.queryset.model):
|
||||
subclass: Policy
|
||||
data.append(
|
||||
{
|
||||
"name": subclass._meta.verbose_name,
|
||||
"description": subclass.__doc__,
|
||||
"component": subclass().component,
|
||||
"model_name": subclass._meta.model_name,
|
||||
}
|
||||
)
|
||||
return Response(TypeCreateSerializer(data, many=True).data)
|
||||
|
||||
@permission_required(None, ["authentik_policies.view_policy_cache"])
|
||||
@extend_schema(responses={200: CacheSerializer(many=False)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
|
@ -3,6 +3,7 @@
|
||||
from collections.abc import Iterable
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
@ -90,6 +91,10 @@ class LDAPProvider(OutpostModel, BackchannelProvider):
|
||||
def component(self) -> str:
|
||||
return "ak-provider-ldap-form"
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/ldap.png")
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.providers.ldap.api import LDAPProviderSerializer
|
||||
|
@ -15,6 +15,7 @@ from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||
from dacite.core import from_dict
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from jwt import encode
|
||||
@ -262,6 +263,10 @@ class OAuth2Provider(Provider):
|
||||
LOGGER.warning("Failed to format launch url", exc=exc)
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/openidconnect.svg")
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-provider-oauth2-form"
|
||||
|
@ -6,6 +6,7 @@ from random import SystemRandom
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
@ -115,6 +116,10 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
def component(self) -> str:
|
||||
return "ak-provider-proxy-form"
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/proxy.svg")
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.providers.proxy.api import ProxyProviderSerializer
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Radius Provider"""
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
@ -46,6 +47,10 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
def component(self) -> str:
|
||||
return "ak-provider-radius-form"
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/radius.svg")
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.providers.radius.api import RadiusProviderSerializer
|
||||
|
@ -1,11 +1,13 @@
|
||||
"""authentik saml_idp Models"""
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.object_types import CreatableType
|
||||
from authentik.core.models import PropertyMapping, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
@ -159,6 +161,10 @@ class SAMLProvider(Provider):
|
||||
except Provider.application.RelatedObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/saml.png")
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.providers.saml.api.providers import SAMLProviderSerializer
|
||||
@ -200,3 +206,20 @@ class SAMLPropertyMapping(PropertyMapping):
|
||||
class Meta:
|
||||
verbose_name = _("SAML Property Mapping")
|
||||
verbose_name_plural = _("SAML Property Mappings")
|
||||
|
||||
|
||||
class SAMLProviderImportModel(CreatableType, Provider):
|
||||
"""Create a SAML Provider by importing its Metadata."""
|
||||
|
||||
@property
|
||||
def component(self):
|
||||
return "ak-provider-saml-import-form"
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/saml.png")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = _("SAML Provider from Metadata")
|
||||
verbose_name_plural = _("SAML Providers from Metadata")
|
||||
|
@ -1,31 +1,25 @@
|
||||
"""Group client"""
|
||||
|
||||
from deepmerge import always_merger
|
||||
from pydantic import ValidationError
|
||||
from pydanticscim.group import GroupMember
|
||||
from pydanticscim.responses import PatchOp, PatchOperation
|
||||
|
||||
from authentik.core.expression.exceptions import (
|
||||
PropertyMappingExpressionException,
|
||||
SkipObjectException,
|
||||
)
|
||||
from authentik.core.models import Group
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.base import Direction
|
||||
from authentik.lib.sync.outgoing.exceptions import (
|
||||
NotFoundSyncException,
|
||||
ObjectExistsSyncException,
|
||||
StopSync,
|
||||
)
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.policies.utils import delete_none_values
|
||||
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 Group as SCIMGroupSchema
|
||||
from authentik.providers.scim.clients.schema import PatchRequest
|
||||
from authentik.providers.scim.models import SCIMGroup, SCIMMapping, SCIMUser
|
||||
from authentik.providers.scim.models import SCIMGroup, SCIMMapping, SCIMProvider, SCIMUser
|
||||
|
||||
|
||||
class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
|
||||
@ -33,41 +27,23 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
|
||||
|
||||
connection_type = SCIMGroup
|
||||
connection_type_query = "group"
|
||||
mapper: PropertyMappingManager
|
||||
|
||||
def __init__(self, provider: SCIMProvider):
|
||||
super().__init__(provider)
|
||||
self.mapper = PropertyMappingManager(
|
||||
self.provider.property_mappings_group.all().order_by("name").select_subclasses(),
|
||||
SCIMMapping,
|
||||
["group", "provider", "creating"],
|
||||
)
|
||||
|
||||
def to_schema(self, obj: Group, creating: bool) -> SCIMGroupSchema:
|
||||
"""Convert authentik user into SCIM"""
|
||||
raw_scim_group = {
|
||||
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
|
||||
}
|
||||
for mapping in (
|
||||
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
|
||||
):
|
||||
if not isinstance(mapping, SCIMMapping):
|
||||
continue
|
||||
try:
|
||||
mapping: SCIMMapping
|
||||
value = mapping.evaluate(
|
||||
user=None,
|
||||
request=None,
|
||||
group=obj,
|
||||
provider=self.provider,
|
||||
creating=creating,
|
||||
)
|
||||
if value is None:
|
||||
continue
|
||||
always_merger.merge(raw_scim_group, value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except (PropertyMappingExpressionException, ValueError) as exc:
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
mapping=mapping,
|
||||
).save()
|
||||
raise StopSync(exc, obj, mapping) from exc
|
||||
if not raw_scim_group:
|
||||
raise StopSync(ValueError("No group mappings configured"), obj)
|
||||
raw_scim_group = super().to_schema(
|
||||
obj,
|
||||
creating,
|
||||
schemas=(SCIM_GROUP_SCHEMA,),
|
||||
)
|
||||
try:
|
||||
scim_group = SCIMGroupSchema.model_validate(delete_none_values(raw_scim_group))
|
||||
except ValidationError as exc:
|
||||
|
@ -1,20 +1,15 @@
|
||||
"""User client"""
|
||||
|
||||
from deepmerge import always_merger
|
||||
from pydantic import ValidationError
|
||||
|
||||
from authentik.core.expression.exceptions import (
|
||||
PropertyMappingExpressionException,
|
||||
SkipObjectException,
|
||||
)
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.policies.utils import delete_none_values
|
||||
from authentik.providers.scim.clients.base import SCIMClient
|
||||
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
|
||||
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMUser
|
||||
from authentik.providers.scim.models import SCIMMapping, SCIMProvider, SCIMUser
|
||||
|
||||
|
||||
class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
|
||||
@ -22,38 +17,23 @@ class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
|
||||
|
||||
connection_type = SCIMUser
|
||||
connection_type_query = "user"
|
||||
mapper: PropertyMappingManager
|
||||
|
||||
def __init__(self, provider: SCIMProvider):
|
||||
super().__init__(provider)
|
||||
self.mapper = PropertyMappingManager(
|
||||
self.provider.property_mappings.all().order_by("name").select_subclasses(),
|
||||
SCIMMapping,
|
||||
["provider", "creating"],
|
||||
)
|
||||
|
||||
def to_schema(self, obj: User, creating: bool) -> SCIMUserSchema:
|
||||
"""Convert authentik user into SCIM"""
|
||||
raw_scim_user = {
|
||||
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
|
||||
}
|
||||
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
|
||||
if not isinstance(mapping, SCIMMapping):
|
||||
continue
|
||||
try:
|
||||
mapping: SCIMMapping
|
||||
value = mapping.evaluate(
|
||||
user=obj,
|
||||
request=None,
|
||||
provider=self.provider,
|
||||
creating=creating,
|
||||
)
|
||||
if value is None:
|
||||
continue
|
||||
always_merger.merge(raw_scim_user, value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except (PropertyMappingExpressionException, ValueError) as exc:
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
mapping=mapping,
|
||||
).save()
|
||||
raise StopSync(exc, obj, mapping) from exc
|
||||
if not raw_scim_user:
|
||||
raise StopSync(ValueError("No user mappings configured"), obj)
|
||||
raw_scim_user = super().to_schema(
|
||||
obj,
|
||||
creating,
|
||||
schemas=(SCIM_USER_SCHEMA,),
|
||||
)
|
||||
try:
|
||||
scim_user = SCIMUserSchema.model_validate(delete_none_values(raw_scim_user))
|
||||
except ValidationError as exc:
|
||||
|
@ -5,6 +5,7 @@ from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
@ -32,6 +33,10 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
help_text=_("Property mappings used for group creation/updating."),
|
||||
)
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return static("authentik/sources/scim.png")
|
||||
|
||||
def client_for_model(
|
||||
self, model: type[User | Group]
|
||||
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
|
||||
|
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.0.6 on 2024-05-19 14:17
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_rbac", "0003_alter_systempermission_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="systempermission",
|
||||
options={
|
||||
"default_permissions": (),
|
||||
"managed": False,
|
||||
"permissions": [
|
||||
("view_system_info", "Can view system info"),
|
||||
("access_admin_interface", "Can access admin interface"),
|
||||
("view_system_settings", "Can view system settings"),
|
||||
("edit_system_settings", "Can edit system settings"),
|
||||
],
|
||||
"verbose_name": "System permission",
|
||||
"verbose_name_plural": "System permissions",
|
||||
},
|
||||
),
|
||||
]
|
@ -67,8 +67,6 @@ class SystemPermission(models.Model):
|
||||
verbose_name_plural = _("System permissions")
|
||||
permissions = [
|
||||
("view_system_info", _("Can view system info")),
|
||||
("view_system_tasks", _("Can view system tasks")),
|
||||
("run_system_tasks", _("Can run system tasks")),
|
||||
("access_admin_interface", _("Can access admin interface")),
|
||||
("view_system_settings", _("Can view system settings")),
|
||||
("edit_system_settings", _("Can edit system settings")),
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""rbac signals"""
|
||||
|
||||
from django.contrib.auth.models import Group as DjangoGroup
|
||||
from django.db.models.signals import m2m_changed, pre_save
|
||||
from django.db.models.signals import m2m_changed, pre_delete, pre_save
|
||||
from django.db.transaction import atomic
|
||||
from django.dispatch import receiver
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import Group
|
||||
@ -21,9 +22,26 @@ def rbac_role_pre_save(sender: type[Role], instance: Role, **_):
|
||||
instance.group = group
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Role)
|
||||
@receiver(pre_delete, sender=Group)
|
||||
def rbac_pre_delete_cleanup(sender: type[Group] | type[Role], instance: Group | Role, **_):
|
||||
"""RBAC: remove permissions from users when a group is deleted"""
|
||||
if sender == Group:
|
||||
for role in instance.roles.all():
|
||||
role.group.user_set.clear()
|
||||
if sender == Role:
|
||||
instance.group.user_set.clear()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Group.roles.through)
|
||||
def rbac_group_role_m2m(sender: type[Group], action: str, instance: Group, reverse: bool, **_):
|
||||
def rbac_group_role_m2m(
|
||||
sender: type[Group], action: str, instance: Group, reverse: bool, pk_set: set, **_
|
||||
):
|
||||
"""RBAC: Sync group members into roles when roles are assigned"""
|
||||
if action == "pre_add":
|
||||
# Validation: check that any of the added roles are not used in any other groups
|
||||
if Group.objects.filter(roles__in=pk_set).exclude(pk=instance.pk).exists():
|
||||
raise ValidationError("Roles can only be used with a single group.")
|
||||
if action not in ["post_add", "post_remove", "post_clear"]:
|
||||
return
|
||||
with atomic():
|
||||
@ -32,12 +50,13 @@ def rbac_group_role_m2m(sender: type[Group], action: str, instance: Group, rever
|
||||
.exclude(users__isnull=True)
|
||||
.values_list("users", flat=True)
|
||||
)
|
||||
if not group_users:
|
||||
return
|
||||
for role in instance.roles.all():
|
||||
role: Role
|
||||
role.group.user_set.set(group_users)
|
||||
LOGGER.debug("Updated users in group", group=instance)
|
||||
for role in Role.objects.filter(pk__in=pk_set):
|
||||
if action == "post_add":
|
||||
role.group.user_set.add(*group_users)
|
||||
# Role(s) in pk_set were removed from group, so remove the users that we added
|
||||
if action == "post_remove":
|
||||
role.group.user_set.remove(*group_users)
|
||||
LOGGER.debug("Updated users in group", group=instance, direction=action, users=group_users)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Group.users.through)
|
||||
|
27
authentik/rbac/tests/test_api.py
Normal file
27
authentik/rbac/tests/test_api.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Test RBACPermissionViewSet api"""
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.rbac.models import Role
|
||||
|
||||
|
||||
class TestRBACAPI(APITestCase):
|
||||
"""Test RBACPermissionViewSet api"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.superuser = create_test_admin_user()
|
||||
|
||||
self.user = create_test_user()
|
||||
self.role = Role.objects.create(name=generate_id())
|
||||
self.group = Group.objects.create(name=generate_id())
|
||||
self.group.roles.add(self.role)
|
||||
self.group.users.add(self.user)
|
||||
|
||||
def test_list(self):
|
||||
self.client.force_login(self.superuser)
|
||||
res = self.client.get(reverse("authentik_api:permission-list"))
|
||||
self.assertEqual(res.status_code, 200)
|
75
authentik/rbac/tests/test_api_permissions_roles.py
Normal file
75
authentik/rbac/tests/test_api_permissions_roles.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""Test RolePermissionViewSet api"""
|
||||
|
||||
from django.urls import reverse
|
||||
from guardian.models import GroupObjectPermission
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.rbac.models import Role
|
||||
from authentik.stages.invitation.models import Invitation
|
||||
|
||||
|
||||
class TestRBACPermissionRoles(APITestCase):
|
||||
"""Test RolePermissionViewSet api"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.superuser = create_test_admin_user()
|
||||
|
||||
self.user = create_test_user()
|
||||
self.role = Role.objects.create(name=generate_id())
|
||||
self.group = Group.objects.create(name=generate_id())
|
||||
self.group.roles.add(self.role)
|
||||
self.group.users.add(self.user)
|
||||
|
||||
def test_list(self):
|
||||
"""Test list of all permissions"""
|
||||
self.client.force_login(self.superuser)
|
||||
inv = Invitation.objects.create(
|
||||
name=generate_id(),
|
||||
created_by=self.superuser,
|
||||
)
|
||||
self.role.assign_permission("authentik_stages_invitation.view_invitation", obj=inv)
|
||||
res = self.client.get(reverse("authentik_api:permissions-roles-list"))
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_list_role(self):
|
||||
"""Test list of all permissions"""
|
||||
self.client.force_login(self.superuser)
|
||||
inv = Invitation.objects.create(
|
||||
name=generate_id(),
|
||||
created_by=self.superuser,
|
||||
)
|
||||
self.role.assign_permission("authentik_stages_invitation.view_invitation", obj=inv)
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:permissions-roles-list") + f"?uuid={self.role.pk}"
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
res.content,
|
||||
{
|
||||
"pagination": {
|
||||
"next": 0,
|
||||
"previous": 0,
|
||||
"count": 1,
|
||||
"current": 1,
|
||||
"total_pages": 1,
|
||||
"start_index": 1,
|
||||
"end_index": 1,
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": GroupObjectPermission.objects.filter(object_pk=inv.pk).first().pk,
|
||||
"codename": "view_invitation",
|
||||
"model": "invitation",
|
||||
"app_label": "authentik_stages_invitation",
|
||||
"object_pk": str(inv.pk),
|
||||
"name": "Can view Invitation",
|
||||
"app_label_verbose": "authentik Stages.Invitation",
|
||||
"model_verbose": "Invitation",
|
||||
"object_description": str(inv),
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
76
authentik/rbac/tests/test_api_permissions_users.py
Normal file
76
authentik/rbac/tests/test_api_permissions_users.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""Test UserPermissionViewSet api"""
|
||||
|
||||
from django.urls import reverse
|
||||
from guardian.models import UserObjectPermission
|
||||
from guardian.shortcuts import assign_perm
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.rbac.models import Role
|
||||
from authentik.stages.invitation.models import Invitation
|
||||
|
||||
|
||||
class TestRBACPermissionUsers(APITestCase):
|
||||
"""Test UserPermissionViewSet api"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.superuser = create_test_admin_user()
|
||||
|
||||
self.user = create_test_user()
|
||||
self.role = Role.objects.create(name=generate_id())
|
||||
self.group = Group.objects.create(name=generate_id())
|
||||
self.group.roles.add(self.role)
|
||||
self.group.users.add(self.user)
|
||||
|
||||
def test_list(self):
|
||||
"""Test list of all permissions"""
|
||||
self.client.force_login(self.superuser)
|
||||
inv = Invitation.objects.create(
|
||||
name=generate_id(),
|
||||
created_by=self.superuser,
|
||||
)
|
||||
assign_perm("authentik_stages_invitation.view_invitation", self.user, inv)
|
||||
res = self.client.get(reverse("authentik_api:permissions-users-list"))
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_list_role(self):
|
||||
"""Test list of all permissions"""
|
||||
self.client.force_login(self.superuser)
|
||||
inv = Invitation.objects.create(
|
||||
name=generate_id(),
|
||||
created_by=self.superuser,
|
||||
)
|
||||
assign_perm("authentik_stages_invitation.view_invitation", self.user, inv)
|
||||
res = self.client.get(
|
||||
reverse("authentik_api:permissions-users-list") + f"?user_id={self.user.pk}"
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
res.content,
|
||||
{
|
||||
"pagination": {
|
||||
"next": 0,
|
||||
"previous": 0,
|
||||
"count": 1,
|
||||
"current": 1,
|
||||
"total_pages": 1,
|
||||
"start_index": 1,
|
||||
"end_index": 1,
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": UserObjectPermission.objects.filter(object_pk=inv.pk).first().pk,
|
||||
"codename": "view_invitation",
|
||||
"model": "invitation",
|
||||
"app_label": "authentik_stages_invitation",
|
||||
"object_pk": str(inv.pk),
|
||||
"name": "Can view Invitation",
|
||||
"app_label_verbose": "authentik Stages.Invitation",
|
||||
"model_verbose": "Invitation",
|
||||
"object_description": str(inv),
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
@ -1,9 +1,10 @@
|
||||
"""RBAC role tests"""
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.rbac.models import Role
|
||||
|
||||
@ -13,18 +14,30 @@ class TestRoles(APITestCase):
|
||||
|
||||
def test_role_create(self):
|
||||
"""Test creation"""
|
||||
user = create_test_admin_user()
|
||||
user = create_test_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.save()
|
||||
role.assign_permission("authentik_core.view_application")
|
||||
group.roles.add(role)
|
||||
group.users.add(user)
|
||||
self.assertEqual(list(role.group.user_set.all()), [user])
|
||||
self.assertTrue(user.has_perm("authentik_core.view_application"))
|
||||
|
||||
def test_role_create_remove(self):
|
||||
def test_role_create_add_reverse(self):
|
||||
"""Test creation (add user in reverse)"""
|
||||
user = create_test_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.assign_permission("authentik_core.view_application")
|
||||
group.roles.add(role)
|
||||
user.ak_groups.add(group)
|
||||
self.assertEqual(list(role.group.user_set.all()), [user])
|
||||
self.assertTrue(user.has_perm("authentik_core.view_application"))
|
||||
|
||||
def test_remove_group_delete(self):
|
||||
"""Test creation and remove"""
|
||||
user = create_test_admin_user()
|
||||
user = create_test_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.assign_permission("authentik_core.view_application")
|
||||
@ -32,5 +45,77 @@ class TestRoles(APITestCase):
|
||||
group.users.add(user)
|
||||
self.assertEqual(list(role.group.user_set.all()), [user])
|
||||
self.assertTrue(user.has_perm("authentik_core.view_application"))
|
||||
user.delete()
|
||||
group.delete()
|
||||
user = User.objects.get(username=user.username)
|
||||
self.assertFalse(user.has_perm("authentik_core.view_application"))
|
||||
self.assertEqual(list(role.group.user_set.all()), [])
|
||||
|
||||
def test_remove_roles_remove(self):
|
||||
"""Test assigning permission to role, then removing role from group"""
|
||||
user = create_test_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.assign_permission("authentik_core.view_application")
|
||||
group.roles.add(role)
|
||||
group.users.add(user)
|
||||
self.assertEqual(list(role.group.user_set.all()), [user])
|
||||
self.assertTrue(user.has_perm("authentik_core.view_application"))
|
||||
group.roles.remove(role)
|
||||
user = User.objects.get(username=user.username)
|
||||
self.assertFalse(user.has_perm("authentik_core.view_application"))
|
||||
self.assertEqual(list(role.group.user_set.all()), [])
|
||||
|
||||
def test_remove_role_delete(self):
|
||||
"""Test assigning permissions to role, then removing role"""
|
||||
user = create_test_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.assign_permission("authentik_core.view_application")
|
||||
group.roles.add(role)
|
||||
group.users.add(user)
|
||||
self.assertEqual(list(role.group.user_set.all()), [user])
|
||||
self.assertTrue(user.has_perm("authentik_core.view_application"))
|
||||
role.delete()
|
||||
user = User.objects.get(username=user.username)
|
||||
self.assertFalse(user.has_perm("authentik_core.view_application"))
|
||||
self.assertEqual(list(role.group.user_set.all()), [])
|
||||
|
||||
def test_role_assign_twice(self):
|
||||
"""Test assigning role to two groups"""
|
||||
group1 = Group.objects.create(name=generate_id())
|
||||
group2 = Group.objects.create(name=generate_id())
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.assign_permission("authentik_core.view_application")
|
||||
group1.roles.add(role)
|
||||
with self.assertRaises(ValidationError):
|
||||
group2.roles.add(role)
|
||||
|
||||
def test_remove_users_remove(self):
|
||||
"""Test assigning permission to role, then removing user from group"""
|
||||
user = create_test_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.assign_permission("authentik_core.view_application")
|
||||
group.roles.add(role)
|
||||
group.users.add(user)
|
||||
self.assertEqual(list(role.group.user_set.all()), [user])
|
||||
self.assertTrue(user.has_perm("authentik_core.view_application"))
|
||||
group.users.remove(user)
|
||||
user = User.objects.get(username=user.username)
|
||||
self.assertFalse(user.has_perm("authentik_core.view_application"))
|
||||
self.assertEqual(list(role.group.user_set.all()), [])
|
||||
|
||||
def test_remove_users_remove_reverse(self):
|
||||
"""Test assigning permission to role, then removing user from group in reverse"""
|
||||
user = create_test_user()
|
||||
group = Group.objects.create(name=generate_id())
|
||||
role = Role.objects.create(name=generate_id())
|
||||
role.assign_permission("authentik_core.view_application")
|
||||
group.roles.add(role)
|
||||
group.users.add(user)
|
||||
self.assertEqual(list(role.group.user_set.all()), [user])
|
||||
self.assertTrue(user.has_perm("authentik_core.view_application"))
|
||||
user.ak_groups.remove(group)
|
||||
user = User.objects.get(username=user.username)
|
||||
self.assertFalse(user.has_perm("authentik_core.view_application"))
|
||||
self.assertEqual(list(role.group.user_set.all()), [])
|
||||
|
@ -10,8 +10,15 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
|
||||
def get_connection_params(self):
|
||||
"""Refresh DB credentials before getting connection params"""
|
||||
CONFIG.refresh("postgresql.password")
|
||||
conn_params = super().get_connection_params()
|
||||
conn_params["user"] = CONFIG.get("postgresql.user")
|
||||
conn_params["password"] = CONFIG.get("postgresql.password")
|
||||
|
||||
prefix = "postgresql"
|
||||
if self.alias.startswith("replica_"):
|
||||
prefix = f"postgresql.read_replicas.{self.alias.removeprefix('replica_')}"
|
||||
|
||||
for setting in ("host", "port", "user", "password"):
|
||||
conn_params[setting] = CONFIG.refresh(f"{prefix}.{setting}")
|
||||
if conn_params[setting] is None and self.alias.startswith("replica_"):
|
||||
conn_params[setting] = CONFIG.refresh(f"postgresql.{setting}")
|
||||
|
||||
return conn_params
|
||||
|
@ -47,8 +47,8 @@ class ReadyView(View):
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
try:
|
||||
db_conn = connections["default"]
|
||||
_ = db_conn.cursor()
|
||||
for db_conn in connections.all():
|
||||
_ = db_conn.cursor()
|
||||
except OperationalError: # pragma: no cover
|
||||
return HttpResponse(status=503)
|
||||
try:
|
||||
|
@ -293,7 +293,7 @@ DATABASES = {
|
||||
"NAME": CONFIG.get("postgresql.name"),
|
||||
"USER": CONFIG.get("postgresql.user"),
|
||||
"PASSWORD": CONFIG.get("postgresql.password"),
|
||||
"PORT": CONFIG.get_int("postgresql.port"),
|
||||
"PORT": CONFIG.get("postgresql.port"),
|
||||
"SSLMODE": CONFIG.get("postgresql.sslmode"),
|
||||
"SSLROOTCERT": CONFIG.get("postgresql.sslrootcert"),
|
||||
"SSLCERT": CONFIG.get("postgresql.sslcert"),
|
||||
@ -313,7 +313,23 @@ if CONFIG.get_bool("postgresql.use_pgbouncer", False):
|
||||
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
|
||||
DATABASES["default"]["CONN_MAX_AGE"] = None # persistent
|
||||
|
||||
DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",)
|
||||
for replica in CONFIG.get_keys("postgresql.read_replicas"):
|
||||
_database = DATABASES["default"].copy()
|
||||
for setting in DATABASES["default"].keys():
|
||||
default = object()
|
||||
if setting in ("TEST",):
|
||||
continue
|
||||
override = CONFIG.get(
|
||||
f"postgresql.read_replicas.{replica}.{setting.lower()}", default=default
|
||||
)
|
||||
if override is not default:
|
||||
_database[setting] = override
|
||||
DATABASES[f"replica_{replica}"] = _database
|
||||
|
||||
DATABASE_ROUTERS = (
|
||||
"authentik.tenants.db.FailoverRouter",
|
||||
"django_tenants.routers.TenantSyncRouter",
|
||||
)
|
||||
|
||||
# Email
|
||||
# These values should never actually be used, emails are only sent from email stages, which
|
||||
|
@ -8,6 +8,7 @@ from tempfile import NamedTemporaryFile, mkdtemp
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db import connection, models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
|
||||
from ldap3.core.exceptions import LDAPException, LDAPInsufficientAccessRightsResult, LDAPSchemaError
|
||||
@ -126,6 +127,10 @@ class LDAPSource(Source):
|
||||
|
||||
return LDAPSourceSerializer
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str:
|
||||
return static("authentik/sources/ldap.png")
|
||||
|
||||
def server(self, **kwargs) -> ServerPool:
|
||||
"""Get LDAP Server/ServerPool"""
|
||||
servers = []
|
||||
|
@ -5,7 +5,6 @@ from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models.base import Model
|
||||
from django.db.models.query import QuerySet
|
||||
from ldap3 import DEREF_ALWAYS, SUBTREE, Connection
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
@ -16,8 +15,11 @@ from authentik.core.expression.exceptions import (
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG, set_path_in_dict
|
||||
from authentik.lib.merge import MERGE_LIST_UNIQUE
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from authentik.sources.ldap.models import LDAPSource
|
||||
|
||||
LDAP_UNIQUENESS = "ldap_uniq"
|
||||
|
||||
@ -38,6 +40,7 @@ class BaseLDAPSynchronizer:
|
||||
_logger: BoundLogger
|
||||
_connection: Connection
|
||||
_messages: list[str]
|
||||
mapper: PropertyMappingManager
|
||||
|
||||
def __init__(self, source: LDAPSource):
|
||||
self._source = source
|
||||
@ -139,52 +142,47 @@ class BaseLDAPSynchronizer:
|
||||
|
||||
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]:
|
||||
"""Build attributes for User object based on property mappings."""
|
||||
props = self._build_object_properties(user_dn, self._source.property_mappings, **kwargs)
|
||||
props = self._build_object_properties(user_dn, **kwargs)
|
||||
props.setdefault("path", self._source.get_user_path())
|
||||
return props
|
||||
|
||||
def build_group_properties(self, group_dn: str, **kwargs) -> dict[str, Any]:
|
||||
"""Build attributes for Group object based on property mappings."""
|
||||
return self._build_object_properties(
|
||||
group_dn, self._source.property_mappings_group, **kwargs
|
||||
)
|
||||
return self._build_object_properties(group_dn, **kwargs)
|
||||
|
||||
def _build_object_properties(
|
||||
self, object_dn: str, mappings: QuerySet, **kwargs
|
||||
) -> dict[str, dict[Any, Any]]:
|
||||
def _build_object_properties(self, object_dn: str, **kwargs) -> dict[str, dict[Any, Any]]:
|
||||
properties = {"attributes": {}}
|
||||
for mapping in mappings.all().select_subclasses():
|
||||
if not isinstance(mapping, LDAPPropertyMapping):
|
||||
continue
|
||||
mapping: LDAPPropertyMapping
|
||||
try:
|
||||
value = mapping.evaluate(
|
||||
user=None, request=None, ldap=kwargs, dn=object_dn, source=self._source
|
||||
)
|
||||
if value is None:
|
||||
self._logger.warning("property mapping returned None", mapping=mapping)
|
||||
continue
|
||||
if isinstance(value, (bytes)):
|
||||
self._logger.warning("property mapping returned bytes", mapping=mapping)
|
||||
continue
|
||||
object_field = mapping.object_field
|
||||
if object_field.startswith("attributes."):
|
||||
# Because returning a list might desired, we can't
|
||||
# rely on flatten here. Instead, just save the result as-is
|
||||
set_path_in_dict(properties, object_field, value)
|
||||
else:
|
||||
properties[object_field] = flatten(value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except PropertyMappingExpressionException as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping: '{mapping.name}'",
|
||||
source=self._source,
|
||||
mapping=mapping,
|
||||
).save()
|
||||
self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
|
||||
continue
|
||||
try:
|
||||
for value, mapping in self.mapper.iter_eval(
|
||||
user=None,
|
||||
request=None,
|
||||
return_mapping=True,
|
||||
ldap=kwargs,
|
||||
dn=object_dn,
|
||||
source=self._source,
|
||||
):
|
||||
try:
|
||||
if isinstance(value, (bytes)):
|
||||
self._logger.warning("property mapping returned bytes", mapping=mapping)
|
||||
continue
|
||||
object_field = mapping.object_field
|
||||
if object_field.startswith("attributes."):
|
||||
# Because returning a list might desired, we can't
|
||||
# rely on flatten here. Instead, just save the result as-is
|
||||
set_path_in_dict(properties, object_field, value)
|
||||
else:
|
||||
properties[object_field] = flatten(value)
|
||||
except SkipObjectException as exc:
|
||||
raise exc from exc
|
||||
except PropertyMappingExpressionException as exc:
|
||||
# Value error can be raised when assigning invalid data to an attribute
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
|
||||
mapping=exc.mapping,
|
||||
).save()
|
||||
self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=exc.mapping)
|
||||
raise StopSync(exc, None, exc.mapping) from exc
|
||||
if self._source.object_uniqueness_field in kwargs:
|
||||
properties["attributes"][LDAP_UNIQUENESS] = flatten(
|
||||
kwargs.get(self._source.object_uniqueness_field)
|
||||
|
@ -9,12 +9,22 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
|
||||
from authentik.core.expression.exceptions import SkipObjectException
|
||||
from authentik.core.models import Group
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer, flatten
|
||||
|
||||
|
||||
class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
"""Sync LDAP Users and groups into authentik"""
|
||||
|
||||
def __init__(self, source: LDAPSource):
|
||||
super().__init__(source)
|
||||
self.mapper = PropertyMappingManager(
|
||||
self._source.property_mappings_group.all().order_by("name").select_subclasses(),
|
||||
LDAPPropertyMapping,
|
||||
["ldap", "dn", "source"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "groups"
|
||||
|
@ -9,6 +9,8 @@ from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
|
||||
from authentik.core.expression.exceptions import SkipObjectException
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.sync.mapper import PropertyMappingManager
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer, flatten
|
||||
from authentik.sources.ldap.sync.vendor.freeipa import FreeIPA
|
||||
from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
|
||||
@ -17,6 +19,14 @@ from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
|
||||
class UserLDAPSynchronizer(BaseLDAPSynchronizer):
|
||||
"""Sync LDAP Users into authentik"""
|
||||
|
||||
def __init__(self, source: LDAPSource):
|
||||
super().__init__(source)
|
||||
self.mapper = PropertyMappingManager(
|
||||
self._source.property_mappings.all().order_by("name").select_subclasses(),
|
||||
LDAPPropertyMapping,
|
||||
["ldap", "dn", "source"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
return "users"
|
||||
|
@ -12,6 +12,7 @@ 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.lib.utils.reflection import class_to_path, path_to_class
|
||||
from authentik.root.celery import CELERY_APP
|
||||
@ -138,7 +139,7 @@ def ldap_sync(self: SystemTask, source_pk: str, sync_class: str, page_cache_key:
|
||||
*messages,
|
||||
)
|
||||
cache.delete(page_cache_key)
|
||||
except LDAPException as exc:
|
||||
except (LDAPException, StopSync) as exc:
|
||||
# No explicit event is created here as .set_status with an error will do that
|
||||
LOGGER.warning(exception_to_string(exc))
|
||||
self.set_error(exc)
|
||||
|
@ -11,6 +11,7 @@ from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.events.models import Event, EventAction, SystemTask
|
||||
from authentik.events.system_tasks import TaskStatus
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.lib.sync.outgoing.exceptions import StopSync
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
from authentik.sources.ldap.sync.groups import GroupLDAPSynchronizer
|
||||
@ -63,12 +64,13 @@ class LDAPSyncTests(TestCase):
|
||||
connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
user_sync = UserLDAPSynchronizer(self.source)
|
||||
user_sync.sync_full()
|
||||
with self.assertRaises(StopSync):
|
||||
user_sync.sync_full()
|
||||
self.assertFalse(User.objects.filter(username="user0_sn").exists())
|
||||
self.assertFalse(User.objects.filter(username="user1_sn").exists())
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.CONFIGURATION_ERROR,
|
||||
context__message="Failed to evaluate property-mapping: 'name'",
|
||||
context__mapping__pk=mapping.pk.hex,
|
||||
)
|
||||
self.assertTrue(events.exists())
|
||||
|
||||
|
@ -8,6 +8,7 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.api.object_types import CreatableType, NonCreatableType
|
||||
from authentik.core.models import Source, UserSourceConnection
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
|
||||
@ -15,7 +16,7 @@ if TYPE_CHECKING:
|
||||
from authentik.sources.oauth.types.registry import SourceType
|
||||
|
||||
|
||||
class OAuthSource(Source):
|
||||
class OAuthSource(NonCreatableType, Source):
|
||||
"""Login using a Generic OAuth provider."""
|
||||
|
||||
provider_type = models.CharField(max_length=255)
|
||||
@ -72,23 +73,35 @@ class OAuthSource(Source):
|
||||
|
||||
return OAuthSourceSerializer
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
# When listing source types, this property might be retrieved from an abstract
|
||||
# model. In that case we can't check self.provider_type or self.icon_url
|
||||
# and as such we attempt to find the correct provider type based on the mode name
|
||||
if self.Meta.abstract:
|
||||
from authentik.sources.oauth.types.registry import registry
|
||||
|
||||
provider_type = registry.find_type(
|
||||
self._meta.model_name.replace(OAuthSource._meta.model_name, "")
|
||||
)
|
||||
return provider_type().icon_url()
|
||||
icon = super().icon_url
|
||||
if not icon:
|
||||
provider_type = self.source_type
|
||||
provider = provider_type()
|
||||
icon = provider.icon_url()
|
||||
return icon
|
||||
|
||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
|
||||
provider_type = self.source_type
|
||||
provider = provider_type()
|
||||
icon = self.icon_url
|
||||
if not icon:
|
||||
icon = provider.icon_url()
|
||||
return UILoginButton(
|
||||
name=self.name,
|
||||
challenge=provider.login_challenge(self, request),
|
||||
icon_url=icon,
|
||||
icon_url=self.icon_url,
|
||||
)
|
||||
|
||||
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||
provider_type = self.source_type
|
||||
icon = self.icon_url
|
||||
if not icon:
|
||||
icon = provider_type().icon_url()
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.name,
|
||||
@ -97,7 +110,7 @@ class OAuthSource(Source):
|
||||
"authentik_sources_oauth:oauth-client-login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
"icon_url": icon,
|
||||
"icon_url": self.icon_url,
|
||||
}
|
||||
)
|
||||
|
||||
@ -109,7 +122,7 @@ class OAuthSource(Source):
|
||||
verbose_name_plural = _("OAuth Sources")
|
||||
|
||||
|
||||
class GitHubOAuthSource(OAuthSource):
|
||||
class GitHubOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using GitHub.com or a GitHub-Enterprise Instance."""
|
||||
|
||||
class Meta:
|
||||
@ -118,7 +131,7 @@ class GitHubOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("GitHub OAuth Sources")
|
||||
|
||||
|
||||
class GitLabOAuthSource(OAuthSource):
|
||||
class GitLabOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using GitLab.com or a GitLab Instance."""
|
||||
|
||||
class Meta:
|
||||
@ -127,7 +140,7 @@ class GitLabOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("GitLab OAuth Sources")
|
||||
|
||||
|
||||
class TwitchOAuthSource(OAuthSource):
|
||||
class TwitchOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Twitch."""
|
||||
|
||||
class Meta:
|
||||
@ -136,7 +149,7 @@ class TwitchOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Twitch OAuth Sources")
|
||||
|
||||
|
||||
class MailcowOAuthSource(OAuthSource):
|
||||
class MailcowOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Mailcow."""
|
||||
|
||||
class Meta:
|
||||
@ -145,7 +158,7 @@ class MailcowOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Mailcow OAuth Sources")
|
||||
|
||||
|
||||
class TwitterOAuthSource(OAuthSource):
|
||||
class TwitterOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Twitter.com"""
|
||||
|
||||
class Meta:
|
||||
@ -154,7 +167,7 @@ class TwitterOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Twitter OAuth Sources")
|
||||
|
||||
|
||||
class FacebookOAuthSource(OAuthSource):
|
||||
class FacebookOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Facebook.com."""
|
||||
|
||||
class Meta:
|
||||
@ -163,7 +176,7 @@ class FacebookOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Facebook OAuth Sources")
|
||||
|
||||
|
||||
class DiscordOAuthSource(OAuthSource):
|
||||
class DiscordOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Discord."""
|
||||
|
||||
class Meta:
|
||||
@ -172,7 +185,7 @@ class DiscordOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Discord OAuth Sources")
|
||||
|
||||
|
||||
class PatreonOAuthSource(OAuthSource):
|
||||
class PatreonOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Patreon."""
|
||||
|
||||
class Meta:
|
||||
@ -181,7 +194,7 @@ class PatreonOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Patreon OAuth Sources")
|
||||
|
||||
|
||||
class GoogleOAuthSource(OAuthSource):
|
||||
class GoogleOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Google or Google Workspace (GSuite)."""
|
||||
|
||||
class Meta:
|
||||
@ -190,7 +203,7 @@ class GoogleOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Google OAuth Sources")
|
||||
|
||||
|
||||
class AzureADOAuthSource(OAuthSource):
|
||||
class AzureADOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Azure AD."""
|
||||
|
||||
class Meta:
|
||||
@ -199,7 +212,7 @@ class AzureADOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Azure AD OAuth Sources")
|
||||
|
||||
|
||||
class OpenIDConnectOAuthSource(OAuthSource):
|
||||
class OpenIDConnectOAuthSource(CreatableType, OAuthSource):
|
||||
"""Login using a Generic OpenID-Connect compliant provider."""
|
||||
|
||||
class Meta:
|
||||
@ -208,7 +221,7 @@ class OpenIDConnectOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("OpenID OAuth Sources")
|
||||
|
||||
|
||||
class AppleOAuthSource(OAuthSource):
|
||||
class AppleOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Apple."""
|
||||
|
||||
class Meta:
|
||||
@ -217,7 +230,7 @@ class AppleOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Apple OAuth Sources")
|
||||
|
||||
|
||||
class OktaOAuthSource(OAuthSource):
|
||||
class OktaOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using Okta."""
|
||||
|
||||
class Meta:
|
||||
@ -226,7 +239,7 @@ class OktaOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Okta OAuth Sources")
|
||||
|
||||
|
||||
class RedditOAuthSource(OAuthSource):
|
||||
class RedditOAuthSource(CreatableType, OAuthSource):
|
||||
"""Social Login using reddit.com."""
|
||||
|
||||
class Meta:
|
||||
|
@ -60,10 +60,14 @@ class PlexSource(Source):
|
||||
|
||||
return PlexSourceSerializer
|
||||
|
||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
|
||||
icon = self.icon_url
|
||||
@property
|
||||
def icon_url(self) -> str:
|
||||
icon = super().icon_url
|
||||
if not icon:
|
||||
icon = static("authentik/sources/plex.svg")
|
||||
return icon
|
||||
|
||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
challenge=PlexAuthenticationChallenge(
|
||||
data={
|
||||
@ -73,20 +77,17 @@ class PlexSource(Source):
|
||||
"slug": self.slug,
|
||||
}
|
||||
),
|
||||
icon_url=icon,
|
||||
icon_url=self.icon_url,
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||
icon = self.icon_url
|
||||
if not icon:
|
||||
icon = static("authentik/sources/plex.svg")
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.name,
|
||||
"component": "ak-user-settings-source-plex",
|
||||
"configure_url": self.client_id,
|
||||
"icon_url": icon,
|
||||
"icon_url": self.icon_url,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -181,6 +181,13 @@ class SAMLSource(Source):
|
||||
|
||||
return SAMLSourceSerializer
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str:
|
||||
icon = super().icon_url
|
||||
if not icon:
|
||||
return static("authentik/sources/saml.png")
|
||||
return icon
|
||||
|
||||
def get_issuer(self, request: HttpRequest) -> str:
|
||||
"""Get Source's Issuer, falling back to our Metadata URL if none is set"""
|
||||
if self.issuer is None:
|
||||
@ -209,9 +216,6 @@ class SAMLSource(Source):
|
||||
)
|
||||
|
||||
def ui_user_settings(self) -> UserSettingSerializer | None:
|
||||
icon = self.icon_url
|
||||
if not icon:
|
||||
icon = static(f"authentik/sources/{self.slug}.svg")
|
||||
return UserSettingSerializer(
|
||||
data={
|
||||
"title": self.name,
|
||||
@ -220,7 +224,7 @@ class SAMLSource(Source):
|
||||
"authentik_sources_saml:login",
|
||||
kwargs={"source_slug": self.slug},
|
||||
),
|
||||
"icon_url": icon,
|
||||
"icon_url": self.icon_url,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
@ -27,6 +28,10 @@ class SCIMSource(Source):
|
||||
"""Return component used to edit this object"""
|
||||
return "ak-source-scim-form"
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str:
|
||||
return static("authentik/sources/scim.png")
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.sources.scim.api.sources import SCIMSourceSerializer
|
||||
|
@ -411,9 +411,12 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
webauthn_device: WebAuthnDevice = response.device
|
||||
self.logger.debug("Set user from user-less flow", user=webauthn_device.user)
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = webauthn_device.user
|
||||
self.executor.plan.context[PLAN_CONTEXT_METHOD] = "auth_webauthn_pwl"
|
||||
self.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS] = {
|
||||
"device": webauthn_device,
|
||||
"device_type": webauthn_device.device_type,
|
||||
}
|
||||
self.executor.plan.context.setdefault(PLAN_CONTEXT_METHOD, "auth_webauthn_pwl")
|
||||
self.executor.plan.context.setdefault(PLAN_CONTEXT_METHOD_ARGS, {})
|
||||
self.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS].update(
|
||||
{
|
||||
"device": webauthn_device,
|
||||
"device_type": webauthn_device.device_type,
|
||||
}
|
||||
)
|
||||
return self.set_valid_mfa_cookie(response.device)
|
||||
|
@ -69,8 +69,8 @@
|
||||
},
|
||||
"d548826e-79b4-db40-a3d8-11116f7e8349": {
|
||||
"name": "Bitwarden",
|
||||
"icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K",
|
||||
"icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjAuMywgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Ikljb24iIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMjQgMTAyNDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiMxNzVEREM7fQoJLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8cmVjdCBpZD0iQmFja2dyb3VuZCIgY2xhc3M9InN0MCIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIvPgo8cGF0aCBpZD0iSWRlbnRpdHkiIGNsYXNzPSJzdDEiIGQ9Ik04MjkuOCwxMjguNmMtNi41LTYuNS0xNC4yLTkuNy0yMy05LjdIMjE3LjJjLTguOSwwLTE2LjUsMy4yLTIzLDkuN3MtOS43LDE0LjItOS43LDIzdjM5My4xCgljMCwyOS4zLDUuNyw1OC40LDE3LjEsODcuM2MxMS40LDI4LjgsMjUuNiw1NC40LDQyLjUsNzYuOGMxNi45LDIyLjMsMzcsNDQuMSw2MC40LDY1LjNzNDUsMzguNyw2NC43LDUyLjcKCWMxOS44LDE0LDQwLjQsMjcuMiw2MS45LDM5LjdzMzYuOCwyMC45LDQ1LjgsMjUuM2M5LDQuNCwxNi4zLDcuOSwyMS43LDEwLjJjNC4xLDIsOC41LDMuMSwxMy4zLDMuMWM0LjgsMCw5LjItMSwxMy4zLTMuMQoJYzUuNS0yLjQsMTIuNy01LjgsMjEuOC0xMC4yYzktNC40LDI0LjMtMTIuOSw0NS44LTI1LjNjMjEuNS0xMi41LDQyLjEtMjUuNyw2MS45LTM5LjdjMTkuOC0xNCw0MS40LTMxLjYsNjQuOC01Mi43CgljMjMuNC0yMS4yLDQzLjUtNDIuOSw2MC40LTY1LjNjMTYuOS0yMi40LDMxLTQ3LjksNDIuNS03Ni44YzExLjQtMjguOCwxNy4xLTU3LjksMTcuMS04Ny4zdi0zOTMKCUM4MzkuNiwxNDIuOCw4MzYuMywxMzUuMSw4MjkuOCwxMjguNnogTTc1My44LDU0OC40YzAsMTQyLjMtMjQxLjgsMjY0LjktMjQxLjgsMjY0LjlWMjAzaDI0MS44Qzc1My44LDIwMyw3NTMuOCw0MDYuMSw3NTMuOCw1NDguNHoKCSIvPgo8L3N2Zz4K"
|
||||
"icon_dark": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDMwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF82OF82MSkiPgo8cGF0aCBkPSJNMzAwIDI1My4xMjVDMzAwIDI3OS4wMjMgMjc5LjAyMyAzMDAgMjUzLjEyNSAzMDBINDYuODc1QzIwLjk3NjYgMzAwIDAgMjc5LjAyMyAwIDI1My4xMjVWNDYuODc1QzAgMjAuOTc2NiAyMC45NzY2IDAgNDYuODc1IDBIMjUzLjEyNUMyNzkuMDIzIDAgMzAwIDIwLjk3NjYgMzAwIDQ2Ljg3NVYyNTMuMTI1WiIgZmlsbD0iIzE3NUREQyIvPgo8cGF0aCBkPSJNMjQzLjEwNSAzNy42NzU4QzI0MS4yMDEgMzUuNzcxNSAyMzguOTQ1IDM0LjgzNCAyMzYuMzY3IDM0LjgzNEg2My42MzI4QzYxLjAyNTQgMzQuODM0IDU4Ljc5ODggMzUuNzcxNSA1Ni44OTQ1IDM3LjY3NThDNTQuOTkwMiAzOS41ODAxIDU0LjA1MjcgNDEuODM1OSA1NC4wNTI3IDQ0LjQxNDFWMTU5LjU4QzU0LjA1MjcgMTY4LjE2NCA1NS43MjI3IDE3Ni42ODkgNTkuMDYyNSAxODUuMTU2QzYyLjQwMjMgMTkzLjU5NCA2Ni41NjI1IDIwMS4wOTQgNzEuNTEzNyAyMDcuNjU2Qzc2LjQ2NDggMjE0LjE4OSA4Mi4zNTM1IDIyMC41NzYgODkuMjA5IDIyNi43ODdDOTYuMDY0NSAyMzIuOTk4IDEwMi4zOTMgMjM4LjEyNSAxMDguMTY0IDI0Mi4yMjdDMTEzLjk2NSAyNDYuMzI4IDEyMCAyNTAuMTk1IDEyNi4yOTkgMjUzLjg1N0MxMzIuNTk4IDI1Ny41MiAxMzcuMDggMjU5Ljk4IDEzOS43MTcgMjYxLjI3QzE0Mi4zNTQgMjYyLjU1OSAxNDQuNDkyIDI2My41ODQgMTQ2LjA3NCAyNjQuMjU4QzE0Ny4yNzUgMjY0Ljg0NCAxNDguNTY0IDI2NS4xNjYgMTQ5Ljk3MSAyNjUuMTY2QzE1MS4zNzcgMjY1LjE2NiAxNTIuNjY2IDI2NC44NzMgMTUzLjg2NyAyNjQuMjU4QzE1NS40NzkgMjYzLjU1NSAxNTcuNTg4IDI2Mi41NTkgMTYwLjI1NCAyNjEuMjdDMTYyLjg5MSAyNTkuOTggMTY3LjM3MyAyNTcuNDkgMTczLjY3MiAyNTMuODU3QzE3OS45NzEgMjUwLjE5NSAxODYuMDA2IDI0Ni4zMjggMTkxLjgwNyAyNDIuMjI3QzE5Ny42MDcgMjM4LjEyNSAyMDMuOTM2IDIzMi45NjkgMjEwLjc5MSAyMjYuNzg3QzIxNy42NDYgMjIwLjU3NiAyMjMuNTM1IDIxNC4yMTkgMjI4LjQ4NiAyMDcuNjU2QzIzMy40MzggMjAxLjA5NCAyMzcuNTY4IDE5My42MjMgMjQwLjkzOCAxODUuMTU2QzI0NC4yNzcgMTc2LjcxOSAyNDUuOTQ3IDE2OC4xOTMgMjQ1Ljk0NyAxNTkuNThWNDQuNDQzNEMyNDUuOTc3IDQxLjgzNTkgMjQ1LjAxIDM5LjU4MDEgMjQzLjEwNSAzNy42NzU4Wk0yMjAuODQgMTYwLjY2NEMyMjAuODQgMjAyLjM1NCAxNTAgMjM4LjI3MSAxNTAgMjM4LjI3MVY1OS41MDJIMjIwLjg0QzIyMC44NCA1OS41MDIgMjIwLjg0IDExOC45NzUgMjIwLjg0IDE2MC42NjRaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzY4XzYxIj4KPHJlY3Qgd2lkdGg9IjMwMCIgaGVpZ2h0PSIzMDAiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==",
|
||||
"icon_light": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDMwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF82OF82MSkiPgo8cGF0aCBkPSJNMzAwIDI1My4xMjVDMzAwIDI3OS4wMjMgMjc5LjAyMyAzMDAgMjUzLjEyNSAzMDBINDYuODc1QzIwLjk3NjYgMzAwIDAgMjc5LjAyMyAwIDI1My4xMjVWNDYuODc1QzAgMjAuOTc2NiAyMC45NzY2IDAgNDYuODc1IDBIMjUzLjEyNUMyNzkuMDIzIDAgMzAwIDIwLjk3NjYgMzAwIDQ2Ljg3NVYyNTMuMTI1WiIgZmlsbD0iIzE3NUREQyIvPgo8cGF0aCBkPSJNMjQzLjEwNSAzNy42NzU4QzI0MS4yMDEgMzUuNzcxNSAyMzguOTQ1IDM0LjgzNCAyMzYuMzY3IDM0LjgzNEg2My42MzI4QzYxLjAyNTQgMzQuODM0IDU4Ljc5ODggMzUuNzcxNSA1Ni44OTQ1IDM3LjY3NThDNTQuOTkwMiAzOS41ODAxIDU0LjA1MjcgNDEuODM1OSA1NC4wNTI3IDQ0LjQxNDFWMTU5LjU4QzU0LjA1MjcgMTY4LjE2NCA1NS43MjI3IDE3Ni42ODkgNTkuMDYyNSAxODUuMTU2QzYyLjQwMjMgMTkzLjU5NCA2Ni41NjI1IDIwMS4wOTQgNzEuNTEzNyAyMDcuNjU2Qzc2LjQ2NDggMjE0LjE4OSA4Mi4zNTM1IDIyMC41NzYgODkuMjA5IDIyNi43ODdDOTYuMDY0NSAyMzIuOTk4IDEwMi4zOTMgMjM4LjEyNSAxMDguMTY0IDI0Mi4yMjdDMTEzLjk2NSAyNDYuMzI4IDEyMCAyNTAuMTk1IDEyNi4yOTkgMjUzLjg1N0MxMzIuNTk4IDI1Ny41MiAxMzcuMDggMjU5Ljk4IDEzOS43MTcgMjYxLjI3QzE0Mi4zNTQgMjYyLjU1OSAxNDQuNDkyIDI2My41ODQgMTQ2LjA3NCAyNjQuMjU4QzE0Ny4yNzUgMjY0Ljg0NCAxNDguNTY0IDI2NS4xNjYgMTQ5Ljk3MSAyNjUuMTY2QzE1MS4zNzcgMjY1LjE2NiAxNTIuNjY2IDI2NC44NzMgMTUzLjg2NyAyNjQuMjU4QzE1NS40NzkgMjYzLjU1NSAxNTcuNTg4IDI2Mi41NTkgMTYwLjI1NCAyNjEuMjdDMTYyLjg5MSAyNTkuOTggMTY3LjM3MyAyNTcuNDkgMTczLjY3MiAyNTMuODU3QzE3OS45NzEgMjUwLjE5NSAxODYuMDA2IDI0Ni4zMjggMTkxLjgwNyAyNDIuMjI3QzE5Ny42MDcgMjM4LjEyNSAyMDMuOTM2IDIzMi45NjkgMjEwLjc5MSAyMjYuNzg3QzIxNy42NDYgMjIwLjU3NiAyMjMuNTM1IDIxNC4yMTkgMjI4LjQ4NiAyMDcuNjU2QzIzMy40MzggMjAxLjA5NCAyMzcuNTY4IDE5My42MjMgMjQwLjkzOCAxODUuMTU2QzI0NC4yNzcgMTc2LjcxOSAyNDUuOTQ3IDE2OC4xOTMgMjQ1Ljk0NyAxNTkuNThWNDQuNDQzNEMyNDUuOTc3IDQxLjgzNTkgMjQ1LjAxIDM5LjU4MDEgMjQzLjEwNSAzNy42NzU4Wk0yMjAuODQgMTYwLjY2NEMyMjAuODQgMjAyLjM1NCAxNTAgMjM4LjI3MSAxNTAgMjM4LjI3MVY1OS41MDJIMjIwLjg0QzIyMC44NCA1OS41MDIgMjIwLjg0IDExOC45NzUgMjIwLjg0IDE2MC42NjRaIiBmaWxsPSJ3aGl0ZSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzY4XzYxIj4KPHJlY3Qgd2lkdGg9IjMwMCIgaGVpZ2h0PSIzMDAiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg=="
|
||||
},
|
||||
"fbfc3007-154e-4ecc-8c0b-6e020557d7bd": {
|
||||
"name": "iCloud Keychain",
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,8 +1,9 @@
|
||||
"""consent tests"""
|
||||
|
||||
from time import sleep
|
||||
from datetime import timedelta
|
||||
|
||||
from django.urls import reverse
|
||||
from freezegun import freeze_time
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tasks import clean_expired_models
|
||||
@ -136,11 +137,12 @@ class TestConsentStage(FlowTestCase):
|
||||
self.assertTrue(
|
||||
UserConsent.objects.filter(user=self.user, application=self.application).exists()
|
||||
)
|
||||
sleep(1)
|
||||
clean_expired_models.delay().get()
|
||||
self.assertFalse(
|
||||
UserConsent.objects.filter(user=self.user, application=self.application).exists()
|
||||
)
|
||||
with freeze_time() as frozen_time:
|
||||
frozen_time.tick(timedelta(seconds=3))
|
||||
clean_expired_models.delay().get()
|
||||
self.assertFalse(
|
||||
UserConsent.objects.filter(user=self.user, application=self.application).exists()
|
||||
)
|
||||
|
||||
def test_permanent_more_perms(self):
|
||||
"""Test permanent consent from user"""
|
||||
|
@ -7,8 +7,8 @@ from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.object_types import TypeCreateSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import TypeCreateSerializer
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.email.models import EmailStage, get_template_choices
|
||||
|
||||
|
@ -3,9 +3,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikStageUserInvitationConfig(AppConfig):
|
||||
class AuthentikStageInvitationConfig(AppConfig):
|
||||
"""authentik invitation stage config"""
|
||||
|
||||
name = "authentik.stages.invitation"
|
||||
label = "authentik_stages_invitation"
|
||||
verbose_name = "authentik Stages.User Invitation"
|
||||
verbose_name = "authentik Stages.Invitation"
|
||||
|
@ -208,7 +208,7 @@ class Prompt(SerializerModel):
|
||||
try:
|
||||
return evaluator.evaluate(self.placeholder)
|
||||
except Exception as exc: # pylint:disable=broad-except
|
||||
wrapped = PropertyMappingExpressionException(str(exc))
|
||||
wrapped = PropertyMappingExpressionException(str(exc), None)
|
||||
LOGGER.warning(
|
||||
"failed to evaluate prompt placeholder",
|
||||
exc=wrapped,
|
||||
|
29
authentik/tenants/db.py
Normal file
29
authentik/tenants/db.py
Normal file
@ -0,0 +1,29 @@
|
||||
from random import choice
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class FailoverRouter:
|
||||
"""Support an primary/read-replica PostgreSQL setup (reading from replicas
|
||||
and write to primary only)"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.database_aliases = set(settings.DATABASES.keys())
|
||||
self.read_replica_aliases = list(self.database_aliases - {"default"})
|
||||
self.replica_enabled = len(self.read_replica_aliases) > 0
|
||||
|
||||
def db_for_read(self, model, **hints):
|
||||
if not self.replica_enabled:
|
||||
return "default"
|
||||
return choice(self.read_replica_aliases) # nosec
|
||||
|
||||
def db_for_write(self, model, **hints):
|
||||
return "default"
|
||||
|
||||
def allow_relation(self, obj1, obj2, **hints):
|
||||
"""Relations between objects are allowed if both objects are
|
||||
in the primary/replica pool."""
|
||||
if obj1._state.db in self.database_aliases and obj2._state.db in self.database_aliases:
|
||||
return True
|
||||
return None
|
@ -1,5 +1,4 @@
|
||||
---
|
||||
version: "3.4"
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
|
2
go.mod
2
go.mod
@ -23,7 +23,7 @@ require (
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/sethvargo/go-envconfig v1.0.2
|
||||
github.com/sethvargo/go-envconfig v1.0.3
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
|
4
go.sum
4
go.sum
@ -248,8 +248,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sethvargo/go-envconfig v1.0.2 h1:BAQnzBLK/mPN3R3pC0d46MLN0htc64YZBVrz/sZfAX4=
|
||||
github.com/sethvargo/go-envconfig v1.0.2/go.mod h1:OKZ02xFaD3MvWBBmEW45fQr08sJEsonGrrOdicvQmQA=
|
||||
github.com/sethvargo/go-envconfig v1.0.3 h1:ZDxFGT1M7RPX0wgDOCdZMidrEB+NrayYr6fL0/+pk4I=
|
||||
github.com/sethvargo/go-envconfig v1.0.3/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-13 00:08+0000\n"
|
||||
"POT-Creation-Date: 2024-05-22 00:07+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"
|
||||
@ -1750,14 +1750,6 @@ msgstr ""
|
||||
msgid "Can view system info"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can view system tasks"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can run system tasks"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can access admin interface"
|
||||
msgstr ""
|
||||
|
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-13 00:08+0000\n"
|
||||
"POT-Creation-Date: 2024-05-22 00:07+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"
|
||||
@ -1782,14 +1782,6 @@ msgstr "系统权限"
|
||||
msgid "Can view system info"
|
||||
msgstr "可以查看系统信息"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can view system tasks"
|
||||
msgstr "可以查看系统任务"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can run system tasks"
|
||||
msgstr "可以运行系统任务"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can access admin interface"
|
||||
msgstr "可以访问管理员界面"
|
||||
|
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-05-13 00:08+0000\n"
|
||||
"POT-Creation-Date: 2024-05-22 00:07+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"
|
||||
@ -1782,14 +1782,6 @@ msgstr "系统权限"
|
||||
msgid "Can view system info"
|
||||
msgstr "可以查看系统信息"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can view system tasks"
|
||||
msgstr "可以查看系统任务"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can run system tasks"
|
||||
msgstr "可以运行系统任务"
|
||||
|
||||
#: authentik/rbac/models.py
|
||||
msgid "Can access admin interface"
|
||||
msgstr "可以访问管理员界面"
|
||||
|
327
poetry.lock
generated
327
poetry.lock
generated
@ -2171,165 +2171,149 @@ pyasn1 = ">=0.4.6"
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "5.2.1"
|
||||
version = "5.2.2"
|
||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"},
|
||||
{file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"},
|
||||
{file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"},
|
||||
{file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"},
|
||||
{file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"},
|
||||
{file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"},
|
||||
{file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"},
|
||||
{file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"},
|
||||
{file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"},
|
||||
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"},
|
||||
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"},
|
||||
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"},
|
||||
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"},
|
||||
{file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"},
|
||||
{file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"},
|
||||
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"},
|
||||
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"},
|
||||
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"},
|
||||
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"},
|
||||
{file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"},
|
||||
{file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"},
|
||||
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"},
|
||||
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"},
|
||||
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"},
|
||||
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"},
|
||||
{file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"},
|
||||
{file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"},
|
||||
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"},
|
||||
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"},
|
||||
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"},
|
||||
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"},
|
||||
{file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"},
|
||||
{file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-win32.whl", hash = "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30"},
|
||||
{file = "lxml-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"},
|
||||
{file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"},
|
||||
{file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-win32.whl", hash = "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3"},
|
||||
{file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"},
|
||||
{file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"},
|
||||
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"},
|
||||
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"},
|
||||
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"},
|
||||
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"},
|
||||
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"},
|
||||
{file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
|
||||
{file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-win32.whl", hash = "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30"},
|
||||
{file = "lxml-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-win32.whl", hash = "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf"},
|
||||
{file = "lxml-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425"},
|
||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"},
|
||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"},
|
||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"},
|
||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"},
|
||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"},
|
||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"},
|
||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"},
|
||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"},
|
||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"},
|
||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"},
|
||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"},
|
||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"},
|
||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"},
|
||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"},
|
||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"},
|
||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"},
|
||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"},
|
||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"},
|
||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"},
|
||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"},
|
||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"},
|
||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"},
|
||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"},
|
||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"},
|
||||
{file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -3041,13 +3025,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pdoc"
|
||||
version = "14.4.0"
|
||||
version = "14.5.0"
|
||||
description = "API Documentation for Python Projects"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pdoc-14.4.0-py3-none-any.whl", hash = "sha256:6ea4fe07620b1f7601e2708a307a257636ec206e20b5611640b30f2e3cab47d6"},
|
||||
{file = "pdoc-14.4.0.tar.gz", hash = "sha256:c92edc425429ccbe287ace2a027953c24f13de53eab484c1a6d31ca72dd2fda9"},
|
||||
{file = "pdoc-14.5.0-py3-none-any.whl", hash = "sha256:9a8a84e19662610c0620fbe9f2e4174e3b090f8b601ed46348786ebb7517c508"},
|
||||
{file = "pdoc-14.5.0.tar.gz", hash = "sha256:79f534dc8a6494638dd6056b78e17a654df7ed34cc92646553ce3a7ba5a4fa4a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -3592,13 +3576,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.2.0"
|
||||
version = "8.2.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"},
|
||||
{file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"},
|
||||
{file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
|
||||
{file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4063,13 +4047,13 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "scim2-filter-parser"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
description = "A customizable parser/transpiler for SCIM2.0 filters."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "scim2_filter_parser-0.5.0-py3-none-any.whl", hash = "sha256:4aca1b3b64655dc038a973a9659056a103a919fb0218614e36bf19d3b5de5b48"},
|
||||
{file = "scim2_filter_parser-0.5.0.tar.gz", hash = "sha256:104c72e6faeb9a6b873950f66b0e3b69134fb19debf67e1d3714e91a6dafd8af"},
|
||||
{file = "scim2_filter_parser-0.5.1-py3-none-any.whl", hash = "sha256:09338fd73389606961d1fd90a068c6f4ffe357a9509bc48adc1dbb70afc2821d"},
|
||||
{file = "scim2_filter_parser-0.5.1.tar.gz", hash = "sha256:d2b88d11fbf000baca8e6b2057edb9bdf9827c4a34b172d05559b2b9f1994edf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4098,13 +4082,13 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.2.0"
|
||||
version = "2.2.1"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "sentry_sdk-2.2.0-py2.py3-none-any.whl", hash = "sha256:674f58da37835ea7447fe0e34c57b4a4277fad558b0a7cb4a6c83bcb263086be"},
|
||||
{file = "sentry_sdk-2.2.0.tar.gz", hash = "sha256:70eca103cf4c6302365a9d7cf522e7ed7720828910eb23d43ada8e50d1ecda9d"},
|
||||
{file = "sentry_sdk-2.2.1-py2.py3-none-any.whl", hash = "sha256:7d617a1b30e80c41f3b542347651fcf90bb0a36f3a398be58b4f06b79c8d85bc"},
|
||||
{file = "sentry_sdk-2.2.1.tar.gz", hash = "sha256:8aa2ec825724d8d9d645cab68e6034928b1a6a148503af3e361db3fa6401183f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -4126,7 +4110,7 @@ django = ["django (>=1.8)"]
|
||||
falcon = ["falcon (>=1.4)"]
|
||||
fastapi = ["fastapi (>=0.79.0)"]
|
||||
flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
|
||||
grpcio = ["grpcio (>=1.21.1)"]
|
||||
grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"]
|
||||
httpx = ["httpx (>=0.16.0)"]
|
||||
huey = ["huey (>=2)"]
|
||||
huggingface-hub = ["huggingface-hub (>=0.22)"]
|
||||
@ -4405,12 +4389,13 @@ typing-extensions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "tenant-schemas-celery"
|
||||
version = "2.2.0"
|
||||
version = "3.0.0"
|
||||
description = "Celery integration for django-tenant-schemas and django-tenants"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tenant-schemas-celery-2.2.0.tar.gz", hash = "sha256:b4fc16959cb98597591afb30f07256f70d8470d97c22c62e3d3af344868cdd6f"},
|
||||
{file = "tenant_schemas_celery-3.0.0-py3-none-any.whl", hash = "sha256:ca0f69e78ef698eb4813468231df5a0ab6a660c08e657b65f5ac92e16887eec8"},
|
||||
{file = "tenant_schemas_celery-3.0.0.tar.gz", hash = "sha256:6be3ae1a5826f262f0f3dd343c6a85a34a1c59b89e04ae37de018f36562fed55"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
34
schema.yml
34
schema.yml
@ -3733,6 +3733,11 @@ paths:
|
||||
format: uuid
|
||||
description: A UUID string identifying this Group.
|
||||
required: true
|
||||
- in: query
|
||||
name: include_users
|
||||
schema:
|
||||
type: boolean
|
||||
default: true
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
@ -10042,7 +10047,7 @@ paths:
|
||||
/outposts/service_connections/all/types/:
|
||||
get:
|
||||
operationId: outposts_service_connections_all_types_list
|
||||
description: Get all creatable service connection types
|
||||
description: Get all creatable types
|
||||
tags:
|
||||
- outposts
|
||||
security:
|
||||
@ -10876,7 +10881,7 @@ paths:
|
||||
/policies/all/types/:
|
||||
get:
|
||||
operationId: policies_all_types_list
|
||||
description: Get all creatable policy types
|
||||
description: Get all creatable types
|
||||
tags:
|
||||
- policies
|
||||
security:
|
||||
@ -13434,7 +13439,7 @@ paths:
|
||||
/propertymappings/all/types/:
|
||||
get:
|
||||
operationId: propertymappings_all_types_list
|
||||
description: Get all creatable property-mapping types
|
||||
description: Get all creatable types
|
||||
tags:
|
||||
- propertymappings
|
||||
security:
|
||||
@ -15894,7 +15899,7 @@ paths:
|
||||
/providers/all/types/:
|
||||
get:
|
||||
operationId: providers_all_types_list
|
||||
description: Get all creatable provider types
|
||||
description: Get all creatable types
|
||||
tags:
|
||||
- providers
|
||||
security:
|
||||
@ -21893,7 +21898,7 @@ paths:
|
||||
/sources/all/types/:
|
||||
get:
|
||||
operationId: sources_all_types_list
|
||||
description: Get all creatable source types
|
||||
description: Get all creatable types
|
||||
tags:
|
||||
- sources
|
||||
security:
|
||||
@ -25527,7 +25532,7 @@ paths:
|
||||
/stages/all/types/:
|
||||
get:
|
||||
operationId: stages_all_types_list
|
||||
description: Get all creatable stage types
|
||||
description: Get all creatable types
|
||||
tags:
|
||||
- stages
|
||||
security:
|
||||
@ -37788,10 +37793,6 @@ components:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |-
|
||||
Get the URL to the Icon. If the name is /static or
|
||||
starts with http it is returned as-is
|
||||
readOnly: true
|
||||
server_uri:
|
||||
type: string
|
||||
@ -39101,9 +39102,6 @@ components:
|
||||
icon:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |-
|
||||
Get the URL to the Icon. If the name is /static or
|
||||
starts with http it is returned as-is
|
||||
readOnly: true
|
||||
provider_type:
|
||||
$ref: '#/components/schemas/ProviderTypeEnum'
|
||||
@ -43795,10 +43793,6 @@ components:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |-
|
||||
Get the URL to the Icon. If the name is /static or
|
||||
starts with http it is returned as-is
|
||||
readOnly: true
|
||||
client_id:
|
||||
type: string
|
||||
@ -45858,10 +45852,6 @@ components:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |-
|
||||
Get the URL to the Icon. If the name is /static or
|
||||
starts with http it is returned as-is
|
||||
readOnly: true
|
||||
pre_authentication_flow:
|
||||
type: string
|
||||
@ -47479,6 +47469,8 @@ components:
|
||||
type: string
|
||||
model_name:
|
||||
type: string
|
||||
icon_url:
|
||||
type: string
|
||||
requires_enterprise:
|
||||
type: boolean
|
||||
default: false
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
container_name: postgres
|
||||
|
@ -12,6 +12,9 @@ with open("local.env.yml", "w", encoding="utf-8") as _config:
|
||||
"secret_key": generate_id(),
|
||||
"postgresql": {
|
||||
"user": "postgres",
|
||||
"read_replicas": {
|
||||
"0": {},
|
||||
},
|
||||
},
|
||||
"outposts": {
|
||||
"container_image_base": "ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s",
|
||||
|
BIN
web/authentik/sources/ldap.png
Normal file
BIN
web/authentik/sources/ldap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.5 KiB |
13
web/authentik/sources/proxy.svg
Normal file
13
web/authentik/sources/proxy.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg id="icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path d="M6,30H26a2,2,0,0,0,2-2V22a2,2,0,0,0-2-2H6a2,2,0,0,0-2,2v6A2,2,0,0,0,6,30Zm0-8H26v6H6Z" transform="translate(0 0)"/>
|
||||
<circle cx="9" cy="25" r="1"/>
|
||||
<path d="M26,2,24.59,3.41,27.17,6H22.315A6.9835,6.9835,0,0,0,9.08,10H4.83L7.41,7.41,6,6,1,11l5,5,1.41-1.41L4.83,12H9.685A6.9835,6.9835,0,0,0,22.92,8h4.25l-2.58,2.59L26,12l5-5ZM21,9a4.983,4.983,0,0,1-8.9745,3H16V10H11.1011a4.9852,4.9852,0,0,1,8.8734-4H16V8h4.8989A5.0019,5.0019,0,0,1,21,9Z" transform="translate(0 0)"/>
|
||||
<rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" class="cls-1" width="32" height="32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 787 B |
1
web/authentik/sources/rac.svg
Normal file
1
web/authentik/sources/rac.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg id="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><style>.cls-1{fill:none;}</style></defs><title>desktop</title><path d="M28,4H4A2,2,0,0,0,2,6V22a2,2,0,0,0,2,2h8v4H8v2H24V28H20V24h8a2,2,0,0,0,2-2V6A2,2,0,0,0,28,4ZM18,28H14V24h4Zm10-6H4V6H28Z" transform="translate(0)"/><rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" class="cls-1" width="32" height="32"/></svg>
|
After Width: | Height: | Size: 417 B |
16
web/authentik/sources/radius.svg
Normal file
16
web/authentik/sources/radius.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg id="icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle cx="16" cy="13.5" r="1.5"/>
|
||||
<path d="M19.5356,10.4648a5,5,0,0,0-7.0717,0L11.05,9.0508a6.9992,6.9992,0,0,1,9.9,0Z" transform="translate(0 0)"/>
|
||||
<path d="M23.0713,6.929a10,10,0,0,0-14.1426,0L7.5146,5.5144a12.0011,12.0011,0,0,1,16.9708,0Z" transform="translate(0 0)"/>
|
||||
<polygon points="21 25 16 30 11 25 12.409 23.581 15 26.153 15 19 17 19 17 26.206 19.591 23.581 21 25"/>
|
||||
<polygon points="24 11 19 16 24 21 25.419 19.591 22.847 17 30 17 30 15 22.794 15 25.419 12.409 24 11"/>
|
||||
<polygon points="8 11 13 16 8 21 6.581 19.591 9.153 17 2 17 2 15 9.206 15 6.581 12.409 8 11"/>
|
||||
<rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" class="cls-1" width="32" height="32"/>
|
||||
</svg>
|
After Width: | Height: | Size: 895 B |
BIN
web/authentik/sources/saml.png
Normal file
BIN
web/authentik/sources/saml.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
BIN
web/authentik/sources/scim.png
Normal file
BIN
web/authentik/sources/scim.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
1042
web/package-lock.json
generated
1042
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,9 +36,9 @@
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/legacy-modes": "^6.4.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@formatjs/intl-listformat": "^7.5.5",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@goauthentik/api": "^2024.4.2-1715271029",
|
||||
"@goauthentik/api": "^2024.4.2-1716338508",
|
||||
"@lit-labs/task": "^3.1.0",
|
||||
"@lit/context": "^1.1.1",
|
||||
"@lit/localize": "^0.12.1",
|
||||
@ -49,17 +49,17 @@
|
||||
"@sentry/browser": "^8.2.1",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^4.4.2",
|
||||
"chart.js": "^4.4.3",
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"core-js": "^3.37.0",
|
||||
"core-js": "^3.37.1",
|
||||
"country-flag-icons": "^1.5.11",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"lit": "^3.1.3",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mermaid": "^10.9.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"rapidoc": "^9.3.4",
|
||||
"showdown": "^2.1.0",
|
||||
"style-mod": "^4.1.2",
|
||||
@ -81,13 +81,13 @@
|
||||
"@lit/localize-tools": "^0.7.2",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@spotlightjs/spotlight": "^1.2.17",
|
||||
"@storybook/addon-essentials": "^8.1.1",
|
||||
"@storybook/addon-links": "^8.1.1",
|
||||
"@storybook/addon-essentials": "^8.1.2",
|
||||
"@storybook/addon-links": "^8.1.2",
|
||||
"@storybook/api": "^7.6.17",
|
||||
"@storybook/blocks": "^8.0.8",
|
||||
"@storybook/manager-api": "^8.1.1",
|
||||
"@storybook/web-components": "^8.1.1",
|
||||
"@storybook/web-components-vite": "^8.1.1",
|
||||
"@storybook/manager-api": "^8.1.2",
|
||||
"@storybook/web-components": "^8.1.2",
|
||||
"@storybook/web-components-vite": "^8.1.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/codemirror": "5.60.15",
|
||||
@ -108,7 +108,7 @@
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"glob": "^10.3.15",
|
||||
"glob": "^10.3.16",
|
||||
"lit-analyzer": "^2.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.2.5",
|
||||
@ -117,7 +117,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"rollup-plugin-modify": "^3.0.0",
|
||||
"rollup-plugin-postcss-lit": "^2.1.0",
|
||||
"storybook": "^8.1.1",
|
||||
"storybook": "^8.1.2",
|
||||
"storybook-addon-mock": "^5.0.0",
|
||||
"ts-lit-plugin": "^2.0.2",
|
||||
"tslib": "^2.6.2",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { PaginatedResponse } from "@goauthentik/authentik/elements/table/Table";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
|
||||
import "@goauthentik/elements/forms/ConfirmationForm";
|
||||
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
|
||||
import { ChartData, ChartOptions } from "chart.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
|
@ -25,167 +25,152 @@ type ModelConverter = (provider: OneOfProvider) => ModelRequest;
|
||||
type ProviderNoteProvider = () => TemplateResult | undefined;
|
||||
type ProviderNote = ProviderNoteProvider | undefined;
|
||||
|
||||
/**
|
||||
* There's an internal key and an API key because "Proxy" has three different subtypes.
|
||||
*/
|
||||
// prettier-ignore
|
||||
type ProviderType = [
|
||||
string, // internal key used by the wizard to distinguish between providers
|
||||
string, // Name of the provider
|
||||
string, // Description
|
||||
ProviderRenderer, // Function that returns the provider's wizard panel as a TemplateResult
|
||||
ProviderModelEnumType, // key used by the API to distinguish between providers
|
||||
ModelConverter, // Handler that takes a generic provider and returns one specifically typed to its panel
|
||||
ProviderNote?,
|
||||
];
|
||||
|
||||
export type LocalTypeCreate = TypeCreate & {
|
||||
formName: string;
|
||||
modelName: ProviderModelEnumType;
|
||||
converter: ModelConverter;
|
||||
note?: ProviderNote;
|
||||
renderer: ProviderRenderer;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
const _providerModelsTable: ProviderType[] = [
|
||||
[
|
||||
"oauth2provider",
|
||||
msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
|
||||
msg("Modern applications, APIs and Single-page applications."),
|
||||
() =>
|
||||
export const providerModelsList: LocalTypeCreate[] = [
|
||||
{
|
||||
formName: "oauth2provider",
|
||||
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
|
||||
description: msg("Modern applications, APIs and Single-page applications."),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
|
||||
ProviderModelEnum.Oauth2Oauth2provider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.Oauth2Oauth2provider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
|
||||
...(provider as OAuth2ProviderRequest),
|
||||
}),
|
||||
],
|
||||
[
|
||||
"ldapprovider",
|
||||
msg("LDAP (Lightweight Directory Access Protocol)"),
|
||||
msg("Provide an LDAP interface for applications and users to authenticate against."),
|
||||
() =>
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/openidconnect.svg",
|
||||
},
|
||||
{
|
||||
formName: "ldapprovider",
|
||||
name: msg("LDAP (Lightweight Directory Access Protocol)"),
|
||||
description: msg(
|
||||
"Provide an LDAP interface for applications and users to authenticate against.",
|
||||
),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
|
||||
ProviderModelEnum.LdapLdapprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.LdapLdapprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.LdapLdapprovider,
|
||||
...(provider as LDAPProviderRequest),
|
||||
}),
|
||||
],
|
||||
[
|
||||
"proxyprovider-proxy",
|
||||
msg("Transparent Reverse Proxy"),
|
||||
msg("For transparent reverse proxies with required authentication"),
|
||||
() =>
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/ldap.png",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-proxy",
|
||||
name: msg("Transparent Reverse Proxy"),
|
||||
description: msg("For transparent reverse proxies with required authentication"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
|
||||
ProviderModelEnum.ProxyProxyprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.Proxy,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"proxyprovider-forwardsingle",
|
||||
msg("Forward Auth (Single Application)"),
|
||||
msg("For nginx's auth_request or traefik's forwardAuth"),
|
||||
() =>
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-forwardsingle",
|
||||
name: msg("Forward Auth (Single Application)"),
|
||||
description: msg("For nginx's auth_request or traefik's forwardAuth"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
|
||||
ProviderModelEnum.ProxyProxyprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.ForwardSingle,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"proxyprovider-forwarddomain",
|
||||
msg("Forward Auth (Domain Level)"),
|
||||
msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
|
||||
() =>
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-forwarddomain",
|
||||
name: msg("Forward Auth (Domain Level)"),
|
||||
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
|
||||
ProviderModelEnum.ProxyProxyprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.ForwardDomain,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"racprovider",
|
||||
msg("Remote Access Provider"),
|
||||
msg("Remotely access computers/servers via RDP/SSH/VNC"),
|
||||
() =>
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "racprovider",
|
||||
name: msg("Remote Access Provider"),
|
||||
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
|
||||
ProviderModelEnum.RacRacprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.RacRacprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.RacRacprovider,
|
||||
...(provider as RACProviderRequest),
|
||||
}),
|
||||
() => html`<ak-license-notice></ak-license-notice>`
|
||||
],
|
||||
[
|
||||
"samlprovider",
|
||||
msg("SAML (Security Assertion Markup Language)"),
|
||||
msg("Configure SAML provider manually"),
|
||||
() =>
|
||||
note: () => html`<ak-license-notice></ak-license-notice>`,
|
||||
requiresEnterprise: true,
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/rac.svg",
|
||||
},
|
||||
{
|
||||
formName: "samlprovider",
|
||||
name: msg("SAML (Security Assertion Markup Language)"),
|
||||
description: msg("Configure SAML provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
|
||||
ProviderModelEnum.SamlSamlprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.SamlSamlprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.SamlSamlprovider,
|
||||
...(provider as SAMLProviderRequest),
|
||||
}),
|
||||
],
|
||||
[
|
||||
"radiusprovider",
|
||||
msg("RADIUS (Remote Authentication Dial-In User Service)"),
|
||||
msg("Configure RADIUS provider manually"),
|
||||
() =>
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/saml.png",
|
||||
},
|
||||
{
|
||||
formName: "radiusprovider",
|
||||
name: msg("RADIUS (Remote Authentication Dial-In User Service)"),
|
||||
description: msg("Configure RADIUS provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
|
||||
ProviderModelEnum.RadiusRadiusprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.RadiusRadiusprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.RadiusRadiusprovider,
|
||||
...(provider as RadiusProviderRequest),
|
||||
}),
|
||||
],
|
||||
[
|
||||
"scimprovider",
|
||||
msg("SCIM (System for Cross-domain Identity Management)"),
|
||||
msg("Configure SCIM provider manually"),
|
||||
() =>
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/radius.svg",
|
||||
},
|
||||
{
|
||||
formName: "scimprovider",
|
||||
name: msg("SCIM (System for Cross-domain Identity Management)"),
|
||||
description: msg("Configure SCIM provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
|
||||
ProviderModelEnum.ScimScimprovider,
|
||||
(provider: OneOfProvider) => ({
|
||||
modelName: ProviderModelEnum.ScimScimprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ScimScimprovider,
|
||||
...(provider as SCIMProviderRequest),
|
||||
}),
|
||||
],
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/scim.png",
|
||||
},
|
||||
];
|
||||
|
||||
function mapProviders([
|
||||
formName,
|
||||
name,
|
||||
description,
|
||||
_,
|
||||
modelName,
|
||||
converter,
|
||||
note,
|
||||
]: ProviderType): LocalTypeCreate {
|
||||
return {
|
||||
formName,
|
||||
name,
|
||||
description,
|
||||
component: "",
|
||||
modelName,
|
||||
converter,
|
||||
note,
|
||||
};
|
||||
}
|
||||
|
||||
export const providerModelsList = _providerModelsTable.map(mapProviders);
|
||||
|
||||
export const providerRendererList = new Map<string, ProviderRenderer>(
|
||||
_providerModelsTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]),
|
||||
providerModelsList.map((tc) => [tc.formName, tc.renderer]),
|
||||
);
|
||||
|
||||
export default providerModelsList;
|
||||
|
@ -3,63 +3,41 @@ import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
|
||||
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { map } from "lit/directives/map.js";
|
||||
import { html } from "lit";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-method-choice")
|
||||
export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) {
|
||||
constructor() {
|
||||
super();
|
||||
this.handleChoice = this.handleChoice.bind(this);
|
||||
this.renderProvider = this.renderProvider.bind(this);
|
||||
}
|
||||
|
||||
handleChoice(ev: InputEvent) {
|
||||
const target = ev.target as HTMLInputElement;
|
||||
this.dispatchWizardUpdate({
|
||||
update: {
|
||||
...this.wizard,
|
||||
providerModel: target.value,
|
||||
errors: {},
|
||||
},
|
||||
status: this.valid ? "valid" : "invalid",
|
||||
});
|
||||
}
|
||||
|
||||
renderProvider(type: LocalTypeCreate) {
|
||||
const method = this.wizard.providerModel;
|
||||
|
||||
return html`<div class="pf-c-radio">
|
||||
<input
|
||||
class="pf-c-radio__input"
|
||||
type="radio"
|
||||
name="type"
|
||||
id="provider-${type.formName}"
|
||||
?disabled=${type.formName === "racprovider" && !this.hasEnterpriseLicense}
|
||||
value=${type.formName}
|
||||
?checked=${type.formName === method}
|
||||
@change=${this.handleChoice}
|
||||
/>
|
||||
<label class="pf-c-radio__label" for="provider-${type.formName}">${type.name}</label>
|
||||
<span class="pf-c-radio__description"
|
||||
>${type.description}${type.note ? type.note() : nothing}</span
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const selectedTypes = providerModelsList.filter(
|
||||
(t) => t.formName === this.wizard.providerModel,
|
||||
);
|
||||
return providerModelsList.length > 0
|
||||
? html`<form class="pf-c-form pf-m-horizontal">
|
||||
${map(providerModelsList, this.renderProvider)}
|
||||
<ak-wizard-page-type-create
|
||||
.types=${providerModelsList}
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
||||
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
||||
this.dispatchWizardUpdate({
|
||||
update: {
|
||||
...this.wizard,
|
||||
providerModel: ev.detail.formName,
|
||||
errors: {},
|
||||
},
|
||||
status: this.valid ? "valid" : "invalid",
|
||||
});
|
||||
}}
|
||||
></ak-wizard-page-type-create>
|
||||
</form>`
|
||||
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { TemplateResult, css, html, nothing } from "lit";
|
||||
import { PropertyValues, TemplateResult, css, html, nothing } from "lit";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
||||
@ -94,8 +94,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
|
||||
|
||||
response?: TransactionApplicationResponse;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
willUpdate(_changedProperties: Map<string, any>) {
|
||||
willUpdate(_changedProperties: PropertyValues<this>) {
|
||||
if (this.commitState === idleState) {
|
||||
this.response = undefined;
|
||||
this.commitState = runningState;
|
||||
|
@ -12,8 +12,6 @@ import "./radius/ak-application-wizard-authentication-by-radius";
|
||||
import "./saml/ak-application-wizard-authentication-by-saml-configuration";
|
||||
import "./scim/ak-application-wizard-authentication-by-scim";
|
||||
|
||||
// prettier-ignore
|
||||
|
||||
@customElement("ak-application-wizard-authentication-method")
|
||||
export class ApplicationWizardApplicationDetails extends BasePanel {
|
||||
render() {
|
||||
|
@ -12,6 +12,7 @@ import { CoreApi, CoreGroupsListRequest, Group } from "@goauthentik/api";
|
||||
async function fetchObjects(query?: string): Promise<Group[]> {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
includeUsers: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
|
@ -9,15 +9,14 @@ import { customElement, property } from "lit/decorators.js";
|
||||
@customElement("ak-license-notice")
|
||||
export class AkLicenceNotice extends WithLicenseSummary(AKElement) {
|
||||
@property()
|
||||
notice = msg("This feature requires an enterprise license.");
|
||||
notice = msg("Enterprise only");
|
||||
|
||||
render() {
|
||||
return this.hasEnterpriseLicense
|
||||
? nothing
|
||||
: html`
|
||||
<ak-alert class="pf-c-radio__description" inline>
|
||||
${this.notice}
|
||||
<a href="#/enterprise/licenses">${msg("Learn more")}</a>
|
||||
<ak-alert class="pf-c-radio__description" inline plain>
|
||||
<a href="#/enterprise/licenses">${this.notice}</a>
|
||||
</ak-alert>
|
||||
`;
|
||||
}
|
||||
|
@ -69,6 +69,7 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
|
||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
includeUsers: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
|
@ -39,6 +39,7 @@ export class GroupForm extends ModelForm<Group, string> {
|
||||
loadInstance(pk: string): Promise<Group> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsRetrieve({
|
||||
groupUuid: pk,
|
||||
includeUsers: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user