commit
1f89b94f66
|
@ -40,7 +40,7 @@ RUN apt-get update && \
|
|||
chown authentik:authentik /backups
|
||||
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pytest.ini /
|
||||
COPY ./pyproject.toml /
|
||||
COPY ./xml /xml
|
||||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
|
|
2
Makefile
2
Makefile
|
@ -6,7 +6,7 @@ test-integration:
|
|||
coverage run manage.py test -v 3 tests/integration
|
||||
|
||||
test-e2e:
|
||||
coverage run manage.py test -v 3 tests/e2e
|
||||
coverage run manage.py test --failfast -v 3 tests/e2e
|
||||
|
||||
coverage:
|
||||
coverage run manage.py test -v 3 authentik
|
||||
|
|
2
Pipfile
2
Pipfile
|
@ -22,7 +22,7 @@ django-storages = "*"
|
|||
djangorestframework = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
docker = "*"
|
||||
drf_yasg2 = "*"
|
||||
drf_yasg = "*"
|
||||
facebook-sdk = "*"
|
||||
geoip2 = "*"
|
||||
gunicorn = "*"
|
||||
|
|
10
Pipfile.lock
generated
10
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "5fce5772178e4bc782d7112fab658f5bbb21abb77bb93fc3c0a66e9db3a23a37"
|
||||
"sha256": "a9d504f00ee8820017f26a4fda2938de456cb72b4bc2f8735fc8c6a6c615d46a"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -416,13 +416,13 @@
|
|||
"index": "pypi",
|
||||
"version": "==4.4.4"
|
||||
},
|
||||
"drf-yasg2": {
|
||||
"drf-yasg": {
|
||||
"hashes": [
|
||||
"sha256:7037a8041eb5d1073fa504a284fc889685f93d0bfd008a963db1b366db786734",
|
||||
"sha256:75e661ca5cf15eb44fcfab408c7b864f87c20794f564aa08b3a31817a857f19d"
|
||||
"sha256:8b72e5b1875931a8d11af407be3a9a5ba8776541492947a0df5bafda6b7f8267",
|
||||
"sha256:d50f197c7f02545d0b736df88c6d5cf874f8fea2507ad85ad7de6ae5bf2d9e5a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.19.4"
|
||||
"version": "==1.20.0"
|
||||
},
|
||||
"facebook-sdk": {
|
||||
"hashes": [
|
||||
|
|
|
@ -3,18 +3,18 @@ import time
|
|||
from collections import Counter
|
||||
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.functions import ExtractHour
|
||||
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.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
||||
|
@ -45,20 +45,14 @@ def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
|
|||
return results
|
||||
|
||||
|
||||
class CoordinateSerializer(Serializer):
|
||||
class CoordinateSerializer(PassiveSerializer):
|
||||
"""Coordinates for diagrams"""
|
||||
|
||||
x_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:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LoginMetricsSerializer(Serializer):
|
||||
class LoginMetricsSerializer(PassiveSerializer):
|
||||
"""Login Metrics per 1h"""
|
||||
|
||||
logins_per_1h = SerializerMethodField()
|
||||
|
@ -74,12 +68,6 @@ class LoginMetricsSerializer(Serializer):
|
|||
"""Get failed logins per hour for the last 24 hours"""
|
||||
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):
|
||||
"""Login Metrics per 1h"""
|
||||
|
|
|
@ -2,22 +2,21 @@
|
|||
from importlib import import_module
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db.models import Model
|
||||
from django.http.response import Http404
|
||||
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.fields import CharField, ChoiceField, DateTimeField, ListField
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
|
||||
|
||||
|
||||
class TaskSerializer(Serializer):
|
||||
class TaskSerializer(PassiveSerializer):
|
||||
"""Serialize TaskInfo and TaskResult"""
|
||||
|
||||
task_name = CharField()
|
||||
|
@ -30,12 +29,6 @@ class TaskSerializer(Serializer):
|
|||
)
|
||||
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):
|
||||
"""Read-only view set that returns all background tasks"""
|
||||
|
|
|
@ -2,22 +2,21 @@
|
|||
from os import environ
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from packaging.version import parse
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __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."""
|
||||
|
||||
version_current = SerializerMethodField()
|
||||
|
@ -47,12 +46,6 @@ class VersionSerializer(Serializer):
|
|||
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):
|
||||
"""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 authentik.admin.views import (
|
||||
applications,
|
||||
certificate_key_pair,
|
||||
events_notifications_rules,
|
||||
events_notifications_transports,
|
||||
flows,
|
||||
groups,
|
||||
outposts,
|
||||
outposts_service_connections,
|
||||
policies,
|
||||
policies_bindings,
|
||||
|
@ -19,22 +12,10 @@ from authentik.admin.views import (
|
|||
stages_bindings,
|
||||
stages_invitations,
|
||||
stages_prompts,
|
||||
users,
|
||||
)
|
||||
from authentik.providers.saml.views.metadata import MetadataImportView
|
||||
|
||||
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
|
||||
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
|
||||
path(
|
||||
|
@ -116,27 +97,6 @@ urlpatterns = [
|
|||
stages_invitations.InvitationCreateView.as_view(),
|
||||
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
|
||||
path(
|
||||
"property-mappings/create/",
|
||||
|
@ -153,48 +113,6 @@ urlpatterns = [
|
|||
property_mappings.PropertyMappingTestView.as_view(),
|
||||
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
|
||||
path(
|
||||
"outpost_service_connections/create/",
|
||||
|
@ -206,26 +124,4 @@ urlpatterns = [
|
|||
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
||||
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"""
|
||||
from typing import Any
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import DeleteView, UpdateView
|
||||
from django.views.generic import UpdateView
|
||||
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
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):
|
||||
"""CreateView for objects using InheritanceManager"""
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
"""API Decorators"""
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
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"""
|
||||
|
||||
def wrapper_outter(func: Callable):
|
||||
|
@ -15,12 +17,14 @@ def permission_required(perm: str, *other_perms: str):
|
|||
|
||||
@wraps(func)
|
||||
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
|
||||
obj = self.get_object()
|
||||
if not request.user.has_perm(perm, obj):
|
||||
return self.permission_denied(request)
|
||||
for other_perm in other_perms:
|
||||
if not request.user.has_perm(other_perm):
|
||||
if perm:
|
||||
obj = self.get_object()
|
||||
if not request.user.has_perm(perm, obj):
|
||||
return self.permission_denied(request)
|
||||
if other_perms:
|
||||
for other_perm in other_perms:
|
||||
if not request.user.has_perm(other_perm):
|
||||
return self.permission_denied(request)
|
||||
return func(self, request, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"""Swagger Pagination Schema class"""
|
||||
from typing import OrderedDict
|
||||
|
||||
from drf_yasg2 import openapi
|
||||
from drf_yasg2.inspectors import PaginatorInspector
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.inspectors import PaginatorInspector
|
||||
|
||||
|
||||
class PaginationInspector(PaginatorInspector):
|
||||
|
|
102
authentik/api/schema.py
Normal file
102
authentik/api/schema.py
Normal file
|
@ -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
authentik/api/tests/__init__.py
Normal file
0
authentik/api/tests/__init__.py
Normal file
24
authentik/api/tests/test_swagger.py
Normal file
24
authentik/api/tests/test_swagger.py
Normal file
|
@ -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"""
|
||||
from django.db.models import Model
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework.fields import BooleanField, CharField, ListField
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class LinkSerializer(Serializer):
|
||||
class FooterLinkSerializer(PassiveSerializer):
|
||||
"""Links returned in Config API"""
|
||||
|
||||
href = 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:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ConfigSerializer(Serializer):
|
||||
class ConfigSerializer(PassiveSerializer):
|
||||
"""Serialize authentik Config into DRF Object"""
|
||||
|
||||
branding_logo = 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_environment = CharField(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):
|
||||
"""Read-only view set that returns the current session's Configs"""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""api v2 urls"""
|
||||
from django.urls import path, re_path
|
||||
from drf_yasg2 import openapi
|
||||
from drf_yasg2.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from rest_framework import routers
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
|
@ -33,7 +33,8 @@ from authentik.outposts.api.outpost_service_connections import (
|
|||
ServiceConnectionViewSet,
|
||||
)
|
||||
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.event_matcher.api import EventMatcherPolicyViewSet
|
||||
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||
|
@ -189,7 +190,7 @@ router.register("policies/dummy", DummyPolicyViewSet)
|
|||
|
||||
info = openapi.Info(
|
||||
title="authentik API",
|
||||
default_version="v2",
|
||||
default_version="v2beta",
|
||||
contact=openapi.Contact(email="hello@beryju.org"),
|
||||
license=openapi.License(
|
||||
name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE"
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
"""Application API Views"""
|
||||
from django.core.cache import cache
|
||||
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.fields import SerializerMethodField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
|
@ -49,7 +52,6 @@ class ApplicationSerializer(ModelSerializer):
|
|||
"meta_icon",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policies",
|
||||
]
|
||||
|
||||
|
||||
|
@ -108,8 +110,33 @@ class ApplicationViewSet(ModelViewSet):
|
|||
serializer = self.get_serializer(allowed_applications, many=True)
|
||||
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(
|
||||
"authentik_core.view_application", "authentik_events.view_event"
|
||||
"authentik_core.view_application", ["authentik_events.view_event"]
|
||||
)
|
||||
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
|
||||
@action(detail=True)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""PropertyMapping API Views"""
|
||||
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.decorators import action
|
||||
from rest_framework.request import Request
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Provider API Views"""
|
||||
from django.urls import reverse
|
||||
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.fields import ReadOnlyField
|
||||
from rest_framework.request import Request
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from typing import Iterable
|
||||
|
||||
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.decorators import action
|
||||
from rest_framework.request import Request
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
"""Tokens API Viewset"""
|
||||
from django.db.models.base import Model
|
||||
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.fields import CharField
|
||||
from rest_framework.request import Request
|
||||
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 authentik.api.decorators import permission_required
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import Token
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
@ -34,17 +35,11 @@ class TokenSerializer(ModelSerializer):
|
|||
depth = 2
|
||||
|
||||
|
||||
class TokenViewSerializer(Serializer):
|
||||
class TokenViewSerializer(PassiveSerializer):
|
||||
"""Show token's current key"""
|
||||
|
||||
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):
|
||||
"""Token Viewset"""
|
||||
|
@ -66,6 +61,7 @@ class TokenViewSet(ModelViewSet):
|
|||
]
|
||||
ordering = ["expires"]
|
||||
|
||||
@permission_required("authentik_core.view_token_key")
|
||||
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
|
||||
@action(detail=True)
|
||||
# pylint: disable=unused-argument
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
"""User API Views"""
|
||||
from django.db.models.base import Model
|
||||
from django.urls import reverse_lazy
|
||||
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 rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
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 authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer
|
||||
from authentik.core.middleware import (
|
||||
SESSION_IMPERSONATE_ORIGINAL_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)
|
||||
and, if this user is being impersonated, the original user in the `original` property."""
|
||||
|
||||
user = UserSerializer()
|
||||
original = UserSerializer(required=False)
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
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):
|
||||
class UserMetricsSerializer(PassiveSerializer):
|
||||
"""User Metrics"""
|
||||
|
||||
logins_per_1h = SerializerMethodField()
|
||||
|
@ -98,12 +80,6 @@ class UserMetricsSerializer(Serializer):
|
|||
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):
|
||||
"""User Viewset"""
|
||||
|
@ -131,7 +107,7 @@ class UserViewSet(ModelViewSet):
|
|||
serializer.is_valid()
|
||||
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)})
|
||||
@action(detail=False)
|
||||
def metrics(self, request: Request) -> Response:
|
||||
|
@ -142,7 +118,7 @@ class UserViewSet(ModelViewSet):
|
|||
|
||||
@permission_required("authentik_core.reset_user_password")
|
||||
@swagger_auto_schema(
|
||||
responses={"200": UserRecoverySerializer(many=False)},
|
||||
responses={"200": LinkSerializer(many=False)},
|
||||
)
|
||||
@action(detail=True)
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
|
|
|
@ -4,18 +4,22 @@ from rest_framework.fields import CharField, IntegerField
|
|||
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"""
|
||||
|
||||
verbose_name = 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:
|
||||
"""Return object's verbose_name"""
|
||||
return obj._meta.verbose_name
|
||||
|
@ -25,27 +29,21 @@ class MetaNameSerializer(Serializer):
|
|||
return obj._meta.verbose_name_plural
|
||||
|
||||
|
||||
class TypeCreateSerializer(Serializer):
|
||||
class TypeCreateSerializer(PassiveSerializer):
|
||||
"""Types of an object that can be created"""
|
||||
|
||||
name = CharField(required=True)
|
||||
description = 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:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CacheSerializer(Serializer):
|
||||
class CacheSerializer(PassiveSerializer):
|
||||
"""Generic cache stats for an object"""
|
||||
|
||||
count = IntegerField(read_only=True)
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
raise NotImplementedError
|
||||
class LinkSerializer(PassiveSerializer):
|
||||
"""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}
|
21
authentik/core/migrations/0018_auto_20210330_1345.py
Normal file
21
authentik/core/migrations/0018_auto_20210330_1345.py
Normal file
|
@ -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=["key"]),
|
||||
]
|
||||
permissions = (("view_token_key", "View token's key"),)
|
||||
|
||||
|
||||
class PropertyMapping(SerializerModel, ManagedModel):
|
||||
|
|
|
@ -17,7 +17,7 @@ def post_save_application(sender, instance, created: bool, **_):
|
|||
|
||||
if sender != Application:
|
||||
return
|
||||
if not created:
|
||||
if not created: # pragma: no cover
|
||||
return
|
||||
# Also delete user application cache
|
||||
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 typing import Optional
|
||||
|
||||
from django.db.models.base import Model
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -21,29 +21,17 @@ class UILoginButton:
|
|||
icon_url: Optional[str] = None
|
||||
|
||||
|
||||
class UILoginButtonSerializer(Serializer):
|
||||
class UILoginButtonSerializer(PassiveSerializer):
|
||||
"""Serializer for Login buttons of sources"""
|
||||
|
||||
name = CharField()
|
||||
url = CharField()
|
||||
icon_url = CharField(required=False)
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
return Model()
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
return Model()
|
||||
|
||||
|
||||
class UserSettingSerializer(Serializer):
|
||||
class UserSettingSerializer(PassiveSerializer):
|
||||
"""Serializer for User settings for stages and sources"""
|
||||
|
||||
object_uid = CharField()
|
||||
component = 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",
|
||||
),
|
||||
# User views
|
||||
path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"),
|
||||
path(
|
||||
"-/user/tokens/create/",
|
||||
user.TokenCreateView.as_view(),
|
||||
|
|
|
@ -1,53 +1,20 @@
|
|||
"""authentik core user views"""
|
||||
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.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.base import TemplateView
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
|
||||
from authentik.core.forms.token import UserTokenForm
|
||||
from authentik.core.forms.users import UserDetailForm
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
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(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
|
|
|
@ -2,15 +2,23 @@
|
|||
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.db.models import Model
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
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.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 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.events.models import Event, EventAction
|
||||
|
||||
|
@ -71,16 +79,20 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||
}
|
||||
|
||||
|
||||
class CertificateDataSerializer(Serializer):
|
||||
class CertificateDataSerializer(PassiveSerializer):
|
||||
"""Get CertificateKeyPair's data"""
|
||||
|
||||
data = 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 CertificateGenerationSerializer(PassiveSerializer):
|
||||
"""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):
|
||||
|
@ -89,6 +101,29 @@ class CertificateKeyPairViewSet(ModelViewSet):
|
|||
queryset = CertificateKeyPair.objects.all()
|
||||
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)})
|
||||
@action(detail=True)
|
||||
# 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 authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.forms import CertificateKeyPairForm
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
class TestCrypto(TestCase):
|
||||
"""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):
|
||||
"""Test API Validation"""
|
||||
keypair = CertificateKeyPair.objects.first()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import django_filters
|
||||
from django.db.models.aggregates import Count
|
||||
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 rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, DictField, IntegerField
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""NotificationTransport API Views"""
|
||||
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.fields import CharField, ListField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
|
@ -36,6 +36,7 @@ class NotificationTransportSerializer(ModelSerializer):
|
|||
"mode",
|
||||
"mode_verbose",
|
||||
"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, **_
|
||||
):
|
||||
"""Signal handler for all object's pre_delete"""
|
||||
if isinstance(instance, (Event, Notification, UserObjectPermission)):
|
||||
if isinstance(
|
||||
instance, (Event, Notification, UserObjectPermission)
|
||||
): # pragma: no cover
|
||||
return
|
||||
|
||||
EventNewThread(
|
||||
|
|
|
@ -3,11 +3,14 @@ from dataclasses import dataclass
|
|||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.http.response import JsonResponse
|
||||
from drf_yasg2 import openapi
|
||||
from drf_yasg2.utils import no_body, swagger_auto_schema
|
||||
from django.http.response import HttpResponseBadRequest, JsonResponse
|
||||
from django.urls import reverse
|
||||
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 rest_framework.decorators import action
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
|
@ -20,11 +23,15 @@ from rest_framework.viewsets import ModelViewSet
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
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.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.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()
|
||||
|
||||
|
@ -56,7 +63,7 @@ class FlowSerializer(ModelSerializer):
|
|||
|
||||
|
||||
class FlowDiagramSerializer(Serializer):
|
||||
"""response of the flow's /diagram/ action"""
|
||||
"""response of the flow's diagram action"""
|
||||
|
||||
diagram = CharField(read_only=True)
|
||||
|
||||
|
@ -88,14 +95,14 @@ class FlowViewSet(ModelViewSet):
|
|||
search_fields = ["name", "slug", "designation", "title"]
|
||||
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)})
|
||||
@action(detail=False)
|
||||
def cache_info(self, request: Request) -> Response:
|
||||
"""Info about cached flows"""
|
||||
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(
|
||||
request_body=no_body,
|
||||
responses={204: "Successfully cleared cache", 400: "Bad request"},
|
||||
|
@ -108,7 +115,61 @@ class FlowViewSet(ModelViewSet):
|
|||
LOGGER.debug("Cleared flow cache", keys=len(keys))
|
||||
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(
|
||||
responses={
|
||||
"200": openapi.Response(
|
||||
|
@ -194,3 +255,57 @@ class FlowViewSet(ModelViewSet):
|
|||
)
|
||||
diagram = "\n".join([str(x) for x in header + body + footer])
|
||||
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 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.decorators import action
|
||||
from rest_framework.request import Request
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.db.models.base import Model
|
||||
from django.http import JsonResponse
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -21,20 +21,14 @@ class ChallengeTypes(Enum):
|
|||
REDIRECT = "redirect"
|
||||
|
||||
|
||||
class ErrorDetailSerializer(Serializer):
|
||||
class ErrorDetailSerializer(PassiveSerializer):
|
||||
"""Serializer for rest_framework's error messages"""
|
||||
|
||||
string = CharField()
|
||||
code = CharField()
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
return Model()
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
return Model()
|
||||
|
||||
|
||||
class Challenge(Serializer):
|
||||
class Challenge(PassiveSerializer):
|
||||
"""Challenge that gets sent to the client based on which stage
|
||||
is currently active"""
|
||||
|
||||
|
@ -49,12 +43,6 @@ class Challenge(Serializer):
|
|||
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):
|
||||
"""Challenge type to redirect the client"""
|
||||
|
@ -81,20 +69,14 @@ class AccessDeniedChallenge(Challenge):
|
|||
error_message = CharField(required=False)
|
||||
|
||||
|
||||
class PermissionSerializer(Serializer):
|
||||
class PermissionSerializer(PassiveSerializer):
|
||||
"""Permission used for consent"""
|
||||
|
||||
name = CharField()
|
||||
id = CharField()
|
||||
|
||||
def create(self, validated_data: dict) -> Model:
|
||||
return Model()
|
||||
|
||||
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||
return Model()
|
||||
|
||||
|
||||
class ChallengeResponse(Serializer):
|
||||
class ChallengeResponse(PassiveSerializer):
|
||||
"""Base class for all challenge responses"""
|
||||
|
||||
stage: Optional["StageView"]
|
||||
|
@ -103,12 +85,6 @@ class ChallengeResponse(Serializer):
|
|||
self.stage = kwargs.pop("stage", None)
|
||||
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):
|
||||
"""Subclass of JsonResponse that uses the `DataclassEncoder`"""
|
||||
|
|
|
@ -1,35 +1,10 @@
|
|||
"""Flow and Stage 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.transfer.importer import FlowImporter
|
||||
from authentik.flows.models import FlowStageBinding, Stage
|
||||
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):
|
||||
"""FlowStageBinding Form"""
|
||||
|
||||
|
@ -56,20 +31,3 @@ class FlowStageBindingForm(forms.ModelForm):
|
|||
widgets = {
|
||||
"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.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.views.generic import View
|
||||
from drf_yasg2 import openapi
|
||||
from drf_yasg2.utils import no_body, swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import no_body, swagger_auto_schema
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.views import APIView
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
"""Managed Object models"""
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
|
@ -22,10 +21,6 @@ class ManagedModel(models.Model):
|
|||
unique=True,
|
||||
)
|
||||
|
||||
def managed_objects(self) -> QuerySet:
|
||||
"""Get all objects which are managed"""
|
||||
return self.objects.exclude(managed__isnull=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
"""Outpost API Views"""
|
||||
from dataclasses import asdict
|
||||
|
||||
from django.db.models.base import Model
|
||||
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.fields import BooleanField, CharField, SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
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 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.utils.reflection import all_subclasses
|
||||
from authentik.outposts.models import (
|
||||
|
@ -43,18 +46,12 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
|||
]
|
||||
|
||||
|
||||
class ServiceConnectionStateSerializer(Serializer):
|
||||
class ServiceConnectionStateSerializer(PassiveSerializer):
|
||||
"""Serializer for Service connection state"""
|
||||
|
||||
healthy = BooleanField(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):
|
||||
"""ServiceConnection Viewset"""
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
"""Outpost API Views"""
|
||||
from django.db.models import Model
|
||||
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.fields import BooleanField, CharField, DateTimeField
|
||||
from rest_framework.request import Request
|
||||
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 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):
|
||||
|
@ -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"""
|
||||
|
||||
last_seen = DateTimeField(read_only=True)
|
||||
|
@ -40,12 +46,6 @@ class OutpostHealthSerializer(Serializer):
|
|||
version_should = CharField(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):
|
||||
"""Outpost Viewset"""
|
||||
|
@ -78,3 +78,10 @@ class OutpostViewSet(ModelViewSet):
|
|||
}
|
||||
)
|
||||
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"
|
||||
|
||||
|
||||
def default_outpost_config():
|
||||
def default_outpost_config(host: Optional[str] = None):
|
||||
"""Get default outpost config"""
|
||||
return asdict(OutpostConfig(authentik_host=""))
|
||||
return asdict(OutpostConfig(authentik_host=host or ""))
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
0
authentik/policies/api/__init__.py
Normal file
0
authentik/policies/api/__init__.py
Normal file
80
authentik/policies/api/bindings.py
Normal file
80
authentik/policies/api/bindings.py
Normal file
|
@ -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"]
|
20
authentik/policies/api/exec.py
Normal file
20
authentik/policies/api/exec.py
Normal file
|
@ -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"""
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
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.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
ModelSerializer,
|
||||
PrimaryKeyRelatedField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
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.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()
|
||||
|
||||
|
||||
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):
|
||||
"""Policy Serializer"""
|
||||
|
||||
|
@ -168,39 +137,32 @@ class PolicyViewSet(
|
|||
cache.delete_many(keys)
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
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,
|
||||
@permission_required("authentik_policies.view_policy")
|
||||
@swagger_auto_schema(
|
||||
request_body=PolicyTestSerializer(),
|
||||
responses={200: PolicyTestResultSerializer()},
|
||||
)
|
||||
@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(
|
||||
pk=test_params["user"]
|
||||
)
|
||||
if not users.exists():
|
||||
raise PermissionDenied()
|
||||
|
||||
model = PolicyBinding
|
||||
fields = [
|
||||
"pk",
|
||||
"policy",
|
||||
"group",
|
||||
"user",
|
||||
"target",
|
||||
"enabled",
|
||||
"order",
|
||||
"timeout",
|
||||
]
|
||||
depth = 2
|
||||
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", {})
|
||||
|
||||
|
||||
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"]
|
||||
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"""
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Event Matcher Policy API"""
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Password Expiry Policy API Views"""
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Expression Policy API"""
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Source API Views"""
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ class PolicyBinding(SerializerModel):
|
|||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.policies.api import PolicyBindingSerializer
|
||||
from authentik.policies.api.bindings import PolicyBindingSerializer
|
||||
|
||||
return PolicyBindingSerializer
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Password Policy API Views"""
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Source API Views"""
|
||||
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 (
|
||||
IPReputation,
|
||||
ReputationPolicy,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""OAuth2Provider API Views"""
|
||||
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.fields import ReadOnlyField
|
||||
from rest_framework.generics import get_object_or_404
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""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.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""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.fields import ReadOnlyField
|
||||
from rest_framework.request import Request
|
||||
|
|
|
@ -126,7 +126,7 @@ INSTALLED_APPS = [
|
|||
"authentik.stages.user_write.apps.AuthentikStageUserWriteConfig",
|
||||
"rest_framework",
|
||||
"django_filters",
|
||||
"drf_yasg2",
|
||||
"drf_yasg",
|
||||
"guardian",
|
||||
"django_prometheus",
|
||||
"channels",
|
||||
|
@ -137,6 +137,7 @@ INSTALLED_APPS = [
|
|||
GUARDIAN_MONKEY_PATCH = False
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
"DEFAULT_AUTO_SCHEMA_CLASS": "authentik.api.schema.ErrorResponseAutoSchema",
|
||||
"DEFAULT_INFO": "authentik.api.v2.urls.info",
|
||||
"DEFAULT_PAGINATOR_INSPECTORS": [
|
||||
"authentik.api.pagination_schema.PaginationInspector",
|
||||
|
@ -437,7 +438,7 @@ _LOGGING_HANDLER_MAP = {
|
|||
"kubernetes": "INFO",
|
||||
"asyncio": "WARNING",
|
||||
"aioredis": "WARNING",
|
||||
"drf_yasg2.utils": "WARNING",
|
||||
"drf_yasg.utils": "WARNING",
|
||||
}
|
||||
for handler_name, level in _LOGGING_HANDLER_MAP.items():
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
|
|
@ -3,17 +3,16 @@ from datetime import datetime
|
|||
from time import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models.base import Model
|
||||
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.fields import DateTimeField
|
||||
from rest_framework.request import Request
|
||||
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 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
|
||||
|
||||
|
||||
|
@ -44,17 +43,11 @@ class LDAPSourceSerializer(SourceSerializer):
|
|||
extra_kwargs = {"bind_password": {"write_only": True}}
|
||||
|
||||
|
||||
class LDAPSourceSyncStatusSerializer(Serializer):
|
||||
class LDAPSourceSyncStatusSerializer(PassiveSerializer):
|
||||
"""LDAP Sync status"""
|
||||
|
||||
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):
|
||||
"""LDAP Source Viewset"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""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.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
"""Validation stage challenge checking"""
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
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_totp.models import TOTPDevice
|
||||
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.webauthn import (
|
||||
AuthenticationRejectedException,
|
||||
|
@ -15,24 +14,19 @@ from webauthn.webauthn import (
|
|||
WebAuthnUserDataMissing,
|
||||
)
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
|
||||
|
||||
|
||||
class DeviceChallenge(Serializer):
|
||||
class DeviceChallenge(PassiveSerializer):
|
||||
"""Single device challenge"""
|
||||
|
||||
device_class = CharField()
|
||||
device_uid = CharField()
|
||||
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:
|
||||
"""Generate challenge for a single device"""
|
||||
|
|
|
@ -8,4 +8,3 @@ class AuthentikStageAuthenticatorWebAuthnConfig(AppConfig):
|
|||
name = "authentik.stages.authenticator_webauthn"
|
||||
label = "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 typing import Any, Callable, Iterator
|
||||
|
||||
from django.db.models.base import Model
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
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 authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
|
@ -26,7 +26,7 @@ LOGGER = get_logger()
|
|||
PLAN_CONTEXT_PROMPT = "prompt_data"
|
||||
|
||||
|
||||
class PromptSerializer(Serializer):
|
||||
class PromptSerializer(PassiveSerializer):
|
||||
"""Serializer for a single Prompt field"""
|
||||
|
||||
field_key = CharField()
|
||||
|
@ -36,12 +36,6 @@ class PromptSerializer(Serializer):
|
|||
placeholder = CharField()
|
||||
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):
|
||||
"""Initial challenge being sent, define fields"""
|
||||
|
|
|
@ -285,9 +285,9 @@ stages:
|
|||
inputs:
|
||||
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
|
||||
sudo chmod 777 -R api/
|
||||
sudo chmod 777 -R web/api/
|
||||
cd web
|
||||
sudo chmod 777 -R api/
|
||||
cd api && npm i && cd ..
|
||||
npm i
|
||||
npm run build
|
||||
- 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.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")
|
||||
self.assertEqual(user.username, "foo")
|
||||
|
@ -198,7 +198,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||
)
|
||||
|
||||
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"))
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ from selenium.webdriver.support.wait import WebDriverWait
|
|||
from structlog.stdlib import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.generators import (
|
||||
generate_client_id,
|
||||
|
@ -160,19 +161,9 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||
|
||||
# Wait until we've logged in
|
||||
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.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",
|
||||
)
|
||||
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
||||
|
||||
@retry()
|
||||
@apply_migration("authentik_core", "0003_default_user")
|
||||
|
@ -255,19 +246,9 @@ class TestSourceOAuth2(SeleniumTestCase):
|
|||
|
||||
# Wait until we've logged in
|
||||
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.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",
|
||||
)
|
||||
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
|
@ -359,17 +340,8 @@ class TestSourceOAuth1(SeleniumTestCase):
|
|||
sleep(2)
|
||||
# Wait until we've logged in
|
||||
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.driver.find_element(By.ID, "id_username").get_attribute("value"),
|
||||
"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",
|
||||
self.assert_user(
|
||||
User(username="example-user", name="test name", email="foo@example.com")
|
||||
)
|
||||
|
|
|
@ -5,12 +5,14 @@ from typing import Any, Optional
|
|||
from unittest.case import skipUnless
|
||||
|
||||
from docker.types import Healthcheck
|
||||
from guardian.utils import get_anonymous_user
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||
|
@ -153,11 +155,12 @@ class TestSourceSAML(SeleniumTestCase):
|
|||
|
||||
# Wait until we're logged in
|
||||
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.assertNotEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
.exclude(pk=get_anonymous_user().pk)
|
||||
.first()
|
||||
)
|
||||
|
||||
@retry()
|
||||
|
@ -233,11 +236,12 @@ class TestSourceSAML(SeleniumTestCase):
|
|||
|
||||
# Wait until we're logged in
|
||||
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.assertNotEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
.exclude(pk=get_anonymous_user().pk)
|
||||
.first()
|
||||
)
|
||||
|
||||
@retry()
|
||||
|
@ -300,9 +304,10 @@ class TestSourceSAML(SeleniumTestCase):
|
|||
|
||||
# Wait until we're logged in
|
||||
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.assertNotEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
.exclude(pk=get_anonymous_user().pk)
|
||||
.first()
|
||||
)
|
||||
|
|
143
web/package-lock.json
generated
143
web/package-lock.json
generated
|
@ -133,6 +133,139 @@
|
|||
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz",
|
||||
"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": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.1.tgz",
|
||||
|
@ -547,6 +680,11 @@
|
|||
"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": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
|
@ -3948,6 +4086,11 @@
|
|||
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.3",
|
||||
"@patternfly/patternfly": "^4.90.5",
|
||||
"@polymer/iron-form": "^3.0.1",
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
"@sentry/browser": "^6.2.3",
|
||||
"@sentry/tracing": "^6.2.3",
|
||||
"@types/chart.js": "^2.9.31",
|
||||
|
@ -31,7 +33,8 @@
|
|||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
"rollup-plugin-external-globals": "^0.6.1",
|
||||
"tslib": "^2.1.0",
|
||||
"webcomponent-qr-code": "^1.0.5"
|
||||
"webcomponent-qr-code": "^1.0.5",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 {
|
||||
next?: number;
|
||||
previous?: number;
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
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 {
|
||||
return `/administration/policies/${rest}`;
|
||||
}
|
||||
|
@ -24,18 +16,10 @@ export class AdminURLManager {
|
|||
return `/administration/property-mappings/${rest}`;
|
||||
}
|
||||
|
||||
static outposts(rest: string): string {
|
||||
return `/administration/outposts/${rest}`;
|
||||
}
|
||||
|
||||
static outpostServiceConnections(rest: string): string {
|
||||
return `/administration/outpost_service_connections/${rest}`;
|
||||
}
|
||||
|
||||
static flows(rest: string): string {
|
||||
return `/administration/flows/${rest}`;
|
||||
}
|
||||
|
||||
static stages(rest: string): string {
|
||||
return `/administration/stages/${rest}`;
|
||||
}
|
||||
|
@ -60,21 +44,6 @@ export class AdminURLManager {
|
|||
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 {
|
||||
|
@ -105,6 +74,10 @@ export class AppURLManager {
|
|||
|
||||
export class FlowURLManager {
|
||||
|
||||
static defaultUnenrollment(): string {
|
||||
return "/flows/-/default/unenrollment/";
|
||||
}
|
||||
|
||||
static configure(stageUuid: string, rest: string): string {
|
||||
return `/flows/-/configure/${stageUuid}/${rest}`;
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ body {
|
|||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--ak-accent: #fd4b2d;
|
||||
--ak-dark-foreground: #fafafa;
|
||||
--ak-dark-foreground-darker: #bebebe;
|
||||
--ak-dark-foreground-link: #5a5cb9;
|
||||
|
@ -100,6 +101,15 @@ body {
|
|||
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
|
||||
--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 */
|
||||
.pf-c-page {
|
||||
--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_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
|
||||
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/addon/display/autorefresh";
|
||||
|
@ -6,6 +7,9 @@ import "codemirror/mode/xml/xml.js";
|
|||
import "codemirror/mode/yaml/yaml.js";
|
||||
import "codemirror/mode/javascript/javascript.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")
|
||||
export class CodeMirrorTextarea extends LitElement {
|
||||
|
@ -15,14 +19,20 @@ export class CodeMirrorTextarea extends LitElement {
|
|||
@property()
|
||||
mode = "yaml";
|
||||
|
||||
@property()
|
||||
name?: string;
|
||||
|
||||
@property()
|
||||
value?: string;
|
||||
|
||||
editor?: CodeMirror.EditorFromTextArea;
|
||||
|
||||
createRenderRoot() : ShadowRoot | Element {
|
||||
return this;
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFForm, CodeMirrorStyle, CodeMirrorTheme];
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
const textarea = this.querySelector("textarea");
|
||||
const textarea = this.shadowRoot?.querySelector("textarea");
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
@ -37,4 +47,8 @@ export class CodeMirrorTextarea extends LitElement {
|
|||
this.editor?.save();
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<textarea class="pf-c-form-control" name=${ifDefined(this.name)}>${this.value || ""}</textarea>`;
|
||||
}
|
||||
}
|
||||
|
|
37
web/src/elements/Divider.ts
Normal file
37
web/src/elements/Divider.ts
Normal file
|
@ -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.apiRequest().then(() => {
|
||||
this.setDone(SUCCESS_CLASS);
|
||||
})
|
||||
.catch((e: Error | Response) => {
|
||||
}).catch((e: Error | Response) => {
|
||||
if (e instanceof Error) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue