Skip to content

Commit 6a9f0e9

Browse files
authored
Fix API key generation when user edits are disabled
This also extends test coverage of the views of the accounts and administration packages.
1 parent b1e7591 commit 6a9f0e9

File tree

6 files changed

+484
-10
lines changed

6 files changed

+484
-10
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ module = [
6666
"src.MCPClient.lib.clientScripts.transcribe_file",
6767
"src.MCPClient.lib.clientScripts.validate_file",
6868
"tests.archivematicaCommon.test_execute_functions",
69+
"tests.dashboard.components.accounts.test_views",
70+
"tests.dashboard.components.administration.test_administration",
6971
"tests.dashboard.fpr.test_views",
7072
"tests.dashboard.test_oidc",
7173
"tests.integration.test_oidc_auth",

src/dashboard/src/components/accounts/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def save(self, commit=True):
149149

150150

151151
class ApiKeyForm(forms.Form):
152-
regenerate_api_key = forms.CharField(
152+
regenerate_api_key = forms.BooleanField(
153153
widget=forms.CheckboxInput,
154154
label="Regenerate API key (shown below)?",
155155
required=False,

src/dashboard/src/components/accounts/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def profile(request):
8888
form = ApiKeyForm(request.POST)
8989
userprofileform = UserProfileForm(request.POST, instance=user_profile)
9090
if form.is_valid() and userprofileform.is_valid():
91-
if form["regenerate_api_key"] != "":
91+
if form.cleaned_data["regenerate_api_key"]:
9292
generate_api_key(user)
9393
userprofileform.save()
9494

src/dashboard/src/components/administration/forms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ class StripCharField(forms.CharField):
211211
"""
212212

213213
def to_python(self, value):
214+
if value is None:
215+
value = ""
214216
return super(forms.CharField, self).to_python(value).strip()
215217

216218
storage_service_url = forms.CharField(

tests/dashboard/components/accounts/test_views.py

Lines changed: 272 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
1+
import hmac
2+
import uuid
3+
from hashlib import sha1
4+
from typing import Type
5+
from unittest import mock
16
from urllib.parse import parse_qs
27
from urllib.parse import urlparse
38

49
import pytest
10+
import pytest_django
11+
from components import helpers
512
from components.accounts.views import get_oidc_logout_url
13+
from django.contrib.auth.models import User
14+
from django.test import Client
15+
from django.test import RequestFactory
16+
from django.urls import reverse
17+
from tastypie.models import ApiKey
618

719

8-
def test_get_oidc_logout_url_fails_if_token_is_not_set(rf):
20+
def test_get_oidc_logout_url_fails_if_token_is_not_set(rf: RequestFactory) -> None:
921
request = rf.get("/")
1022
request.session = {}
1123

1224
with pytest.raises(ValueError, match="ID token not found in session."):
1325
get_oidc_logout_url(request)
1426

1527

16-
def test_get_oidc_logout_url_fails_if_logout_endpoint_is_not_set(rf):
28+
def test_get_oidc_logout_url_fails_if_logout_endpoint_is_not_set(
29+
rf: RequestFactory,
30+
) -> None:
1731
request = rf.get("/")
1832
request.session = {"oidc_id_token": "mytoken"}
1933

@@ -23,7 +37,9 @@ def test_get_oidc_logout_url_fails_if_logout_endpoint_is_not_set(rf):
2337
get_oidc_logout_url(request)
2438

2539

26-
def test_get_oidc_logout_url_returns_logout_url(rf, settings):
40+
def test_get_oidc_logout_url_returns_logout_url(
41+
rf: RequestFactory, settings: pytest_django.fixtures.SettingsWrapper
42+
) -> None:
2743
settings.OIDC_OP_LOGOUT_ENDPOINT = "http://example.com/logout"
2844
token = "mytoken"
2945
request = rf.get("/")
@@ -36,3 +52,256 @@ def test_get_oidc_logout_url_returns_logout_url(rf, settings):
3652
assert set(query_dict) == {"id_token_hint", "post_logout_redirect_uri"}
3753
assert query_dict["id_token_hint"] == [token]
3854
assert query_dict["post_logout_redirect_uri"] == ["http://testserver/"]
55+
56+
57+
@pytest.fixture
58+
def dashboard_uuid() -> None:
59+
helpers.set_setting("dashboard_uuid", str(uuid.uuid4()))
60+
61+
62+
@pytest.fixture
63+
def non_administrative_user(django_user_model: Type[User]) -> User:
64+
return django_user_model.objects.create_user(
65+
username="test",
66+
password="test",
67+
first_name="Foo",
68+
last_name="Bar",
69+
70+
)
71+
72+
73+
@pytest.mark.django_db
74+
def test_edit_user_view_denies_access_to_non_admin_users_editing_others(
75+
dashboard_uuid: None,
76+
non_administrative_user: User,
77+
admin_user: User,
78+
client: Client,
79+
) -> None:
80+
client.force_login(non_administrative_user)
81+
82+
response = client.get(
83+
reverse("accounts:edit", kwargs={"id": admin_user.id}), follow=True
84+
)
85+
assert response.status_code == 200
86+
87+
content = response.content.decode()
88+
assert "Forbidden" in content
89+
assert "You do not have enough access privileges for this operation" in content
90+
91+
92+
@pytest.fixture
93+
def non_administrative_user_apikey(non_administrative_user: User) -> ApiKey:
94+
return ApiKey.objects.create(user=non_administrative_user)
95+
96+
97+
@pytest.mark.django_db
98+
def test_edit_user_view_renders_user_profile_fields(
99+
dashboard_uuid: None,
100+
non_administrative_user: User,
101+
non_administrative_user_apikey: ApiKey,
102+
admin_client: Client,
103+
) -> None:
104+
response = admin_client.get(
105+
reverse("accounts:edit", kwargs={"id": non_administrative_user.id}), follow=True
106+
)
107+
assert response.status_code == 200
108+
109+
content = response.content.decode()
110+
assert f"Edit user {non_administrative_user.username}" in content
111+
assert f'name="username" value="{non_administrative_user.username}"' in content
112+
assert f'name="first_name" value="{non_administrative_user.first_name}"' in content
113+
assert f'name="last_name" value="{non_administrative_user.last_name}"' in content
114+
assert f'name="email" value="{non_administrative_user.email}"' in content
115+
assert f"<code>{non_administrative_user_apikey.key}</code>" in content
116+
117+
118+
@pytest.mark.django_db
119+
def test_edit_user_view_updates_user_profile_fields(
120+
dashboard_uuid: None,
121+
non_administrative_user: User,
122+
admin_client: Client,
123+
) -> None:
124+
new_username = "newusername"
125+
new_password = "newpassword"
126+
new_first_name = "bar"
127+
new_last_name = "foo"
128+
new_email = "[email protected]"
129+
data = {
130+
"username": new_username,
131+
"password": new_password,
132+
"password_confirmation": new_password,
133+
"first_name": new_first_name,
134+
"last_name": new_last_name,
135+
"email": new_email,
136+
}
137+
138+
response = admin_client.post(
139+
reverse("accounts:edit", kwargs={"id": non_administrative_user.id}),
140+
data,
141+
follow=True,
142+
)
143+
assert response.status_code == 200
144+
145+
content = response.content.decode()
146+
assert "Saved" in content
147+
assert (
148+
f'<a href="{reverse("accounts:edit", kwargs={"id": non_administrative_user.id})}">{new_username}</a>'
149+
in content
150+
)
151+
assert f"<td>{new_first_name} {new_last_name}</td>" in content
152+
assert f"<td>{new_email}</td>" in content
153+
154+
non_administrative_user.refresh_from_db()
155+
assert non_administrative_user.check_password(new_password)
156+
157+
158+
@pytest.mark.django_db
159+
def test_edit_user_view_regenerates_api_key(
160+
dashboard_uuid: None,
161+
non_administrative_user: User,
162+
non_administrative_user_apikey: ApiKey,
163+
admin_client: Client,
164+
) -> None:
165+
data = {
166+
"username": non_administrative_user.username,
167+
"first_name": non_administrative_user.first_name,
168+
"last_name": non_administrative_user.last_name,
169+
"email": non_administrative_user.email,
170+
"regenerate_api_key": True,
171+
}
172+
expected_uuid = uuid.uuid4()
173+
expected_key = hmac.new(expected_uuid.bytes, digestmod=sha1).hexdigest()
174+
175+
with mock.patch("uuid.uuid4", return_value=expected_uuid):
176+
response = admin_client.post(
177+
reverse("accounts:edit", kwargs={"id": non_administrative_user.id}),
178+
data,
179+
follow=True,
180+
)
181+
assert response.status_code == 200
182+
183+
assert "Saved" in response.content.decode()
184+
185+
non_administrative_user_apikey.refresh_from_db()
186+
assert non_administrative_user_apikey.key == expected_key
187+
188+
189+
@pytest.mark.django_db
190+
def test_user_profile_view_allows_users_to_edit_their_profile_fields(
191+
dashboard_uuid: None,
192+
non_administrative_user: User,
193+
non_administrative_user_apikey: ApiKey,
194+
client: Client,
195+
settings: pytest_django.fixtures.SettingsWrapper,
196+
) -> None:
197+
settings.ALLOW_USER_EDITS = True
198+
client.force_login(non_administrative_user)
199+
200+
response = client.get(
201+
reverse("accounts:profile"),
202+
follow=True,
203+
)
204+
assert response.status_code == 200
205+
206+
content = response.content.decode()
207+
assert f"Edit your profile ({non_administrative_user.username})" in content
208+
assert f'name="username" value="{non_administrative_user.username}"' in content
209+
assert f'name="first_name" value="{non_administrative_user.first_name}"' in content
210+
assert f'name="last_name" value="{non_administrative_user.last_name}"' in content
211+
assert f'name="email" value="{non_administrative_user.email}"' in content
212+
assert f"<code>{non_administrative_user_apikey.key}</code>" in content
213+
214+
215+
@pytest.mark.django_db
216+
def test_user_profile_view_denies_editing_profile_fields_if_setting_disables_it(
217+
dashboard_uuid: None,
218+
non_administrative_user: User,
219+
non_administrative_user_apikey: ApiKey,
220+
client: Client,
221+
settings: pytest_django.fixtures.SettingsWrapper,
222+
) -> None:
223+
settings.ALLOW_USER_EDITS = False
224+
client.force_login(non_administrative_user)
225+
226+
response = client.get(
227+
reverse("accounts:profile"),
228+
follow=True,
229+
)
230+
assert response.status_code == 200
231+
232+
content = response.content.decode()
233+
assert f"Your profile ({non_administrative_user.username})" in content
234+
assert f"<dd>{non_administrative_user.username}</dd>" in content
235+
assert (
236+
f"<dd>{non_administrative_user.first_name} {non_administrative_user.last_name}</dd>"
237+
in content
238+
)
239+
assert f"<dd>{non_administrative_user.email}</dd>" in content
240+
assert (
241+
f'<dd>{"yes" if non_administrative_user.is_superuser else "no"}</dd>' in content
242+
)
243+
assert f"<code>{non_administrative_user_apikey.key}</code>" in content
244+
245+
246+
@pytest.mark.django_db
247+
def test_user_profile_view_regenerates_api_key_if_setting_disables_editing(
248+
dashboard_uuid: None,
249+
non_administrative_user: User,
250+
non_administrative_user_apikey: ApiKey,
251+
client: Client,
252+
settings: pytest_django.fixtures.SettingsWrapper,
253+
) -> None:
254+
settings.ALLOW_USER_EDITS = False
255+
client.force_login(non_administrative_user)
256+
data = {"regenerate_api_key": True}
257+
expected_uuid = uuid.uuid4()
258+
expected_key = hmac.new(expected_uuid.bytes, digestmod=sha1).hexdigest()
259+
260+
with mock.patch("uuid.uuid4", return_value=expected_uuid):
261+
response = client.post(
262+
reverse("accounts:profile"),
263+
data,
264+
follow=True,
265+
)
266+
assert response.status_code == 200
267+
268+
content = response.content.decode()
269+
assert f"Your profile ({non_administrative_user.username})" in content
270+
assert f"<dd>{non_administrative_user.username}</dd>" in content
271+
assert (
272+
f"<dd>{non_administrative_user.first_name} {non_administrative_user.last_name}</dd>"
273+
in content
274+
)
275+
assert f"<dd>{non_administrative_user.email}</dd>" in content
276+
assert (
277+
f'<dd>{"yes" if non_administrative_user.is_superuser else "no"}</dd>' in content
278+
)
279+
assert f"<code>{expected_key}</code>" in content
280+
281+
282+
@pytest.mark.django_db
283+
def test_user_profile_view_does_not_regenerate_api_key_if_not_requested(
284+
dashboard_uuid: None,
285+
non_administrative_user: User,
286+
non_administrative_user_apikey: ApiKey,
287+
client: Client,
288+
settings: pytest_django.fixtures.SettingsWrapper,
289+
) -> None:
290+
settings.ALLOW_USER_EDITS = False
291+
client.force_login(non_administrative_user)
292+
293+
response = client.post(reverse("accounts:profile"), {}, follow=True)
294+
assert response.status_code == 200
295+
296+
content = response.content.decode()
297+
assert f"Your profile ({non_administrative_user.username})" in content
298+
assert f"<dd>{non_administrative_user.username}</dd>" in content
299+
assert (
300+
f"<dd>{non_administrative_user.first_name} {non_administrative_user.last_name}</dd>"
301+
in content
302+
)
303+
assert f"<dd>{non_administrative_user.email}</dd>" in content
304+
assert (
305+
f'<dd>{"yes" if non_administrative_user.is_superuser else "no"}</dd>' in content
306+
)
307+
assert f"<code>{non_administrative_user_apikey.key}</code>" in content

0 commit comments

Comments
 (0)