commit
1f89b94f66
|
@ -40,7 +40,7 @@ RUN apt-get update && \
|
||||||
chown authentik:authentik /backups
|
chown authentik:authentik /backups
|
||||||
|
|
||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pytest.ini /
|
COPY ./pyproject.toml /
|
||||||
COPY ./xml /xml
|
COPY ./xml /xml
|
||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -6,7 +6,7 @@ test-integration:
|
||||||
coverage run manage.py test -v 3 tests/integration
|
coverage run manage.py test -v 3 tests/integration
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
coverage run manage.py test -v 3 tests/e2e
|
coverage run manage.py test --failfast -v 3 tests/e2e
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
coverage run manage.py test -v 3 authentik
|
coverage run manage.py test -v 3 authentik
|
||||||
|
|
2
Pipfile
2
Pipfile
|
@ -22,7 +22,7 @@ django-storages = "*"
|
||||||
djangorestframework = "*"
|
djangorestframework = "*"
|
||||||
djangorestframework-guardian = "*"
|
djangorestframework-guardian = "*"
|
||||||
docker = "*"
|
docker = "*"
|
||||||
drf_yasg2 = "*"
|
drf_yasg = "*"
|
||||||
facebook-sdk = "*"
|
facebook-sdk = "*"
|
||||||
geoip2 = "*"
|
geoip2 = "*"
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "5fce5772178e4bc782d7112fab658f5bbb21abb77bb93fc3c0a66e9db3a23a37"
|
"sha256": "a9d504f00ee8820017f26a4fda2938de456cb72b4bc2f8735fc8c6a6c615d46a"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -416,13 +416,13 @@
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.4.4"
|
"version": "==4.4.4"
|
||||||
},
|
},
|
||||||
"drf-yasg2": {
|
"drf-yasg": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7037a8041eb5d1073fa504a284fc889685f93d0bfd008a963db1b366db786734",
|
"sha256:8b72e5b1875931a8d11af407be3a9a5ba8776541492947a0df5bafda6b7f8267",
|
||||||
"sha256:75e661ca5cf15eb44fcfab408c7b864f87c20794f564aa08b3a31817a857f19d"
|
"sha256:d50f197c7f02545d0b736df88c6d5cf874f8fea2507ad85ad7de6ae5bf2d9e5a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.19.4"
|
"version": "==1.20.0"
|
||||||
},
|
},
|
||||||
"facebook-sdk": {
|
"facebook-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -3,18 +3,18 @@ import time
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.db.models import Count, ExpressionWrapper, F, Model
|
from django.db.models import Count, ExpressionWrapper, F
|
||||||
from django.db.models.fields import DurationField
|
from django.db.models.fields import DurationField
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method
|
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
|
||||||
from rest_framework.fields import IntegerField, SerializerMethodField
|
from rest_framework.fields import IntegerField, SerializerMethodField
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,20 +45,14 @@ def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
class CoordinateSerializer(Serializer):
|
class CoordinateSerializer(PassiveSerializer):
|
||||||
"""Coordinates for diagrams"""
|
"""Coordinates for diagrams"""
|
||||||
|
|
||||||
x_cord = IntegerField(read_only=True)
|
x_cord = IntegerField(read_only=True)
|
||||||
y_cord = IntegerField(read_only=True)
|
y_cord = IntegerField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class LoginMetricsSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class LoginMetricsSerializer(Serializer):
|
|
||||||
"""Login Metrics per 1h"""
|
"""Login Metrics per 1h"""
|
||||||
|
|
||||||
logins_per_1h = SerializerMethodField()
|
logins_per_1h = SerializerMethodField()
|
||||||
|
@ -74,12 +68,6 @@ class LoginMetricsSerializer(Serializer):
|
||||||
"""Get failed logins per hour for the last 24 hours"""
|
"""Get failed logins per hour for the last 24 hours"""
|
||||||
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
|
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class AdministrationMetricsViewSet(ViewSet):
|
class AdministrationMetricsViewSet(ViewSet):
|
||||||
"""Login Metrics per 1h"""
|
"""Login Metrics per 1h"""
|
||||||
|
|
|
@ -2,22 +2,21 @@
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db.models import Model
|
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
|
from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
|
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
|
||||||
|
|
||||||
|
|
||||||
class TaskSerializer(Serializer):
|
class TaskSerializer(PassiveSerializer):
|
||||||
"""Serialize TaskInfo and TaskResult"""
|
"""Serialize TaskInfo and TaskResult"""
|
||||||
|
|
||||||
task_name = CharField()
|
task_name = CharField()
|
||||||
|
@ -30,12 +29,6 @@ class TaskSerializer(Serializer):
|
||||||
)
|
)
|
||||||
messages = ListField(source="result.messages")
|
messages = ListField(source="result.messages")
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class TaskViewSet(ViewSet):
|
class TaskViewSet(ViewSet):
|
||||||
"""Read-only view set that returns all background tasks"""
|
"""Read-only view set that returns all background tasks"""
|
||||||
|
|
|
@ -2,22 +2,21 @@
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.mixins import ListModelMixin
|
from rest_framework.mixins import ListModelMixin
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
|
||||||
|
|
||||||
class VersionSerializer(Serializer):
|
class VersionSerializer(PassiveSerializer):
|
||||||
"""Get running and latest version."""
|
"""Get running and latest version."""
|
||||||
|
|
||||||
version_current = SerializerMethodField()
|
version_current = SerializerMethodField()
|
||||||
|
@ -47,12 +46,6 @@ class VersionSerializer(Serializer):
|
||||||
self.get_version_latest(instance)
|
self.get_version_latest(instance)
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class VersionViewSet(ListModelMixin, GenericViewSet):
|
class VersionViewSet(ListModelMixin, GenericViewSet):
|
||||||
"""Get running and latest version."""
|
"""Get running and latest version."""
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
"""authentik administrative user forms"""
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.admin.fields import CodeMirrorWidget, YAMLField
|
|
||||||
from authentik.core.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserForm(forms.ModelForm):
|
|
||||||
"""Update User Details"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = User
|
|
||||||
fields = ["username", "name", "email", "is_active", "attributes"]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput,
|
|
||||||
"attributes": CodeMirrorWidget,
|
|
||||||
}
|
|
||||||
field_classes = {
|
|
||||||
"attributes": YAMLField,
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
"""authentik admin mixins"""
|
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
|
||||||
|
|
||||||
|
|
||||||
class AdminRequiredMixin(UserPassesTestMixin):
|
|
||||||
"""Make sure user is administrator"""
|
|
||||||
|
|
||||||
def test_func(self):
|
|
||||||
return self.request.user.is_superuser
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends base_template|default:"generic/form.html" %}
|
|
||||||
|
|
||||||
{% load authentik_utils %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% trans 'Generate Certificate-Key Pair' %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block action %}
|
|
||||||
{% trans 'Generate Certificate-Key Pair' %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{% extends base_template|default:"generic/form.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block above_form %}
|
|
||||||
<h1>
|
|
||||||
{% trans 'Import Flow' %}
|
|
||||||
</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block action %}
|
|
||||||
{% trans 'Import Flow' %}
|
|
||||||
{% endblock %}
|
|
|
@ -2,13 +2,6 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.admin.views import (
|
from authentik.admin.views import (
|
||||||
applications,
|
|
||||||
certificate_key_pair,
|
|
||||||
events_notifications_rules,
|
|
||||||
events_notifications_transports,
|
|
||||||
flows,
|
|
||||||
groups,
|
|
||||||
outposts,
|
|
||||||
outposts_service_connections,
|
outposts_service_connections,
|
||||||
policies,
|
policies,
|
||||||
policies_bindings,
|
policies_bindings,
|
||||||
|
@ -19,22 +12,10 @@ from authentik.admin.views import (
|
||||||
stages_bindings,
|
stages_bindings,
|
||||||
stages_invitations,
|
stages_invitations,
|
||||||
stages_prompts,
|
stages_prompts,
|
||||||
users,
|
|
||||||
)
|
)
|
||||||
from authentik.providers.saml.views.metadata import MetadataImportView
|
from authentik.providers.saml.views.metadata import MetadataImportView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Applications
|
|
||||||
path(
|
|
||||||
"applications/create/",
|
|
||||||
applications.ApplicationCreateView.as_view(),
|
|
||||||
name="application-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"applications/<uuid:pk>/update/",
|
|
||||||
applications.ApplicationUpdateView.as_view(),
|
|
||||||
name="application-update",
|
|
||||||
),
|
|
||||||
# Sources
|
# Sources
|
||||||
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
|
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
|
||||||
path(
|
path(
|
||||||
|
@ -116,27 +97,6 @@ urlpatterns = [
|
||||||
stages_invitations.InvitationCreateView.as_view(),
|
stages_invitations.InvitationCreateView.as_view(),
|
||||||
name="stage-invitation-create",
|
name="stage-invitation-create",
|
||||||
),
|
),
|
||||||
# Flows
|
|
||||||
path(
|
|
||||||
"flows/create/",
|
|
||||||
flows.FlowCreateView.as_view(),
|
|
||||||
name="flow-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"flows/import/",
|
|
||||||
flows.FlowImportView.as_view(),
|
|
||||||
name="flow-import",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"flows/<uuid:pk>/update/",
|
|
||||||
flows.FlowUpdateView.as_view(),
|
|
||||||
name="flow-update",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"flows/<uuid:pk>/execute/",
|
|
||||||
flows.FlowDebugExecuteView.as_view(),
|
|
||||||
name="flow-execute",
|
|
||||||
),
|
|
||||||
# Property Mappings
|
# Property Mappings
|
||||||
path(
|
path(
|
||||||
"property-mappings/create/",
|
"property-mappings/create/",
|
||||||
|
@ -153,48 +113,6 @@ urlpatterns = [
|
||||||
property_mappings.PropertyMappingTestView.as_view(),
|
property_mappings.PropertyMappingTestView.as_view(),
|
||||||
name="property-mapping-test",
|
name="property-mapping-test",
|
||||||
),
|
),
|
||||||
# Users
|
|
||||||
path("users/create/", users.UserCreateView.as_view(), name="user-create"),
|
|
||||||
path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"),
|
|
||||||
path(
|
|
||||||
"users/<int:pk>/reset/",
|
|
||||||
users.UserPasswordResetView.as_view(),
|
|
||||||
name="user-password-reset",
|
|
||||||
),
|
|
||||||
# Groups
|
|
||||||
path("groups/create/", groups.GroupCreateView.as_view(), name="group-create"),
|
|
||||||
path(
|
|
||||||
"groups/<uuid:pk>/update/",
|
|
||||||
groups.GroupUpdateView.as_view(),
|
|
||||||
name="group-update",
|
|
||||||
),
|
|
||||||
# Certificate-Key Pairs
|
|
||||||
path(
|
|
||||||
"crypto/certificates/create/",
|
|
||||||
certificate_key_pair.CertificateKeyPairCreateView.as_view(),
|
|
||||||
name="certificatekeypair-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"crypto/certificates/generate/",
|
|
||||||
certificate_key_pair.CertificateKeyPairGenerateView.as_view(),
|
|
||||||
name="certificatekeypair-generate",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"crypto/certificates/<uuid:pk>/update/",
|
|
||||||
certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
|
|
||||||
name="certificatekeypair-update",
|
|
||||||
),
|
|
||||||
# Outposts
|
|
||||||
path(
|
|
||||||
"outposts/create/",
|
|
||||||
outposts.OutpostCreateView.as_view(),
|
|
||||||
name="outpost-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"outposts/<uuid:pk>/update/",
|
|
||||||
outposts.OutpostUpdateView.as_view(),
|
|
||||||
name="outpost-update",
|
|
||||||
),
|
|
||||||
# Outpost Service Connections
|
# Outpost Service Connections
|
||||||
path(
|
path(
|
||||||
"outpost_service_connections/create/",
|
"outpost_service_connections/create/",
|
||||||
|
@ -206,26 +124,4 @@ urlpatterns = [
|
||||||
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
||||||
name="outpost-service-connection-update",
|
name="outpost-service-connection-update",
|
||||||
),
|
),
|
||||||
# Event Notification Transpots
|
|
||||||
path(
|
|
||||||
"events/transports/create/",
|
|
||||||
events_notifications_transports.NotificationTransportCreateView.as_view(),
|
|
||||||
name="notification-transport-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/transports/<uuid:pk>/update/",
|
|
||||||
events_notifications_transports.NotificationTransportUpdateView.as_view(),
|
|
||||||
name="notification-transport-update",
|
|
||||||
),
|
|
||||||
# Event Notification Rules
|
|
||||||
path(
|
|
||||||
"events/rules/create/",
|
|
||||||
events_notifications_rules.NotificationRuleCreateView.as_view(),
|
|
||||||
name="notification-rule-create",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"events/rules/<uuid:pk>/update/",
|
|
||||||
events_notifications_rules.NotificationRuleUpdateView.as_view(),
|
|
||||||
name="notification-rule-update",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
"""authentik Application administration"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
from guardian.shortcuts import get_objects_for_user
|
|
||||||
|
|
||||||
from authentik.core.forms.applications import ApplicationForm
|
|
||||||
from authentik.core.models import Application
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Application"""
|
|
||||||
|
|
||||||
model = Application
|
|
||||||
form_class = ApplicationForm
|
|
||||||
permission_required = "authentik_core.add_application"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Application")
|
|
||||||
|
|
||||||
def get_initial(self) -> dict[str, Any]:
|
|
||||||
if "provider" in self.request.GET:
|
|
||||||
try:
|
|
||||||
initial_provider_pk = int(self.request.GET["provider"])
|
|
||||||
except ValueError:
|
|
||||||
return super().get_initial()
|
|
||||||
providers = (
|
|
||||||
get_objects_for_user(self.request.user, "authentik_core.view_provider")
|
|
||||||
.filter(pk=initial_provider_pk)
|
|
||||||
.select_subclasses()
|
|
||||||
)
|
|
||||||
if not providers.exists():
|
|
||||||
return {}
|
|
||||||
return {"provider": providers.first()}
|
|
||||||
return super().get_initial()
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update application"""
|
|
||||||
|
|
||||||
model = Application
|
|
||||||
form_class = ApplicationForm
|
|
||||||
permission_required = "authentik_core.change_application"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Application")
|
|
|
@ -1,81 +0,0 @@
|
||||||
"""authentik CertificateKeyPair administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from django.views.generic.edit import FormView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.crypto.builder import CertificateBuilder
|
|
||||||
from authentik.crypto.forms import (
|
|
||||||
CertificateKeyPairForm,
|
|
||||||
CertificateKeyPairGenerateForm,
|
|
||||||
)
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new CertificateKeyPair"""
|
|
||||||
|
|
||||||
model = CertificateKeyPair
|
|
||||||
form_class = CertificateKeyPairForm
|
|
||||||
permission_required = "authentik_crypto.add_certificatekeypair"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully created Certificate-Key Pair")
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairGenerateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
FormView,
|
|
||||||
):
|
|
||||||
"""Generate new CertificateKeyPair"""
|
|
||||||
|
|
||||||
model = CertificateKeyPair
|
|
||||||
form_class = CertificateKeyPairGenerateForm
|
|
||||||
permission_required = "authentik_crypto.add_certificatekeypair"
|
|
||||||
|
|
||||||
template_name = "administration/certificatekeypair/generate.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully generated Certificate-Key Pair")
|
|
||||||
|
|
||||||
def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse:
|
|
||||||
builder = CertificateBuilder()
|
|
||||||
builder.common_name = form.data["common_name"]
|
|
||||||
builder.build(
|
|
||||||
subject_alt_names=form.data.get("subject_alt_name", "").split(","),
|
|
||||||
validity_days=int(form.data["validity_days"]),
|
|
||||||
)
|
|
||||||
builder.save()
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update certificatekeypair"""
|
|
||||||
|
|
||||||
model = CertificateKeyPair
|
|
||||||
form_class = CertificateKeyPairForm
|
|
||||||
permission_required = "authentik_crypto.change_certificatekeypair"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully updated Certificate-Key Pair")
|
|
|
@ -1,47 +0,0 @@
|
||||||
"""authentik NotificationRule administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.events.forms import NotificationRuleForm
|
|
||||||
from authentik.events.models import NotificationRule
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new NotificationRule"""
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
form_class = NotificationRuleForm
|
|
||||||
permission_required = "authentik_events.add_NotificationRule"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Notification Rule")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update application"""
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
form_class = NotificationRuleForm
|
|
||||||
permission_required = "authentik_events.change_NotificationRule"
|
|
||||||
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Notification Rule")
|
|
|
@ -1,45 +0,0 @@
|
||||||
"""authentik NotificationTransport administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.events.forms import NotificationTransportForm
|
|
||||||
from authentik.events.models import NotificationTransport
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new NotificationTransport"""
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
form_class = NotificationTransportForm
|
|
||||||
permission_required = "authentik_events.add_notificationtransport"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Notification Transport")
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update application"""
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
form_class = NotificationTransportForm
|
|
||||||
permission_required = "authentik_events.change_notificationtransport"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Notification Transport")
|
|
|
@ -1,108 +0,0 @@
|
||||||
"""authentik Flow administration"""
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import DetailView, FormView, UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
|
||||||
from authentik.flows.forms import FlowForm, FlowImportForm
|
|
||||||
from authentik.flows.models import Flow
|
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
|
||||||
from authentik.flows.transfer.importer import FlowImporter
|
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN, FlowPlanner
|
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
|
||||||
from authentik.lib.views import CreateAssignPermView, bad_request_message
|
|
||||||
|
|
||||||
|
|
||||||
class FlowCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Flow"""
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
form_class = FlowForm
|
|
||||||
permission_required = "authentik_flows.add_flow"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully created Flow")
|
|
||||||
|
|
||||||
|
|
||||||
class FlowUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update flow"""
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
form_class = FlowForm
|
|
||||||
permission_required = "authentik_flows.change_flow"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully updated Flow")
|
|
||||||
|
|
||||||
|
|
||||||
class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
||||||
"""Debug exectue flow, setting the current user as pending user"""
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
permission_required = "authentik_flows.view_flow"
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def get(self, request: HttpRequest, pk: str) -> HttpResponse:
|
|
||||||
"""Debug exectue flow, setting the current user as pending user"""
|
|
||||||
flow: Flow = self.get_object()
|
|
||||||
planner = FlowPlanner(flow)
|
|
||||||
planner.use_cache = False
|
|
||||||
try:
|
|
||||||
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
|
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
|
||||||
except FlowNonApplicableException as exc:
|
|
||||||
return bad_request_message(
|
|
||||||
request,
|
|
||||||
_(
|
|
||||||
"Flow not applicable to current user/request: %(messages)s"
|
|
||||||
% {"messages": str(exc)}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return redirect_with_qs(
|
|
||||||
"authentik_core:if-flow",
|
|
||||||
self.request.GET,
|
|
||||||
flow_slug=flow.slug,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FlowImportView(LoginRequiredMixin, FormView):
|
|
||||||
"""Import flow from JSON Export; only allowed for superusers
|
|
||||||
as these flows can contain python code"""
|
|
||||||
|
|
||||||
form_class = FlowImportForm
|
|
||||||
template_name = "administration/flow/import.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
if not request.user.is_superuser:
|
|
||||||
return self.handle_no_permission()
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def form_valid(self, form: FlowImportForm) -> HttpResponse:
|
|
||||||
importer = FlowImporter(form.cleaned_data["flow"].read().decode())
|
|
||||||
successful = importer.apply()
|
|
||||||
if not successful:
|
|
||||||
messages.error(self.request, _("Failed to import flow."))
|
|
||||||
else:
|
|
||||||
messages.success(self.request, _("Successfully imported flow."))
|
|
||||||
return super().form_valid(form)
|
|
|
@ -1,48 +0,0 @@
|
||||||
"""authentik Group administration"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.core.forms.groups import GroupForm
|
|
||||||
from authentik.core.models import Group
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class GroupCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Group"""
|
|
||||||
|
|
||||||
model = Group
|
|
||||||
form_class = GroupForm
|
|
||||||
permission_required = "authentik_core.add_group"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully created Group")
|
|
||||||
|
|
||||||
|
|
||||||
class GroupUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update group"""
|
|
||||||
|
|
||||||
model = Group
|
|
||||||
form_class = GroupForm
|
|
||||||
permission_required = "authentik_core.change_group"
|
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully updated Group")
|
|
|
@ -1,55 +0,0 @@
|
||||||
"""authentik Outpost administration"""
|
|
||||||
from dataclasses import asdict
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
from authentik.outposts.forms import OutpostForm
|
|
||||||
from authentik.outposts.models import Outpost, OutpostConfig
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create new Outpost"""
|
|
||||||
|
|
||||||
model = Outpost
|
|
||||||
form_class = OutpostForm
|
|
||||||
permission_required = "authentik_outposts.add_outpost"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_message = _("Successfully created Outpost")
|
|
||||||
|
|
||||||
def get_initial(self) -> dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"_config": asdict(
|
|
||||||
OutpostConfig(authentik_host=self.request.build_absolute_uri("/"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update outpost"""
|
|
||||||
|
|
||||||
model = Outpost
|
|
||||||
form_class = OutpostForm
|
|
||||||
permission_required = "authentik_outposts.change_outpost"
|
|
||||||
success_url = "/"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_message = _("Successfully updated Outpost")
|
|
|
@ -1,74 +0,0 @@
|
||||||
"""authentik User administration"""
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.auth.mixins import (
|
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
|
||||||
)
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import redirect
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.http import urlencode
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import DetailView, UpdateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from authentik.admin.forms.users import UserForm
|
|
||||||
from authentik.core.models import Token, User
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
|
||||||
|
|
||||||
|
|
||||||
class UserCreateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
DjangoPermissionRequiredMixin,
|
|
||||||
CreateAssignPermView,
|
|
||||||
):
|
|
||||||
"""Create user"""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
form_class = UserForm
|
|
||||||
permission_required = "authentik_core.add_user"
|
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully created User")
|
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateView(
|
|
||||||
SuccessMessageMixin,
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionRequiredMixin,
|
|
||||||
UpdateView,
|
|
||||||
):
|
|
||||||
"""Update user"""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
form_class = UserForm
|
|
||||||
permission_required = "authentik_core.change_user"
|
|
||||||
|
|
||||||
# By default the object's name is user which is used by other checks
|
|
||||||
context_object_name = "object"
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
success_message = _("Successfully updated User")
|
|
||||||
|
|
||||||
|
|
||||||
class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|
||||||
"""Get Password reset link for user"""
|
|
||||||
|
|
||||||
model = User
|
|
||||||
permission_required = "authentik_core.reset_user_password"
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
"""Create token for user and return link"""
|
|
||||||
super().get(request, *args, **kwargs)
|
|
||||||
token, __ = Token.objects.get_or_create(
|
|
||||||
identifier="password-reset-temp", user=self.object
|
|
||||||
)
|
|
||||||
querystring = urlencode({"token": token.key})
|
|
||||||
link = request.build_absolute_uri(
|
|
||||||
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
|
|
||||||
)
|
|
||||||
messages.success(request, _("Password reset link: %(link)s" % {"link": link}))
|
|
||||||
return redirect("/")
|
|
|
@ -1,26 +1,13 @@
|
||||||
"""authentik admin util views"""
|
"""authentik admin util views"""
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.urls import reverse_lazy
|
from django.views.generic import UpdateView
|
||||||
from django.views.generic import DeleteView, UpdateView
|
|
||||||
|
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
from authentik.lib.views import CreateAssignPermView
|
from authentik.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class DeleteMessageView(SuccessMessageMixin, DeleteView):
|
|
||||||
"""DeleteView which shows `self.success_message` on successful deletion"""
|
|
||||||
|
|
||||||
success_url = reverse_lazy("authentik_core:if-admin")
|
|
||||||
|
|
||||||
def delete(self, request, *args, **kwargs):
|
|
||||||
messages.success(self.request, self.success_message)
|
|
||||||
return super().delete(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class InheritanceCreateView(CreateAssignPermView):
|
class InheritanceCreateView(CreateAssignPermView):
|
||||||
"""CreateView for objects using InheritanceManager"""
|
"""CreateView for objects using InheritanceManager"""
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
"""API Decorators"""
|
"""API Decorators"""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
|
||||||
def permission_required(perm: str, *other_perms: str):
|
def permission_required(
|
||||||
|
perm: Optional[str] = None, other_perms: Optional[list[str]] = None
|
||||||
|
):
|
||||||
"""Check permissions for a single custom action"""
|
"""Check permissions for a single custom action"""
|
||||||
|
|
||||||
def wrapper_outter(func: Callable):
|
def wrapper_outter(func: Callable):
|
||||||
|
@ -15,9 +17,11 @@ def permission_required(perm: str, *other_perms: str):
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
|
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
|
||||||
|
if perm:
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
if not request.user.has_perm(perm, obj):
|
if not request.user.has_perm(perm, obj):
|
||||||
return self.permission_denied(request)
|
return self.permission_denied(request)
|
||||||
|
if other_perms:
|
||||||
for other_perm in other_perms:
|
for other_perm in other_perms:
|
||||||
if not request.user.has_perm(other_perm):
|
if not request.user.has_perm(other_perm):
|
||||||
return self.permission_denied(request)
|
return self.permission_denied(request)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""Swagger Pagination Schema class"""
|
"""Swagger Pagination Schema class"""
|
||||||
from typing import OrderedDict
|
from typing import OrderedDict
|
||||||
|
|
||||||
from drf_yasg2 import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg2.inspectors import PaginatorInspector
|
from drf_yasg.inspectors import PaginatorInspector
|
||||||
|
|
||||||
|
|
||||||
class PaginationInspector(PaginatorInspector):
|
class PaginationInspector(PaginatorInspector):
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.inspectors.view import SwaggerAutoSchema
|
||||||
|
from drf_yasg.utils import force_real_str, is_list_view
|
||||||
|
from rest_framework import exceptions, status
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorResponseAutoSchema(SwaggerAutoSchema):
|
||||||
|
"""Inspector which includes an error schema"""
|
||||||
|
|
||||||
|
def get_generic_error_schema(self):
|
||||||
|
"""Get a generic error schema"""
|
||||||
|
return openapi.Schema(
|
||||||
|
"Generic API Error",
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
"detail": openapi.Schema(
|
||||||
|
type=openapi.TYPE_STRING, description="Error details"
|
||||||
|
),
|
||||||
|
"code": openapi.Schema(
|
||||||
|
type=openapi.TYPE_STRING, description="Error code"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
required=["detail"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_validation_error_schema(self):
|
||||||
|
"""Get a generic validation error schema"""
|
||||||
|
return openapi.Schema(
|
||||||
|
"Validation Error",
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
api_settings.NON_FIELD_ERRORS_KEY: openapi.Schema(
|
||||||
|
description="List of validation errors not related to any field",
|
||||||
|
type=openapi.TYPE_ARRAY,
|
||||||
|
items=openapi.Schema(type=openapi.TYPE_STRING),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
additional_properties=openapi.Schema(
|
||||||
|
description=(
|
||||||
|
"A list of error messages for each "
|
||||||
|
"field that triggered a validation error"
|
||||||
|
),
|
||||||
|
type=openapi.TYPE_ARRAY,
|
||||||
|
items=openapi.Schema(type=openapi.TYPE_STRING),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_response_serializers(self):
|
||||||
|
responses = super().get_response_serializers()
|
||||||
|
definitions = self.components.with_scope(
|
||||||
|
openapi.SCHEMA_DEFINITIONS
|
||||||
|
) # type: openapi.ReferenceResolver
|
||||||
|
|
||||||
|
definitions.setdefault("GenericError", self.get_generic_error_schema)
|
||||||
|
definitions.setdefault("ValidationError", self.get_validation_error_schema)
|
||||||
|
definitions.setdefault("APIException", self.get_generic_error_schema)
|
||||||
|
|
||||||
|
if self.get_request_serializer() or self.get_query_serializer():
|
||||||
|
responses.setdefault(
|
||||||
|
exceptions.ValidationError.status_code,
|
||||||
|
openapi.Response(
|
||||||
|
description=force_real_str(
|
||||||
|
exceptions.ValidationError.default_detail
|
||||||
|
),
|
||||||
|
schema=openapi.SchemaRef(definitions, "ValidationError"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
security = self.get_security()
|
||||||
|
if security is None or len(security) > 0:
|
||||||
|
# Note: 401 error codes are coerced into 403 see
|
||||||
|
# rest_framework/views.py:433:handle_exception
|
||||||
|
# This is b/c the API uses token auth which doesn't have WWW-Authenticate header
|
||||||
|
responses.setdefault(
|
||||||
|
status.HTTP_403_FORBIDDEN,
|
||||||
|
openapi.Response(
|
||||||
|
description="Authentication credentials were invalid, absent or insufficient.",
|
||||||
|
schema=openapi.SchemaRef(definitions, "GenericError"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not is_list_view(self.path, self.method, self.view):
|
||||||
|
responses.setdefault(
|
||||||
|
exceptions.PermissionDenied.status_code,
|
||||||
|
openapi.Response(
|
||||||
|
description="Permission denied.",
|
||||||
|
schema=openapi.SchemaRef(definitions, "APIException"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
responses.setdefault(
|
||||||
|
exceptions.NotFound.status_code,
|
||||||
|
openapi.Response(
|
||||||
|
description=(
|
||||||
|
"Object does not exist or caller "
|
||||||
|
"has insufficient permissions to access it."
|
||||||
|
),
|
||||||
|
schema=openapi.SchemaRef(definitions, "APIException"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return responses
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""Swagger generation tests"""
|
||||||
|
from json import loads
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from yaml import safe_load
|
||||||
|
|
||||||
|
|
||||||
|
class TestSwaggerGeneration(APITestCase):
|
||||||
|
"""Generic admin tests"""
|
||||||
|
|
||||||
|
def test_yaml(self):
|
||||||
|
"""Test YAML generation"""
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:schema-json", kwargs={"format": ".yaml"}),
|
||||||
|
)
|
||||||
|
self.assertTrue(safe_load(response.content.decode()))
|
||||||
|
|
||||||
|
def test_json(self):
|
||||||
|
"""Test JSON generation"""
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:schema-json", kwargs={"format": ".json"}),
|
||||||
|
)
|
||||||
|
self.assertTrue(loads(response.content.decode()))
|
|
@ -1,46 +1,33 @@
|
||||||
"""core Configs API"""
|
"""core Configs API"""
|
||||||
from django.db.models import Model
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
|
||||||
from rest_framework.fields import BooleanField, CharField, ListField
|
from rest_framework.fields import BooleanField, CharField, ListField
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
class LinkSerializer(Serializer):
|
class FooterLinkSerializer(PassiveSerializer):
|
||||||
"""Links returned in Config API"""
|
"""Links returned in Config API"""
|
||||||
|
|
||||||
href = CharField(read_only=True)
|
href = CharField(read_only=True)
|
||||||
name = CharField(read_only=True)
|
name = CharField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class ConfigSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigSerializer(Serializer):
|
|
||||||
"""Serialize authentik Config into DRF Object"""
|
"""Serialize authentik Config into DRF Object"""
|
||||||
|
|
||||||
branding_logo = CharField(read_only=True)
|
branding_logo = CharField(read_only=True)
|
||||||
branding_title = CharField(read_only=True)
|
branding_title = CharField(read_only=True)
|
||||||
ui_footer_links = ListField(child=LinkSerializer(), read_only=True)
|
ui_footer_links = ListField(child=FooterLinkSerializer(), read_only=True)
|
||||||
|
|
||||||
error_reporting_enabled = BooleanField(read_only=True)
|
error_reporting_enabled = BooleanField(read_only=True)
|
||||||
error_reporting_environment = CharField(read_only=True)
|
error_reporting_environment = CharField(read_only=True)
|
||||||
error_reporting_send_pii = BooleanField(read_only=True)
|
error_reporting_send_pii = BooleanField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigsViewSet(ViewSet):
|
class ConfigsViewSet(ViewSet):
|
||||||
"""Read-only view set that returns the current session's Configs"""
|
"""Read-only view set that returns the current session's Configs"""
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""api v2 urls"""
|
"""api v2 urls"""
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
from drf_yasg2 import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg2.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
@ -33,7 +33,8 @@ from authentik.outposts.api.outpost_service_connections import (
|
||||||
ServiceConnectionViewSet,
|
ServiceConnectionViewSet,
|
||||||
)
|
)
|
||||||
from authentik.outposts.api.outposts import OutpostViewSet
|
from authentik.outposts.api.outposts import OutpostViewSet
|
||||||
from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet
|
from authentik.policies.api.bindings import PolicyBindingViewSet
|
||||||
|
from authentik.policies.api.policies import PolicyViewSet
|
||||||
from authentik.policies.dummy.api import DummyPolicyViewSet
|
from authentik.policies.dummy.api import DummyPolicyViewSet
|
||||||
from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
|
from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
|
||||||
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
|
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||||
|
@ -189,7 +190,7 @@ router.register("policies/dummy", DummyPolicyViewSet)
|
||||||
|
|
||||||
info = openapi.Info(
|
info = openapi.Info(
|
||||||
title="authentik API",
|
title="authentik API",
|
||||||
default_version="v2",
|
default_version="v2beta",
|
||||||
contact=openapi.Contact(email="hello@beryju.org"),
|
contact=openapi.Contact(email="hello@beryju.org"),
|
||||||
license=openapi.License(
|
license=openapi.License(
|
||||||
name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE"
|
name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE"
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
"""Application API Views"""
|
"""Application API Views"""
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from django.http.response import HttpResponseBadRequest
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.utils import no_body, swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
@ -49,7 +52,6 @@ class ApplicationSerializer(ModelSerializer):
|
||||||
"meta_icon",
|
"meta_icon",
|
||||||
"meta_description",
|
"meta_description",
|
||||||
"meta_publisher",
|
"meta_publisher",
|
||||||
"policies",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,8 +110,33 @@ class ApplicationViewSet(ModelViewSet):
|
||||||
serializer = self.get_serializer(allowed_applications, many=True)
|
serializer = self.get_serializer(allowed_applications, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@permission_required("authentik_core.change_application")
|
||||||
|
@swagger_auto_schema(
|
||||||
|
request_body=no_body,
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
name="file",
|
||||||
|
in_=openapi.IN_FORM,
|
||||||
|
type=openapi.TYPE_FILE,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: "Success"},
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=["POST"], parser_classes=(MultiPartParser,))
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def set_icon(self, request: Request, slug: str):
|
||||||
|
"""Set application icon"""
|
||||||
|
app: Application = self.get_object()
|
||||||
|
icon = request.FILES.get("file", None)
|
||||||
|
if not icon:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
app.meta_icon = icon
|
||||||
|
app.save()
|
||||||
|
return Response({})
|
||||||
|
|
||||||
@permission_required(
|
@permission_required(
|
||||||
"authentik_core.view_application", "authentik_events.view_event"
|
"authentik_core.view_application", ["authentik_events.view_event"]
|
||||||
)
|
)
|
||||||
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
|
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""PropertyMapping API Views"""
|
"""PropertyMapping API Views"""
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Provider API Views"""
|
"""Provider API Views"""
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
"""Tokens API Viewset"""
|
"""Tokens API Viewset"""
|
||||||
from django.db.models.base import Model
|
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, Serializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import Token
|
from authentik.core.models import Token
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
@ -34,17 +35,11 @@ class TokenSerializer(ModelSerializer):
|
||||||
depth = 2
|
depth = 2
|
||||||
|
|
||||||
|
|
||||||
class TokenViewSerializer(Serializer):
|
class TokenViewSerializer(PassiveSerializer):
|
||||||
"""Show token's current key"""
|
"""Show token's current key"""
|
||||||
|
|
||||||
key = CharField(read_only=True)
|
key = CharField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class TokenViewSet(ModelViewSet):
|
class TokenViewSet(ModelViewSet):
|
||||||
"""Token Viewset"""
|
"""Token Viewset"""
|
||||||
|
@ -66,6 +61,7 @@ class TokenViewSet(ModelViewSet):
|
||||||
]
|
]
|
||||||
ordering = ["expires"]
|
ordering = ["expires"]
|
||||||
|
|
||||||
|
@permission_required("authentik_core.view_token_key")
|
||||||
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
|
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
from django.db.models.base import Model
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method
|
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, SerializerMethodField
|
from rest_framework.fields import CharField, SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import BooleanField, ModelSerializer, Serializer
|
from rest_framework.serializers import BooleanField, ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
|
from authentik.core.api.utils import LinkSerializer, PassiveSerializer
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||||
SESSION_IMPERSONATE_USER,
|
SESSION_IMPERSONATE_USER,
|
||||||
|
@ -43,33 +43,15 @@ class UserSerializer(ModelSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SessionUserSerializer(Serializer):
|
class SessionUserSerializer(PassiveSerializer):
|
||||||
"""Response for the /user/me endpoint, returns the currently active user (as `user` property)
|
"""Response for the /user/me endpoint, returns the currently active user (as `user` property)
|
||||||
and, if this user is being impersonated, the original user in the `original` property."""
|
and, if this user is being impersonated, the original user in the `original` property."""
|
||||||
|
|
||||||
user = UserSerializer()
|
user = UserSerializer()
|
||||||
original = UserSerializer(required=False)
|
original = UserSerializer(required=False)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class UserMetricsSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class UserRecoverySerializer(Serializer):
|
|
||||||
"""Recovery link for a user to reset their password"""
|
|
||||||
|
|
||||||
link = CharField()
|
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class UserMetricsSerializer(Serializer):
|
|
||||||
"""User Metrics"""
|
"""User Metrics"""
|
||||||
|
|
||||||
logins_per_1h = SerializerMethodField()
|
logins_per_1h = SerializerMethodField()
|
||||||
|
@ -98,12 +80,6 @@ class UserMetricsSerializer(Serializer):
|
||||||
action=EventAction.AUTHORIZE_APPLICATION, user__pk=request.user.pk
|
action=EventAction.AUTHORIZE_APPLICATION, user__pk=request.user.pk
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(ModelViewSet):
|
||||||
"""User Viewset"""
|
"""User Viewset"""
|
||||||
|
@ -131,7 +107,7 @@ class UserViewSet(ModelViewSet):
|
||||||
serializer.is_valid()
|
serializer.is_valid()
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@permission_required("authentik_core.view_user", "authentik_events.view_event")
|
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||||
@swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)})
|
@swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def metrics(self, request: Request) -> Response:
|
def metrics(self, request: Request) -> Response:
|
||||||
|
@ -142,7 +118,7 @@ class UserViewSet(ModelViewSet):
|
||||||
|
|
||||||
@permission_required("authentik_core.reset_user_password")
|
@permission_required("authentik_core.reset_user_password")
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
responses={"200": UserRecoverySerializer(many=False)},
|
responses={"200": LinkSerializer(many=False)},
|
||||||
)
|
)
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
# pylint: disable=invalid-name, unused-argument
|
# pylint: disable=invalid-name, unused-argument
|
||||||
|
|
|
@ -4,18 +4,22 @@ from rest_framework.fields import CharField, IntegerField
|
||||||
from rest_framework.serializers import Serializer, SerializerMethodField
|
from rest_framework.serializers import Serializer, SerializerMethodField
|
||||||
|
|
||||||
|
|
||||||
class MetaNameSerializer(Serializer):
|
class PassiveSerializer(Serializer):
|
||||||
|
"""Base serializer class which doesn't implement create/update methods"""
|
||||||
|
|
||||||
|
def create(self, validated_data: dict) -> Model:
|
||||||
|
return Model()
|
||||||
|
|
||||||
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
|
return Model()
|
||||||
|
|
||||||
|
|
||||||
|
class MetaNameSerializer(PassiveSerializer):
|
||||||
"""Add verbose names to response"""
|
"""Add verbose names to response"""
|
||||||
|
|
||||||
verbose_name = SerializerMethodField()
|
verbose_name = SerializerMethodField()
|
||||||
verbose_name_plural = SerializerMethodField()
|
verbose_name_plural = SerializerMethodField()
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_verbose_name(self, obj: Model) -> str:
|
def get_verbose_name(self, obj: Model) -> str:
|
||||||
"""Return object's verbose_name"""
|
"""Return object's verbose_name"""
|
||||||
return obj._meta.verbose_name
|
return obj._meta.verbose_name
|
||||||
|
@ -25,27 +29,21 @@ class MetaNameSerializer(Serializer):
|
||||||
return obj._meta.verbose_name_plural
|
return obj._meta.verbose_name_plural
|
||||||
|
|
||||||
|
|
||||||
class TypeCreateSerializer(Serializer):
|
class TypeCreateSerializer(PassiveSerializer):
|
||||||
"""Types of an object that can be created"""
|
"""Types of an object that can be created"""
|
||||||
|
|
||||||
name = CharField(required=True)
|
name = CharField(required=True)
|
||||||
description = CharField(required=True)
|
description = CharField(required=True)
|
||||||
link = CharField(required=True)
|
link = CharField(required=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class CacheSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class CacheSerializer(Serializer):
|
|
||||||
"""Generic cache stats for an object"""
|
"""Generic cache stats for an object"""
|
||||||
|
|
||||||
count = IntegerField(read_only=True)
|
count = IntegerField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class LinkSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
"""Returns a single link"""
|
||||||
|
|
||||||
|
link = CharField()
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
"""authentik Core Application forms"""
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider
|
|
||||||
from authentik.lib.widgets import GroupedModelChoiceField
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationForm(forms.ModelForm):
|
|
||||||
"""Application Form"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs): # pragma: no cover
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["provider"].queryset = (
|
|
||||||
Provider.objects.all().order_by("name").select_subclasses()
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Application
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"slug",
|
|
||||||
"provider",
|
|
||||||
"meta_launch_url",
|
|
||||||
"meta_icon",
|
|
||||||
"meta_description",
|
|
||||||
"meta_publisher",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"meta_launch_url": forms.TextInput(),
|
|
||||||
"meta_publisher": forms.TextInput(),
|
|
||||||
"meta_icon": forms.FileInput(),
|
|
||||||
}
|
|
||||||
help_texts = {
|
|
||||||
"meta_launch_url": _(
|
|
||||||
(
|
|
||||||
"If left empty, authentik will try to extract the launch URL "
|
|
||||||
"based on the selected provider."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
field_classes = {"provider": GroupedModelChoiceField}
|
|
||||||
labels = {
|
|
||||||
"meta_launch_url": _("Launch URL"),
|
|
||||||
"meta_icon": _("Icon"),
|
|
||||||
"meta_description": _("Description"),
|
|
||||||
"meta_publisher": _("Publisher"),
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
"""authentik Core Group forms"""
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.admin.fields import CodeMirrorWidget, YAMLField
|
|
||||||
from authentik.core.models import Group, User
|
|
||||||
|
|
||||||
|
|
||||||
class GroupForm(forms.ModelForm):
|
|
||||||
"""Group Form"""
|
|
||||||
|
|
||||||
members = forms.ModelMultipleChoiceField(
|
|
||||||
User.objects.all(),
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
if self.instance.pk:
|
|
||||||
self.initial["members"] = self.instance.users.values_list("pk", flat=True)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
instance = super().save(*args, **kwargs)
|
|
||||||
if instance.pk:
|
|
||||||
instance.users.clear()
|
|
||||||
instance.users.add(*self.cleaned_data["members"])
|
|
||||||
return instance
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Group
|
|
||||||
fields = ["name", "is_superuser", "parent", "members", "attributes"]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"attributes": CodeMirrorWidget,
|
|
||||||
}
|
|
||||||
field_classes = {
|
|
||||||
"attributes": YAMLField,
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
"""authentik core user forms"""
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
|
|
||||||
from authentik.core.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserDetailForm(forms.ModelForm):
|
|
||||||
"""Update User Details"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = User
|
|
||||||
fields = ["username", "name", "email"]
|
|
||||||
widgets = {"name": forms.TextInput}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.1.7 on 2021-03-30 13:45
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0017_managed"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="token",
|
||||||
|
options={
|
||||||
|
"permissions": (("view_token_key", "View token's key"),),
|
||||||
|
"verbose_name": "Token",
|
||||||
|
"verbose_name_plural": "Tokens",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -369,6 +369,7 @@ class Token(ManagedModel, ExpiringModel):
|
||||||
models.Index(fields=["identifier"]),
|
models.Index(fields=["identifier"]),
|
||||||
models.Index(fields=["key"]),
|
models.Index(fields=["key"]),
|
||||||
]
|
]
|
||||||
|
permissions = (("view_token_key", "View token's key"),)
|
||||||
|
|
||||||
|
|
||||||
class PropertyMapping(SerializerModel, ManagedModel):
|
class PropertyMapping(SerializerModel, ManagedModel):
|
||||||
|
|
|
@ -17,7 +17,7 @@ def post_save_application(sender, instance, created: bool, **_):
|
||||||
|
|
||||||
if sender != Application:
|
if sender != Application:
|
||||||
return
|
return
|
||||||
if not created:
|
if not created: # pragma: no cover
|
||||||
return
|
return
|
||||||
# Also delete user application cache
|
# Also delete user application cache
|
||||||
keys = cache.keys(user_app_cache_key("*"))
|
keys = cache.keys(user_app_cache_key("*"))
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
<div class="pf-c-card">
|
|
||||||
<div class="pf-c-card__title">
|
|
||||||
{% trans 'Update details' %}
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-card__body">
|
|
||||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
|
||||||
{% include 'partials/form_horizontal.html' with form=form %}
|
|
||||||
{% block beneath_form %}
|
|
||||||
{% endblock %}
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
|
||||||
<div class="pf-c-form__horizontal-group">
|
|
||||||
<div class="pf-c-form__actions">
|
|
||||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
|
|
||||||
{% if unenrollment_enabled %}
|
|
||||||
<a class="pf-c-button pf-m-danger"
|
|
||||||
href="{% url 'authentik_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{%
|
|
||||||
trans "Delete account" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,30 +0,0 @@
|
||||||
"""authentik user view tests"""
|
|
||||||
import string
|
|
||||||
from random import SystemRandom
|
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from authentik.core.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserViews(TestCase):
|
|
||||||
"""Test User Views"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.user = User.objects.create_user(
|
|
||||||
username="unittest user",
|
|
||||||
email="unittest@example.com",
|
|
||||||
password="".join(
|
|
||||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
|
||||||
for _ in range(8)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def test_user_details(self):
|
|
||||||
"""Test UserDetailsView"""
|
|
||||||
self.assertEqual(
|
|
||||||
self.client.get(reverse("authentik_core:user-details")).status_code, 200
|
|
||||||
)
|
|
|
@ -2,9 +2,9 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.db.models.base import Model
|
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
from rest_framework.serializers import Serializer
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -21,29 +21,17 @@ class UILoginButton:
|
||||||
icon_url: Optional[str] = None
|
icon_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class UILoginButtonSerializer(Serializer):
|
class UILoginButtonSerializer(PassiveSerializer):
|
||||||
"""Serializer for Login buttons of sources"""
|
"""Serializer for Login buttons of sources"""
|
||||||
|
|
||||||
name = CharField()
|
name = CharField()
|
||||||
url = CharField()
|
url = CharField()
|
||||||
icon_url = CharField(required=False)
|
icon_url = CharField(required=False)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class UserSettingSerializer(PassiveSerializer):
|
||||||
return Model()
|
|
||||||
|
|
||||||
|
|
||||||
class UserSettingSerializer(Serializer):
|
|
||||||
"""Serializer for User settings for stages and sources"""
|
"""Serializer for User settings for stages and sources"""
|
||||||
|
|
||||||
object_uid = CharField()
|
object_uid = CharField()
|
||||||
component = CharField()
|
component = CharField()
|
||||||
title = CharField()
|
title = CharField()
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ urlpatterns = [
|
||||||
name="root-redirect",
|
name="root-redirect",
|
||||||
),
|
),
|
||||||
# User views
|
# User views
|
||||||
path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"),
|
|
||||||
path(
|
path(
|
||||||
"-/user/tokens/create/",
|
"-/user/tokens/create/",
|
||||||
user.TokenCreateView.as_view(),
|
user.TokenCreateView.as_view(),
|
||||||
|
|
|
@ -1,53 +1,20 @@
|
||||||
"""authentik core user views"""
|
"""authentik core user views"""
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.mixins import (
|
from django.contrib.auth.mixins import (
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import UpdateView
|
from django.views.generic import UpdateView
|
||||||
from django.views.generic.base import TemplateView
|
|
||||||
from guardian.mixins import PermissionRequiredMixin
|
from guardian.mixins import PermissionRequiredMixin
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
|
|
||||||
from authentik.core.forms.token import UserTokenForm
|
from authentik.core.forms.token import UserTokenForm
|
||||||
from authentik.core.forms.users import UserDetailForm
|
|
||||||
from authentik.core.models import Token, TokenIntents
|
from authentik.core.models import Token, TokenIntents
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
|
||||||
from authentik.lib.views import CreateAssignPermView
|
from authentik.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsView(TemplateView):
|
|
||||||
"""Multiple SiteShells for user details and all stages"""
|
|
||||||
|
|
||||||
template_name = "user/settings.html"
|
|
||||||
|
|
||||||
|
|
||||||
class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
|
||||||
"""Update User details"""
|
|
||||||
|
|
||||||
template_name = "user/details.html"
|
|
||||||
form_class = UserDetailForm
|
|
||||||
|
|
||||||
success_message = _("Successfully updated user.")
|
|
||||||
success_url = reverse_lazy("authentik_core:user-details")
|
|
||||||
|
|
||||||
def get_object(self):
|
|
||||||
return self.request.user
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
||||||
kwargs = super().get_context_data(**kwargs)
|
|
||||||
unenrollment_flow = Flow.with_policy(
|
|
||||||
self.request, designation=FlowDesignation.UNRENOLLMENT
|
|
||||||
)
|
|
||||||
kwargs["unenrollment_enabled"] = bool(unenrollment_flow)
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
|
||||||
class TokenCreateView(
|
class TokenCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
|
|
|
@ -2,15 +2,23 @@
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
from cryptography.x509 import load_pem_x509_certificate
|
from cryptography.x509 import load_pem_x509_certificate
|
||||||
from django.db.models import Model
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, DateTimeField, SerializerMethodField
|
from rest_framework.fields import (
|
||||||
|
CharField,
|
||||||
|
DateTimeField,
|
||||||
|
IntegerField,
|
||||||
|
SerializerMethodField,
|
||||||
|
)
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, Serializer, ValidationError
|
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.api.decorators import permission_required
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
@ -71,16 +79,20 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CertificateDataSerializer(Serializer):
|
class CertificateDataSerializer(PassiveSerializer):
|
||||||
"""Get CertificateKeyPair's data"""
|
"""Get CertificateKeyPair's data"""
|
||||||
|
|
||||||
data = CharField(read_only=True)
|
data = CharField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class CertificateGenerationSerializer(PassiveSerializer):
|
||||||
raise NotImplementedError
|
"""Certificate generation parameters"""
|
||||||
|
|
||||||
|
common_name = CharField()
|
||||||
|
subject_alt_name = CharField(
|
||||||
|
required=False, allow_blank=True, label=_("Subject-alt name")
|
||||||
|
)
|
||||||
|
validity_days = IntegerField(initial=365)
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairViewSet(ModelViewSet):
|
class CertificateKeyPairViewSet(ModelViewSet):
|
||||||
|
@ -89,6 +101,29 @@ class CertificateKeyPairViewSet(ModelViewSet):
|
||||||
queryset = CertificateKeyPair.objects.all()
|
queryset = CertificateKeyPair.objects.all()
|
||||||
serializer_class = CertificateKeyPairSerializer
|
serializer_class = CertificateKeyPairSerializer
|
||||||
|
|
||||||
|
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
|
||||||
|
@swagger_auto_schema(
|
||||||
|
request_body=CertificateGenerationSerializer(),
|
||||||
|
responses={200: CertificateKeyPairSerializer},
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=["POST"])
|
||||||
|
def generate(self, request: Request) -> Response:
|
||||||
|
"""Generate a new, self-signed certificate-key pair"""
|
||||||
|
data = CertificateGenerationSerializer(data=request.data)
|
||||||
|
if not data.is_valid():
|
||||||
|
return Response(data.errors, status=400)
|
||||||
|
builder = CertificateBuilder()
|
||||||
|
builder.common_name = data.validated_data["common_name"]
|
||||||
|
builder.build(
|
||||||
|
subject_alt_names=data.validated_data.get("subject_alt_name", "").split(
|
||||||
|
","
|
||||||
|
),
|
||||||
|
validity_days=int(data.validated_data["validity_days"]),
|
||||||
|
)
|
||||||
|
instance = builder.save()
|
||||||
|
serializer = self.get_serializer(instance)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
@swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)})
|
@swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)})
|
||||||
@action(detail=True)
|
@action(detail=True)
|
||||||
# pylint: disable=invalid-name, unused-argument
|
# pylint: disable=invalid-name, unused-argument
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
"""authentik Crypto forms"""
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
||||||
from cryptography.x509 import load_pem_x509_certificate
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairGenerateForm(forms.Form):
|
|
||||||
"""CertificateKeyPair generation form"""
|
|
||||||
|
|
||||||
common_name = forms.CharField()
|
|
||||||
subject_alt_name = forms.CharField(required=False, label=_("Subject-alt name"))
|
|
||||||
validity_days = forms.IntegerField(initial=365)
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairForm(forms.ModelForm):
|
|
||||||
"""CertificateKeyPair Form"""
|
|
||||||
|
|
||||||
def clean_certificate_data(self):
|
|
||||||
"""Verify that input is a valid PEM x509 Certificate"""
|
|
||||||
certificate_data = self.cleaned_data["certificate_data"]
|
|
||||||
try:
|
|
||||||
load_pem_x509_certificate(
|
|
||||||
certificate_data.encode("utf-8"), default_backend()
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
raise forms.ValidationError("Unable to load certificate.")
|
|
||||||
return certificate_data
|
|
||||||
|
|
||||||
def clean_key_data(self):
|
|
||||||
"""Verify that input is a valid PEM RSA Key"""
|
|
||||||
key_data = self.cleaned_data["key_data"]
|
|
||||||
# Since this field is optional, data can be empty.
|
|
||||||
if key_data != "":
|
|
||||||
try:
|
|
||||||
load_pem_private_key(
|
|
||||||
str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
|
|
||||||
password=None,
|
|
||||||
backend=default_backend(),
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
raise forms.ValidationError("Unable to load private key.")
|
|
||||||
return key_data
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = CertificateKeyPair
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"certificate_data",
|
|
||||||
"key_data",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"certificate_data": forms.Textarea(attrs={"class": "monospaced"}),
|
|
||||||
"key_data": forms.Textarea(attrs={"class": "monospaced"}),
|
|
||||||
}
|
|
||||||
labels = {
|
|
||||||
"certificate_data": _("Certificate"),
|
|
||||||
"key_data": _("Private Key"),
|
|
||||||
}
|
|
|
@ -2,31 +2,12 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.crypto.api import CertificateKeyPairSerializer
|
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||||
from authentik.crypto.forms import CertificateKeyPairForm
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
|
||||||
|
|
||||||
class TestCrypto(TestCase):
|
class TestCrypto(TestCase):
|
||||||
"""Test Crypto validation"""
|
"""Test Crypto validation"""
|
||||||
|
|
||||||
def test_form(self):
|
|
||||||
"""Test form validation"""
|
|
||||||
keypair = CertificateKeyPair.objects.first()
|
|
||||||
self.assertTrue(
|
|
||||||
CertificateKeyPairForm(
|
|
||||||
{
|
|
||||||
"name": keypair.name,
|
|
||||||
"certificate_data": keypair.certificate_data,
|
|
||||||
"key_data": keypair.key_data,
|
|
||||||
}
|
|
||||||
).is_valid()
|
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
CertificateKeyPairForm(
|
|
||||||
{"name": keypair.name, "certificate_data": "test", "key_data": "test"}
|
|
||||||
).is_valid()
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_serializer(self):
|
def test_serializer(self):
|
||||||
"""Test API Validation"""
|
"""Test API Validation"""
|
||||||
keypair = CertificateKeyPair.objects.first()
|
keypair = CertificateKeyPair.objects.first()
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.db.models.aggregates import Count
|
from django.db.models.aggregates import Count
|
||||||
from django.db.models.fields.json import KeyTextTransform
|
from django.db.models.fields.json import KeyTextTransform
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, DictField, IntegerField
|
from rest_framework.fields import CharField, DictField, IntegerField
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""NotificationTransport API Views"""
|
"""NotificationTransport API Views"""
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from drf_yasg2.utils import no_body, swagger_auto_schema
|
from drf_yasg.utils import no_body, swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
@ -36,6 +36,7 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||||
"mode",
|
"mode",
|
||||||
"mode_verbose",
|
"mode_verbose",
|
||||||
"webhook_url",
|
"webhook_url",
|
||||||
|
"send_once",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
"""authentik events NotificationTransport forms"""
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from authentik.events.models import NotificationRule, NotificationTransport
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportForm(forms.ModelForm):
|
|
||||||
"""NotificationTransport Form"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = NotificationTransport
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"mode",
|
|
||||||
"webhook_url",
|
|
||||||
"send_once",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"webhook_url": forms.TextInput(),
|
|
||||||
}
|
|
||||||
labels = {
|
|
||||||
"webhook_url": _("Webhook URL"),
|
|
||||||
}
|
|
||||||
help_texts = {
|
|
||||||
"webhook_url": _(
|
|
||||||
("Only required when the Generic or Slack Webhook is used.")
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleForm(forms.ModelForm):
|
|
||||||
"""NotificationRule Form"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = NotificationRule
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"group",
|
|
||||||
"transports",
|
|
||||||
"severity",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
}
|
|
|
@ -76,7 +76,9 @@ class AuditMiddleware:
|
||||||
user: User, request: HttpRequest, sender, instance: Model, **_
|
user: User, request: HttpRequest, sender, instance: Model, **_
|
||||||
):
|
):
|
||||||
"""Signal handler for all object's pre_delete"""
|
"""Signal handler for all object's pre_delete"""
|
||||||
if isinstance(instance, (Event, Notification, UserObjectPermission)):
|
if isinstance(
|
||||||
|
instance, (Event, Notification, UserObjectPermission)
|
||||||
|
): # pragma: no cover
|
||||||
return
|
return
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
|
|
|
@ -3,11 +3,14 @@ from dataclasses import dataclass
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http.response import JsonResponse
|
from django.http.response import HttpResponseBadRequest, JsonResponse
|
||||||
from drf_yasg2 import openapi
|
from django.urls import reverse
|
||||||
from drf_yasg2.utils import no_body, swagger_auto_schema
|
from django.utils.translation import gettext as _
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.utils import no_body, swagger_auto_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import (
|
||||||
|
@ -20,11 +23,15 @@ from rest_framework.viewsets import ModelViewSet
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.utils import CacheSerializer
|
from authentik.core.api.utils import CacheSerializer, LinkSerializer
|
||||||
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.flows.planner import cache_key
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||||
from authentik.flows.transfer.common import DataclassEncoder
|
from authentik.flows.transfer.common import DataclassEncoder
|
||||||
from authentik.flows.transfer.exporter import FlowExporter
|
from authentik.flows.transfer.exporter import FlowExporter
|
||||||
|
from authentik.flows.transfer.importer import FlowImporter
|
||||||
|
from authentik.flows.views import SESSION_KEY_PLAN
|
||||||
|
from authentik.lib.views import bad_request_message
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -56,7 +63,7 @@ class FlowSerializer(ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class FlowDiagramSerializer(Serializer):
|
class FlowDiagramSerializer(Serializer):
|
||||||
"""response of the flow's /diagram/ action"""
|
"""response of the flow's diagram action"""
|
||||||
|
|
||||||
diagram = CharField(read_only=True)
|
diagram = CharField(read_only=True)
|
||||||
|
|
||||||
|
@ -88,14 +95,14 @@ class FlowViewSet(ModelViewSet):
|
||||||
search_fields = ["name", "slug", "designation", "title"]
|
search_fields = ["name", "slug", "designation", "title"]
|
||||||
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
|
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
|
||||||
|
|
||||||
@permission_required("authentik_flows.view_flow_cache")
|
@permission_required(None, ["authentik_flows.view_flow_cache"])
|
||||||
@swagger_auto_schema(responses={200: CacheSerializer(many=False)})
|
@swagger_auto_schema(responses={200: CacheSerializer(many=False)})
|
||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def cache_info(self, request: Request) -> Response:
|
def cache_info(self, request: Request) -> Response:
|
||||||
"""Info about cached flows"""
|
"""Info about cached flows"""
|
||||||
return Response(data={"count": len(cache.keys("flow_*"))})
|
return Response(data={"count": len(cache.keys("flow_*"))})
|
||||||
|
|
||||||
@permission_required("authentik_flows.clear_flow_cache")
|
@permission_required(None, ["authentik_flows.clear_flow_cache"])
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
request_body=no_body,
|
request_body=no_body,
|
||||||
responses={204: "Successfully cleared cache", 400: "Bad request"},
|
responses={204: "Successfully cleared cache", 400: "Bad request"},
|
||||||
|
@ -108,7 +115,61 @@ class FlowViewSet(ModelViewSet):
|
||||||
LOGGER.debug("Cleared flow cache", keys=len(keys))
|
LOGGER.debug("Cleared flow cache", keys=len(keys))
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
@permission_required("authentik_flows.export_flow")
|
@permission_required(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
"authentik_flows.add_flow",
|
||||||
|
"authentik_flows.change_flow",
|
||||||
|
"authentik_flows.add_flowstagebinding",
|
||||||
|
"authentik_flows.change_flowstagebinding",
|
||||||
|
"authentik_flows.add_stage",
|
||||||
|
"authentik_flows.change_stage",
|
||||||
|
"authentik_policies.add_policy",
|
||||||
|
"authentik_policies.change_policy",
|
||||||
|
"authentik_policies.add_policybinding",
|
||||||
|
"authentik_policies.change_policybinding",
|
||||||
|
"authentik_stages_prompt.add_prompt",
|
||||||
|
"authentik_stages_prompt.change_prompt",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@swagger_auto_schema(
|
||||||
|
request_body=no_body,
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
name="file",
|
||||||
|
in_=openapi.IN_FORM,
|
||||||
|
type=openapi.TYPE_FILE,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={204: "Successfully imported flow", 400: "Bad request"},
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
|
||||||
|
def import_flow(self, request: Request) -> Response:
|
||||||
|
"""Import flow from .akflow file"""
|
||||||
|
file = request.FILES.get("file", None)
|
||||||
|
if not file:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
importer = FlowImporter(file.read().decode())
|
||||||
|
valid = importer.validate()
|
||||||
|
if not valid:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
successful = importer.apply()
|
||||||
|
if not successful:
|
||||||
|
return Response(status=204)
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
@permission_required(
|
||||||
|
"authentik_flows.export_flow",
|
||||||
|
[
|
||||||
|
"authentik_flows.view_flow",
|
||||||
|
"authentik_flows.view_flowstagebinding",
|
||||||
|
"authentik_flows.view_stage",
|
||||||
|
"authentik_policies.view_policy",
|
||||||
|
"authentik_policies.view_policybinding",
|
||||||
|
"authentik_stages_prompt.view_prompt",
|
||||||
|
],
|
||||||
|
)
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
responses={
|
responses={
|
||||||
"200": openapi.Response(
|
"200": openapi.Response(
|
||||||
|
@ -194,3 +255,57 @@ class FlowViewSet(ModelViewSet):
|
||||||
)
|
)
|
||||||
diagram = "\n".join([str(x) for x in header + body + footer])
|
diagram = "\n".join([str(x) for x in header + body + footer])
|
||||||
return Response({"diagram": diagram})
|
return Response({"diagram": diagram})
|
||||||
|
|
||||||
|
@permission_required("authentik_flows.change_flow")
|
||||||
|
@swagger_auto_schema(
|
||||||
|
request_body=no_body,
|
||||||
|
manual_parameters=[
|
||||||
|
openapi.Parameter(
|
||||||
|
name="file",
|
||||||
|
in_=openapi.IN_FORM,
|
||||||
|
type=openapi.TYPE_FILE,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
responses={200: "Success"},
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=["POST"], parser_classes=(MultiPartParser,))
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def set_background(self, request: Request, slug: str):
|
||||||
|
"""Set Flow background"""
|
||||||
|
app: Flow = self.get_object()
|
||||||
|
icon = request.FILES.get("file", None)
|
||||||
|
if not icon:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
app.background = icon
|
||||||
|
app.save()
|
||||||
|
return Response({})
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
responses={200: LinkSerializer(many=False)},
|
||||||
|
)
|
||||||
|
@action(detail=True)
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def execute(self, request: Request, slug: str):
|
||||||
|
"""Execute flow for current user"""
|
||||||
|
flow: Flow = self.get_object()
|
||||||
|
planner = FlowPlanner(flow)
|
||||||
|
planner.use_cache = False
|
||||||
|
try:
|
||||||
|
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
|
||||||
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
except FlowNonApplicableException as exc:
|
||||||
|
return bad_request_message(
|
||||||
|
request,
|
||||||
|
_(
|
||||||
|
"Flow not applicable to current user/request: %(messages)s"
|
||||||
|
% {"messages": str(exc)}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"link": request._request.build_absolute_uri(
|
||||||
|
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from django.db.models.base import Model
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from rest_framework.fields import ChoiceField, DictField
|
from rest_framework.fields import ChoiceField, DictField
|
||||||
from rest_framework.serializers import CharField, Serializer
|
from rest_framework.serializers import CharField
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.flows.transfer.common import DataclassEncoder
|
from authentik.flows.transfer.common import DataclassEncoder
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -21,20 +21,14 @@ class ChallengeTypes(Enum):
|
||||||
REDIRECT = "redirect"
|
REDIRECT = "redirect"
|
||||||
|
|
||||||
|
|
||||||
class ErrorDetailSerializer(Serializer):
|
class ErrorDetailSerializer(PassiveSerializer):
|
||||||
"""Serializer for rest_framework's error messages"""
|
"""Serializer for rest_framework's error messages"""
|
||||||
|
|
||||||
string = CharField()
|
string = CharField()
|
||||||
code = CharField()
|
code = CharField()
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class Challenge(PassiveSerializer):
|
||||||
return Model()
|
|
||||||
|
|
||||||
|
|
||||||
class Challenge(Serializer):
|
|
||||||
"""Challenge that gets sent to the client based on which stage
|
"""Challenge that gets sent to the client based on which stage
|
||||||
is currently active"""
|
is currently active"""
|
||||||
|
|
||||||
|
@ -49,12 +43,6 @@ class Challenge(Serializer):
|
||||||
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
|
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
|
|
||||||
class RedirectChallenge(Challenge):
|
class RedirectChallenge(Challenge):
|
||||||
"""Challenge type to redirect the client"""
|
"""Challenge type to redirect the client"""
|
||||||
|
@ -81,20 +69,14 @@ class AccessDeniedChallenge(Challenge):
|
||||||
error_message = CharField(required=False)
|
error_message = CharField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class PermissionSerializer(Serializer):
|
class PermissionSerializer(PassiveSerializer):
|
||||||
"""Permission used for consent"""
|
"""Permission used for consent"""
|
||||||
|
|
||||||
name = CharField()
|
name = CharField()
|
||||||
id = CharField()
|
id = CharField()
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
class ChallengeResponse(PassiveSerializer):
|
||||||
return Model()
|
|
||||||
|
|
||||||
|
|
||||||
class ChallengeResponse(Serializer):
|
|
||||||
"""Base class for all challenge responses"""
|
"""Base class for all challenge responses"""
|
||||||
|
|
||||||
stage: Optional["StageView"]
|
stage: Optional["StageView"]
|
||||||
|
@ -103,12 +85,6 @@ class ChallengeResponse(Serializer):
|
||||||
self.stage = kwargs.pop("stage", None)
|
self.stage = kwargs.pop("stage", None)
|
||||||
super().__init__(instance=instance, data=data, **kwargs)
|
super().__init__(instance=instance, data=data, **kwargs)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
|
|
||||||
class HttpChallengeResponse(JsonResponse):
|
class HttpChallengeResponse(JsonResponse):
|
||||||
"""Subclass of JsonResponse that uses the `DataclassEncoder`"""
|
"""Subclass of JsonResponse that uses the `DataclassEncoder`"""
|
||||||
|
|
|
@ -1,35 +1,10 @@
|
||||||
"""Flow and Stage forms"""
|
"""Flow and Stage forms"""
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import FileExtensionValidator
|
|
||||||
from django.forms import ValidationError
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
from authentik.flows.models import FlowStageBinding, Stage
|
||||||
from authentik.flows.transfer.importer import FlowImporter
|
|
||||||
from authentik.lib.widgets import GroupedModelChoiceField
|
from authentik.lib.widgets import GroupedModelChoiceField
|
||||||
|
|
||||||
|
|
||||||
class FlowForm(forms.ModelForm):
|
|
||||||
"""Flow Form"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
|
|
||||||
model = Flow
|
|
||||||
fields = [
|
|
||||||
"name",
|
|
||||||
"title",
|
|
||||||
"slug",
|
|
||||||
"designation",
|
|
||||||
"background",
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
"name": forms.TextInput(),
|
|
||||||
"title": forms.TextInput(),
|
|
||||||
"background": forms.FileInput(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class FlowStageBindingForm(forms.ModelForm):
|
class FlowStageBindingForm(forms.ModelForm):
|
||||||
"""FlowStageBinding Form"""
|
"""FlowStageBinding Form"""
|
||||||
|
|
||||||
|
@ -56,20 +31,3 @@ class FlowStageBindingForm(forms.ModelForm):
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FlowImportForm(forms.Form):
|
|
||||||
"""Form used for flow importing"""
|
|
||||||
|
|
||||||
flow = forms.FileField(
|
|
||||||
validators=[FileExtensionValidator(allowed_extensions=["akflow"])]
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_flow(self):
|
|
||||||
"""Check if the flow is valid and rewind the file to the start"""
|
|
||||||
flow = self.cleaned_data["flow"].read()
|
|
||||||
valid = FlowImporter(flow.decode()).validate()
|
|
||||||
if not valid:
|
|
||||||
raise ValidationError(_("Flow invalid."))
|
|
||||||
self.cleaned_data["flow"].seek(0)
|
|
||||||
return self.cleaned_data["flow"]
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from drf_yasg2 import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg2.utils import no_body, swagger_auto_schema
|
from drf_yasg.utils import no_body, swagger_auto_schema
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"""Managed Object models"""
|
"""Managed Object models"""
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import QuerySet
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,10 +21,6 @@ class ManagedModel(models.Model):
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def managed_objects(self) -> QuerySet:
|
|
||||||
"""Get all objects which are managed"""
|
|
||||||
return self.objects.exclude(managed__isnull=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
"""Outpost API Views"""
|
"""Outpost API Views"""
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
|
||||||
from django.db.models.base import Model
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import BooleanField, CharField, SerializerMethodField
|
from rest_framework.fields import BooleanField, CharField, SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, Serializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
from authentik.core.api.utils import (
|
||||||
|
MetaNameSerializer,
|
||||||
|
PassiveSerializer,
|
||||||
|
TypeCreateSerializer,
|
||||||
|
)
|
||||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
from authentik.lib.templatetags.authentik_utils import verbose_name
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
from authentik.outposts.models import (
|
from authentik.outposts.models import (
|
||||||
|
@ -43,18 +46,12 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ServiceConnectionStateSerializer(Serializer):
|
class ServiceConnectionStateSerializer(PassiveSerializer):
|
||||||
"""Serializer for Service connection state"""
|
"""Serializer for Service connection state"""
|
||||||
|
|
||||||
healthy = BooleanField(read_only=True)
|
healthy = BooleanField(read_only=True)
|
||||||
version = CharField(read_only=True)
|
version = CharField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceConnectionViewSet(ModelViewSet):
|
class ServiceConnectionViewSet(ModelViewSet):
|
||||||
"""ServiceConnection Viewset"""
|
"""ServiceConnection Viewset"""
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
"""Outpost API Views"""
|
"""Outpost API Views"""
|
||||||
from django.db.models import Model
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import BooleanField, CharField, DateTimeField
|
from rest_framework.fields import BooleanField, CharField, DateTimeField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import JSONField, ModelSerializer, Serializer
|
from rest_framework.serializers import JSONField, ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
from authentik.outposts.models import Outpost
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.outposts.models import Outpost, default_outpost_config
|
||||||
|
|
||||||
|
|
||||||
class OutpostSerializer(ModelSerializer):
|
class OutpostSerializer(ModelSerializer):
|
||||||
|
@ -32,7 +32,13 @@ class OutpostSerializer(ModelSerializer):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class OutpostHealthSerializer(Serializer):
|
class OutpostDefaultConfigSerializer(PassiveSerializer):
|
||||||
|
"""Global default outpost config"""
|
||||||
|
|
||||||
|
config = JSONField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class OutpostHealthSerializer(PassiveSerializer):
|
||||||
"""Outpost health status"""
|
"""Outpost health status"""
|
||||||
|
|
||||||
last_seen = DateTimeField(read_only=True)
|
last_seen = DateTimeField(read_only=True)
|
||||||
|
@ -40,12 +46,6 @@ class OutpostHealthSerializer(Serializer):
|
||||||
version_should = CharField(read_only=True)
|
version_should = CharField(read_only=True)
|
||||||
version_outdated = BooleanField(read_only=True)
|
version_outdated = BooleanField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostViewSet(ModelViewSet):
|
class OutpostViewSet(ModelViewSet):
|
||||||
"""Outpost Viewset"""
|
"""Outpost Viewset"""
|
||||||
|
@ -78,3 +78,10 @@ class OutpostViewSet(ModelViewSet):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return Response(OutpostHealthSerializer(states, many=True).data)
|
return Response(OutpostHealthSerializer(states, many=True).data)
|
||||||
|
|
||||||
|
@swagger_auto_schema(responses={200: OutpostDefaultConfigSerializer(many=False)})
|
||||||
|
@action(detail=False, methods=["GET"])
|
||||||
|
def default_settings(self, request: Request) -> Response:
|
||||||
|
"""Global default outpost config"""
|
||||||
|
host = self.request.build_absolute_uri("/")
|
||||||
|
return Response({"config": default_outpost_config(host)})
|
||||||
|
|
|
@ -80,9 +80,9 @@ class OutpostType(models.TextChoices):
|
||||||
PROXY = "proxy"
|
PROXY = "proxy"
|
||||||
|
|
||||||
|
|
||||||
def default_outpost_config():
|
def default_outpost_config(host: Optional[str] = None):
|
||||||
"""Get default outpost config"""
|
"""Get default outpost config"""
|
||||||
return asdict(OutpostConfig(authentik_host=""))
|
return asdict(OutpostConfig(authentik_host=host or ""))
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
"""policy binding API Views"""
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.api.groups import GroupSerializer
|
||||||
|
from authentik.policies.models import PolicyBinding, PolicyBindingModel
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyBindingModelForeignKey(PrimaryKeyRelatedField):
|
||||||
|
"""rest_framework PrimaryKeyRelatedField which resolves
|
||||||
|
model_manager's InheritanceQuerySet"""
|
||||||
|
|
||||||
|
def use_pk_only_optimization(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# pylint: disable=inconsistent-return-statements
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if self.pk_field is not None:
|
||||||
|
data = self.pk_field.to_internal_value(data)
|
||||||
|
try:
|
||||||
|
# Due to inheritance, a direct DB lookup for the primary key
|
||||||
|
# won't return anything. This is because the direct lookup
|
||||||
|
# checks the PK of PolicyBindingModel (for example),
|
||||||
|
# but we get given the Primary Key of the inheriting class
|
||||||
|
for model in self.get_queryset().select_subclasses().all().select_related():
|
||||||
|
if model.pk == data:
|
||||||
|
return model
|
||||||
|
# as a fallback we still try a direct lookup
|
||||||
|
return self.get_queryset().get_subclass(pk=data)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
self.fail("does_not_exist", pk_value=data)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self.fail("incorrect_type", data_type=type(data).__name__)
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
correct_model = PolicyBindingModel.objects.get_subclass(pbm_uuid=value.pbm_uuid)
|
||||||
|
return correct_model.pk
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyBindingSerializer(ModelSerializer):
|
||||||
|
"""PolicyBinding Serializer"""
|
||||||
|
|
||||||
|
# Because we're not interested in the PolicyBindingModel's PK but rather the subclasses PK,
|
||||||
|
# we have to manually declare this field
|
||||||
|
target = PolicyBindingModelForeignKey(
|
||||||
|
queryset=PolicyBindingModel.objects.select_subclasses(),
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
group = GroupSerializer(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = PolicyBinding
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"policy",
|
||||||
|
"group",
|
||||||
|
"user",
|
||||||
|
"target",
|
||||||
|
"enabled",
|
||||||
|
"order",
|
||||||
|
"timeout",
|
||||||
|
]
|
||||||
|
depth = 2
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyBindingViewSet(ModelViewSet):
|
||||||
|
"""PolicyBinding Viewset"""
|
||||||
|
|
||||||
|
queryset = PolicyBinding.objects.all().select_related(
|
||||||
|
"policy", "target", "group", "user"
|
||||||
|
)
|
||||||
|
serializer_class = PolicyBindingSerializer
|
||||||
|
filterset_fields = ["policy", "target", "enabled", "order", "timeout"]
|
||||||
|
search_fields = ["policy__name"]
|
|
@ -0,0 +1,20 @@
|
||||||
|
"""Serializer for policy execution"""
|
||||||
|
from rest_framework.fields import BooleanField, CharField, JSONField, ListField
|
||||||
|
from rest_framework.relations import PrimaryKeyRelatedField
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
|
from authentik.core.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyTestSerializer(PassiveSerializer):
|
||||||
|
"""Test policy execution for a user with context"""
|
||||||
|
|
||||||
|
user = PrimaryKeyRelatedField(queryset=User.objects.all())
|
||||||
|
context = JSONField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyTestResultSerializer(PassiveSerializer):
|
||||||
|
"""result of a policy test"""
|
||||||
|
|
||||||
|
passing = BooleanField()
|
||||||
|
messages = ListField(child=CharField(), read_only=True)
|
|
@ -1,19 +1,16 @@
|
||||||
"""policy API Views"""
|
"""policy API Views"""
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from drf_yasg2.utils import no_body, swagger_auto_schema
|
from drf_yasg.utils import no_body, swagger_auto_schema
|
||||||
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
ModelSerializer,
|
from rest_framework.viewsets import GenericViewSet
|
||||||
PrimaryKeyRelatedField,
|
|
||||||
SerializerMethodField,
|
|
||||||
)
|
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
|
@ -25,42 +22,14 @@ from authentik.core.api.utils import (
|
||||||
)
|
)
|
||||||
from authentik.lib.templatetags.authentik_utils import verbose_name
|
from authentik.lib.templatetags.authentik_utils import verbose_name
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer
|
||||||
|
from authentik.policies.models import Policy, PolicyBinding
|
||||||
|
from authentik.policies.process import PolicyProcess
|
||||||
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingModelForeignKey(PrimaryKeyRelatedField):
|
|
||||||
"""rest_framework PrimaryKeyRelatedField which resolves
|
|
||||||
model_manager's InheritanceQuerySet"""
|
|
||||||
|
|
||||||
def use_pk_only_optimization(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# pylint: disable=inconsistent-return-statements
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
if self.pk_field is not None:
|
|
||||||
data = self.pk_field.to_internal_value(data)
|
|
||||||
try:
|
|
||||||
# Due to inheritance, a direct DB lookup for the primary key
|
|
||||||
# won't return anything. This is because the direct lookup
|
|
||||||
# checks the PK of PolicyBindingModel (for example),
|
|
||||||
# but we get given the Primary Key of the inheriting class
|
|
||||||
for model in self.get_queryset().select_subclasses().all().select_related():
|
|
||||||
if model.pk == data:
|
|
||||||
return model
|
|
||||||
# as a fallback we still try a direct lookup
|
|
||||||
return self.get_queryset().get_subclass(pk=data)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
self.fail("does_not_exist", pk_value=data)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
self.fail("incorrect_type", data_type=type(data).__name__)
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
correct_model = PolicyBindingModel.objects.get_subclass(pbm_uuid=value.pbm_uuid)
|
|
||||||
return correct_model.pk
|
|
||||||
|
|
||||||
|
|
||||||
class PolicySerializer(ModelSerializer, MetaNameSerializer):
|
class PolicySerializer(ModelSerializer, MetaNameSerializer):
|
||||||
"""Policy Serializer"""
|
"""Policy Serializer"""
|
||||||
|
|
||||||
|
@ -168,39 +137,32 @@ class PolicyViewSet(
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
|
@permission_required("authentik_policies.view_policy")
|
||||||
class PolicyBindingSerializer(ModelSerializer):
|
@swagger_auto_schema(
|
||||||
"""PolicyBinding Serializer"""
|
request_body=PolicyTestSerializer(),
|
||||||
|
responses={200: PolicyTestResultSerializer()},
|
||||||
# Because we're not interested in the PolicyBindingModel's PK but rather the subclasses PK,
|
|
||||||
# we have to manually declare this field
|
|
||||||
target = PolicyBindingModelForeignKey(
|
|
||||||
queryset=PolicyBindingModel.objects.select_subclasses(),
|
|
||||||
required=True,
|
|
||||||
)
|
)
|
||||||
|
@action(detail=True, methods=["POST"])
|
||||||
|
def test(self, request: Request) -> Response:
|
||||||
|
"""Test policy"""
|
||||||
|
policy = self.get_object()
|
||||||
|
test_params = PolicyTestSerializer(request.data)
|
||||||
|
if not test_params.is_valid():
|
||||||
|
return Response(test_params.errors, status=400)
|
||||||
|
|
||||||
class Meta:
|
# User permission check, only allow policy testing for users that are readable
|
||||||
|
users = get_objects_for_user(request.user, "authentik_core.view_user").filter(
|
||||||
model = PolicyBinding
|
pk=test_params["user"]
|
||||||
fields = [
|
|
||||||
"pk",
|
|
||||||
"policy",
|
|
||||||
"group",
|
|
||||||
"user",
|
|
||||||
"target",
|
|
||||||
"enabled",
|
|
||||||
"order",
|
|
||||||
"timeout",
|
|
||||||
]
|
|
||||||
depth = 2
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingViewSet(ModelViewSet):
|
|
||||||
"""PolicyBinding Viewset"""
|
|
||||||
|
|
||||||
queryset = PolicyBinding.objects.all().select_related(
|
|
||||||
"policy", "target", "group", "user"
|
|
||||||
)
|
)
|
||||||
serializer_class = PolicyBindingSerializer
|
if not users.exists():
|
||||||
filterset_fields = ["policy", "target", "enabled", "order", "timeout"]
|
raise PermissionDenied()
|
||||||
search_fields = ["policy__name"]
|
|
||||||
|
p_request = PolicyRequest(users.first())
|
||||||
|
p_request.debug = True
|
||||||
|
p_request.set_http_request(self.request)
|
||||||
|
p_request.context = test_params.validated_data.get("context", {})
|
||||||
|
|
||||||
|
proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None)
|
||||||
|
result = proc.execute()
|
||||||
|
response = PolicyTestResultSerializer(result)
|
||||||
|
return Response(response)
|
|
@ -1,7 +1,7 @@
|
||||||
"""Dummy Policy API Views"""
|
"""Dummy Policy API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.policies.api import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Event Matcher Policy API"""
|
"""Event Matcher Policy API"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.policies.api import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.event_matcher.models import EventMatcherPolicy
|
from authentik.policies.event_matcher.models import EventMatcherPolicy
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Password Expiry Policy API Views"""
|
"""Password Expiry Policy API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.policies.api import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.expiry.models import PasswordExpiryPolicy
|
from authentik.policies.expiry.models import PasswordExpiryPolicy
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Expression Policy API"""
|
"""Expression Policy API"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.policies.api import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.policies.api import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.hibp.models import HaveIBeenPwendPolicy
|
from authentik.policies.hibp.models import HaveIBeenPwendPolicy
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ class PolicyBinding(SerializerModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> BaseSerializer:
|
def serializer(self) -> BaseSerializer:
|
||||||
from authentik.policies.api import PolicyBindingSerializer
|
from authentik.policies.api.bindings import PolicyBindingSerializer
|
||||||
|
|
||||||
return PolicyBindingSerializer
|
return PolicyBindingSerializer
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Password Policy API Views"""
|
"""Password Policy API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.policies.api import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.password.models import PasswordPolicy
|
from authentik.policies.password.models import PasswordPolicy
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.policies.api import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.reputation.models import (
|
from authentik.policies.reputation.models import (
|
||||||
IPReputation,
|
IPReputation,
|
||||||
ReputationPolicy,
|
ReputationPolicy,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""OAuth2Provider API Views"""
|
"""OAuth2Provider API Views"""
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""ProxyProvider API Views"""
|
"""ProxyProvider API Views"""
|
||||||
from drf_yasg2.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""SAMLProvider API Views"""
|
"""SAMLProvider API Views"""
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
|
@ -126,7 +126,7 @@ INSTALLED_APPS = [
|
||||||
"authentik.stages.user_write.apps.AuthentikStageUserWriteConfig",
|
"authentik.stages.user_write.apps.AuthentikStageUserWriteConfig",
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
"django_filters",
|
"django_filters",
|
||||||
"drf_yasg2",
|
"drf_yasg",
|
||||||
"guardian",
|
"guardian",
|
||||||
"django_prometheus",
|
"django_prometheus",
|
||||||
"channels",
|
"channels",
|
||||||
|
@ -137,6 +137,7 @@ INSTALLED_APPS = [
|
||||||
GUARDIAN_MONKEY_PATCH = False
|
GUARDIAN_MONKEY_PATCH = False
|
||||||
|
|
||||||
SWAGGER_SETTINGS = {
|
SWAGGER_SETTINGS = {
|
||||||
|
"DEFAULT_AUTO_SCHEMA_CLASS": "authentik.api.schema.ErrorResponseAutoSchema",
|
||||||
"DEFAULT_INFO": "authentik.api.v2.urls.info",
|
"DEFAULT_INFO": "authentik.api.v2.urls.info",
|
||||||
"DEFAULT_PAGINATOR_INSPECTORS": [
|
"DEFAULT_PAGINATOR_INSPECTORS": [
|
||||||
"authentik.api.pagination_schema.PaginationInspector",
|
"authentik.api.pagination_schema.PaginationInspector",
|
||||||
|
@ -437,7 +438,7 @@ _LOGGING_HANDLER_MAP = {
|
||||||
"kubernetes": "INFO",
|
"kubernetes": "INFO",
|
||||||
"asyncio": "WARNING",
|
"asyncio": "WARNING",
|
||||||
"aioredis": "WARNING",
|
"aioredis": "WARNING",
|
||||||
"drf_yasg2.utils": "WARNING",
|
"drf_yasg.utils": "WARNING",
|
||||||
}
|
}
|
||||||
for handler_name, level in _LOGGING_HANDLER_MAP.items():
|
for handler_name, level in _LOGGING_HANDLER_MAP.items():
|
||||||
# pyright: reportGeneralTypeIssues=false
|
# pyright: reportGeneralTypeIssues=false
|
||||||
|
|
|
@ -3,17 +3,16 @@ from datetime import datetime
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.base import Model
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, Serializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.sources import SourceSerializer
|
from authentik.core.api.sources import SourceSerializer
|
||||||
from authentik.core.api.utils import MetaNameSerializer
|
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer
|
||||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,17 +43,11 @@ class LDAPSourceSerializer(SourceSerializer):
|
||||||
extra_kwargs = {"bind_password": {"write_only": True}}
|
extra_kwargs = {"bind_password": {"write_only": True}}
|
||||||
|
|
||||||
|
|
||||||
class LDAPSourceSyncStatusSerializer(Serializer):
|
class LDAPSourceSyncStatusSerializer(PassiveSerializer):
|
||||||
"""LDAP Sync status"""
|
"""LDAP Sync status"""
|
||||||
|
|
||||||
last_sync = DateTimeField(read_only=True)
|
last_sync = DateTimeField(read_only=True)
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class LDAPSourceViewSet(ModelViewSet):
|
class LDAPSourceViewSet(ModelViewSet):
|
||||||
"""LDAP Source Viewset"""
|
"""LDAP Source Viewset"""
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""SAMLSource API Views"""
|
"""SAMLSource API Views"""
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Validation stage challenge checking"""
|
"""Validation stage challenge checking"""
|
||||||
from django.db.models import Model
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_otp import match_token
|
from django_otp import match_token
|
||||||
|
@ -7,7 +6,7 @@ from django_otp.models import Device
|
||||||
from django_otp.plugins.otp_static.models import StaticDevice
|
from django_otp.plugins.otp_static.models import StaticDevice
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
from rest_framework.fields import CharField, JSONField
|
from rest_framework.fields import CharField, JSONField
|
||||||
from rest_framework.serializers import Serializer, ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
|
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
|
||||||
from webauthn.webauthn import (
|
from webauthn.webauthn import (
|
||||||
AuthenticationRejectedException,
|
AuthenticationRejectedException,
|
||||||
|
@ -15,24 +14,19 @@ from webauthn.webauthn import (
|
||||||
WebAuthnUserDataMissing,
|
WebAuthnUserDataMissing,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
|
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
|
||||||
|
|
||||||
|
|
||||||
class DeviceChallenge(Serializer):
|
class DeviceChallenge(PassiveSerializer):
|
||||||
"""Single device challenge"""
|
"""Single device challenge"""
|
||||||
|
|
||||||
device_class = CharField()
|
device_class = CharField()
|
||||||
device_uid = CharField()
|
device_uid = CharField()
|
||||||
challenge = JSONField()
|
challenge = JSONField()
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
|
def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
|
||||||
"""Generate challenge for a single device"""
|
"""Generate challenge for a single device"""
|
||||||
|
|
|
@ -8,4 +8,3 @@ class AuthentikStageAuthenticatorWebAuthnConfig(AppConfig):
|
||||||
name = "authentik.stages.authenticator_webauthn"
|
name = "authentik.stages.authenticator_webauthn"
|
||||||
label = "authentik_stages_authenticator_webauthn"
|
label = "authentik_stages_authenticator_webauthn"
|
||||||
verbose_name = "authentik Stages.Authenticator.WebAuthn"
|
verbose_name = "authentik Stages.Authenticator.WebAuthn"
|
||||||
mountpoint = "-/user/authenticator/webauthn/"
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
"""WebAuthn urls"""
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from authentik.stages.authenticator_webauthn.views import DeviceUpdateView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("devices/<int:pk>/update/", DeviceUpdateView.as_view(), name="device-update"),
|
|
||||||
]
|
|
|
@ -1,25 +0,0 @@
|
||||||
"""webauthn views"""
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
|
||||||
from django.http.response import Http404
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django.views.generic import UpdateView
|
|
||||||
|
|
||||||
from authentik.stages.authenticator_webauthn.forms import DeviceEditForm
|
|
||||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
|
||||||
"""Update device"""
|
|
||||||
|
|
||||||
model = WebAuthnDevice
|
|
||||||
form_class = DeviceEditForm
|
|
||||||
template_name = "generic/update.html"
|
|
||||||
success_url = "/"
|
|
||||||
success_message = _("Successfully updated Device")
|
|
||||||
|
|
||||||
def get_object(self) -> WebAuthnDevice:
|
|
||||||
device: WebAuthnDevice = super().get_object()
|
|
||||||
if device.user != self.request.user:
|
|
||||||
raise Http404
|
|
||||||
return device
|
|
|
@ -3,16 +3,16 @@ from email.policy import Policy
|
||||||
from types import MethodType
|
from types import MethodType
|
||||||
from typing import Any, Callable, Iterator
|
from typing import Any, Callable, Iterator
|
||||||
|
|
||||||
from django.db.models.base import Model
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from rest_framework.fields import BooleanField, CharField, IntegerField
|
from rest_framework.fields import BooleanField, CharField, IntegerField
|
||||||
from rest_framework.serializers import Serializer, ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
|
@ -26,7 +26,7 @@ LOGGER = get_logger()
|
||||||
PLAN_CONTEXT_PROMPT = "prompt_data"
|
PLAN_CONTEXT_PROMPT = "prompt_data"
|
||||||
|
|
||||||
|
|
||||||
class PromptSerializer(Serializer):
|
class PromptSerializer(PassiveSerializer):
|
||||||
"""Serializer for a single Prompt field"""
|
"""Serializer for a single Prompt field"""
|
||||||
|
|
||||||
field_key = CharField()
|
field_key = CharField()
|
||||||
|
@ -36,12 +36,6 @@ class PromptSerializer(Serializer):
|
||||||
placeholder = CharField()
|
placeholder = CharField()
|
||||||
order = IntegerField()
|
order = IntegerField()
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
|
||||||
return Model()
|
|
||||||
|
|
||||||
|
|
||||||
class PromptChallenge(Challenge):
|
class PromptChallenge(Challenge):
|
||||||
"""Initial challenge being sent, define fields"""
|
"""Initial challenge being sent, define fields"""
|
||||||
|
|
|
@ -285,9 +285,9 @@ stages:
|
||||||
inputs:
|
inputs:
|
||||||
script: |
|
script: |
|
||||||
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||||
sudo chmod 777 -R api/
|
sudo chmod 777 -R web/api/
|
||||||
cd web
|
cd web
|
||||||
sudo chmod 777 -R api/
|
cd api && npm i && cd ..
|
||||||
npm i
|
npm i
|
||||||
npm run build
|
npm run build
|
||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
|
|
4392
swagger.yaml
4392
swagger.yaml
File diff suppressed because it is too large
Load Diff
|
@ -98,7 +98,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||||
wait = WebDriverWait(interface_admin, self.wait_timeout)
|
wait = WebDriverWait(interface_admin, self.wait_timeout)
|
||||||
|
|
||||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
|
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
|
||||||
self.driver.get(self.if_admin_url("authentik_core:user-details"))
|
self.driver.get(self.if_admin_url("/user"))
|
||||||
|
|
||||||
user = User.objects.get(username="foo")
|
user = User.objects.get(username="foo")
|
||||||
self.assertEqual(user.username, "foo")
|
self.assertEqual(user.username, "foo")
|
||||||
|
@ -198,7 +198,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
|
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
|
||||||
self.driver.get(self.if_admin_url("authentik_core:user-details"))
|
self.driver.get(self.if_admin_url("/user"))
|
||||||
|
|
||||||
self.assert_user(User.objects.get(username="foo"))
|
self.assert_user(User.objects.get(username="foo"))
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ from selenium.webdriver.support.wait import WebDriverWait
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.providers.oauth2.generators import (
|
from authentik.providers.oauth2.generators import (
|
||||||
generate_client_id,
|
generate_client_id,
|
||||||
|
@ -160,19 +161,9 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||||
|
|
||||||
# Wait until we've logged in
|
# Wait until we've logged in
|
||||||
self.wait_for_url(self.if_admin_url("/library"))
|
self.wait_for_url(self.if_admin_url("/library"))
|
||||||
self.driver.get(self.url("authentik_core:user-details"))
|
self.driver.get(self.if_admin_url("/user"))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
|
||||||
"admin",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
|
||||||
"admin@example.com",
|
|
||||||
)
|
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
@apply_migration("authentik_core", "0003_default_user")
|
@apply_migration("authentik_core", "0003_default_user")
|
||||||
|
@ -255,19 +246,9 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||||
|
|
||||||
# Wait until we've logged in
|
# Wait until we've logged in
|
||||||
self.wait_for_url(self.if_admin_url("/library"))
|
self.wait_for_url(self.if_admin_url("/library"))
|
||||||
self.driver.get(self.url("authentik_core:user-details"))
|
self.driver.get(self.if_admin_url("/user"))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
|
||||||
"admin",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
|
||||||
"admin@example.com",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
@ -359,17 +340,8 @@ class TestSourceOAuth1(SeleniumTestCase):
|
||||||
sleep(2)
|
sleep(2)
|
||||||
# Wait until we've logged in
|
# Wait until we've logged in
|
||||||
self.wait_for_url(self.if_admin_url("/library"))
|
self.wait_for_url(self.if_admin_url("/library"))
|
||||||
self.driver.get(self.url("authentik_core:user-details"))
|
self.driver.get(self.if_admin_url("/user"))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assert_user(
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"),
|
User(username="example-user", name="test name", email="foo@example.com")
|
||||||
"example-user",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
|
||||||
"test name",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
|
||||||
"foo@example.com",
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,12 +5,14 @@ from typing import Any, Optional
|
||||||
from unittest.case import skipUnless
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
|
from guardian.utils import get_anonymous_user
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.support import expected_conditions as ec
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
from selenium.webdriver.support.wait import WebDriverWait
|
from selenium.webdriver.support.wait import WebDriverWait
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
|
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||||
|
@ -153,11 +155,12 @@ class TestSourceSAML(SeleniumTestCase):
|
||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait_for_url(self.if_admin_url("/library"))
|
self.wait_for_url(self.if_admin_url("/library"))
|
||||||
self.driver.get(self.url("authentik_core:user-details"))
|
self.driver.get(self.if_admin_url("/user"))
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
self.assert_user(
|
||||||
self.assertNotEqual(
|
User.objects.exclude(username="akadmin")
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
.exclude(pk=get_anonymous_user().pk)
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
|
@ -233,11 +236,12 @@ class TestSourceSAML(SeleniumTestCase):
|
||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait_for_url(self.if_admin_url("/library"))
|
self.wait_for_url(self.if_admin_url("/library"))
|
||||||
self.driver.get(self.url("authentik_core:user-details"))
|
self.driver.get(self.if_admin_url("/user"))
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
self.assert_user(
|
||||||
self.assertNotEqual(
|
User.objects.exclude(username="akadmin")
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
.exclude(pk=get_anonymous_user().pk)
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
@retry()
|
@retry()
|
||||||
|
@ -300,9 +304,10 @@ class TestSourceSAML(SeleniumTestCase):
|
||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait_for_url(self.if_admin_url("/library"))
|
self.wait_for_url(self.if_admin_url("/library"))
|
||||||
self.driver.get(self.url("authentik_core:user-details"))
|
self.driver.get(self.if_admin_url("/user"))
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
self.assert_user(
|
||||||
self.assertNotEqual(
|
User.objects.exclude(username="akadmin")
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
.exclude(pk=get_anonymous_user().pk)
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
|
|
|
@ -133,6 +133,139 @@
|
||||||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz",
|
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz",
|
||||||
"integrity": "sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw=="
|
"integrity": "sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw=="
|
||||||
},
|
},
|
||||||
|
"@polymer/font-roboto": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/font-roboto/-/font-roboto-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA=="
|
||||||
|
},
|
||||||
|
"@polymer/iron-a11y-announcer": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-lc5i4NKB8kSQHH0Hwu8WS3ym93m+J69OHJWSSBxwd17FI+h2wmgxDzeG9LI4ojMMck17/uc2pLe7g/UHt5/K/A==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-a11y-keys-behavior": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-lnrjKq3ysbBPT/74l0Fj0U9H9C35Tpw2C/tpJ8a+5g8Y3YJs1WSZYnEl1yOkw6sEyaxOq/1DkzH0+60gGu5/PQ==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-ajax": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-ajax/-/iron-ajax-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-7+TPEAfWsRdhj1Y8UeF1759ktpVu+c3sG16rJiUC3wF9+woQ9xI1zUm2d59i7Yc3aDEJrR/Q8Y262KlOvyGVNg==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-autogrow-textarea": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/iron-behaviors": "^3.0.0-pre.26",
|
||||||
|
"@polymer/iron-flex-layout": "^3.0.0-pre.26",
|
||||||
|
"@polymer/iron-validatable-behavior": "^3.0.0-pre.26",
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-behaviors": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-behaviors/-/iron-behaviors-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-IMEwcv1lhf1HSQxuyWOUIL0lOBwmeaoSTpgCJeP9IBYnuB1SPQngmfRuHKgK6/m9LQ9F9miC7p3HeQQUdKAE0w==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26",
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-flex-layout": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-form": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-form/-/iron-form-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-JwSQXHjYALsytCeBkXlY8aRwqgZuYIqzOk3iHuugb1RXOdZ7MZHyJhMDVBbscHjxqPKu/KaVzAjrcfwNNafzEA==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/iron-ajax": "^3.0.0-pre.26",
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-form-element-behavior": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-form-element-behavior/-/iron-form-element-behavior-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-G/e2KXyL5AY7mMjmomHkGpgS0uAf4ovNpKhkuUTRnMuMJuf589bKqE85KN4ovE1Tzhv2hJoh/igyD6ekHiYU1A==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-input": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-input/-/iron-input-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-WLx13kEcbH9GKbj9+pWR6pbJkA5kxn3796ynx6eQd2rueMyUfVTR3GzOvadBKsciUuIuzrxpBWZ2+3UcueVUQQ==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/iron-a11y-announcer": "^3.0.0-pre.26",
|
||||||
|
"@polymer/iron-validatable-behavior": "^3.0.0-pre.26",
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-meta": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/iron-validatable-behavior": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wwpYh6wOa4fNI+jH5EYKC7TVPYQ2OfgQqocWat7GsNWcsblKYhLYbwsvEY5nO0n2xKqNfZzDLrUom5INJN7msQ==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/iron-meta": "^3.0.0-pre.26",
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/paper-input": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/paper-input/-/paper-input-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26",
|
||||||
|
"@polymer/iron-autogrow-textarea": "^3.0.0-pre.26",
|
||||||
|
"@polymer/iron-behaviors": "^3.0.0-pre.26",
|
||||||
|
"@polymer/iron-form-element-behavior": "^3.0.0-pre.26",
|
||||||
|
"@polymer/iron-input": "^3.0.0-pre.26",
|
||||||
|
"@polymer/paper-styles": "^3.0.0-pre.26",
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/paper-styles": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/paper-styles/-/paper-styles-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-y6hmObLqlCx602TQiSBKHqjwkE7xmDiFkoxdYGaNjtv4xcysOTdVJsDR/R9UHwIaxJ7gHlthMSykir1nv78++g==",
|
||||||
|
"requires": {
|
||||||
|
"@polymer/font-roboto": "^3.0.1",
|
||||||
|
"@polymer/iron-flex-layout": "^3.0.0-pre.26",
|
||||||
|
"@polymer/polymer": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@polymer/polymer": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==",
|
||||||
|
"requires": {
|
||||||
|
"@webcomponents/shadycss": "^1.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@rollup/plugin-typescript": {
|
"@rollup/plugin-typescript": {
|
||||||
"version": "8.2.1",
|
"version": "8.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.1.tgz",
|
||||||
|
@ -547,6 +680,11 @@
|
||||||
"eslint-visitor-keys": "^2.0.0"
|
"eslint-visitor-keys": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@webcomponents/shadycss": {
|
||||||
|
"version": "1.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.10.2.tgz",
|
||||||
|
"integrity": "sha512-9Iseu8bRtecb0klvv+WXZOVZatsRkbaH7M97Z+f+Pt909R4lDfgUODAnra23DOZTpeMTAkVpf4m/FZztN7Ox1A=="
|
||||||
|
},
|
||||||
"acorn": {
|
"acorn": {
|
||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
|
@ -3948,6 +4086,11 @@
|
||||||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"yaml": {
|
||||||
|
"version": "1.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||||
|
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
|
||||||
|
},
|
||||||
"yargs": {
|
"yargs": {
|
||||||
"version": "15.4.1",
|
"version": "15.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||||
"@patternfly/patternfly": "^4.90.5",
|
"@patternfly/patternfly": "^4.90.5",
|
||||||
|
"@polymer/iron-form": "^3.0.1",
|
||||||
|
"@polymer/paper-input": "^3.2.1",
|
||||||
"@sentry/browser": "^6.2.3",
|
"@sentry/browser": "^6.2.3",
|
||||||
"@sentry/tracing": "^6.2.3",
|
"@sentry/tracing": "^6.2.3",
|
||||||
"@types/chart.js": "^2.9.31",
|
"@types/chart.js": "^2.9.31",
|
||||||
|
@ -31,7 +33,8 @@
|
||||||
"rollup-plugin-cssimport": "^1.0.2",
|
"rollup-plugin-cssimport": "^1.0.2",
|
||||||
"rollup-plugin-external-globals": "^0.6.1",
|
"rollup-plugin-external-globals": "^0.6.1",
|
||||||
"tslib": "^2.1.0",
|
"tslib": "^2.1.0",
|
||||||
"webcomponent-qr-code": "^1.0.5"
|
"webcomponent-qr-code": "^1.0.5",
|
||||||
|
"yaml": "^1.10.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.1",
|
||||||
|
|
|
@ -1,16 +1,3 @@
|
||||||
export interface QueryArguments {
|
|
||||||
page?: number;
|
|
||||||
page_size?: number;
|
|
||||||
[key: string]: number | string | boolean | undefined | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BaseInheritanceModel {
|
|
||||||
objectType: string;
|
|
||||||
|
|
||||||
verboseName: string;
|
|
||||||
verboseNamePlural: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AKPagination {
|
export interface AKPagination {
|
||||||
next?: number;
|
next?: number;
|
||||||
previous?: number;
|
previous?: number;
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
export class AdminURLManager {
|
export class AdminURLManager {
|
||||||
|
|
||||||
static applications(rest: string): string {
|
|
||||||
return `/administration/applications/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static cryptoCertificates(rest: string): string {
|
|
||||||
return `/administration/crypto/certificates/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static policies(rest: string): string {
|
static policies(rest: string): string {
|
||||||
return `/administration/policies/${rest}`;
|
return `/administration/policies/${rest}`;
|
||||||
}
|
}
|
||||||
|
@ -24,18 +16,10 @@ export class AdminURLManager {
|
||||||
return `/administration/property-mappings/${rest}`;
|
return `/administration/property-mappings/${rest}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static outposts(rest: string): string {
|
|
||||||
return `/administration/outposts/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static outpostServiceConnections(rest: string): string {
|
static outpostServiceConnections(rest: string): string {
|
||||||
return `/administration/outpost_service_connections/${rest}`;
|
return `/administration/outpost_service_connections/${rest}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static flows(rest: string): string {
|
|
||||||
return `/administration/flows/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static stages(rest: string): string {
|
static stages(rest: string): string {
|
||||||
return `/administration/stages/${rest}`;
|
return `/administration/stages/${rest}`;
|
||||||
}
|
}
|
||||||
|
@ -60,21 +44,6 @@ export class AdminURLManager {
|
||||||
return `/administration/tokens/${rest}`;
|
return `/administration/tokens/${rest}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static eventRules(rest: string): string {
|
|
||||||
return `/administration/events/rules/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static eventTransports(rest: string): string {
|
|
||||||
return `/administration/events/transports/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static users(rest: string): string {
|
|
||||||
return `/administration/users/${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static groups(rest: string): string {
|
|
||||||
return `/administration/groups/${rest}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserURLManager {
|
export class UserURLManager {
|
||||||
|
@ -105,6 +74,10 @@ export class AppURLManager {
|
||||||
|
|
||||||
export class FlowURLManager {
|
export class FlowURLManager {
|
||||||
|
|
||||||
|
static defaultUnenrollment(): string {
|
||||||
|
return "/flows/-/default/unenrollment/";
|
||||||
|
}
|
||||||
|
|
||||||
static configure(stageUuid: string, rest: string): string {
|
static configure(stageUuid: string, rest: string): string {
|
||||||
return `/flows/-/configure/${stageUuid}/${rest}`;
|
return `/flows/-/configure/${stageUuid}/${rest}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ body {
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
|
--ak-accent: #fd4b2d;
|
||||||
--ak-dark-foreground: #fafafa;
|
--ak-dark-foreground: #fafafa;
|
||||||
--ak-dark-foreground-darker: #bebebe;
|
--ak-dark-foreground-darker: #bebebe;
|
||||||
--ak-dark-foreground-link: #5a5cb9;
|
--ak-dark-foreground-link: #5a5cb9;
|
||||||
|
@ -100,6 +101,15 @@ body {
|
||||||
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
|
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
|
||||||
--pf-global--link--Color: var(--ak-dark-foreground-link);
|
--pf-global--link--Color: var(--ak-dark-foreground-link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paper-input {
|
||||||
|
/* --paper-input-container-input-color: var(--ak-dark-foreground); */
|
||||||
|
--primary-text-color: var(--ak-dark-foreground);
|
||||||
|
}
|
||||||
|
paper-checkbox {
|
||||||
|
--primary-text-color: var(--ak-dark-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
/* Global page background colour */
|
/* Global page background colour */
|
||||||
.pf-c-page {
|
.pf-c-page {
|
||||||
--pf-c-page--BackgroundColor: var(--ak-dark-background);
|
--pf-c-page--BackgroundColor: var(--ak-dark-background);
|
||||||
|
|
|
@ -9,3 +9,4 @@ export const EVENT_REFRESH = "ak-refresh";
|
||||||
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";
|
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";
|
||||||
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
|
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
|
||||||
export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh";
|
export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh";
|
||||||
|
export const TITLE_SUFFIX = "authentik";
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { customElement, LitElement, property } from "lit-element";
|
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||||
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||||
|
|
||||||
import CodeMirror from "codemirror";
|
import CodeMirror from "codemirror";
|
||||||
import "codemirror/addon/display/autorefresh";
|
import "codemirror/addon/display/autorefresh";
|
||||||
|
@ -6,6 +7,9 @@ import "codemirror/mode/xml/xml.js";
|
||||||
import "codemirror/mode/yaml/yaml.js";
|
import "codemirror/mode/yaml/yaml.js";
|
||||||
import "codemirror/mode/javascript/javascript.js";
|
import "codemirror/mode/javascript/javascript.js";
|
||||||
import "codemirror/mode/python/python.js";
|
import "codemirror/mode/python/python.js";
|
||||||
|
import CodeMirrorStyle from "codemirror/lib/codemirror.css";
|
||||||
|
import CodeMirrorTheme from "codemirror/theme/monokai.css";
|
||||||
|
import { ifDefined } from "lit-html/directives/if-defined";
|
||||||
|
|
||||||
@customElement("ak-codemirror")
|
@customElement("ak-codemirror")
|
||||||
export class CodeMirrorTextarea extends LitElement {
|
export class CodeMirrorTextarea extends LitElement {
|
||||||
|
@ -15,14 +19,20 @@ export class CodeMirrorTextarea extends LitElement {
|
||||||
@property()
|
@property()
|
||||||
mode = "yaml";
|
mode = "yaml";
|
||||||
|
|
||||||
|
@property()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
value?: string;
|
||||||
|
|
||||||
editor?: CodeMirror.EditorFromTextArea;
|
editor?: CodeMirror.EditorFromTextArea;
|
||||||
|
|
||||||
createRenderRoot() : ShadowRoot | Element {
|
static get styles(): CSSResult[] {
|
||||||
return this;
|
return [PFForm, CodeMirrorStyle, CodeMirrorTheme];
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
const textarea = this.querySelector("textarea");
|
const textarea = this.shadowRoot?.querySelector("textarea");
|
||||||
if (!textarea) {
|
if (!textarea) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -37,4 +47,8 @@ export class CodeMirrorTextarea extends LitElement {
|
||||||
this.editor?.save();
|
this.editor?.save();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html`<textarea class="pf-c-form-control" name=${ifDefined(this.name)}>${this.value || ""}</textarea>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { css, CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
|
||||||
|
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||||
|
import AKGlobal from "../authentik.css";
|
||||||
|
|
||||||
|
@customElement("ak-divider")
|
||||||
|
export class Divider extends LitElement {
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [PFBase, AKGlobal, css`
|
||||||
|
.separator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator::before,
|
||||||
|
.separator::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: 1px solid var(--pf-global--Color--100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator:not(:empty)::before {
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator:not(:empty)::after {
|
||||||
|
margin-left: .25em;
|
||||||
|
}
|
||||||
|
`];
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
return html`<div class="separator"><slot></slot></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,8 +23,7 @@ export class ActionButton extends SpinnerButton {
|
||||||
this.setLoading();
|
this.setLoading();
|
||||||
this.apiRequest().then(() => {
|
this.apiRequest().then(() => {
|
||||||
this.setDone(SUCCESS_CLASS);
|
this.setDone(SUCCESS_CLASS);
|
||||||
})
|
}).catch((e: Error | Response) => {
|
||||||
.catch((e: Error | Response) => {
|
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
showMessage({
|
showMessage({
|
||||||
level: MessageLevel.error,
|
level: MessageLevel.error,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue