{% trans "Group users together and give them permissions based on the membership." %} -
-{% trans 'Name' %} | -{% trans 'Parent' %} | -{% trans 'Members' %} | -- |
---|---|---|---|
- - {{ group.name }} - - | -- - {{ group.parent }} - - | -- - {{ group.users.all|length }} - - | -
- |
-
{% trans "Outpost Service-Connections define how authentik connects to external platforms to manage and deploy Outposts." %}
-{% trans 'Name' %} | -{% trans 'Type' %} | -{% trans 'Local?' %} | -{% trans 'Status' %} | -- |
---|---|---|---|---|
- {{ sc.name }} - | -- - {{ sc|verbose_name }} - - | -- - {{ sc.local|yesno:"Yes,No" }} - - | -- - {% if sc.state.healthy %} - {{ sc.state.version }} - {% else %} - {% trans 'Unhealthy' %} - {% endif %} - - | -
- |
-
{% trans "Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Stages." %}
-{% trans 'Name' %} | -{% trans 'Type' %} | -- |
---|---|---|
-
-
- {{ policy.name }}
- {% if not policy.bindings.exists and not policy.promptstage_set.exists %}
-
- {% trans 'Warning: Policy is not assigned.' %}
- {% else %}
-
- {% blocktrans with object_count=policy.bindings.all|length %}Assigned to {{ object_count }} objects.{% endblocktrans %}
- {% endif %}
- |
- - - {{ policy|verbose_name }} - - | -
- |
-
{% trans "Bind existing Policies to Models accepting policies." %}
-{% trans 'Policy' %} | -{% trans 'Enabled' %} | -{% trans 'Order' %} | -{% trans 'Timeout' %} | -- |
---|---|---|---|---|
- {{ pbm }} - - {{ pbm|fieldtype }} - - | -- | - | - | - |
- {{ binding.policy }}
-
- {{ binding.policy|fieldtype }}
-
- |
-
- {{ binding.enabled }}
- |
-
- {{ binding.order }}
- |
-
- {{ binding.timeout }}
- |
-
- |
-
{% trans "Stages are single steps of a Flow that a user is guided through." %}
-{% trans 'Name' %} | -{% trans 'Flows' %} | -- |
---|---|---|
-
-
- {{ stage.name }}
- {{ stage|verbose_name }}
- |
-
-
|
-
- |
-
{% trans "Bind existing Stages to Flows." %}
-{% trans 'Order' %} | -{% trans 'Name' %} | -{% trans 'Stage Type' %} | -- |
---|---|---|---|
- {% blocktrans with slug=flow.grouper.slug %} - Flow {{ slug }} - {% endblocktrans %} - | -- | - | - |
- - {{ binding.order }} - - | -
-
-
- {{ binding.target.slug }}
-
- {{ binding.target.name }}
-
- |
-
-
-
-
- {{ binding.stage.name }}
-
-
- {{ binding.stage }}
-
- |
-
- |
-
{% trans "Create Invitation Links to enroll Users, and optionally force specific attributes of their account." %} -
-{% trans 'ID' %} | -{% trans 'Created by' %} | -{% trans 'Expiry' %} | -- |
---|---|---|---|
- - {{ invitation.invite_uuid }} - - | -- - {{ invitation.created_by }} - - | -- - {{ invitation.expiry|default:"-" }} - - | -
- |
-
{% trans "Single Prompts that can be used for Prompt Stages." %}
-{% trans 'Field' %} | -{% trans 'Label' %} | -{% trans 'Type' %} | -{% trans 'Order' %} | -{% trans 'Flows' %} | -- |
---|---|---|---|---|---|
-
-
- {{ prompt.field_key }}
- |
-
-
- {{ prompt.label }}
-
- |
-
-
- {{ prompt.type }}
-
- |
-
-
- {{ prompt.order }}
-
- |
-
-
|
-
- |
-
{% trans "Long-running operations which authentik executes in the background." %}
-{% trans 'Identifier' %} | -{% trans 'Description' %} | -{% trans 'Last Run' %} | -{% trans 'Status' %} | -{% trans 'Messages' %} | -- |
---|---|---|---|---|---|
- {{ task.html_name|join:"_" }} - | -- - {{ task.task_description }} - - | -- - {{ task.finish_timestamp|naturaltime }} - - | -- - {% if task.result.status == task_successful %} - {% trans 'Successful' %} - {% elif task.result.status == task_warning %} - {% trans 'Warning' %} - {% elif task.result.status == task_error %} - {% trans 'Error' %} - {% else %} - {% trans 'Unknown' %} - {% endif %} - - | -
- {% for message in task.result.messages %}
-
- {{ message }}
-
- {% endfor %}
- |
-
- |
-
{% trans "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access." %}
-{% trans 'Identifier' %} | -{% trans 'User' %} | -{% trans 'Expires?' %} | -{% trans 'Expiry Date' %} | -- |
---|---|---|---|---|
- {{ token.identifier }}
- |
- - - {{ token.user }} - - | -- - {{ token.expiring|yesno:"Yes,No" }} - - | -- - {% if not token.expiring %} - - - {% else %} - {{ token.expires }} - {% endif %} - - | -
- |
-
{% trans 'Name' %} | -{% trans 'Active' %} | -{% trans 'Last Login' %} | -- |
---|---|---|---|
-
-
- {{ user.username }}
- {{ user.name }}
- |
- - - {{ user.is_active }} - - | -- - {{ user.last_login }} - - | -
- |
-
%(link)s" % {"link": link}) ) - return redirect("authentik_admin:users") + return redirect("/") diff --git a/authentik/admin/views/utils.py b/authentik/admin/views/utils.py index 17c33f434..f17b1b354 100644 --- a/authentik/admin/views/utils.py +++ b/authentik/admin/views/utils.py @@ -1,15 +1,10 @@ """authentik admin util views""" -from typing import Any, Optional -from urllib.parse import urlparse +from typing import Any from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.postgres.search import SearchQuery, SearchVector -from django.db.models import QuerySet from django.http import Http404 -from django.http.request import HttpRequest -from django.views.generic import DeleteView, ListView, UpdateView -from django.views.generic.list import MultipleObjectMixin +from django.views.generic import DeleteView, UpdateView from authentik.lib.utils.reflection import all_subclasses from authentik.lib.views import CreateAssignPermView @@ -25,37 +20,6 @@ class DeleteMessageView(SuccessMessageMixin, DeleteView): return super().delete(request, *args, **kwargs) -class InheritanceListView(ListView): - """ListView for objects using InheritanceManager""" - - def get_context_data(self, **kwargs): - kwargs["types"] = {x.__name__: x for x in all_subclasses(self.model)} - return super().get_context_data(**kwargs) - - def get_queryset(self): - return super().get_queryset().select_subclasses() - - -class SearchListMixin(MultipleObjectMixin): - """Accept search query using `search` querystring parameter. Requires self.search_fields, - a list of all fields to search. Can contain special lookups like __icontains""" - - search_fields: list[str] - - def get_queryset(self) -> QuerySet: - queryset = super().get_queryset() - if "search" in self.request.GET: - raw_query = self.request.GET["search"] - if raw_query == "": - # Empty query, don't search at all - return queryset - search = SearchQuery(raw_query, search_type="websearch") - return queryset.annotate(search=SearchVector(*self.search_fields)).filter( - search=search - ) - return queryset - - class InheritanceCreateView(CreateAssignPermView): """CreateView for objects using InheritanceManager""" @@ -96,31 +60,3 @@ class InheritanceUpdateView(UpdateView): .select_subclasses() .first() ) - - -class BackSuccessUrlMixin: - """Checks if a relative URL has been given as ?back param, and redirect to it. Otherwise - default to self.success_url.""" - - request: HttpRequest - - success_url: Optional[str] - - def get_success_url(self) -> str: - """get_success_url from FormMixin""" - back_param = self.request.GET.get("back") - if back_param: - if not bool(urlparse(back_param).netloc): - return back_param - return str(self.success_url) - - -class UserPaginateListMixin: - """Get paginate_by value from user's attributes, defaulting to 15""" - - request: HttpRequest - - # pylint: disable=unused-argument - def get_paginate_by(self, queryset: QuerySet) -> int: - """get_paginate_by Function of ListView""" - return self.request.user.attributes.get("paginate_by", 15) diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index 8416166ff..df1bd26eb 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -23,12 +23,9 @@ from authentik.events.api.event import EventViewSet from authentik.events.api.notification import NotificationViewSet from authentik.events.api.notification_rule import NotificationRuleViewSet from authentik.events.api.notification_transport import NotificationTransportViewSet -from authentik.flows.api import ( - FlowCacheViewSet, - FlowStageBindingViewSet, - FlowViewSet, - StageViewSet, -) +from authentik.flows.api.bindings import FlowStageBindingViewSet +from authentik.flows.api.flows import FlowViewSet +from authentik.flows.api.stages import StageViewSet from authentik.flows.views import FlowExecutorView from authentik.outposts.api.outpost_service_connections import ( DockerServiceConnectionViewSet, @@ -36,11 +33,7 @@ from authentik.outposts.api.outpost_service_connections import ( ServiceConnectionViewSet, ) from authentik.outposts.api.outposts import OutpostViewSet -from authentik.policies.api import ( - PolicyBindingViewSet, - PolicyCacheViewSet, - PolicyViewSet, -) +from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet from authentik.policies.dummy.api import DummyPolicyViewSet from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet @@ -101,7 +94,6 @@ router.register( router.register("outposts/proxy", ProxyOutpostConfigViewSet) router.register("flows/instances", FlowViewSet) -router.register("flows/cached", FlowCacheViewSet, basename="flows_cache") router.register("flows/bindings", FlowStageBindingViewSet) router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet) @@ -117,7 +109,6 @@ router.register("sources/saml", SAMLSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet) router.register("policies/all", PolicyViewSet) -router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache") router.register("policies/bindings", PolicyBindingViewSet) router.register("policies/expression", ExpressionPolicyViewSet) router.register("policies/event_matcher", EventMatcherPolicyViewSet) @@ -146,8 +137,8 @@ router.register("stages/captcha", CaptchaStageViewSet) router.register("stages/consent", ConsentStageViewSet) router.register("stages/email", EmailStageViewSet) router.register("stages/identification", IdentificationStageViewSet) -router.register("stages/invitation", InvitationStageViewSet) router.register("stages/invitation/invitations", InvitationViewSet) +router.register("stages/invitation/stages", InvitationStageViewSet) router.register("stages/password", PasswordStageViewSet) router.register("stages/prompt/prompts", PromptViewSet) router.register("stages/prompt/stages", PromptStageViewSet) diff --git a/authentik/core/api/groups.py b/authentik/core/api/groups.py index fa1b8953d..152c1ea9f 100644 --- a/authentik/core/api/groups.py +++ b/authentik/core/api/groups.py @@ -19,3 +19,5 @@ class GroupViewSet(ModelViewSet): queryset = Group.objects.all() serializer_class = GroupSerializer + search_fields = ["name", "is_superuser"] + filterset_fields = ["name", "is_superuser"] diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py index b87865c86..147ba44fe 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/propertymappings.py @@ -1,9 +1,16 @@ """PropertyMapping API Views""" +from django.shortcuts import reverse +from drf_yasg2.utils import swagger_auto_schema +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.viewsets import ReadOnlyModelViewSet -from authentik.core.api.utils import MetaNameSerializer +from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.models import PropertyMapping +from authentik.lib.templatetags.authentik_utils import verbose_name +from authentik.lib.utils.reflection import all_subclasses class PropertyMappingSerializer(ModelSerializer, MetaNameSerializer): @@ -47,3 +54,19 @@ class PropertyMappingViewSet(ReadOnlyModelViewSet): def get_queryset(self): return PropertyMapping.objects.select_subclasses() + + @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) + @action(detail=False) + def types(self, request: Request) -> Response: + """Get all creatable property-mapping types""" + data = [] + for subclass in all_subclasses(self.queryset.model): + data.append( + { + "name": verbose_name(subclass), + "description": subclass.__doc__, + "link": reverse("authentik_admin:property-mapping-create") + + f"?type={subclass.__name__}", + } + ) + return Response(TypeCreateSerializer(data, many=True).data) diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index f30ba4f4b..aef414bff 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.viewsets import ModelViewSet +from authentik.core.api.users import UserSerializer from authentik.core.models import Token from authentik.events.models import Event, EventAction @@ -16,10 +17,21 @@ from authentik.events.models import Event, EventAction class TokenSerializer(ModelSerializer): """Token Serializer""" + user = UserSerializer() + class Meta: model = Token - fields = ["pk", "identifier", "intent", "user", "description"] + fields = [ + "pk", + "identifier", + "intent", + "user", + "description", + "expires", + "expiring", + ] + depth = 2 class TokenViewSerializer(Serializer): @@ -40,6 +52,19 @@ class TokenViewSet(ModelViewSet): lookup_field = "identifier" queryset = Token.filter_not_expired() serializer_class = TokenSerializer + search_fields = [ + "identifier", + "intent", + "user__username", + "description", + ] + filterset_fields = [ + "identifier", + "intent", + "user__username", + "description", + ] + ordering = ["expires"] @swagger_auto_schema(responses={200: TokenViewSerializer(many=False)}) @action(detail=True) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index e75d451da..573e04cd1 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -28,7 +28,17 @@ class UserSerializer(ModelSerializer): class Meta: model = User - fields = ["pk", "username", "name", "is_superuser", "email", "avatar"] + fields = [ + "pk", + "username", + "name", + "is_active", + "last_login", + "is_superuser", + "email", + "avatar", + "attributes", + ] class UserViewSet(ModelViewSet): @@ -36,6 +46,8 @@ class UserViewSet(ModelViewSet): queryset = User.objects.none() serializer_class = UserSerializer + search_fields = ["username", "name", "is_active"] + filterset_fields = ["username", "name", "is_active"] def get_queryset(self): return User.objects.all().exclude(pk=get_anonymous_user().pk) diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py index be583bdcc..b93f75155 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -1,6 +1,6 @@ """API Utilities""" from django.db.models import Model -from rest_framework.fields import CharField +from rest_framework.fields import CharField, IntegerField from rest_framework.serializers import Serializer, SerializerMethodField @@ -37,3 +37,15 @@ class TypeCreateSerializer(Serializer): def update(self, instance: Model, validated_data: dict) -> Model: raise NotImplementedError + + +class CacheSerializer(Serializer): + """Generic cache stats for an object""" + + count = IntegerField(read_only=True) + + def create(self, validated_data: dict) -> Model: + raise NotImplementedError + + def update(self, instance: Model, validated_data: dict) -> Model: + raise NotImplementedError diff --git a/authentik/core/templates/user/settings.html b/authentik/core/templates/user/settings.html index 047288c59..6305bc773 100644 --- a/authentik/core/templates/user/settings.html +++ b/authentik/core/templates/user/settings.html @@ -24,9 +24,7 @@