2019-10-28 13:44:46 +00:00
|
|
|
"""User API Views"""
|
2021-09-16 07:57:34 +00:00
|
|
|
from datetime import timedelta
|
2021-05-04 16:14:25 +00:00
|
|
|
from json import loads
|
2022-04-06 08:41:35 +00:00
|
|
|
from typing import Any, Optional
|
2021-05-04 16:14:25 +00:00
|
|
|
|
2022-01-03 12:30:39 +00:00
|
|
|
from django.contrib.auth import update_session_auth_hash
|
2021-05-04 23:02:47 +00:00
|
|
|
from django.db.models.query import QuerySet
|
2021-08-24 18:09:22 +00:00
|
|
|
from django.db.transaction import atomic
|
|
|
|
from django.db.utils import IntegrityError
|
2021-03-29 13:36:35 +00:00
|
|
|
from django.urls import reverse_lazy
|
|
|
|
from django.utils.http import urlencode
|
2021-09-20 11:43:25 +00:00
|
|
|
from django.utils.text import slugify
|
2021-09-16 07:57:34 +00:00
|
|
|
from django.utils.timezone import now
|
2021-08-10 11:54:59 +00:00
|
|
|
from django.utils.translation import gettext as _
|
2021-08-21 14:23:07 +00:00
|
|
|
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
|
2021-05-04 16:14:25 +00:00
|
|
|
from django_filters.filterset import FilterSet
|
2021-08-10 11:54:59 +00:00
|
|
|
from drf_spectacular.types import OpenApiTypes
|
2021-08-24 18:09:22 +00:00
|
|
|
from drf_spectacular.utils import (
|
|
|
|
OpenApiParameter,
|
2022-05-08 14:48:53 +00:00
|
|
|
OpenApiResponse,
|
2021-08-24 18:09:22 +00:00
|
|
|
extend_schema,
|
|
|
|
extend_schema_field,
|
|
|
|
inline_serializer,
|
|
|
|
)
|
2021-08-10 11:54:59 +00:00
|
|
|
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
2020-11-22 12:43:41 +00:00
|
|
|
from rest_framework.decorators import action
|
2022-06-25 22:46:40 +00:00
|
|
|
from rest_framework.fields import (
|
|
|
|
CharField,
|
|
|
|
IntegerField,
|
|
|
|
JSONField,
|
|
|
|
ListField,
|
|
|
|
SerializerMethodField,
|
|
|
|
)
|
2020-11-22 12:43:41 +00:00
|
|
|
from rest_framework.request import Request
|
|
|
|
from rest_framework.response import Response
|
2021-05-04 18:57:48 +00:00
|
|
|
from rest_framework.serializers import (
|
|
|
|
BooleanField,
|
|
|
|
ListSerializer,
|
|
|
|
ModelSerializer,
|
2021-08-10 17:31:30 +00:00
|
|
|
PrimaryKeyRelatedField,
|
2021-05-04 18:57:48 +00:00
|
|
|
ValidationError,
|
|
|
|
)
|
2020-11-22 18:36:40 +00:00
|
|
|
from rest_framework.viewsets import ModelViewSet
|
2021-05-04 23:02:47 +00:00
|
|
|
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
2021-08-10 11:54:59 +00:00
|
|
|
from structlog.stdlib import get_logger
|
2019-10-28 13:27:43 +00:00
|
|
|
|
2021-12-14 20:33:19 +00:00
|
|
|
from authentik.admin.api.metrics import CoordinateSerializer
|
2021-03-29 13:36:35 +00:00
|
|
|
from authentik.api.decorators import permission_required
|
2021-04-26 21:25:03 +00:00
|
|
|
from authentik.core.api.groups import GroupSerializer
|
2021-06-10 09:58:12 +00:00
|
|
|
from authentik.core.api.used_by import UsedByMixin
|
2021-04-11 21:05:19 +00:00
|
|
|
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
2022-05-31 19:53:23 +00:00
|
|
|
from authentik.core.middleware import (
|
|
|
|
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
|
|
|
|
SESSION_KEY_IMPERSONATE_USER,
|
|
|
|
)
|
2021-08-24 18:09:22 +00:00
|
|
|
from authentik.core.models import (
|
|
|
|
USER_ATTRIBUTE_SA,
|
|
|
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
2022-06-15 10:12:26 +00:00
|
|
|
USER_PATH_SERVICE_ACCOUNT,
|
2021-08-24 18:09:22 +00:00
|
|
|
Group,
|
|
|
|
Token,
|
|
|
|
TokenIntents,
|
|
|
|
User,
|
|
|
|
)
|
2021-03-20 16:30:01 +00:00
|
|
|
from authentik.events.models import EventAction
|
2022-07-02 12:17:41 +00:00
|
|
|
from authentik.flows.models import FlowToken
|
|
|
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
|
|
|
from authentik.flows.views.executor import QS_KEY_TOKEN
|
2021-08-10 11:54:59 +00:00
|
|
|
from authentik.stages.email.models import EmailStage
|
|
|
|
from authentik.stages.email.tasks import send_mails
|
|
|
|
from authentik.stages.email.utils import TemplateEmailMessage
|
2021-05-29 16:30:55 +00:00
|
|
|
from authentik.tenants.models import Tenant
|
2019-10-28 13:27:43 +00:00
|
|
|
|
2021-08-10 11:54:59 +00:00
|
|
|
LOGGER = get_logger()
|
|
|
|
|
2019-10-28 13:27:43 +00:00
|
|
|
|
|
|
|
class UserSerializer(ModelSerializer):
|
2019-10-28 13:44:46 +00:00
|
|
|
"""User Serializer"""
|
2019-10-28 13:27:43 +00:00
|
|
|
|
2020-09-15 20:37:31 +00:00
|
|
|
is_superuser = BooleanField(read_only=True)
|
2021-02-25 14:20:59 +00:00
|
|
|
avatar = CharField(read_only=True)
|
2021-04-11 21:20:45 +00:00
|
|
|
attributes = JSONField(validators=[is_dict], required=False)
|
2021-08-10 17:31:30 +00:00
|
|
|
groups = PrimaryKeyRelatedField(
|
2021-08-11 18:16:45 +00:00
|
|
|
allow_empty=True, many=True, source="ak_groups", queryset=Group.objects.all()
|
2021-08-10 17:31:30 +00:00
|
|
|
)
|
|
|
|
groups_obj = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
|
2021-05-05 09:48:16 +00:00
|
|
|
uid = CharField(read_only=True)
|
2022-05-24 17:40:54 +00:00
|
|
|
username = CharField(max_length=150)
|
2020-09-15 20:37:31 +00:00
|
|
|
|
2022-06-15 10:12:26 +00:00
|
|
|
def validate_path(self, path: str) -> str:
|
|
|
|
"""Validate path"""
|
|
|
|
if path[:1] == "/" or path[-1] == "/":
|
|
|
|
raise ValidationError(_("No leading or trailing slashes allowed."))
|
|
|
|
for segment in path.split("/"):
|
|
|
|
if segment == "":
|
|
|
|
raise ValidationError(_("No empty segments in user path allowed."))
|
|
|
|
return path
|
|
|
|
|
2019-10-28 13:27:43 +00:00
|
|
|
class Meta:
|
|
|
|
|
|
|
|
model = User
|
2021-02-19 17:43:57 +00:00
|
|
|
fields = [
|
|
|
|
"pk",
|
|
|
|
"username",
|
|
|
|
"name",
|
|
|
|
"is_active",
|
|
|
|
"last_login",
|
|
|
|
"is_superuser",
|
2021-04-26 17:51:24 +00:00
|
|
|
"groups",
|
2021-08-10 17:31:30 +00:00
|
|
|
"groups_obj",
|
2021-02-19 17:43:57 +00:00
|
|
|
"email",
|
|
|
|
"avatar",
|
2021-02-19 17:59:24 +00:00
|
|
|
"attributes",
|
2021-05-05 09:48:16 +00:00
|
|
|
"uid",
|
2022-06-15 10:12:26 +00:00
|
|
|
"path",
|
2021-02-19 17:43:57 +00:00
|
|
|
]
|
2021-10-05 09:23:27 +00:00
|
|
|
extra_kwargs = {
|
|
|
|
"name": {"allow_blank": True},
|
|
|
|
}
|
2019-10-28 13:27:43 +00:00
|
|
|
|
|
|
|
|
2021-08-05 15:38:48 +00:00
|
|
|
class UserSelfSerializer(ModelSerializer):
|
2022-04-06 08:41:35 +00:00
|
|
|
"""User Serializer for information a user can retrieve about themselves"""
|
2021-08-05 15:38:48 +00:00
|
|
|
|
|
|
|
is_superuser = BooleanField(read_only=True)
|
|
|
|
avatar = CharField(read_only=True)
|
2021-10-05 10:31:25 +00:00
|
|
|
groups = SerializerMethodField()
|
2021-08-05 15:38:48 +00:00
|
|
|
uid = CharField(read_only=True)
|
2022-04-06 08:41:35 +00:00
|
|
|
settings = SerializerMethodField()
|
2021-08-05 15:38:48 +00:00
|
|
|
|
2021-10-05 11:10:44 +00:00
|
|
|
@extend_schema_field(
|
|
|
|
ListSerializer(
|
|
|
|
child=inline_serializer(
|
|
|
|
"UserSelfGroups",
|
|
|
|
{"name": CharField(read_only=True), "pk": CharField(read_only=True)},
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2021-10-20 14:01:41 +00:00
|
|
|
def get_groups(self, _: User):
|
2021-10-05 10:31:25 +00:00
|
|
|
"""Return only the group names a user is member of"""
|
2021-10-20 14:01:41 +00:00
|
|
|
for group in self.instance.ak_groups.all():
|
2021-10-05 11:10:44 +00:00
|
|
|
yield {
|
|
|
|
"name": group.name,
|
|
|
|
"pk": group.pk,
|
|
|
|
}
|
2021-10-05 10:31:25 +00:00
|
|
|
|
2022-04-06 08:41:35 +00:00
|
|
|
def get_settings(self, user: User) -> dict[str, Any]:
|
|
|
|
"""Get user settings with tenant and group settings applied"""
|
|
|
|
return user.group_attributes(self._context["request"]).get("settings", {})
|
|
|
|
|
2021-08-05 15:38:48 +00:00
|
|
|
class Meta:
|
|
|
|
|
|
|
|
model = User
|
|
|
|
fields = [
|
|
|
|
"pk",
|
|
|
|
"username",
|
|
|
|
"name",
|
|
|
|
"is_active",
|
|
|
|
"is_superuser",
|
|
|
|
"groups",
|
|
|
|
"email",
|
|
|
|
"avatar",
|
|
|
|
"uid",
|
2021-10-09 13:35:18 +00:00
|
|
|
"settings",
|
2021-08-05 15:38:48 +00:00
|
|
|
]
|
|
|
|
extra_kwargs = {
|
|
|
|
"is_active": {"read_only": True},
|
2021-10-05 09:23:27 +00:00
|
|
|
"name": {"allow_blank": True},
|
2021-08-05 15:38:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-03-30 13:50:00 +00:00
|
|
|
class SessionUserSerializer(PassiveSerializer):
|
2021-03-22 12:44:17 +00:00
|
|
|
"""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."""
|
|
|
|
|
2021-08-05 15:38:48 +00:00
|
|
|
user = UserSelfSerializer()
|
|
|
|
original = UserSelfSerializer(required=False)
|
2021-03-22 12:44:17 +00:00
|
|
|
|
|
|
|
|
2021-03-30 13:50:00 +00:00
|
|
|
class UserMetricsSerializer(PassiveSerializer):
|
2021-03-20 16:30:01 +00:00
|
|
|
"""User Metrics"""
|
|
|
|
|
|
|
|
logins_per_1h = SerializerMethodField()
|
|
|
|
logins_failed_per_1h = SerializerMethodField()
|
|
|
|
authorizations_per_1h = SerializerMethodField()
|
|
|
|
|
2021-05-15 21:57:28 +00:00
|
|
|
@extend_schema_field(CoordinateSerializer(many=True))
|
2021-03-20 16:30:01 +00:00
|
|
|
def get_logins_per_1h(self, _):
|
|
|
|
"""Get successful logins per hour for the last 24 hours"""
|
2021-04-03 18:33:59 +00:00
|
|
|
user = self.context["user"]
|
2021-12-14 20:33:19 +00:00
|
|
|
return (
|
|
|
|
get_objects_for_user(user, "authentik_events.view_event")
|
|
|
|
.filter(action=EventAction.LOGIN, user__pk=user.pk)
|
|
|
|
.get_events_per_hour()
|
|
|
|
)
|
2021-03-20 16:30:01 +00:00
|
|
|
|
2021-05-15 21:57:28 +00:00
|
|
|
@extend_schema_field(CoordinateSerializer(many=True))
|
2021-03-20 16:30:01 +00:00
|
|
|
def get_logins_failed_per_1h(self, _):
|
|
|
|
"""Get failed logins per hour for the last 24 hours"""
|
2021-04-03 18:33:59 +00:00
|
|
|
user = self.context["user"]
|
2021-12-14 20:33:19 +00:00
|
|
|
return (
|
|
|
|
get_objects_for_user(user, "authentik_events.view_event")
|
|
|
|
.filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
|
|
|
|
.get_events_per_hour()
|
|
|
|
)
|
2021-03-20 16:30:01 +00:00
|
|
|
|
2021-05-15 21:57:28 +00:00
|
|
|
@extend_schema_field(CoordinateSerializer(many=True))
|
2021-03-20 16:30:01 +00:00
|
|
|
def get_authorizations_per_1h(self, _):
|
|
|
|
"""Get failed logins per hour for the last 24 hours"""
|
2021-04-03 18:33:59 +00:00
|
|
|
user = self.context["user"]
|
2021-12-14 20:33:19 +00:00
|
|
|
return (
|
|
|
|
get_objects_for_user(user, "authentik_events.view_event")
|
|
|
|
.filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
|
|
|
|
.get_events_per_hour()
|
|
|
|
)
|
2021-03-20 16:30:01 +00:00
|
|
|
|
|
|
|
|
2021-05-04 16:14:25 +00:00
|
|
|
class UsersFilter(FilterSet):
|
|
|
|
"""Filter for users"""
|
|
|
|
|
|
|
|
attributes = CharFilter(
|
|
|
|
field_name="attributes",
|
|
|
|
lookup_expr="",
|
|
|
|
label="Attributes",
|
|
|
|
method="filter_attributes",
|
|
|
|
)
|
|
|
|
|
2021-05-06 11:58:58 +00:00
|
|
|
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
2022-03-14 09:35:55 +00:00
|
|
|
uuid = CharFilter(field_name="uuid")
|
2021-05-06 11:44:42 +00:00
|
|
|
|
2022-06-15 10:12:26 +00:00
|
|
|
path = CharFilter(
|
|
|
|
field_name="path",
|
|
|
|
)
|
|
|
|
path_startswith = CharFilter(field_name="path", lookup_expr="startswith")
|
|
|
|
|
2021-08-21 14:23:07 +00:00
|
|
|
groups_by_name = ModelMultipleChoiceFilter(
|
2021-08-21 15:53:09 +00:00
|
|
|
field_name="ak_groups__name",
|
|
|
|
to_field_name="name",
|
2021-08-21 14:23:07 +00:00
|
|
|
queryset=Group.objects.all(),
|
|
|
|
)
|
|
|
|
groups_by_pk = ModelMultipleChoiceFilter(
|
|
|
|
field_name="ak_groups",
|
|
|
|
queryset=Group.objects.all(),
|
|
|
|
)
|
|
|
|
|
2021-05-04 16:14:25 +00:00
|
|
|
# pylint: disable=unused-argument
|
|
|
|
def filter_attributes(self, queryset, name, value):
|
|
|
|
"""Filter attributes by query args"""
|
|
|
|
try:
|
|
|
|
value = loads(value)
|
|
|
|
except ValueError:
|
|
|
|
raise ValidationError(detail="filter: failed to parse JSON")
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
raise ValidationError(detail="filter: value must be key:value mapping")
|
|
|
|
qs = {}
|
|
|
|
for key, _value in value.items():
|
|
|
|
qs[f"attributes__{key}"] = _value
|
2021-12-09 18:53:47 +00:00
|
|
|
try:
|
|
|
|
_ = len(queryset.filter(**qs))
|
|
|
|
return queryset.filter(**qs)
|
|
|
|
except ValueError:
|
|
|
|
return queryset
|
2021-05-04 16:14:25 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = User
|
2021-07-22 18:10:37 +00:00
|
|
|
fields = [
|
|
|
|
"username",
|
|
|
|
"email",
|
|
|
|
"name",
|
|
|
|
"is_active",
|
|
|
|
"is_superuser",
|
|
|
|
"attributes",
|
2021-08-21 14:23:07 +00:00
|
|
|
"groups_by_name",
|
|
|
|
"groups_by_pk",
|
2021-07-22 18:10:37 +00:00
|
|
|
]
|
2021-05-04 16:14:25 +00:00
|
|
|
|
|
|
|
|
2021-06-10 09:58:12 +00:00
|
|
|
class UserViewSet(UsedByMixin, ModelViewSet):
|
2019-10-28 13:44:46 +00:00
|
|
|
"""User Viewset"""
|
2019-10-28 13:27:43 +00:00
|
|
|
|
2021-01-11 17:43:59 +00:00
|
|
|
queryset = User.objects.none()
|
2021-10-05 09:26:41 +00:00
|
|
|
ordering = ["username"]
|
2019-10-28 13:27:43 +00:00
|
|
|
serializer_class = UserSerializer
|
2022-03-14 09:35:55 +00:00
|
|
|
search_fields = ["username", "name", "is_active", "email", "uuid"]
|
2021-05-04 16:14:25 +00:00
|
|
|
filterset_class = UsersFilter
|
2020-11-22 12:43:41 +00:00
|
|
|
|
2021-05-21 19:04:56 +00:00
|
|
|
def get_queryset(self): # pragma: no cover
|
2020-12-25 22:45:28 +00:00
|
|
|
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
|
|
|
|
2021-08-10 11:54:59 +00:00
|
|
|
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
|
|
|
|
"""Create a recovery link (when the current tenant has a recovery flow set),
|
|
|
|
that can either be shown to an admin or sent to the user directly"""
|
|
|
|
tenant: Tenant = self.request._request.tenant
|
|
|
|
# Check that there is a recovery flow, if not return an error
|
|
|
|
flow = tenant.flow_recovery
|
|
|
|
if not flow:
|
|
|
|
LOGGER.debug("No recovery flow set")
|
|
|
|
return None, None
|
|
|
|
user: User = self.get_object()
|
2022-07-02 12:17:41 +00:00
|
|
|
planner = FlowPlanner(flow)
|
|
|
|
planner.allow_empty_flows = True
|
|
|
|
plan = planner.plan(
|
|
|
|
self.request._request,
|
|
|
|
{
|
|
|
|
PLAN_CONTEXT_PENDING_USER: user,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
token, __ = FlowToken.objects.update_or_create(
|
2021-08-10 11:54:59 +00:00
|
|
|
identifier=f"{user.uid}-password-reset",
|
2022-07-02 12:17:41 +00:00
|
|
|
defaults={
|
|
|
|
"user": user,
|
|
|
|
"flow": flow,
|
|
|
|
"_plan": FlowToken.pickle(plan),
|
|
|
|
},
|
2021-08-10 11:54:59 +00:00
|
|
|
)
|
2022-07-02 12:17:41 +00:00
|
|
|
querystring = urlencode({QS_KEY_TOKEN: token.key})
|
2021-08-10 11:54:59 +00:00
|
|
|
link = self.request.build_absolute_uri(
|
|
|
|
reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
|
|
|
+ f"?{querystring}"
|
|
|
|
)
|
|
|
|
return link, token
|
|
|
|
|
2021-08-24 18:09:22 +00:00
|
|
|
@permission_required(None, ["authentik_core.add_user", "authentik_core.add_token"])
|
|
|
|
@extend_schema(
|
|
|
|
request=inline_serializer(
|
|
|
|
"UserServiceAccountSerializer",
|
|
|
|
{
|
|
|
|
"name": CharField(required=True),
|
|
|
|
"create_group": BooleanField(default=False),
|
|
|
|
},
|
|
|
|
),
|
|
|
|
responses={
|
|
|
|
200: inline_serializer(
|
|
|
|
"UserServiceAccountResponse",
|
|
|
|
{
|
|
|
|
"username": CharField(required=True),
|
|
|
|
"token": CharField(required=True),
|
2022-06-25 22:46:40 +00:00
|
|
|
"user_uid": CharField(required=True),
|
|
|
|
"user_pk": IntegerField(required=True),
|
|
|
|
"group_pk": CharField(required=False),
|
2021-08-24 18:09:22 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
@action(detail=False, methods=["POST"], pagination_class=None, filter_backends=[])
|
|
|
|
def service_account(self, request: Request) -> Response:
|
|
|
|
"""Create a new user account that is marked as a service account"""
|
|
|
|
username = request.data.get("name")
|
|
|
|
create_group = request.data.get("create_group", False)
|
|
|
|
with atomic():
|
|
|
|
try:
|
|
|
|
user = User.objects.create(
|
|
|
|
username=username,
|
|
|
|
name=username,
|
|
|
|
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
2022-06-15 10:12:26 +00:00
|
|
|
path=USER_PATH_SERVICE_ACCOUNT,
|
2021-08-24 18:09:22 +00:00
|
|
|
)
|
2022-06-25 22:46:40 +00:00
|
|
|
response = {
|
|
|
|
"username": user.username,
|
|
|
|
"user_uid": user.uid,
|
|
|
|
"user_pk": user.pk,
|
|
|
|
}
|
2021-12-06 11:33:29 +00:00
|
|
|
if create_group and self.request.user.has_perm("authentik_core.add_group"):
|
2021-08-24 18:09:22 +00:00
|
|
|
group = Group.objects.create(
|
|
|
|
name=username,
|
|
|
|
)
|
|
|
|
group.users.add(user)
|
2022-06-25 22:46:40 +00:00
|
|
|
response["group_pk"] = str(group.pk)
|
2021-08-24 18:09:22 +00:00
|
|
|
token = Token.objects.create(
|
2021-09-20 11:43:25 +00:00
|
|
|
identifier=slugify(f"service-account-{username}-password"),
|
2021-08-24 18:09:22 +00:00
|
|
|
intent=TokenIntents.INTENT_APP_PASSWORD,
|
|
|
|
user=user,
|
2021-09-16 07:57:34 +00:00
|
|
|
expires=now() + timedelta(days=360),
|
2021-08-24 18:09:22 +00:00
|
|
|
)
|
2022-06-25 22:46:40 +00:00
|
|
|
response["token"] = token.key
|
|
|
|
return Response(response)
|
2021-08-24 18:09:22 +00:00
|
|
|
except (IntegrityError) as exc:
|
|
|
|
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
|
|
|
|
2021-05-15 21:57:28 +00:00
|
|
|
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
2021-04-02 11:27:18 +00:00
|
|
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
2020-11-22 18:36:40 +00:00
|
|
|
# pylint: disable=invalid-name
|
2020-11-22 12:43:41 +00:00
|
|
|
def me(self, request: Request) -> Response:
|
|
|
|
"""Get information about current user"""
|
2022-04-06 08:41:35 +00:00
|
|
|
context = {"request": request}
|
2021-10-27 09:54:19 +00:00
|
|
|
serializer = SessionUserSerializer(
|
2022-04-06 08:41:35 +00:00
|
|
|
data={"user": UserSelfSerializer(instance=request.user, context=context).data}
|
2021-10-27 09:54:19 +00:00
|
|
|
)
|
2022-05-31 19:53:23 +00:00
|
|
|
if SESSION_KEY_IMPERSONATE_USER in request._request.session:
|
2021-08-05 15:38:48 +00:00
|
|
|
serializer.initial_data["original"] = UserSelfSerializer(
|
2022-05-31 19:53:23 +00:00
|
|
|
instance=request._request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER],
|
2022-04-06 08:41:35 +00:00
|
|
|
context=context,
|
2021-03-22 12:44:17 +00:00
|
|
|
).data
|
2022-06-22 06:48:14 +00:00
|
|
|
self.request.session.modified = True
|
2021-10-27 09:54:19 +00:00
|
|
|
return Response(serializer.initial_data)
|
2021-03-20 16:30:01 +00:00
|
|
|
|
2022-01-03 12:30:39 +00:00
|
|
|
@permission_required("authentik_core.reset_user_password")
|
|
|
|
@extend_schema(
|
|
|
|
request=inline_serializer(
|
|
|
|
"UserPasswordSetSerializer",
|
|
|
|
{
|
|
|
|
"password": CharField(required=True),
|
|
|
|
},
|
|
|
|
),
|
|
|
|
responses={
|
2022-05-08 14:48:53 +00:00
|
|
|
204: OpenApiResponse(description="Successfully changed password"),
|
|
|
|
400: OpenApiResponse(description="Bad request"),
|
2022-01-03 12:30:39 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
@action(detail=True, methods=["POST"])
|
|
|
|
# pylint: disable=invalid-name, unused-argument
|
|
|
|
def set_password(self, request: Request, pk: int) -> Response:
|
|
|
|
"""Set password for user"""
|
|
|
|
user: User = self.get_object()
|
|
|
|
try:
|
|
|
|
user.set_password(request.data.get("password"))
|
|
|
|
user.save()
|
|
|
|
except (ValidationError, IntegrityError) as exc:
|
|
|
|
LOGGER.debug("Failed to set password", exc=exc)
|
|
|
|
return Response(status=400)
|
2022-05-31 19:53:23 +00:00
|
|
|
if user.pk == request.user.pk and SESSION_KEY_IMPERSONATE_USER not in self.request.session:
|
2022-01-03 12:30:39 +00:00
|
|
|
LOGGER.debug("Updating session hash after password change")
|
|
|
|
update_session_auth_hash(self.request, user)
|
|
|
|
return Response(status=204)
|
|
|
|
|
2021-03-29 15:33:55 +00:00
|
|
|
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
2021-05-15 21:57:28 +00:00
|
|
|
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
2021-04-03 18:33:59 +00:00
|
|
|
@action(detail=True, pagination_class=None, filter_backends=[])
|
|
|
|
# pylint: disable=invalid-name, unused-argument
|
|
|
|
def metrics(self, request: Request, pk: int) -> Response:
|
2021-03-20 16:30:01 +00:00
|
|
|
"""User metrics per 1h"""
|
2021-04-03 18:33:59 +00:00
|
|
|
user: User = self.get_object()
|
2021-03-20 16:30:01 +00:00
|
|
|
serializer = UserMetricsSerializer(True)
|
2021-04-03 18:33:59 +00:00
|
|
|
serializer.context["user"] = user
|
2021-03-20 16:30:01 +00:00
|
|
|
return Response(serializer.data)
|
2021-03-29 13:36:35 +00:00
|
|
|
|
|
|
|
@permission_required("authentik_core.reset_user_password")
|
2021-05-15 21:57:28 +00:00
|
|
|
@extend_schema(
|
2021-05-16 11:26:58 +00:00
|
|
|
responses={
|
|
|
|
"200": LinkSerializer(many=False),
|
2021-07-04 10:44:57 +00:00
|
|
|
"404": LinkSerializer(many=False),
|
2021-05-16 11:26:58 +00:00
|
|
|
},
|
2021-03-29 13:36:35 +00:00
|
|
|
)
|
2021-04-02 11:27:18 +00:00
|
|
|
@action(detail=True, pagination_class=None, filter_backends=[])
|
2021-03-29 13:36:35 +00:00
|
|
|
# pylint: disable=invalid-name, unused-argument
|
|
|
|
def recovery(self, request: Request, pk: int) -> Response:
|
|
|
|
"""Create a temporary link that a user can use to recover their accounts"""
|
2021-08-10 11:54:59 +00:00
|
|
|
link, _ = self._create_recovery_link()
|
|
|
|
if not link:
|
|
|
|
LOGGER.debug("Couldn't create token")
|
2021-07-04 10:44:57 +00:00
|
|
|
return Response({"link": ""}, status=404)
|
2021-03-29 13:36:35 +00:00
|
|
|
return Response({"link": link})
|
2021-05-04 23:02:47 +00:00
|
|
|
|
2021-08-10 11:54:59 +00:00
|
|
|
@permission_required("authentik_core.reset_user_password")
|
|
|
|
@extend_schema(
|
|
|
|
parameters=[
|
|
|
|
OpenApiParameter(
|
|
|
|
name="email_stage",
|
|
|
|
location=OpenApiParameter.QUERY,
|
|
|
|
type=OpenApiTypes.STR,
|
|
|
|
required=True,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
responses={
|
2022-05-08 14:48:53 +00:00
|
|
|
"204": OpenApiResponse(description="Successfully sent recover email"),
|
|
|
|
"404": OpenApiResponse(description="Bad request"),
|
2021-08-10 11:54:59 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
@action(detail=True, pagination_class=None, filter_backends=[])
|
|
|
|
# pylint: disable=invalid-name, unused-argument
|
|
|
|
def recovery_email(self, request: Request, pk: int) -> Response:
|
|
|
|
"""Create a temporary link that a user can use to recover their accounts"""
|
|
|
|
for_user = self.get_object()
|
|
|
|
if for_user.email == "":
|
|
|
|
LOGGER.debug("User doesn't have an email address")
|
|
|
|
return Response(status=404)
|
|
|
|
link, token = self._create_recovery_link()
|
|
|
|
if not link:
|
|
|
|
LOGGER.debug("Couldn't create token")
|
|
|
|
return Response(status=404)
|
|
|
|
# Lookup the email stage to assure the current user can access it
|
|
|
|
stages = get_objects_for_user(
|
|
|
|
request.user, "authentik_stages_email.view_emailstage"
|
|
|
|
).filter(pk=request.query_params.get("email_stage"))
|
|
|
|
if not stages.exists():
|
|
|
|
LOGGER.debug("Email stage does not exist/user has no permissions")
|
|
|
|
return Response(status=404)
|
|
|
|
email_stage: EmailStage = stages.first()
|
|
|
|
message = TemplateEmailMessage(
|
|
|
|
subject=_(email_stage.subject),
|
|
|
|
template_name=email_stage.template,
|
|
|
|
to=[for_user.email],
|
|
|
|
template_context={
|
|
|
|
"url": link,
|
|
|
|
"user": for_user,
|
|
|
|
"expires": token.expires,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
send_mails(email_stage, message)
|
|
|
|
return Response(status=204)
|
|
|
|
|
2021-05-04 23:02:47 +00:00
|
|
|
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
|
|
|
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
|
|
|
for backend in list(self.filter_backends):
|
|
|
|
if backend == ObjectPermissionsFilter:
|
|
|
|
continue
|
|
|
|
queryset = backend().filter_queryset(self.request, queryset, self)
|
|
|
|
return queryset
|
|
|
|
|
|
|
|
def filter_queryset(self, queryset):
|
2021-07-23 16:38:47 +00:00
|
|
|
if self.request.user.has_perm("authentik_core.view_user"):
|
2021-05-04 23:02:47 +00:00
|
|
|
return self._filter_queryset_for_list(queryset)
|
|
|
|
return super().filter_queryset(queryset)
|
2022-06-15 10:12:26 +00:00
|
|
|
|
|
|
|
@extend_schema(
|
|
|
|
responses={
|
|
|
|
200: inline_serializer(
|
|
|
|
"UserPathSerializer", {"paths": ListField(child=CharField(), read_only=True)}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
parameters=[
|
|
|
|
OpenApiParameter(
|
|
|
|
name="search",
|
|
|
|
location=OpenApiParameter.QUERY,
|
|
|
|
type=OpenApiTypes.STR,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
@action(detail=False, pagination_class=None)
|
|
|
|
def paths(self, request: Request) -> Response:
|
|
|
|
"""Get all user paths"""
|
|
|
|
return Response(
|
|
|
|
data={
|
|
|
|
"paths": list(
|
|
|
|
self.filter_queryset(self.get_queryset())
|
|
|
|
.values("path")
|
|
|
|
.distinct()
|
|
|
|
.order_by("path")
|
|
|
|
.values_list("path", flat=True)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|