2019-10-28 13:44:46 +00:00
|
|
|
"""User API Views"""
|
2021-04-16 08:09:46 +00:00
|
|
|
from django.http.response import Http404
|
2021-03-29 13:36:35 +00:00
|
|
|
from django.urls import reverse_lazy
|
|
|
|
from django.utils.http import urlencode
|
2021-03-29 18:45:45 +00:00
|
|
|
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
|
2020-12-25 21:42:53 +00:00
|
|
|
from guardian.utils import get_anonymous_user
|
2020-11-22 12:43:41 +00:00
|
|
|
from rest_framework.decorators import action
|
2021-04-11 21:05:19 +00:00
|
|
|
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
2020-11-22 12:43:41 +00:00
|
|
|
from rest_framework.request import Request
|
|
|
|
from rest_framework.response import Response
|
2021-04-26 17:51:24 +00:00
|
|
|
from rest_framework.serializers import BooleanField, ListSerializer, ModelSerializer
|
2020-11-22 18:36:40 +00:00
|
|
|
from rest_framework.viewsets import ModelViewSet
|
2019-10-28 13:27:43 +00:00
|
|
|
|
2021-03-20 16:30:01 +00:00
|
|
|
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
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-04-11 21:05:19 +00:00
|
|
|
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
2021-03-22 12:44:17 +00:00
|
|
|
from authentik.core.middleware import (
|
|
|
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
|
|
|
SESSION_IMPERSONATE_USER,
|
|
|
|
)
|
2021-03-29 13:36:35 +00:00
|
|
|
from authentik.core.models import Token, TokenIntents, User
|
2021-03-20 16:30:01 +00:00
|
|
|
from authentik.events.models import EventAction
|
2021-04-16 08:09:46 +00:00
|
|
|
from authentik.flows.models import Flow, FlowDesignation
|
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-04-26 17:51:24 +00:00
|
|
|
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
|
2020-09-15 20:37:31 +00:00
|
|
|
|
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-02-19 17:43:57 +00:00
|
|
|
"email",
|
|
|
|
"avatar",
|
2021-02-19 17:59:24 +00:00
|
|
|
"attributes",
|
2021-02-19 17:43:57 +00:00
|
|
|
]
|
2019-10-28 13:27:43 +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."""
|
|
|
|
|
|
|
|
user = UserSerializer()
|
|
|
|
original = UserSerializer(required=False)
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
|
|
|
|
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"]
|
|
|
|
return get_events_per_1h(action=EventAction.LOGIN, user__pk=user.pk)
|
2021-03-20 16:30:01 +00:00
|
|
|
|
|
|
|
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
|
|
|
|
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-03-20 16:30:01 +00:00
|
|
|
return get_events_per_1h(
|
2021-04-03 18:33:59 +00:00
|
|
|
action=EventAction.LOGIN_FAILED, context__username=user.username
|
2021-03-20 16:30:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
@swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True))
|
|
|
|
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-03-20 16:30:01 +00:00
|
|
|
return get_events_per_1h(
|
2021-04-03 18:33:59 +00:00
|
|
|
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
|
2021-03-20 16:30:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2019-10-28 13:27:43 +00:00
|
|
|
class UserViewSet(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()
|
2019-10-28 13:27:43 +00:00
|
|
|
serializer_class = UserSerializer
|
2021-02-19 17:59:24 +00:00
|
|
|
search_fields = ["username", "name", "is_active"]
|
|
|
|
filterset_fields = ["username", "name", "is_active"]
|
2020-11-22 12:43:41 +00:00
|
|
|
|
2020-12-25 22:45:28 +00:00
|
|
|
def get_queryset(self):
|
|
|
|
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
|
|
|
|
2021-03-22 12:44:17 +00:00
|
|
|
@swagger_auto_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"""
|
2021-03-22 12:44:17 +00:00
|
|
|
serializer = SessionUserSerializer(
|
|
|
|
data={"user": UserSerializer(request.user).data}
|
|
|
|
)
|
|
|
|
if SESSION_IMPERSONATE_USER in request._request.session:
|
|
|
|
serializer.initial_data["original"] = UserSerializer(
|
|
|
|
request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
|
|
|
).data
|
|
|
|
serializer.is_valid()
|
|
|
|
return Response(serializer.data)
|
2021-03-20 16:30:01 +00:00
|
|
|
|
2021-03-29 15:33:55 +00:00
|
|
|
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
2021-03-20 16:30:01 +00:00
|
|
|
@swagger_auto_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")
|
|
|
|
@swagger_auto_schema(
|
2021-04-16 08:09:46 +00:00
|
|
|
responses={"200": LinkSerializer(many=False), "404": "No recovery flow found."},
|
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-04-16 08:09:46 +00:00
|
|
|
# Check that there is a recovery flow, if not return an error
|
|
|
|
flow = Flow.with_policy(request, designation=FlowDesignation.RECOVERY)
|
|
|
|
if not flow:
|
|
|
|
raise Http404
|
2021-03-29 13:36:35 +00:00
|
|
|
user: User = self.get_object()
|
|
|
|
token, __ = Token.objects.get_or_create(
|
|
|
|
identifier=f"{user.uid}-password-reset",
|
|
|
|
user=user,
|
|
|
|
intent=TokenIntents.INTENT_RECOVERY,
|
|
|
|
)
|
|
|
|
querystring = urlencode({"token": token.key})
|
|
|
|
link = request.build_absolute_uri(
|
|
|
|
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
|
|
|
|
)
|
|
|
|
return Response({"link": link})
|