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:
parent
6a95de4e8a
commit
61fab497cf
|
@ -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 a new issue