Merge pull request #664 from BeryJu/new-forms

web: New forms
This commit is contained in:
Jens L 2021-03-30 16:32:50 +02:00 committed by GitHub
commit 1f89b94f66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
146 changed files with 6703 additions and 2341 deletions

View File

@ -40,7 +40,7 @@ RUN apt-get update && \
chown authentik:authentik /backups chown authentik:authentik /backups
COPY ./authentik/ /authentik COPY ./authentik/ /authentik
COPY ./pytest.ini / COPY ./pyproject.toml /
COPY ./xml /xml COPY ./xml /xml
COPY ./manage.py / COPY ./manage.py /
COPY ./lifecycle/ /lifecycle COPY ./lifecycle/ /lifecycle

View File

@ -6,7 +6,7 @@ test-integration:
coverage run manage.py test -v 3 tests/integration coverage run manage.py test -v 3 tests/integration
test-e2e: test-e2e:
coverage run manage.py test -v 3 tests/e2e coverage run manage.py test --failfast -v 3 tests/e2e
coverage: coverage:
coverage run manage.py test -v 3 authentik coverage run manage.py test -v 3 authentik

View File

@ -22,7 +22,7 @@ django-storages = "*"
djangorestframework = "*" djangorestframework = "*"
djangorestframework-guardian = "*" djangorestframework-guardian = "*"
docker = "*" docker = "*"
drf_yasg2 = "*" drf_yasg = "*"
facebook-sdk = "*" facebook-sdk = "*"
geoip2 = "*" geoip2 = "*"
gunicorn = "*" gunicorn = "*"

10
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "5fce5772178e4bc782d7112fab658f5bbb21abb77bb93fc3c0a66e9db3a23a37" "sha256": "a9d504f00ee8820017f26a4fda2938de456cb72b4bc2f8735fc8c6a6c615d46a"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -416,13 +416,13 @@
"index": "pypi", "index": "pypi",
"version": "==4.4.4" "version": "==4.4.4"
}, },
"drf-yasg2": { "drf-yasg": {
"hashes": [ "hashes": [
"sha256:7037a8041eb5d1073fa504a284fc889685f93d0bfd008a963db1b366db786734", "sha256:8b72e5b1875931a8d11af407be3a9a5ba8776541492947a0df5bafda6b7f8267",
"sha256:75e661ca5cf15eb44fcfab408c7b864f87c20794f564aa08b3a31817a857f19d" "sha256:d50f197c7f02545d0b736df88c6d5cf874f8fea2507ad85ad7de6ae5bf2d9e5a"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.19.4" "version": "==1.20.0"
}, },
"facebook-sdk": { "facebook-sdk": {
"hashes": [ "hashes": [

View File

@ -3,18 +3,18 @@ import time
from collections import Counter from collections import Counter
from datetime import timedelta from datetime import timedelta
from django.db.models import Count, ExpressionWrapper, F, Model from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour from django.db.models.functions import ExtractHour
from django.utils.timezone import now from django.utils.timezone import now
from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
from rest_framework.fields import IntegerField, SerializerMethodField from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -45,20 +45,14 @@ def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
return results return results
class CoordinateSerializer(Serializer): class CoordinateSerializer(PassiveSerializer):
"""Coordinates for diagrams""" """Coordinates for diagrams"""
x_cord = IntegerField(read_only=True) x_cord = IntegerField(read_only=True)
y_cord = IntegerField(read_only=True) y_cord = IntegerField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model: class LoginMetricsSerializer(PassiveSerializer):
raise NotImplementedError
class LoginMetricsSerializer(Serializer):
"""Login Metrics per 1h""" """Login Metrics per 1h"""
logins_per_1h = SerializerMethodField() logins_per_1h = SerializerMethodField()
@ -74,12 +68,6 @@ class LoginMetricsSerializer(Serializer):
"""Get failed logins per hour for the last 24 hours""" """Get failed logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN_FAILED) return get_events_per_1h(action=EventAction.LOGIN_FAILED)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class AdministrationMetricsViewSet(ViewSet): class AdministrationMetricsViewSet(ViewSet):
"""Login Metrics per 1h""" """Login Metrics per 1h"""

View File

@ -2,22 +2,21 @@
from importlib import import_module from importlib import import_module
from django.contrib import messages from django.contrib import messages
from django.db.models import Model
from django.http.response import Http404 from django.http.response import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from authentik.core.api.utils import PassiveSerializer
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
class TaskSerializer(Serializer): class TaskSerializer(PassiveSerializer):
"""Serialize TaskInfo and TaskResult""" """Serialize TaskInfo and TaskResult"""
task_name = CharField() task_name = CharField()
@ -30,12 +29,6 @@ class TaskSerializer(Serializer):
) )
messages = ListField(source="result.messages") messages = ListField(source="result.messages")
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class TaskViewSet(ViewSet): class TaskViewSet(ViewSet):
"""Read-only view set that returns all background tasks""" """Read-only view set that returns all background tasks"""

View File

@ -2,22 +2,21 @@
from os import environ from os import environ
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Model from drf_yasg.utils import swagger_auto_schema
from drf_yasg2.utils import swagger_auto_schema
from packaging.version import parse from packaging.version import parse
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik import ENV_GIT_HASH_KEY, __version__ from authentik import ENV_GIT_HASH_KEY, __version__
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from authentik.core.api.utils import PassiveSerializer
class VersionSerializer(Serializer): class VersionSerializer(PassiveSerializer):
"""Get running and latest version.""" """Get running and latest version."""
version_current = SerializerMethodField() version_current = SerializerMethodField()
@ -47,12 +46,6 @@ class VersionSerializer(Serializer):
self.get_version_latest(instance) self.get_version_latest(instance)
) )
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class VersionViewSet(ListModelMixin, GenericViewSet): class VersionViewSet(ListModelMixin, GenericViewSet):
"""Get running and latest version.""" """Get running and latest version."""

View File

@ -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,
}

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -2,13 +2,6 @@
from django.urls import path from django.urls import path
from authentik.admin.views import ( from authentik.admin.views import (
applications,
certificate_key_pair,
events_notifications_rules,
events_notifications_transports,
flows,
groups,
outposts,
outposts_service_connections, outposts_service_connections,
policies, policies,
policies_bindings, policies_bindings,
@ -19,22 +12,10 @@ from authentik.admin.views import (
stages_bindings, stages_bindings,
stages_invitations, stages_invitations,
stages_prompts, stages_prompts,
users,
) )
from authentik.providers.saml.views.metadata import MetadataImportView from authentik.providers.saml.views.metadata import MetadataImportView
urlpatterns = [ urlpatterns = [
# Applications
path(
"applications/create/",
applications.ApplicationCreateView.as_view(),
name="application-create",
),
path(
"applications/<uuid:pk>/update/",
applications.ApplicationUpdateView.as_view(),
name="application-update",
),
# Sources # Sources
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"), path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
path( path(
@ -116,27 +97,6 @@ urlpatterns = [
stages_invitations.InvitationCreateView.as_view(), stages_invitations.InvitationCreateView.as_view(),
name="stage-invitation-create", name="stage-invitation-create",
), ),
# Flows
path(
"flows/create/",
flows.FlowCreateView.as_view(),
name="flow-create",
),
path(
"flows/import/",
flows.FlowImportView.as_view(),
name="flow-import",
),
path(
"flows/<uuid:pk>/update/",
flows.FlowUpdateView.as_view(),
name="flow-update",
),
path(
"flows/<uuid:pk>/execute/",
flows.FlowDebugExecuteView.as_view(),
name="flow-execute",
),
# Property Mappings # Property Mappings
path( path(
"property-mappings/create/", "property-mappings/create/",
@ -153,48 +113,6 @@ urlpatterns = [
property_mappings.PropertyMappingTestView.as_view(), property_mappings.PropertyMappingTestView.as_view(),
name="property-mapping-test", name="property-mapping-test",
), ),
# Users
path("users/create/", users.UserCreateView.as_view(), name="user-create"),
path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"),
path(
"users/<int:pk>/reset/",
users.UserPasswordResetView.as_view(),
name="user-password-reset",
),
# Groups
path("groups/create/", groups.GroupCreateView.as_view(), name="group-create"),
path(
"groups/<uuid:pk>/update/",
groups.GroupUpdateView.as_view(),
name="group-update",
),
# Certificate-Key Pairs
path(
"crypto/certificates/create/",
certificate_key_pair.CertificateKeyPairCreateView.as_view(),
name="certificatekeypair-create",
),
path(
"crypto/certificates/generate/",
certificate_key_pair.CertificateKeyPairGenerateView.as_view(),
name="certificatekeypair-generate",
),
path(
"crypto/certificates/<uuid:pk>/update/",
certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
name="certificatekeypair-update",
),
# Outposts
path(
"outposts/create/",
outposts.OutpostCreateView.as_view(),
name="outpost-create",
),
path(
"outposts/<uuid:pk>/update/",
outposts.OutpostUpdateView.as_view(),
name="outpost-update",
),
# Outpost Service Connections # Outpost Service Connections
path( path(
"outpost_service_connections/create/", "outpost_service_connections/create/",
@ -206,26 +124,4 @@ urlpatterns = [
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(), outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
name="outpost-service-connection-update", name="outpost-service-connection-update",
), ),
# Event Notification Transpots
path(
"events/transports/create/",
events_notifications_transports.NotificationTransportCreateView.as_view(),
name="notification-transport-create",
),
path(
"events/transports/<uuid:pk>/update/",
events_notifications_transports.NotificationTransportUpdateView.as_view(),
name="notification-transport-update",
),
# Event Notification Rules
path(
"events/rules/create/",
events_notifications_rules.NotificationRuleCreateView.as_view(),
name="notification-rule-create",
),
path(
"events/rules/<uuid:pk>/update/",
events_notifications_rules.NotificationRuleUpdateView.as_view(),
name="notification-rule-update",
),
] ]

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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)

View File

@ -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")

View File

@ -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")

View File

@ -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("/")

View File

@ -1,26 +1,13 @@
"""authentik admin util views""" """authentik admin util views"""
from typing import Any from typing import Any
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404 from django.http import Http404
from django.urls import reverse_lazy from django.views.generic import UpdateView
from django.views.generic import DeleteView, UpdateView
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
class DeleteMessageView(SuccessMessageMixin, DeleteView):
"""DeleteView which shows `self.success_message` on successful deletion"""
success_url = reverse_lazy("authentik_core:if-admin")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class InheritanceCreateView(CreateAssignPermView): class InheritanceCreateView(CreateAssignPermView):
"""CreateView for objects using InheritanceManager""" """CreateView for objects using InheritanceManager"""

View File

@ -1,13 +1,15 @@
"""API Decorators""" """API Decorators"""
from functools import wraps from functools import wraps
from typing import Callable from typing import Callable, Optional
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
def permission_required(perm: str, *other_perms: str): def permission_required(
perm: Optional[str] = None, other_perms: Optional[list[str]] = None
):
"""Check permissions for a single custom action""" """Check permissions for a single custom action"""
def wrapper_outter(func: Callable): def wrapper_outter(func: Callable):
@ -15,12 +17,14 @@ def permission_required(perm: str, *other_perms: str):
@wraps(func) @wraps(func)
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response: def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
obj = self.get_object() if perm:
if not request.user.has_perm(perm, obj): obj = self.get_object()
return self.permission_denied(request) if not request.user.has_perm(perm, obj):
for other_perm in other_perms:
if not request.user.has_perm(other_perm):
return self.permission_denied(request) 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 func(self, request, *args, **kwargs)
return wrapper return wrapper

View File

@ -1,8 +1,8 @@
"""Swagger Pagination Schema class""" """Swagger Pagination Schema class"""
from typing import OrderedDict from typing import OrderedDict
from drf_yasg2 import openapi from drf_yasg import openapi
from drf_yasg2.inspectors import PaginatorInspector from drf_yasg.inspectors import PaginatorInspector
class PaginationInspector(PaginatorInspector): class PaginationInspector(PaginatorInspector):

102
authentik/api/schema.py Normal file
View 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

View File

View 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()))

View File

@ -1,46 +1,33 @@
"""core Configs API""" """core Configs API"""
from django.db.models import Model from drf_yasg.utils import swagger_auto_schema
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.fields import BooleanField, CharField, ListField from rest_framework.fields import BooleanField, CharField, ListField
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from authentik.core.api.utils import PassiveSerializer
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
class LinkSerializer(Serializer): class FooterLinkSerializer(PassiveSerializer):
"""Links returned in Config API""" """Links returned in Config API"""
href = CharField(read_only=True) href = CharField(read_only=True)
name = CharField(read_only=True) name = CharField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model: class ConfigSerializer(PassiveSerializer):
raise NotImplementedError
class ConfigSerializer(Serializer):
"""Serialize authentik Config into DRF Object""" """Serialize authentik Config into DRF Object"""
branding_logo = CharField(read_only=True) branding_logo = CharField(read_only=True)
branding_title = CharField(read_only=True) branding_title = CharField(read_only=True)
ui_footer_links = ListField(child=LinkSerializer(), read_only=True) ui_footer_links = ListField(child=FooterLinkSerializer(), read_only=True)
error_reporting_enabled = BooleanField(read_only=True) error_reporting_enabled = BooleanField(read_only=True)
error_reporting_environment = CharField(read_only=True) error_reporting_environment = CharField(read_only=True)
error_reporting_send_pii = BooleanField(read_only=True) error_reporting_send_pii = BooleanField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class ConfigsViewSet(ViewSet): class ConfigsViewSet(ViewSet):
"""Read-only view set that returns the current session's Configs""" """Read-only view set that returns the current session's Configs"""

View File

@ -1,7 +1,7 @@
"""api v2 urls""" """api v2 urls"""
from django.urls import path, re_path from django.urls import path, re_path
from drf_yasg2 import openapi from drf_yasg import openapi
from drf_yasg2.views import get_schema_view from drf_yasg.views import get_schema_view
from rest_framework import routers from rest_framework import routers
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
@ -33,7 +33,8 @@ from authentik.outposts.api.outpost_service_connections import (
ServiceConnectionViewSet, ServiceConnectionViewSet,
) )
from authentik.outposts.api.outposts import OutpostViewSet from authentik.outposts.api.outposts import OutpostViewSet
from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet from authentik.policies.api.bindings import PolicyBindingViewSet
from authentik.policies.api.policies import PolicyViewSet
from authentik.policies.dummy.api import DummyPolicyViewSet from authentik.policies.dummy.api import DummyPolicyViewSet
from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
@ -189,7 +190,7 @@ router.register("policies/dummy", DummyPolicyViewSet)
info = openapi.Info( info = openapi.Info(
title="authentik API", title="authentik API",
default_version="v2", default_version="v2beta",
contact=openapi.Contact(email="hello@beryju.org"), contact=openapi.Contact(email="hello@beryju.org"),
license=openapi.License( license=openapi.License(
name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE" name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE"

View File

@ -1,9 +1,12 @@
"""Application API Views""" """Application API Views"""
from django.core.cache import cache from django.core.cache import cache
from django.db.models import QuerySet from django.db.models import QuerySet
from drf_yasg2.utils import swagger_auto_schema from django.http.response import HttpResponseBadRequest
from drf_yasg import openapi
from drf_yasg.utils import no_body, swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
@ -49,7 +52,6 @@ class ApplicationSerializer(ModelSerializer):
"meta_icon", "meta_icon",
"meta_description", "meta_description",
"meta_publisher", "meta_publisher",
"policies",
] ]
@ -108,8 +110,33 @@ class ApplicationViewSet(ModelViewSet):
serializer = self.get_serializer(allowed_applications, many=True) serializer = self.get_serializer(allowed_applications, many=True)
return self.get_paginated_response(serializer.data) return self.get_paginated_response(serializer.data)
@permission_required("authentik_core.change_application")
@swagger_auto_schema(
request_body=no_body,
manual_parameters=[
openapi.Parameter(
name="file",
in_=openapi.IN_FORM,
type=openapi.TYPE_FILE,
required=True,
)
],
responses={200: "Success"},
)
@action(detail=True, methods=["POST"], parser_classes=(MultiPartParser,))
# pylint: disable=unused-argument
def set_icon(self, request: Request, slug: str):
"""Set application icon"""
app: Application = self.get_object()
icon = request.FILES.get("file", None)
if not icon:
return HttpResponseBadRequest()
app.meta_icon = icon
app.save()
return Response({})
@permission_required( @permission_required(
"authentik_core.view_application", "authentik_events.view_event" "authentik_core.view_application", ["authentik_events.view_event"]
) )
@swagger_auto_schema(responses={200: CoordinateSerializer(many=True)}) @swagger_auto_schema(responses={200: CoordinateSerializer(many=True)})
@action(detail=True) @action(detail=True)

View File

@ -1,6 +1,6 @@
"""PropertyMapping API Views""" """PropertyMapping API Views"""
from django.urls import reverse from django.urls import reverse
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request

View File

@ -1,7 +1,7 @@
"""Provider API Views""" """Provider API Views"""
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField from rest_framework.fields import ReadOnlyField
from rest_framework.request import Request from rest_framework.request import Request

View File

@ -2,7 +2,7 @@
from typing import Iterable from typing import Iterable
from django.urls import reverse from django.urls import reverse
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request

View File

@ -1,15 +1,16 @@
"""Tokens API Viewset""" """Tokens API Viewset"""
from django.db.models.base import Model
from django.http.response import Http404 from django.http.response import Http404
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField from rest_framework.fields import CharField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Token from authentik.core.models import Token
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -34,17 +35,11 @@ class TokenSerializer(ModelSerializer):
depth = 2 depth = 2
class TokenViewSerializer(Serializer): class TokenViewSerializer(PassiveSerializer):
"""Show token's current key""" """Show token's current key"""
key = CharField(read_only=True) key = CharField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class TokenViewSet(ModelViewSet): class TokenViewSet(ModelViewSet):
"""Token Viewset""" """Token Viewset"""
@ -66,6 +61,7 @@ class TokenViewSet(ModelViewSet):
] ]
ordering = ["expires"] ordering = ["expires"]
@permission_required("authentik_core.view_token_key")
@swagger_auto_schema(responses={200: TokenViewSerializer(many=False)}) @swagger_auto_schema(responses={200: TokenViewSerializer(many=False)})
@action(detail=True) @action(detail=True)
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -1,18 +1,18 @@
"""User API Views""" """User API Views"""
from django.db.models.base import Model
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.http import urlencode from django.utils.http import urlencode
from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
from guardian.utils import get_anonymous_user from guardian.utils import get_anonymous_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, SerializerMethodField from rest_framework.fields import CharField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import BooleanField, ModelSerializer, Serializer from rest_framework.serializers import BooleanField, ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.utils import LinkSerializer, PassiveSerializer
from authentik.core.middleware import ( from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_ORIGINAL_USER,
SESSION_IMPERSONATE_USER, SESSION_IMPERSONATE_USER,
@ -43,33 +43,15 @@ class UserSerializer(ModelSerializer):
] ]
class SessionUserSerializer(Serializer): class SessionUserSerializer(PassiveSerializer):
"""Response for the /user/me endpoint, returns the currently active user (as `user` property) """Response for the /user/me endpoint, returns the currently active user (as `user` property)
and, if this user is being impersonated, the original user in the `original` property.""" and, if this user is being impersonated, the original user in the `original` property."""
user = UserSerializer() user = UserSerializer()
original = UserSerializer(required=False) original = UserSerializer(required=False)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model: class UserMetricsSerializer(PassiveSerializer):
raise NotImplementedError
class UserRecoverySerializer(Serializer):
"""Recovery link for a user to reset their password"""
link = CharField()
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class UserMetricsSerializer(Serializer):
"""User Metrics""" """User Metrics"""
logins_per_1h = SerializerMethodField() logins_per_1h = SerializerMethodField()
@ -98,12 +80,6 @@ class UserMetricsSerializer(Serializer):
action=EventAction.AUTHORIZE_APPLICATION, user__pk=request.user.pk action=EventAction.AUTHORIZE_APPLICATION, user__pk=request.user.pk
) )
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class UserViewSet(ModelViewSet): class UserViewSet(ModelViewSet):
"""User Viewset""" """User Viewset"""
@ -131,7 +107,7 @@ class UserViewSet(ModelViewSet):
serializer.is_valid() serializer.is_valid()
return Response(serializer.data) return Response(serializer.data)
@permission_required("authentik_core.view_user", "authentik_events.view_event") @permission_required("authentik_core.view_user", ["authentik_events.view_event"])
@swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)}) @swagger_auto_schema(responses={200: UserMetricsSerializer(many=False)})
@action(detail=False) @action(detail=False)
def metrics(self, request: Request) -> Response: def metrics(self, request: Request) -> Response:
@ -142,7 +118,7 @@ class UserViewSet(ModelViewSet):
@permission_required("authentik_core.reset_user_password") @permission_required("authentik_core.reset_user_password")
@swagger_auto_schema( @swagger_auto_schema(
responses={"200": UserRecoverySerializer(many=False)}, responses={"200": LinkSerializer(many=False)},
) )
@action(detail=True) @action(detail=True)
# pylint: disable=invalid-name, unused-argument # pylint: disable=invalid-name, unused-argument

View File

@ -4,18 +4,22 @@ from rest_framework.fields import CharField, IntegerField
from rest_framework.serializers import Serializer, SerializerMethodField from rest_framework.serializers import Serializer, SerializerMethodField
class MetaNameSerializer(Serializer): class PassiveSerializer(Serializer):
"""Base serializer class which doesn't implement create/update methods"""
def create(self, validated_data: dict) -> Model:
return Model()
def update(self, instance: Model, validated_data: dict) -> Model:
return Model()
class MetaNameSerializer(PassiveSerializer):
"""Add verbose names to response""" """Add verbose names to response"""
verbose_name = SerializerMethodField() verbose_name = SerializerMethodField()
verbose_name_plural = SerializerMethodField() verbose_name_plural = SerializerMethodField()
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
def get_verbose_name(self, obj: Model) -> str: def get_verbose_name(self, obj: Model) -> str:
"""Return object's verbose_name""" """Return object's verbose_name"""
return obj._meta.verbose_name return obj._meta.verbose_name
@ -25,27 +29,21 @@ class MetaNameSerializer(Serializer):
return obj._meta.verbose_name_plural return obj._meta.verbose_name_plural
class TypeCreateSerializer(Serializer): class TypeCreateSerializer(PassiveSerializer):
"""Types of an object that can be created""" """Types of an object that can be created"""
name = CharField(required=True) name = CharField(required=True)
description = CharField(required=True) description = CharField(required=True)
link = CharField(required=True) link = CharField(required=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model: class CacheSerializer(PassiveSerializer):
raise NotImplementedError
class CacheSerializer(Serializer):
"""Generic cache stats for an object""" """Generic cache stats for an object"""
count = IntegerField(read_only=True) count = IntegerField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model: class LinkSerializer(PassiveSerializer):
raise NotImplementedError """Returns a single link"""
link = CharField()

View File

@ -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"),
}

View File

@ -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,
}

View File

@ -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}

View 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",
},
),
]

View File

@ -369,6 +369,7 @@ class Token(ManagedModel, ExpiringModel):
models.Index(fields=["identifier"]), models.Index(fields=["identifier"]),
models.Index(fields=["key"]), models.Index(fields=["key"]),
] ]
permissions = (("view_token_key", "View token's key"),)
class PropertyMapping(SerializerModel, ManagedModel): class PropertyMapping(SerializerModel, ManagedModel):

View File

@ -17,7 +17,7 @@ def post_save_application(sender, instance, created: bool, **_):
if sender != Application: if sender != Application:
return return
if not created: if not created: # pragma: no cover
return return
# Also delete user application cache # Also delete user application cache
keys = cache.keys(user_app_cache_key("*")) keys = cache.keys(user_app_cache_key("*"))

View File

@ -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>

View File

@ -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
)

View File

@ -2,9 +2,9 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from django.db.models.base import Model
from rest_framework.fields import CharField from rest_framework.fields import CharField
from rest_framework.serializers import Serializer
from authentik.core.api.utils import PassiveSerializer
@dataclass @dataclass
@ -21,29 +21,17 @@ class UILoginButton:
icon_url: Optional[str] = None icon_url: Optional[str] = None
class UILoginButtonSerializer(Serializer): class UILoginButtonSerializer(PassiveSerializer):
"""Serializer for Login buttons of sources""" """Serializer for Login buttons of sources"""
name = CharField() name = CharField()
url = CharField() url = CharField()
icon_url = CharField(required=False) icon_url = CharField(required=False)
def create(self, validated_data: dict) -> Model:
return Model()
def update(self, instance: Model, validated_data: dict) -> Model: class UserSettingSerializer(PassiveSerializer):
return Model()
class UserSettingSerializer(Serializer):
"""Serializer for User settings for stages and sources""" """Serializer for User settings for stages and sources"""
object_uid = CharField() object_uid = CharField()
component = CharField() component = CharField()
title = CharField() title = CharField()
def create(self, validated_data: dict) -> Model:
return Model()
def update(self, instance: Model, validated_data: dict) -> Model:
return Model()

View File

@ -14,7 +14,6 @@ urlpatterns = [
name="root-redirect", name="root-redirect",
), ),
# User views # User views
path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"),
path( path(
"-/user/tokens/create/", "-/user/tokens/create/",
user.TokenCreateView.as_view(), user.TokenCreateView.as_view(),

View File

@ -1,53 +1,20 @@
"""authentik core user views""" """authentik core user views"""
from typing import Any
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import ( from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin, PermissionRequiredMixin as DjangoPermissionRequiredMixin,
) )
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import UpdateView from django.views.generic import UpdateView
from django.views.generic.base import TemplateView
from guardian.mixins import PermissionRequiredMixin from guardian.mixins import PermissionRequiredMixin
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from authentik.core.forms.token import UserTokenForm from authentik.core.forms.token import UserTokenForm
from authentik.core.forms.users import UserDetailForm
from authentik.core.models import Token, TokenIntents from authentik.core.models import Token, TokenIntents
from authentik.flows.models import Flow, FlowDesignation
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
class UserSettingsView(TemplateView):
"""Multiple SiteShells for user details and all stages"""
template_name = "user/settings.html"
class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
"""Update User details"""
template_name = "user/details.html"
form_class = UserDetailForm
success_message = _("Successfully updated user.")
success_url = reverse_lazy("authentik_core:user-details")
def get_object(self):
return self.request.user
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
unenrollment_flow = Flow.with_policy(
self.request, designation=FlowDesignation.UNRENOLLMENT
)
kwargs["unenrollment_enabled"] = bool(unenrollment_flow)
return kwargs
class TokenCreateView( class TokenCreateView(
SuccessMessageMixin, SuccessMessageMixin,
LoginRequiredMixin, LoginRequiredMixin,

View File

@ -2,15 +2,23 @@
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import load_pem_x509_certificate from cryptography.x509 import load_pem_x509_certificate
from django.db.models import Model from django.utils.translation import gettext_lazy as _
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, DateTimeField, SerializerMethodField from rest_framework.fields import (
CharField,
DateTimeField,
IntegerField,
SerializerMethodField,
)
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer, ValidationError from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -71,16 +79,20 @@ class CertificateKeyPairSerializer(ModelSerializer):
} }
class CertificateDataSerializer(Serializer): class CertificateDataSerializer(PassiveSerializer):
"""Get CertificateKeyPair's data""" """Get CertificateKeyPair's data"""
data = CharField(read_only=True) data = CharField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model: class CertificateGenerationSerializer(PassiveSerializer):
raise NotImplementedError """Certificate generation parameters"""
common_name = CharField()
subject_alt_name = CharField(
required=False, allow_blank=True, label=_("Subject-alt name")
)
validity_days = IntegerField(initial=365)
class CertificateKeyPairViewSet(ModelViewSet): class CertificateKeyPairViewSet(ModelViewSet):
@ -89,6 +101,29 @@ class CertificateKeyPairViewSet(ModelViewSet):
queryset = CertificateKeyPair.objects.all() queryset = CertificateKeyPair.objects.all()
serializer_class = CertificateKeyPairSerializer serializer_class = CertificateKeyPairSerializer
@permission_required(None, ["authentik_crypto.add_certificatekeypair"])
@swagger_auto_schema(
request_body=CertificateGenerationSerializer(),
responses={200: CertificateKeyPairSerializer},
)
@action(detail=False, methods=["POST"])
def generate(self, request: Request) -> Response:
"""Generate a new, self-signed certificate-key pair"""
data = CertificateGenerationSerializer(data=request.data)
if not data.is_valid():
return Response(data.errors, status=400)
builder = CertificateBuilder()
builder.common_name = data.validated_data["common_name"]
builder.build(
subject_alt_names=data.validated_data.get("subject_alt_name", "").split(
","
),
validity_days=int(data.validated_data["validity_days"]),
)
instance = builder.save()
serializer = self.get_serializer(instance)
return Response(serializer.data)
@swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)}) @swagger_auto_schema(responses={200: CertificateDataSerializer(many=False)})
@action(detail=True) @action(detail=True)
# pylint: disable=invalid-name, unused-argument # pylint: disable=invalid-name, unused-argument

View File

@ -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"),
}

View File

@ -2,31 +2,12 @@
from django.test import TestCase from django.test import TestCase
from authentik.crypto.api import CertificateKeyPairSerializer from authentik.crypto.api import CertificateKeyPairSerializer
from authentik.crypto.forms import CertificateKeyPairForm
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
class TestCrypto(TestCase): class TestCrypto(TestCase):
"""Test Crypto validation""" """Test Crypto validation"""
def test_form(self):
"""Test form validation"""
keypair = CertificateKeyPair.objects.first()
self.assertTrue(
CertificateKeyPairForm(
{
"name": keypair.name,
"certificate_data": keypair.certificate_data,
"key_data": keypair.key_data,
}
).is_valid()
)
self.assertFalse(
CertificateKeyPairForm(
{"name": keypair.name, "certificate_data": "test", "key_data": "test"}
).is_valid()
)
def test_serializer(self): def test_serializer(self):
"""Test API Validation""" """Test API Validation"""
keypair = CertificateKeyPair.objects.first() keypair = CertificateKeyPair.objects.first()

View File

@ -2,7 +2,7 @@
import django_filters import django_filters
from django.db.models.aggregates import Count from django.db.models.aggregates import Count
from django.db.models.fields.json import KeyTextTransform from django.db.models.fields.json import KeyTextTransform
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, DictField, IntegerField from rest_framework.fields import CharField, DictField, IntegerField

View File

@ -1,6 +1,6 @@
"""NotificationTransport API Views""" """NotificationTransport API Views"""
from django.http.response import Http404 from django.http.response import Http404
from drf_yasg2.utils import no_body, swagger_auto_schema from drf_yasg.utils import no_body, swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
@ -36,6 +36,7 @@ class NotificationTransportSerializer(ModelSerializer):
"mode", "mode",
"mode_verbose", "mode_verbose",
"webhook_url", "webhook_url",
"send_once",
] ]

View File

@ -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(),
}

View File

@ -76,7 +76,9 @@ class AuditMiddleware:
user: User, request: HttpRequest, sender, instance: Model, **_ user: User, request: HttpRequest, sender, instance: Model, **_
): ):
"""Signal handler for all object's pre_delete""" """Signal handler for all object's pre_delete"""
if isinstance(instance, (Event, Notification, UserObjectPermission)): if isinstance(
instance, (Event, Notification, UserObjectPermission)
): # pragma: no cover
return return
EventNewThread( EventNewThread(

View File

@ -3,11 +3,14 @@ from dataclasses import dataclass
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Model from django.db.models import Model
from django.http.response import JsonResponse from django.http.response import HttpResponseBadRequest, JsonResponse
from drf_yasg2 import openapi from django.urls import reverse
from drf_yasg2.utils import no_body, swagger_auto_schema from django.utils.translation import gettext as _
from drf_yasg import openapi
from drf_yasg.utils import no_body, swagger_auto_schema
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ( from rest_framework.serializers import (
@ -20,11 +23,15 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.utils import CacheSerializer from authentik.core.api.utils import CacheSerializer, LinkSerializer
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.flows.planner import cache_key from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.transfer.common import DataclassEncoder from authentik.flows.transfer.common import DataclassEncoder
from authentik.flows.transfer.exporter import FlowExporter from authentik.flows.transfer.exporter import FlowExporter
from authentik.flows.transfer.importer import FlowImporter
from authentik.flows.views import SESSION_KEY_PLAN
from authentik.lib.views import bad_request_message
LOGGER = get_logger() LOGGER = get_logger()
@ -56,7 +63,7 @@ class FlowSerializer(ModelSerializer):
class FlowDiagramSerializer(Serializer): class FlowDiagramSerializer(Serializer):
"""response of the flow's /diagram/ action""" """response of the flow's diagram action"""
diagram = CharField(read_only=True) diagram = CharField(read_only=True)
@ -88,14 +95,14 @@ class FlowViewSet(ModelViewSet):
search_fields = ["name", "slug", "designation", "title"] search_fields = ["name", "slug", "designation", "title"]
filterset_fields = ["flow_uuid", "name", "slug", "designation"] filterset_fields = ["flow_uuid", "name", "slug", "designation"]
@permission_required("authentik_flows.view_flow_cache") @permission_required(None, ["authentik_flows.view_flow_cache"])
@swagger_auto_schema(responses={200: CacheSerializer(many=False)}) @swagger_auto_schema(responses={200: CacheSerializer(many=False)})
@action(detail=False) @action(detail=False)
def cache_info(self, request: Request) -> Response: def cache_info(self, request: Request) -> Response:
"""Info about cached flows""" """Info about cached flows"""
return Response(data={"count": len(cache.keys("flow_*"))}) return Response(data={"count": len(cache.keys("flow_*"))})
@permission_required("authentik_flows.clear_flow_cache") @permission_required(None, ["authentik_flows.clear_flow_cache"])
@swagger_auto_schema( @swagger_auto_schema(
request_body=no_body, request_body=no_body,
responses={204: "Successfully cleared cache", 400: "Bad request"}, responses={204: "Successfully cleared cache", 400: "Bad request"},
@ -108,7 +115,61 @@ class FlowViewSet(ModelViewSet):
LOGGER.debug("Cleared flow cache", keys=len(keys)) LOGGER.debug("Cleared flow cache", keys=len(keys))
return Response(status=204) return Response(status=204)
@permission_required("authentik_flows.export_flow") @permission_required(
None,
[
"authentik_flows.add_flow",
"authentik_flows.change_flow",
"authentik_flows.add_flowstagebinding",
"authentik_flows.change_flowstagebinding",
"authentik_flows.add_stage",
"authentik_flows.change_stage",
"authentik_policies.add_policy",
"authentik_policies.change_policy",
"authentik_policies.add_policybinding",
"authentik_policies.change_policybinding",
"authentik_stages_prompt.add_prompt",
"authentik_stages_prompt.change_prompt",
],
)
@swagger_auto_schema(
request_body=no_body,
manual_parameters=[
openapi.Parameter(
name="file",
in_=openapi.IN_FORM,
type=openapi.TYPE_FILE,
required=True,
)
],
responses={204: "Successfully imported flow", 400: "Bad request"},
)
@action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,))
def import_flow(self, request: Request) -> Response:
"""Import flow from .akflow file"""
file = request.FILES.get("file", None)
if not file:
return HttpResponseBadRequest()
importer = FlowImporter(file.read().decode())
valid = importer.validate()
if not valid:
return HttpResponseBadRequest()
successful = importer.apply()
if not successful:
return Response(status=204)
return HttpResponseBadRequest()
@permission_required(
"authentik_flows.export_flow",
[
"authentik_flows.view_flow",
"authentik_flows.view_flowstagebinding",
"authentik_flows.view_stage",
"authentik_policies.view_policy",
"authentik_policies.view_policybinding",
"authentik_stages_prompt.view_prompt",
],
)
@swagger_auto_schema( @swagger_auto_schema(
responses={ responses={
"200": openapi.Response( "200": openapi.Response(
@ -194,3 +255,57 @@ class FlowViewSet(ModelViewSet):
) )
diagram = "\n".join([str(x) for x in header + body + footer]) diagram = "\n".join([str(x) for x in header + body + footer])
return Response({"diagram": diagram}) return Response({"diagram": diagram})
@permission_required("authentik_flows.change_flow")
@swagger_auto_schema(
request_body=no_body,
manual_parameters=[
openapi.Parameter(
name="file",
in_=openapi.IN_FORM,
type=openapi.TYPE_FILE,
required=True,
)
],
responses={200: "Success"},
)
@action(detail=True, methods=["POST"], parser_classes=(MultiPartParser,))
# pylint: disable=unused-argument
def set_background(self, request: Request, slug: str):
"""Set Flow background"""
app: Flow = self.get_object()
icon = request.FILES.get("file", None)
if not icon:
return HttpResponseBadRequest()
app.background = icon
app.save()
return Response({})
@swagger_auto_schema(
responses={200: LinkSerializer(many=False)},
)
@action(detail=True)
# pylint: disable=unused-argument
def execute(self, request: Request, slug: str):
"""Execute flow for current user"""
flow: Flow = self.get_object()
planner = FlowPlanner(flow)
planner.use_cache = False
try:
plan = planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: request.user})
self.request.session[SESSION_KEY_PLAN] = plan
except FlowNonApplicableException as exc:
return bad_request_message(
request,
_(
"Flow not applicable to current user/request: %(messages)s"
% {"messages": str(exc)}
),
)
return Response(
{
"link": request._request.build_absolute_uri(
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
)
}
)

View File

@ -2,7 +2,7 @@
from typing import Iterable from typing import Iterable
from django.urls import reverse from django.urls import reverse
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request

View File

@ -2,11 +2,11 @@
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from django.db.models.base import Model
from django.http import JsonResponse from django.http import JsonResponse
from rest_framework.fields import ChoiceField, DictField from rest_framework.fields import ChoiceField, DictField
from rest_framework.serializers import CharField, Serializer from rest_framework.serializers import CharField
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.transfer.common import DataclassEncoder from authentik.flows.transfer.common import DataclassEncoder
if TYPE_CHECKING: if TYPE_CHECKING:
@ -21,20 +21,14 @@ class ChallengeTypes(Enum):
REDIRECT = "redirect" REDIRECT = "redirect"
class ErrorDetailSerializer(Serializer): class ErrorDetailSerializer(PassiveSerializer):
"""Serializer for rest_framework's error messages""" """Serializer for rest_framework's error messages"""
string = CharField() string = CharField()
code = CharField() code = CharField()
def create(self, validated_data: dict) -> Model:
return Model()
def update(self, instance: Model, validated_data: dict) -> Model: class Challenge(PassiveSerializer):
return Model()
class Challenge(Serializer):
"""Challenge that gets sent to the client based on which stage """Challenge that gets sent to the client based on which stage
is currently active""" is currently active"""
@ -49,12 +43,6 @@ class Challenge(Serializer):
child=ErrorDetailSerializer(many=True), allow_empty=True, required=False child=ErrorDetailSerializer(many=True), allow_empty=True, required=False
) )
def create(self, validated_data: dict) -> Model:
return Model()
def update(self, instance: Model, validated_data: dict) -> Model:
return Model()
class RedirectChallenge(Challenge): class RedirectChallenge(Challenge):
"""Challenge type to redirect the client""" """Challenge type to redirect the client"""
@ -81,20 +69,14 @@ class AccessDeniedChallenge(Challenge):
error_message = CharField(required=False) error_message = CharField(required=False)
class PermissionSerializer(Serializer): class PermissionSerializer(PassiveSerializer):
"""Permission used for consent""" """Permission used for consent"""
name = CharField() name = CharField()
id = CharField() id = CharField()
def create(self, validated_data: dict) -> Model:
return Model()
def update(self, instance: Model, validated_data: dict) -> Model: class ChallengeResponse(PassiveSerializer):
return Model()
class ChallengeResponse(Serializer):
"""Base class for all challenge responses""" """Base class for all challenge responses"""
stage: Optional["StageView"] stage: Optional["StageView"]
@ -103,12 +85,6 @@ class ChallengeResponse(Serializer):
self.stage = kwargs.pop("stage", None) self.stage = kwargs.pop("stage", None)
super().__init__(instance=instance, data=data, **kwargs) super().__init__(instance=instance, data=data, **kwargs)
def create(self, validated_data: dict) -> Model:
return Model()
def update(self, instance: Model, validated_data: dict) -> Model:
return Model()
class HttpChallengeResponse(JsonResponse): class HttpChallengeResponse(JsonResponse):
"""Subclass of JsonResponse that uses the `DataclassEncoder`""" """Subclass of JsonResponse that uses the `DataclassEncoder`"""

View File

@ -1,35 +1,10 @@
"""Flow and Stage forms""" """Flow and Stage forms"""
from django import forms from django import forms
from django.core.validators import FileExtensionValidator
from django.forms import ValidationError
from django.utils.translation import gettext_lazy as _
from authentik.flows.models import Flow, FlowStageBinding, Stage from authentik.flows.models import FlowStageBinding, Stage
from authentik.flows.transfer.importer import FlowImporter
from authentik.lib.widgets import GroupedModelChoiceField from authentik.lib.widgets import GroupedModelChoiceField
class FlowForm(forms.ModelForm):
"""Flow Form"""
class Meta:
model = Flow
fields = [
"name",
"title",
"slug",
"designation",
"background",
]
widgets = {
"name": forms.TextInput(),
"title": forms.TextInput(),
"background": forms.FileInput(),
}
class FlowStageBindingForm(forms.ModelForm): class FlowStageBindingForm(forms.ModelForm):
"""FlowStageBinding Form""" """FlowStageBinding Form"""
@ -56,20 +31,3 @@ class FlowStageBindingForm(forms.ModelForm):
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
} }
class FlowImportForm(forms.Form):
"""Form used for flow importing"""
flow = forms.FileField(
validators=[FileExtensionValidator(allowed_extensions=["akflow"])]
)
def clean_flow(self):
"""Check if the flow is valid and rewind the file to the start"""
flow = self.cleaned_data["flow"].read()
valid = FlowImporter(flow.decode()).validate()
if not valid:
raise ValidationError(_("Flow invalid."))
self.cleaned_data["flow"].seek(0)
return self.cleaned_data["flow"]

View File

@ -10,8 +10,8 @@ from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import View from django.views.generic import View
from drf_yasg2 import openapi from drf_yasg import openapi
from drf_yasg2.utils import no_body, swagger_auto_schema from drf_yasg.utils import no_body, swagger_auto_schema
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.views import APIView from rest_framework.views import APIView
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger

View File

@ -1,6 +1,5 @@
"""Managed Object models""" """Managed Object models"""
from django.db import models from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -22,10 +21,6 @@ class ManagedModel(models.Model):
unique=True, unique=True,
) )
def managed_objects(self) -> QuerySet:
"""Get all objects which are managed"""
return self.objects.exclude(managed__isnull=True)
class Meta: class Meta:
abstract = True abstract = True

View File

@ -1,17 +1,20 @@
"""Outpost API Views""" """Outpost API Views"""
from dataclasses import asdict from dataclasses import asdict
from django.db.models.base import Model
from django.urls import reverse from django.urls import reverse
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, SerializerMethodField from rest_framework.fields import BooleanField, CharField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.api.utils import (
MetaNameSerializer,
PassiveSerializer,
TypeCreateSerializer,
)
from authentik.lib.templatetags.authentik_utils import verbose_name from authentik.lib.templatetags.authentik_utils import verbose_name
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
from authentik.outposts.models import ( from authentik.outposts.models import (
@ -43,18 +46,12 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
] ]
class ServiceConnectionStateSerializer(Serializer): class ServiceConnectionStateSerializer(PassiveSerializer):
"""Serializer for Service connection state""" """Serializer for Service connection state"""
healthy = BooleanField(read_only=True) healthy = BooleanField(read_only=True)
version = CharField(read_only=True) version = CharField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class ServiceConnectionViewSet(ModelViewSet): class ServiceConnectionViewSet(ModelViewSet):
"""ServiceConnection Viewset""" """ServiceConnection Viewset"""

View File

@ -1,15 +1,15 @@
"""Outpost API Views""" """Outpost API Views"""
from django.db.models import Model from drf_yasg.utils import swagger_auto_schema
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, DateTimeField from rest_framework.fields import BooleanField, CharField, DateTimeField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import JSONField, ModelSerializer, Serializer from rest_framework.serializers import JSONField, ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.outposts.models import Outpost from authentik.core.api.utils import PassiveSerializer
from authentik.outposts.models import Outpost, default_outpost_config
class OutpostSerializer(ModelSerializer): class OutpostSerializer(ModelSerializer):
@ -32,7 +32,13 @@ class OutpostSerializer(ModelSerializer):
] ]
class OutpostHealthSerializer(Serializer): class OutpostDefaultConfigSerializer(PassiveSerializer):
"""Global default outpost config"""
config = JSONField(read_only=True)
class OutpostHealthSerializer(PassiveSerializer):
"""Outpost health status""" """Outpost health status"""
last_seen = DateTimeField(read_only=True) last_seen = DateTimeField(read_only=True)
@ -40,12 +46,6 @@ class OutpostHealthSerializer(Serializer):
version_should = CharField(read_only=True) version_should = CharField(read_only=True)
version_outdated = BooleanField(read_only=True) version_outdated = BooleanField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class OutpostViewSet(ModelViewSet): class OutpostViewSet(ModelViewSet):
"""Outpost Viewset""" """Outpost Viewset"""
@ -78,3 +78,10 @@ class OutpostViewSet(ModelViewSet):
} }
) )
return Response(OutpostHealthSerializer(states, many=True).data) return Response(OutpostHealthSerializer(states, many=True).data)
@swagger_auto_schema(responses={200: OutpostDefaultConfigSerializer(many=False)})
@action(detail=False, methods=["GET"])
def default_settings(self, request: Request) -> Response:
"""Global default outpost config"""
host = self.request.build_absolute_uri("/")
return Response({"config": default_outpost_config(host)})

View File

@ -80,9 +80,9 @@ class OutpostType(models.TextChoices):
PROXY = "proxy" PROXY = "proxy"
def default_outpost_config(): def default_outpost_config(host: Optional[str] = None):
"""Get default outpost config""" """Get default outpost config"""
return asdict(OutpostConfig(authentik_host="")) return asdict(OutpostConfig(authentik_host=host or ""))
@dataclass @dataclass

View File

View 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"]

View 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)

View File

@ -1,19 +1,16 @@
"""policy API Views""" """policy API Views"""
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http.response import HttpResponseBadRequest from django.http.response import HttpResponseBadRequest
from django.urls import reverse from django.urls import reverse
from drf_yasg2.utils import no_body, swagger_auto_schema from drf_yasg.utils import no_body, swagger_auto_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ( from rest_framework.serializers import ModelSerializer, SerializerMethodField
ModelSerializer, from rest_framework.viewsets import GenericViewSet
PrimaryKeyRelatedField,
SerializerMethodField,
)
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
@ -25,42 +22,14 @@ from authentik.core.api.utils import (
) )
from authentik.lib.templatetags.authentik_utils import verbose_name from authentik.lib.templatetags.authentik_utils import verbose_name
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest
LOGGER = get_logger() LOGGER = get_logger()
class PolicyBindingModelForeignKey(PrimaryKeyRelatedField):
"""rest_framework PrimaryKeyRelatedField which resolves
model_manager's InheritanceQuerySet"""
def use_pk_only_optimization(self):
return False
# pylint: disable=inconsistent-return-statements
def to_internal_value(self, data):
if self.pk_field is not None:
data = self.pk_field.to_internal_value(data)
try:
# Due to inheritance, a direct DB lookup for the primary key
# won't return anything. This is because the direct lookup
# checks the PK of PolicyBindingModel (for example),
# but we get given the Primary Key of the inheriting class
for model in self.get_queryset().select_subclasses().all().select_related():
if model.pk == data:
return model
# as a fallback we still try a direct lookup
return self.get_queryset().get_subclass(pk=data)
except ObjectDoesNotExist:
self.fail("does_not_exist", pk_value=data)
except (TypeError, ValueError):
self.fail("incorrect_type", data_type=type(data).__name__)
def to_representation(self, value):
correct_model = PolicyBindingModel.objects.get_subclass(pbm_uuid=value.pbm_uuid)
return correct_model.pk
class PolicySerializer(ModelSerializer, MetaNameSerializer): class PolicySerializer(ModelSerializer, MetaNameSerializer):
"""Policy Serializer""" """Policy Serializer"""
@ -168,39 +137,32 @@ class PolicyViewSet(
cache.delete_many(keys) cache.delete_many(keys)
return Response(status=204) return Response(status=204)
@permission_required("authentik_policies.view_policy")
class PolicyBindingSerializer(ModelSerializer): @swagger_auto_schema(
"""PolicyBinding Serializer""" request_body=PolicyTestSerializer(),
responses={200: PolicyTestResultSerializer()},
# Because we're not interested in the PolicyBindingModel's PK but rather the subclasses PK,
# we have to manually declare this field
target = PolicyBindingModelForeignKey(
queryset=PolicyBindingModel.objects.select_subclasses(),
required=True,
) )
@action(detail=True, methods=["POST"])
def test(self, request: Request) -> Response:
"""Test policy"""
policy = self.get_object()
test_params = PolicyTestSerializer(request.data)
if not test_params.is_valid():
return Response(test_params.errors, status=400)
class Meta: # User permission check, only allow policy testing for users that are readable
users = get_objects_for_user(request.user, "authentik_core.view_user").filter(
pk=test_params["user"]
)
if not users.exists():
raise PermissionDenied()
model = PolicyBinding p_request = PolicyRequest(users.first())
fields = [ p_request.debug = True
"pk", p_request.set_http_request(self.request)
"policy", p_request.context = test_params.validated_data.get("context", {})
"group",
"user",
"target",
"enabled",
"order",
"timeout",
]
depth = 2
proc = PolicyProcess(PolicyBinding(policy=policy), p_request, None)
class PolicyBindingViewSet(ModelViewSet): result = proc.execute()
"""PolicyBinding Viewset""" response = PolicyTestResultSerializer(result)
return Response(response)
queryset = PolicyBinding.objects.all().select_related(
"policy", "target", "group", "user"
)
serializer_class = PolicyBindingSerializer
filterset_fields = ["policy", "target", "enabled", "order", "timeout"]
search_fields = ["policy__name"]

View File

@ -1,7 +1,7 @@
"""Dummy Policy API Views""" """Dummy Policy API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.policies.api import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.dummy.models import DummyPolicy from authentik.policies.dummy.models import DummyPolicy

View File

@ -1,7 +1,7 @@
"""Event Matcher Policy API""" """Event Matcher Policy API"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.policies.api import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.event_matcher.models import EventMatcherPolicy from authentik.policies.event_matcher.models import EventMatcherPolicy

View File

@ -1,7 +1,7 @@
"""Password Expiry Policy API Views""" """Password Expiry Policy API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.policies.api import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.expiry.models import PasswordExpiryPolicy from authentik.policies.expiry.models import PasswordExpiryPolicy

View File

@ -1,7 +1,7 @@
"""Expression Policy API""" """Expression Policy API"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.policies.api import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.expression.models import ExpressionPolicy from authentik.policies.expression.models import ExpressionPolicy

View File

@ -1,7 +1,7 @@
"""Source API Views""" """Source API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.policies.api import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.hibp.models import HaveIBeenPwendPolicy from authentik.policies.hibp.models import HaveIBeenPwendPolicy

View File

@ -95,7 +95,7 @@ class PolicyBinding(SerializerModel):
@property @property
def serializer(self) -> BaseSerializer: def serializer(self) -> BaseSerializer:
from authentik.policies.api import PolicyBindingSerializer from authentik.policies.api.bindings import PolicyBindingSerializer
return PolicyBindingSerializer return PolicyBindingSerializer

View File

@ -1,7 +1,7 @@
"""Password Policy API Views""" """Password Policy API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.policies.api import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.password.models import PasswordPolicy from authentik.policies.password.models import PasswordPolicy

View File

@ -1,7 +1,7 @@
"""Source API Views""" """Source API Views"""
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.policies.api import PolicySerializer from authentik.policies.api.policies import PolicySerializer
from authentik.policies.reputation.models import ( from authentik.policies.reputation.models import (
IPReputation, IPReputation,
ReputationPolicy, ReputationPolicy,

View File

@ -1,6 +1,6 @@
"""OAuth2Provider API Views""" """OAuth2Provider API Views"""
from django.urls import reverse from django.urls import reverse
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField from rest_framework.fields import ReadOnlyField
from rest_framework.generics import get_object_or_404 from rest_framework.generics import get_object_or_404

View File

@ -1,5 +1,5 @@
"""ProxyProvider API Views""" """ProxyProvider API Views"""
from drf_yasg2.utils import swagger_serializer_method from drf_yasg.utils import swagger_serializer_method
from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response

View File

@ -1,5 +1,5 @@
"""SAMLProvider API Views""" """SAMLProvider API Views"""
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField from rest_framework.fields import ReadOnlyField
from rest_framework.request import Request from rest_framework.request import Request

View File

@ -126,7 +126,7 @@ INSTALLED_APPS = [
"authentik.stages.user_write.apps.AuthentikStageUserWriteConfig", "authentik.stages.user_write.apps.AuthentikStageUserWriteConfig",
"rest_framework", "rest_framework",
"django_filters", "django_filters",
"drf_yasg2", "drf_yasg",
"guardian", "guardian",
"django_prometheus", "django_prometheus",
"channels", "channels",
@ -137,6 +137,7 @@ INSTALLED_APPS = [
GUARDIAN_MONKEY_PATCH = False GUARDIAN_MONKEY_PATCH = False
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
"DEFAULT_AUTO_SCHEMA_CLASS": "authentik.api.schema.ErrorResponseAutoSchema",
"DEFAULT_INFO": "authentik.api.v2.urls.info", "DEFAULT_INFO": "authentik.api.v2.urls.info",
"DEFAULT_PAGINATOR_INSPECTORS": [ "DEFAULT_PAGINATOR_INSPECTORS": [
"authentik.api.pagination_schema.PaginationInspector", "authentik.api.pagination_schema.PaginationInspector",
@ -437,7 +438,7 @@ _LOGGING_HANDLER_MAP = {
"kubernetes": "INFO", "kubernetes": "INFO",
"asyncio": "WARNING", "asyncio": "WARNING",
"aioredis": "WARNING", "aioredis": "WARNING",
"drf_yasg2.utils": "WARNING", "drf_yasg.utils": "WARNING",
} }
for handler_name, level in _LOGGING_HANDLER_MAP.items(): for handler_name, level in _LOGGING_HANDLER_MAP.items():
# pyright: reportGeneralTypeIssues=false # pyright: reportGeneralTypeIssues=false

View File

@ -3,17 +3,16 @@ from datetime import datetime
from time import time from time import time
from django.core.cache import cache from django.core.cache import cache
from django.db.models.base import Model from drf_yasg.utils import swagger_auto_schema
from drf_yasg2.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import DateTimeField from rest_framework.fields import DateTimeField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.utils import MetaNameSerializer from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
@ -44,17 +43,11 @@ class LDAPSourceSerializer(SourceSerializer):
extra_kwargs = {"bind_password": {"write_only": True}} extra_kwargs = {"bind_password": {"write_only": True}}
class LDAPSourceSyncStatusSerializer(Serializer): class LDAPSourceSyncStatusSerializer(PassiveSerializer):
"""LDAP Sync status""" """LDAP Sync status"""
last_sync = DateTimeField(read_only=True) last_sync = DateTimeField(read_only=True)
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
class LDAPSourceViewSet(ModelViewSet): class LDAPSourceViewSet(ModelViewSet):
"""LDAP Source Viewset""" """LDAP Source Viewset"""

View File

@ -1,5 +1,5 @@
"""SAMLSource API Views""" """SAMLSource API Views"""
from drf_yasg2.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response

View File

@ -1,5 +1,4 @@
"""Validation stage challenge checking""" """Validation stage challenge checking"""
from django.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_otp import match_token from django_otp import match_token
@ -7,7 +6,7 @@ from django_otp.models import Device
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from rest_framework.fields import CharField, JSONField from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import Serializer, ValidationError from rest_framework.serializers import ValidationError
from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser
from webauthn.webauthn import ( from webauthn.webauthn import (
AuthenticationRejectedException, AuthenticationRejectedException,
@ -15,24 +14,19 @@ from webauthn.webauthn import (
WebAuthnUserDataMissing, WebAuthnUserDataMissing,
) )
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User from authentik.core.models import User
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin from authentik.stages.authenticator_webauthn.utils import generate_challenge, get_origin
class DeviceChallenge(Serializer): class DeviceChallenge(PassiveSerializer):
"""Single device challenge""" """Single device challenge"""
device_class = CharField() device_class = CharField()
device_uid = CharField() device_uid = CharField()
challenge = JSONField() challenge = JSONField()
def create(self, validated_data: dict) -> Model:
raise NotImplementedError
def update(self, instance: Model, validated_data: dict) -> Model:
raise NotImplementedError
def get_challenge_for_device(request: HttpRequest, device: Device) -> dict: def get_challenge_for_device(request: HttpRequest, device: Device) -> dict:
"""Generate challenge for a single device""" """Generate challenge for a single device"""

View File

@ -8,4 +8,3 @@ class AuthentikStageAuthenticatorWebAuthnConfig(AppConfig):
name = "authentik.stages.authenticator_webauthn" name = "authentik.stages.authenticator_webauthn"
label = "authentik_stages_authenticator_webauthn" label = "authentik_stages_authenticator_webauthn"
verbose_name = "authentik Stages.Authenticator.WebAuthn" verbose_name = "authentik Stages.Authenticator.WebAuthn"
mountpoint = "-/user/authenticator/webauthn/"

View File

@ -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"),
]

View File

@ -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

View File

@ -3,16 +3,16 @@ from email.policy import Policy
from types import MethodType from types import MethodType
from typing import Any, Callable, Iterator from typing import Any, Callable, Iterator
from django.db.models.base import Model
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict from django.http.request import QueryDict
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from rest_framework.fields import BooleanField, CharField, IntegerField from rest_framework.fields import BooleanField, CharField, IntegerField
from rest_framework.serializers import Serializer, ValidationError from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User from authentik.core.models import User
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
@ -26,7 +26,7 @@ LOGGER = get_logger()
PLAN_CONTEXT_PROMPT = "prompt_data" PLAN_CONTEXT_PROMPT = "prompt_data"
class PromptSerializer(Serializer): class PromptSerializer(PassiveSerializer):
"""Serializer for a single Prompt field""" """Serializer for a single Prompt field"""
field_key = CharField() field_key = CharField()
@ -36,12 +36,6 @@ class PromptSerializer(Serializer):
placeholder = CharField() placeholder = CharField()
order = IntegerField() order = IntegerField()
def create(self, validated_data: dict) -> Model:
return Model()
def update(self, instance: Model, validated_data: dict) -> Model:
return Model()
class PromptChallenge(Challenge): class PromptChallenge(Challenge):
"""Initial challenge being sent, define fields""" """Initial challenge being sent, define fields"""

View File

@ -285,9 +285,9 @@ stages:
inputs: inputs:
script: | script: |
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0 docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
sudo chmod 777 -R api/ sudo chmod 777 -R web/api/
cd web cd web
sudo chmod 777 -R api/ cd api && npm i && cd ..
npm i npm i
npm run build npm run build
- task: CmdLine@2 - task: CmdLine@2

File diff suppressed because it is too large Load Diff

View File

@ -98,7 +98,7 @@ class TestFlowsEnroll(SeleniumTestCase):
wait = WebDriverWait(interface_admin, self.wait_timeout) wait = WebDriverWait(interface_admin, self.wait_timeout)
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar"))) wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
self.driver.get(self.if_admin_url("authentik_core:user-details")) self.driver.get(self.if_admin_url("/user"))
user = User.objects.get(username="foo") user = User.objects.get(username="foo")
self.assertEqual(user.username, "foo") self.assertEqual(user.username, "foo")
@ -198,7 +198,7 @@ class TestFlowsEnroll(SeleniumTestCase):
) )
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar"))) wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-sidebar")))
self.driver.get(self.if_admin_url("authentik_core:user-details")) self.driver.get(self.if_admin_url("/user"))
self.assert_user(User.objects.get(username="foo")) self.assert_user(User.objects.get(username="foo"))

View File

@ -15,6 +15,7 @@ from selenium.webdriver.support.wait import WebDriverWait
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from yaml import safe_dump from yaml import safe_dump
from authentik.core.models import User
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.providers.oauth2.generators import ( from authentik.providers.oauth2.generators import (
generate_client_id, generate_client_id,
@ -160,19 +161,9 @@ class TestSourceOAuth2(SeleniumTestCase):
# Wait until we've logged in # Wait until we've logged in
self.wait_for_url(self.if_admin_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details")) self.driver.get(self.if_admin_url("/user"))
self.assertEqual( self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"admin",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"admin@example.com",
)
@retry() @retry()
@apply_migration("authentik_core", "0003_default_user") @apply_migration("authentik_core", "0003_default_user")
@ -255,19 +246,9 @@ class TestSourceOAuth2(SeleniumTestCase):
# Wait until we've logged in # Wait until we've logged in
self.wait_for_url(self.if_admin_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details")) self.driver.get(self.if_admin_url("/user"))
self.assertEqual( self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"admin",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"admin@example.com",
)
@skipUnless(platform.startswith("linux"), "requires local docker") @skipUnless(platform.startswith("linux"), "requires local docker")
@ -359,17 +340,8 @@ class TestSourceOAuth1(SeleniumTestCase):
sleep(2) sleep(2)
# Wait until we've logged in # Wait until we've logged in
self.wait_for_url(self.if_admin_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details")) self.driver.get(self.if_admin_url("/user"))
self.assertEqual( self.assert_user(
self.driver.find_element(By.ID, "id_username").get_attribute("value"), User(username="example-user", name="test name", email="foo@example.com")
"example-user",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
"test name",
)
self.assertEqual(
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
"foo@example.com",
) )

View File

@ -5,12 +5,14 @@ from typing import Any, Optional
from unittest.case import skipUnless from unittest.case import skipUnless
from docker.types import Healthcheck from docker.types import Healthcheck
from guardian.utils import get_anonymous_user
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
@ -153,11 +155,12 @@ class TestSourceSAML(SeleniumTestCase):
# Wait until we're logged in # Wait until we're logged in
self.wait_for_url(self.if_admin_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details")) self.driver.get(self.if_admin_url("/user"))
# Wait until we've loaded the user info page self.assert_user(
self.assertNotEqual( User.objects.exclude(username="akadmin")
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" .exclude(pk=get_anonymous_user().pk)
.first()
) )
@retry() @retry()
@ -233,11 +236,12 @@ class TestSourceSAML(SeleniumTestCase):
# Wait until we're logged in # Wait until we're logged in
self.wait_for_url(self.if_admin_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details")) self.driver.get(self.if_admin_url("/user"))
# Wait until we've loaded the user info page self.assert_user(
self.assertNotEqual( User.objects.exclude(username="akadmin")
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" .exclude(pk=get_anonymous_user().pk)
.first()
) )
@retry() @retry()
@ -300,9 +304,10 @@ class TestSourceSAML(SeleniumTestCase):
# Wait until we're logged in # Wait until we're logged in
self.wait_for_url(self.if_admin_url("/library")) self.wait_for_url(self.if_admin_url("/library"))
self.driver.get(self.url("authentik_core:user-details")) self.driver.get(self.if_admin_url("/user"))
# Wait until we've loaded the user info page self.assert_user(
self.assertNotEqual( User.objects.exclude(username="akadmin")
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "" .exclude(pk=get_anonymous_user().pk)
.first()
) )

143
web/package-lock.json generated
View File

@ -133,6 +133,139 @@
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz", "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz",
"integrity": "sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw==" "integrity": "sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw=="
}, },
"@polymer/font-roboto": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@polymer/font-roboto/-/font-roboto-3.0.2.tgz",
"integrity": "sha512-tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA=="
},
"@polymer/iron-a11y-announcer": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.1.0.tgz",
"integrity": "sha512-lc5i4NKB8kSQHH0Hwu8WS3ym93m+J69OHJWSSBxwd17FI+h2wmgxDzeG9LI4ojMMck17/uc2pLe7g/UHt5/K/A==",
"requires": {
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-a11y-keys-behavior": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz",
"integrity": "sha512-lnrjKq3ysbBPT/74l0Fj0U9H9C35Tpw2C/tpJ8a+5g8Y3YJs1WSZYnEl1yOkw6sEyaxOq/1DkzH0+60gGu5/PQ==",
"requires": {
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-ajax": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-ajax/-/iron-ajax-3.0.1.tgz",
"integrity": "sha512-7+TPEAfWsRdhj1Y8UeF1759ktpVu+c3sG16rJiUC3wF9+woQ9xI1zUm2d59i7Yc3aDEJrR/Q8Y262KlOvyGVNg==",
"requires": {
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-autogrow-textarea": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.3.tgz",
"integrity": "sha512-5r0VkWrIlm0JIp5E5wlnvkw7slK72lFRZXncmrsLZF+6n1dg2rI8jt7xpFzSmUWrqpcyXwyKaGaDvUjl3j4JLA==",
"requires": {
"@polymer/iron-behaviors": "^3.0.0-pre.26",
"@polymer/iron-flex-layout": "^3.0.0-pre.26",
"@polymer/iron-validatable-behavior": "^3.0.0-pre.26",
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-behaviors": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-behaviors/-/iron-behaviors-3.0.1.tgz",
"integrity": "sha512-IMEwcv1lhf1HSQxuyWOUIL0lOBwmeaoSTpgCJeP9IBYnuB1SPQngmfRuHKgK6/m9LQ9F9miC7p3HeQQUdKAE0w==",
"requires": {
"@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26",
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-flex-layout": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz",
"integrity": "sha512-7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==",
"requires": {
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-form": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-form/-/iron-form-3.0.1.tgz",
"integrity": "sha512-JwSQXHjYALsytCeBkXlY8aRwqgZuYIqzOk3iHuugb1RXOdZ7MZHyJhMDVBbscHjxqPKu/KaVzAjrcfwNNafzEA==",
"requires": {
"@polymer/iron-ajax": "^3.0.0-pre.26",
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-form-element-behavior": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-form-element-behavior/-/iron-form-element-behavior-3.0.1.tgz",
"integrity": "sha512-G/e2KXyL5AY7mMjmomHkGpgS0uAf4ovNpKhkuUTRnMuMJuf589bKqE85KN4ovE1Tzhv2hJoh/igyD6ekHiYU1A==",
"requires": {
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-input": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-input/-/iron-input-3.0.1.tgz",
"integrity": "sha512-WLx13kEcbH9GKbj9+pWR6pbJkA5kxn3796ynx6eQd2rueMyUfVTR3GzOvadBKsciUuIuzrxpBWZ2+3UcueVUQQ==",
"requires": {
"@polymer/iron-a11y-announcer": "^3.0.0-pre.26",
"@polymer/iron-validatable-behavior": "^3.0.0-pre.26",
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-meta": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz",
"integrity": "sha512-pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==",
"requires": {
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/iron-validatable-behavior": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz",
"integrity": "sha512-wwpYh6wOa4fNI+jH5EYKC7TVPYQ2OfgQqocWat7GsNWcsblKYhLYbwsvEY5nO0n2xKqNfZzDLrUom5INJN7msQ==",
"requires": {
"@polymer/iron-meta": "^3.0.0-pre.26",
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/paper-input": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@polymer/paper-input/-/paper-input-3.2.1.tgz",
"integrity": "sha512-6ghgwQKM6mS0hAQxQqj+tkeEY1VUBqAsrasAm8V5RpNcfSWQC/hhRFxU0beGuKTAhndzezDzWYP6Zz4b8fExGg==",
"requires": {
"@polymer/iron-a11y-keys-behavior": "^3.0.0-pre.26",
"@polymer/iron-autogrow-textarea": "^3.0.0-pre.26",
"@polymer/iron-behaviors": "^3.0.0-pre.26",
"@polymer/iron-form-element-behavior": "^3.0.0-pre.26",
"@polymer/iron-input": "^3.0.0-pre.26",
"@polymer/paper-styles": "^3.0.0-pre.26",
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/paper-styles": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@polymer/paper-styles/-/paper-styles-3.0.1.tgz",
"integrity": "sha512-y6hmObLqlCx602TQiSBKHqjwkE7xmDiFkoxdYGaNjtv4xcysOTdVJsDR/R9UHwIaxJ7gHlthMSykir1nv78++g==",
"requires": {
"@polymer/font-roboto": "^3.0.1",
"@polymer/iron-flex-layout": "^3.0.0-pre.26",
"@polymer/polymer": "^3.0.0"
}
},
"@polymer/polymer": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.4.1.tgz",
"integrity": "sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==",
"requires": {
"@webcomponents/shadycss": "^1.9.1"
}
},
"@rollup/plugin-typescript": { "@rollup/plugin-typescript": {
"version": "8.2.1", "version": "8.2.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.1.tgz",
@ -547,6 +680,11 @@
"eslint-visitor-keys": "^2.0.0" "eslint-visitor-keys": "^2.0.0"
} }
}, },
"@webcomponents/shadycss": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.10.2.tgz",
"integrity": "sha512-9Iseu8bRtecb0klvv+WXZOVZatsRkbaH7M97Z+f+Pt909R4lDfgUODAnra23DOZTpeMTAkVpf4m/FZztN7Ox1A=="
},
"acorn": { "acorn": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@ -3948,6 +4086,11 @@
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true "dev": true
}, },
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
},
"yargs": { "yargs": {
"version": "15.4.1", "version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",

View File

@ -12,6 +12,8 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3", "@fortawesome/fontawesome-free": "^5.15.3",
"@patternfly/patternfly": "^4.90.5", "@patternfly/patternfly": "^4.90.5",
"@polymer/iron-form": "^3.0.1",
"@polymer/paper-input": "^3.2.1",
"@sentry/browser": "^6.2.3", "@sentry/browser": "^6.2.3",
"@sentry/tracing": "^6.2.3", "@sentry/tracing": "^6.2.3",
"@types/chart.js": "^2.9.31", "@types/chart.js": "^2.9.31",
@ -31,7 +33,8 @@
"rollup-plugin-cssimport": "^1.0.2", "rollup-plugin-cssimport": "^1.0.2",
"rollup-plugin-external-globals": "^0.6.1", "rollup-plugin-external-globals": "^0.6.1",
"tslib": "^2.1.0", "tslib": "^2.1.0",
"webcomponent-qr-code": "^1.0.5" "webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-typescript": "^8.2.1", "@rollup/plugin-typescript": "^8.2.1",

View File

@ -1,16 +1,3 @@
export interface QueryArguments {
page?: number;
page_size?: number;
[key: string]: number | string | boolean | undefined | null;
}
export interface BaseInheritanceModel {
objectType: string;
verboseName: string;
verboseNamePlural: string;
}
export interface AKPagination { export interface AKPagination {
next?: number; next?: number;
previous?: number; previous?: number;

View File

@ -1,13 +1,5 @@
export class AdminURLManager { export class AdminURLManager {
static applications(rest: string): string {
return `/administration/applications/${rest}`;
}
static cryptoCertificates(rest: string): string {
return `/administration/crypto/certificates/${rest}`;
}
static policies(rest: string): string { static policies(rest: string): string {
return `/administration/policies/${rest}`; return `/administration/policies/${rest}`;
} }
@ -24,18 +16,10 @@ export class AdminURLManager {
return `/administration/property-mappings/${rest}`; return `/administration/property-mappings/${rest}`;
} }
static outposts(rest: string): string {
return `/administration/outposts/${rest}`;
}
static outpostServiceConnections(rest: string): string { static outpostServiceConnections(rest: string): string {
return `/administration/outpost_service_connections/${rest}`; return `/administration/outpost_service_connections/${rest}`;
} }
static flows(rest: string): string {
return `/administration/flows/${rest}`;
}
static stages(rest: string): string { static stages(rest: string): string {
return `/administration/stages/${rest}`; return `/administration/stages/${rest}`;
} }
@ -60,21 +44,6 @@ export class AdminURLManager {
return `/administration/tokens/${rest}`; return `/administration/tokens/${rest}`;
} }
static eventRules(rest: string): string {
return `/administration/events/rules/${rest}`;
}
static eventTransports(rest: string): string {
return `/administration/events/transports/${rest}`;
}
static users(rest: string): string {
return `/administration/users/${rest}`;
}
static groups(rest: string): string {
return `/administration/groups/${rest}`;
}
} }
export class UserURLManager { export class UserURLManager {
@ -105,6 +74,10 @@ export class AppURLManager {
export class FlowURLManager { export class FlowURLManager {
static defaultUnenrollment(): string {
return "/flows/-/default/unenrollment/";
}
static configure(stageUuid: string, rest: string): string { static configure(stageUuid: string, rest: string): string {
return `/flows/-/configure/${stageUuid}/${rest}`; return `/flows/-/configure/${stageUuid}/${rest}`;
} }

View File

@ -88,6 +88,7 @@ body {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--ak-accent: #fd4b2d;
--ak-dark-foreground: #fafafa; --ak-dark-foreground: #fafafa;
--ak-dark-foreground-darker: #bebebe; --ak-dark-foreground-darker: #bebebe;
--ak-dark-foreground-link: #5a5cb9; --ak-dark-foreground-link: #5a5cb9;
@ -100,6 +101,15 @@ body {
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker); --pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--pf-global--link--Color: var(--ak-dark-foreground-link); --pf-global--link--Color: var(--ak-dark-foreground-link);
} }
paper-input {
/* --paper-input-container-input-color: var(--ak-dark-foreground); */
--primary-text-color: var(--ak-dark-foreground);
}
paper-checkbox {
--primary-text-color: var(--ak-dark-foreground);
}
/* Global page background colour */ /* Global page background colour */
.pf-c-page { .pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background); --pf-c-page--BackgroundColor: var(--ak-dark-background);

View File

@ -9,3 +9,4 @@ export const EVENT_REFRESH = "ak-refresh";
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle"; export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle"; export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh"; export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh";
export const TITLE_SUFFIX = "authentik";

View File

@ -1,4 +1,5 @@
import { customElement, LitElement, property } from "lit-element"; import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import CodeMirror from "codemirror"; import CodeMirror from "codemirror";
import "codemirror/addon/display/autorefresh"; import "codemirror/addon/display/autorefresh";
@ -6,6 +7,9 @@ import "codemirror/mode/xml/xml.js";
import "codemirror/mode/yaml/yaml.js"; import "codemirror/mode/yaml/yaml.js";
import "codemirror/mode/javascript/javascript.js"; import "codemirror/mode/javascript/javascript.js";
import "codemirror/mode/python/python.js"; import "codemirror/mode/python/python.js";
import CodeMirrorStyle from "codemirror/lib/codemirror.css";
import CodeMirrorTheme from "codemirror/theme/monokai.css";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-codemirror") @customElement("ak-codemirror")
export class CodeMirrorTextarea extends LitElement { export class CodeMirrorTextarea extends LitElement {
@ -15,14 +19,20 @@ export class CodeMirrorTextarea extends LitElement {
@property() @property()
mode = "yaml"; mode = "yaml";
@property()
name?: string;
@property()
value?: string;
editor?: CodeMirror.EditorFromTextArea; editor?: CodeMirror.EditorFromTextArea;
createRenderRoot() : ShadowRoot | Element { static get styles(): CSSResult[] {
return this; return [PFForm, CodeMirrorStyle, CodeMirrorTheme];
} }
firstUpdated(): void { firstUpdated(): void {
const textarea = this.querySelector("textarea"); const textarea = this.shadowRoot?.querySelector("textarea");
if (!textarea) { if (!textarea) {
return; return;
} }
@ -37,4 +47,8 @@ export class CodeMirrorTextarea extends LitElement {
this.editor?.save(); this.editor?.save();
}); });
} }
render(): TemplateResult {
return html`<textarea class="pf-c-form-control" name=${ifDefined(this.name)}>${this.value || ""}</textarea>`;
}
} }

View 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>`;
}
}

View File

@ -23,8 +23,7 @@ export class ActionButton extends SpinnerButton {
this.setLoading(); this.setLoading();
this.apiRequest().then(() => { this.apiRequest().then(() => {
this.setDone(SUCCESS_CLASS); this.setDone(SUCCESS_CLASS);
}) }).catch((e: Error | Response) => {
.catch((e: Error | Response) => {
if (e instanceof Error) { if (e instanceof Error) {
showMessage({ showMessage({
level: MessageLevel.error, level: MessageLevel.error,

Some files were not shown because too many files have changed in this diff Show More