diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py
index 75da4fdc6..253c7fef1 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_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)
diff --git a/authentik/core/models.py b/authentik/core/models.py
index d60452dd1..0c73a5619 100644
--- a/authentik/core/models.py
+++ b/authentik/core/models.py
@@ -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"
diff --git a/authentik/core/tests/test_users_api.py b/authentik/core/tests/test_users_api.py
index 20dfbb227..643ac38e4 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
+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)
diff --git a/authentik/sources/oauth/api/source_connection.py b/authentik/sources/oauth/api/source_connection.py
index 8c18b5bea..a810a250d 100644
--- a/authentik/sources/oauth/api/source_connection.py
+++ b/authentik/sources/oauth/api/source_connection.py
@@ -38,3 +38,4 @@ class UserOAuthSourceConnectionViewSet(
filterset_fields = ["source__slug"]
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
+ ordering = ["source__slug"]
diff --git a/website/docs/expressions/_functions.md b/website/docs/expressions/_functions.md
index 7a05dd828..e65ab66f1 100644
--- a/website/docs/expressions/_functions.md
+++ b/website/docs/expressions/_functions.md
@@ -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:
diff --git a/website/docs/policies/expression.mdx b/website/docs/policies/expression.mdx
index fc387646e..5beca9b8c 100644
--- a/website/docs/policies/expression.mdx
+++ b/website/docs/policies/expression.mdx
@@ -53,7 +53,7 @@ import Objects from '../expressions/_objects.md'
- `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.
diff --git a/website/docs/property-mappings/expression.mdx b/website/docs/property-mappings/expression.mdx
index 2621d1549..0d69974b1 100644
--- a/website/docs/property-mappings/expression.mdx
+++ b/website/docs/property-mappings/expression.mdx
@@ -17,6 +17,6 @@ import Objects from '../expressions/_objects.md'
-- `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.
diff --git a/website/docs/expressions/reference/user-object.md b/website/docs/user-group/user.md
similarity index 56%
rename from website/docs/expressions/reference/user-object.md
rename to website/docs/user-group/user.md
index f56560b66..bbcdaeb7b 100644
--- a/website/docs/expressions/reference/user-object.md
+++ b/website/docs/user-group/user.md
@@ -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
diff --git a/website/sidebars.js b/website/sidebars.js
index d453a46ae..aabce2d6f 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -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",