core: add user flag to prevent users from changing their usernames
closes #1590 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -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_USERNAME, | ||||
|     USER_ATTRIBUTE_SA, | ||||
|     USER_ATTRIBUTE_TOKEN_EXPIRING, | ||||
|     Group, | ||||
| @ -113,14 +114,22 @@ class UserSelfSerializer(ModelSerializer): | ||||
|             ) | ||||
|         ) | ||||
|     ) | ||||
|     def get_groups(self, user: User): | ||||
|     def get_groups(self, _: User): | ||||
|         """Return only the group names a user is member of""" | ||||
|         for group in user.ak_groups.all(): | ||||
|         for group in self.instance.ak_groups.all(): | ||||
|             yield { | ||||
|                 "name": group.name, | ||||
|                 "pk": group.pk, | ||||
|             } | ||||
|  | ||||
|     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): | ||||
|             return username | ||||
|         if username != self.instance.username: | ||||
|             raise ValidationError("Not allowed to change username.") | ||||
|         return username | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = User | ||||
| @ -337,7 +346,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": UserSelfSerializer(request.user).data}) | ||||
|         serializer = SessionUserSerializer(data={"user": data.data}) | ||||
|         serializer.is_valid() | ||||
|         return Response(serializer.data) | ||||
|  | ||||
|  | ||||
| @ -39,6 +39,7 @@ 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_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips" | ||||
|  | ||||
| GRAVATAR_URL = "https://secure.gravatar.com" | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| from django.urls.base import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.core.models import 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 | ||||
| @ -15,6 +15,24 @@ class TestUsersAPI(APITestCase): | ||||
|         self.admin = User.objects.get(username="akadmin") | ||||
|         self.user = User.objects.create(username="test-user") | ||||
|  | ||||
|     def test_update_self(self): | ||||
|         """Test update_self""" | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.put( | ||||
|             reverse("authentik_api:user-update-self"), data={"username": "foo", "name": "foo"} | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_update_self_username_denied(self): | ||||
|         """Test update_self""" | ||||
|         self.admin.attributes[USER_ATTRIBUTE_CHANGE_USERNAME] = False | ||||
|         self.admin.save() | ||||
|         self.client.force_login(self.admin) | ||||
|         response = self.client.put( | ||||
|             reverse("authentik_api:user-update-self"), data={"username": "foo", "name": "foo"} | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 400) | ||||
|  | ||||
|     def test_metrics(self): | ||||
|         """Test user's metrics""" | ||||
|         self.client.force_login(self.admin) | ||||
|  | ||||
| @ -38,3 +38,4 @@ class UserOAuthSourceConnectionViewSet( | ||||
|     filterset_fields = ["source__slug"] | ||||
|     permission_classes = [OwnerPermissions] | ||||
|     filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||
|     ordering = ["source__slug"] | ||||
|  | ||||
| @ -32,7 +32,7 @@ return ak_is_group_member(request.user, name="test_group") | ||||
|  | ||||
| Fetch a user matching `**filters`. | ||||
|  | ||||
| Returns "None" if no user was found, otherwise [User](/docs/expressions/reference/user-object) | ||||
| Returns "None" if no user was found, otherwise [User](/docs/user-group/user) | ||||
|  | ||||
| Example: | ||||
|  | ||||
|  | ||||
| @ -53,7 +53,7 @@ import Objects from '../expressions/_objects.md' | ||||
| <Objects /> | ||||
|  | ||||
| - `request`: A PolicyRequest object, which has the following properties: | ||||
|     - `request.user`: The current user, against which the policy is applied. See [User](../expressions/reference/user-object.md) | ||||
|     - `request.user`: The current user, against which the policy is applied. See [User](../user-group/user.md#object-attributes) | ||||
|     - `request.http_request`: The Django HTTP Request. See ([Django documentation](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects)) | ||||
|     - `request.obj`: A Django Model instance. This is only set if the policy is ran against an object. | ||||
|     - `request.context`: A dictionary with dynamic data. This depends on the origin of the execution. | ||||
| @ -75,7 +75,7 @@ This includes the following: | ||||
|  | ||||
| - `context['prompt_data']`: Data which has been saved from a prompt stage or an external source. | ||||
| - `context['application']`: The application the user is in the process of authorizing. | ||||
| - `context['pending_user']`: The currently pending user, see [User](/docs/expressions/reference/user-object) | ||||
| - `context['pending_user']`: The currently pending user, see [User](../user-group/user.md#object-attributes) | ||||
| - `context['auth_method']`: Authentication method set (this value is set by password stages) | ||||
|  | ||||
|     Depending on method, `context['auth_method_args']` is also set. | ||||
|  | ||||
| @ -17,6 +17,6 @@ import Objects from '../expressions/_objects.md' | ||||
|  | ||||
| <Objects /> | ||||
|  | ||||
| - `user`: The current user. This may be `None` if there is no contextual user. See ([User](../expressions/reference/user-object.md)) | ||||
| - `user`: The current user. This may be `None` if there is no contextual user. See ([User](../user-group/user.md#object-attributes)) | ||||
| - `request`: The current request. This may be `None` if there is no contextual request. See ([Django documentation](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects)) | ||||
| - Other arbitrary arguments given by the provider, this is documented on the Provider/Source. | ||||
|  | ||||
| @ -1,7 +1,23 @@ | ||||
| --- | ||||
| title: User Object | ||||
| title: User | ||||
| --- | ||||
| 
 | ||||
| ## Attributes | ||||
| 
 | ||||
| ### `goauthentik.io/user/can-change-username` | ||||
| 
 | ||||
| Optional flag, when set to false prevents the user from changing their own username. | ||||
| 
 | ||||
| ### `goauthentik.io/user/token-expires`: | ||||
| 
 | ||||
| Optional flag, when set to false, Tokens created by the user will not expire. | ||||
| 
 | ||||
| ### `goauthentik.io/user/debug`: | ||||
| 
 | ||||
| See [Troubleshooting access problems](../troubleshooting/access.md), when set, the user gets a more detailed explanation of access decisions. | ||||
| 
 | ||||
| ## Object attributes | ||||
| 
 | ||||
| The User object has the following attributes: | ||||
| 
 | ||||
| - `username`: User's username. | ||||
| @ -11,8 +27,8 @@ The User object has the following attributes: | ||||
| - `is_active` Boolean field if user is active. | ||||
| - `date_joined` Date user joined/was created. | ||||
| - `password_change_date` Date password was last changed. | ||||
| - `attributes` Dynamic attributes. | ||||
| - `group_attributes` Merged attributes of all groups the user is member of and the user's own attributes. | ||||
| - `attributes` Dynamic attributes, see above | ||||
| - `group_attributes()` Merged attributes of all groups the user is member of and the user's own attributes. | ||||
| - `ak_groups` This is a queryset of all the user's groups. | ||||
| 
 | ||||
|     You can do additional filtering like | ||||
| @ -8,6 +8,13 @@ module.exports = { | ||||
|             type: "doc", | ||||
|             id: "terminology", | ||||
|         }, | ||||
|         { | ||||
|             type: "category", | ||||
|             label: "Users & Groups", | ||||
|             items: [ | ||||
|                 "user-group/user" | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             type: "category", | ||||
|             label: "Installation", | ||||
| @ -145,17 +152,6 @@ module.exports = { | ||||
|             label: "Property Mappings", | ||||
|             items: ["property-mappings/index", "property-mappings/expression"], | ||||
|         }, | ||||
|         { | ||||
|             type: "category", | ||||
|             label: "Expressions", | ||||
|             items: [ | ||||
|                 { | ||||
|                     type: "category", | ||||
|                     label: "Reference", | ||||
|                     items: ["expressions/reference/user-object"], | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         { | ||||
|             type: "category", | ||||
|             label: "Events", | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer