Skip to content

Commit 1256518

Browse files
committed
feat: Return password hashes in the user list API
1 parent 07e373e commit 1256518

File tree

3 files changed

+73
-1
lines changed

3 files changed

+73
-1
lines changed

authentik/core/api/users.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,24 @@ def __init__(self, *args, **kwargs):
156156
required=False, child=ChoiceField(choices=get_permission_choices())
157157
)
158158

159+
if self._should_show_passwords:
160+
self.fields["password"] = CharField(required=False, allow_null=True)
161+
162+
@property
163+
def _should_show_passwords(self) -> bool:
164+
request: Request = self.context.get("request", None)
165+
166+
if not request:
167+
return False
168+
169+
if request.query_params.get("include_password", "true") != "true":
170+
return False
171+
172+
if not request.user.has_perm("authentik_core.view_password_hashes"):
173+
return False
174+
175+
return True
176+
159177
def create(self, validated_data: dict) -> User:
160178
"""If this serializer is used in the blueprint context, we allow for
161179
directly setting a password. However should be done via the `set_password`
@@ -419,6 +437,7 @@ def get_queryset(self):
419437
@extend_schema(
420438
parameters=[
421439
OpenApiParameter("include_groups", bool, default=True),
440+
OpenApiParameter("include_password", bool, default=False),
422441
]
423442
)
424443
def list(self, request, *args, **kwargs):

authentik/core/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ class Meta:
282282
permissions = [
283283
("reset_user_password", _("Reset Password")),
284284
("impersonate", _("Can impersonate other users")),
285+
("view_password_hashes", _("Can read password hashes from other users")),
285286
("assign_user_permissions", _("Can assign permissions to users")),
286287
("unassign_user_permissions", _("Can unassign permissions from users")),
287288
("preview_user", _("Can preview user data sent to providers")),

authentik/core/tests/test_users_api.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from json import loads
55

66
from django.urls.base import reverse
7+
from guardian.shortcuts import assign_perm
78
from rest_framework.test import APITestCase
89

910
from authentik.brands.models import Brand
@@ -78,8 +79,59 @@ def test_filter_is_superuser(self):
7879
def test_list_with_groups(self):
7980
"""Test listing with groups"""
8081
self.client.force_login(self.admin)
81-
response = self.client.get(reverse("authentik_api:user-list"), {"include_groups": "true"})
82+
response = self.client.get(
83+
reverse("authentik_api:user-list"),
84+
data={"include_groups": True},
85+
)
86+
self.assertEqual(response.status_code, 200)
87+
88+
def test_list_with_passwords(self):
89+
"""Test listing with groups"""
90+
User.objects.all().delete()
91+
admin = create_test_admin_user()
92+
self.client.force_login(admin)
93+
94+
response = self.client.get(
95+
reverse("authentik_api:user-list"),
96+
data={"include_password": "true"},
97+
)
98+
self.assertEqual(response.status_code, 200)
99+
body = loads(response.content)
100+
self.assertIsNotNone(body["results"][0].get("password"))
101+
102+
def test_list_with_passwords_no_perm(self):
103+
"""Test listing with groups not having permissions"""
104+
User.objects.all().delete()
105+
user = create_test_user()
106+
assign_perm("authentik_core.view_user", user)
107+
self.client.force_login(user)
108+
response = self.client.get(
109+
reverse("authentik_api:user-list"),
110+
data={
111+
"include_password": "true",
112+
},
113+
)
82114
self.assertEqual(response.status_code, 200)
115+
body = loads(response.content)
116+
self.assertIsNone(body["results"][0].get("password"))
117+
118+
def test_list_with_passwords_with_perm(self):
119+
"""Test listing with groups not having permissions"""
120+
User.objects.all().delete()
121+
user = create_test_user()
122+
assign_perm("authentik_core.view_user", user)
123+
assign_perm("authentik_core.view_password_hashes", user)
124+
self.client.force_login(user)
125+
126+
response = self.client.get(
127+
reverse("authentik_api:user-list"),
128+
date={
129+
"include_password": "true",
130+
},
131+
)
132+
self.assertEqual(response.status_code, 200)
133+
body = loads(response.content)
134+
self.assertIsNotNone(body["results"][0].get("password"))
83135

84136
def test_recovery_no_flow(self):
85137
"""Test user recovery link (no recovery flow set)"""

0 commit comments

Comments
 (0)