diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 253c7fef1..e6f05a62f 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -45,6 +45,7 @@ from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER from authentik.core.models import ( + USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, USER_ATTRIBUTE_SA, USER_ATTRIBUTE_TOKEN_EXPIRING, @@ -122,6 +123,14 @@ class UserSelfSerializer(ModelSerializer): "pk": group.pk, } + def validate_email(self, email: str): + """Check if the user is allowed to change their email""" + if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_EMAIL, True): + return email + if email != self.instance.email: + raise ValidationError("Not allowed to change email.") + return email + def validate_username(self, username: str): """Check if the user is allowed to change their username""" if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_USERNAME, True): @@ -320,13 +329,14 @@ class UserViewSet(UsedByMixin, ModelViewSet): # pylint: disable=invalid-name def me(self, request: Request) -> Response: """Get information about current user""" - serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data}) + serializer = SessionUserSerializer( + data={"user": UserSelfSerializer(instance=request.user).data} + ) if SESSION_IMPERSONATE_USER in request._request.session: serializer.initial_data["original"] = UserSelfSerializer( - request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER] + instance=request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER] ).data - serializer.is_valid() - return Response(serializer.data) + return Response(serializer.initial_data) @extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)}) @action( @@ -346,9 +356,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): # since it caches the full object if SESSION_IMPERSONATE_USER in request.session: request.session[SESSION_IMPERSONATE_USER] = new_user - serializer = SessionUserSerializer(data={"user": data.data}) - serializer.is_valid() - return Response(serializer.data) + return Response({"user": data.data}) @permission_required("authentik_core.view_user", ["authentik_events.view_event"]) @extend_schema(responses={200: UserMetricsSerializer(many=False)}) diff --git a/authentik/core/models.py b/authentik/core/models.py index 0c73a5619..e27a104b7 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -39,7 +39,8 @@ USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account" USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources" USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec -USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username" # nosec +USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username" +USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email" USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips" GRAVATAR_URL = "https://secure.gravatar.com" diff --git a/authentik/core/tests/test_users_api.py b/authentik/core/tests/test_users_api.py index 643ac38e4..28b4a00e4 100644 --- a/authentik/core/tests/test_users_api.py +++ b/authentik/core/tests/test_users_api.py @@ -2,7 +2,7 @@ from django.urls.base import reverse from rest_framework.test import APITestCase -from authentik.core.models import USER_ATTRIBUTE_CHANGE_USERNAME, User +from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User from authentik.flows.models import Flow, FlowDesignation from authentik.stages.email.models import EmailStage from authentik.tenants.models import Tenant @@ -33,6 +33,16 @@ class TestUsersAPI(APITestCase): ) self.assertEqual(response.status_code, 400) + def test_update_self_email_denied(self): + """Test update_self""" + self.admin.attributes[USER_ATTRIBUTE_CHANGE_EMAIL] = False + self.admin.save() + self.client.force_login(self.admin) + response = self.client.put( + reverse("authentik_api:user-update-self"), data={"email": "foo", "name": "foo"} + ) + self.assertEqual(response.status_code, 400) + def test_metrics(self): """Test user's metrics""" self.client.force_login(self.admin) diff --git a/website/docs/user-group/user.md b/website/docs/user-group/user.md index bbcdaeb7b..bf7193a80 100644 --- a/website/docs/user-group/user.md +++ b/website/docs/user-group/user.md @@ -8,6 +8,10 @@ title: User Optional flag, when set to false prevents the user from changing their own username. +### `goauthentik.io/user/can-change-email` + +Optional flag, when set to false prevents the user from changing their own email. + ### `goauthentik.io/user/token-expires`: Optional flag, when set to false, Tokens created by the user will not expire.