Skip to content

Commit 475fee7

Browse files
committed
Add tests
1 parent 9da76fa commit 475fee7

File tree

2 files changed

+297
-1
lines changed

2 files changed

+297
-1
lines changed

readthedocs/api/v2/views/model_views.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import structlog
55
from allauth.socialaccount.models import SocialAccount
66
from django.conf import settings
7+
from django.core.exceptions import PermissionDenied
78
from django.db.models import BooleanField, Case, Value, When
89
from django.http import Http404
910
from django.shortcuts import get_object_or_404
@@ -251,7 +252,7 @@ def concurrent(self, request, **kwargs):
251252
build_api_key = request.build_api_key
252253
if build_api_key:
253254
if project_slug != build_api_key.project.slug:
254-
raise Http404
255+
raise Http404()
255256
project = build_api_key.project
256257
else:
257258
project = get_object_or_404(Project, slug=project_slug)
@@ -326,6 +327,16 @@ class BuildCommandViewSet(DisableListEndpoint, CreateModelMixin, UserSelectViewS
326327
serializer_class = BuildCommandSerializer
327328
model = BuildCommandResult
328329

330+
def perform_create(self, serializer):
331+
"""Restrict creation to builds attached to the project from the api key."""
332+
build_pk = serializer.validated_data["build"].pk
333+
api_key = self.request.build_api_key
334+
if api_key and not api_key.project.builds.filter(pk=build_pk).exists():
335+
raise PermissionDenied()
336+
# If the request isn't attached to a build api key,
337+
# the user doing the request is a superuser, so it has access to all projects.
338+
return super().perform_create(serializer)
339+
329340
def get_queryset(self):
330341
api_key = self.request.build_api_key
331342
if api_key:

readthedocs/rtd_tests/tests/test_api.py

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from rest_framework import status
1515
from rest_framework.test import APIClient
1616

17+
from readthedocs.api.v2.models import BuildAPIKey
1718
from readthedocs.api.v2.views.integrations import (
1819
GITHUB_CREATE,
1920
GITHUB_DELETE,
@@ -604,6 +605,31 @@ def test_build_filter_by_commit(self):
604605
class APITests(TestCase):
605606
fixtures = ['eric.json', 'test_data.json']
606607

608+
def test_revoke_build_api_key(self):
609+
user = get(User)
610+
project = get(Project, users=[user])
611+
_, build_api_key = BuildAPIKey.objects.create_key(project)
612+
client = APIClient()
613+
revoke_url = "/api/v2/revoke/"
614+
self.assertTrue(BuildAPIKey.objects.is_valid(build_api_key))
615+
616+
# Anonymous request.
617+
client.logout()
618+
resp = client.post(revoke_url)
619+
self.assertEqual(resp.status_code, 403)
620+
self.assertTrue(BuildAPIKey.objects.is_valid(build_api_key))
621+
622+
# Using user/password.
623+
client.force_login(user)
624+
resp = client.post(revoke_url)
625+
self.assertEqual(resp.status_code, 403)
626+
self.assertTrue(BuildAPIKey.objects.is_valid(build_api_key))
627+
628+
client.logout()
629+
resp = client.post(revoke_url, HTTP_AUTHORIZATION=f"Token {build_api_key}")
630+
self.assertEqual(resp.status_code, 204)
631+
self.assertFalse(BuildAPIKey.objects.is_valid(build_api_key))
632+
607633
def test_user_doesnt_get_full_api_return(self):
608634
user_normal = get(User, is_staff=False)
609635
user_admin = get(User, is_staff=True)
@@ -699,6 +725,52 @@ def test_project_read_and_write_endpoints_for_staff_user(self):
699725
resp = client.patch(f"/api/v2/project/{project.pk}/")
700726
self.assertEqual(resp.status_code, 200)
701727

728+
def test_project_read_and_write_endpoints_for_build_api_token(self):
729+
user_normal = get(User, is_staff=False)
730+
user_admin = get(User, is_staff=True)
731+
732+
project_a = get(Project, users=[user_normal], privacy_level=PUBLIC)
733+
project_b = get(Project, users=[user_admin], privacy_level=PUBLIC)
734+
project_c = get(Project, privacy_level=PUBLIC)
735+
client = APIClient()
736+
737+
_, build_api_key = BuildAPIKey.objects.create_key(project_a)
738+
client.credentials(HTTP_AUTHORIZATION=f"Token {build_api_key}")
739+
740+
# List operations without a filter aren't allowed.
741+
resp = client.get("/api/v2/project/")
742+
self.assertEqual(resp.status_code, 410)
743+
744+
# We don't allow creating projects.
745+
resp = client.post("/api/v2/project/")
746+
self.assertEqual(resp.status_code, 405)
747+
748+
# The key grants access to project_a only.
749+
resp = client.get(f"/api/v2/project/{project_a.pk}/")
750+
self.assertEqual(resp.status_code, 200)
751+
752+
# We don't allow deleting projects.
753+
resp = client.delete(f"/api/v2/project/{project_a.pk}/")
754+
self.assertEqual(resp.status_code, 405)
755+
756+
# Update is fine.
757+
resp = client.patch(f"/api/v2/project/{project_a.pk}/")
758+
self.assertEqual(resp.status_code, 200)
759+
760+
disallowed_projects = [
761+
project_b,
762+
project_c,
763+
]
764+
for project in disallowed_projects:
765+
resp = client.get(f"/api/v2/project/{project.pk}/")
766+
self.assertEqual(resp.status_code, 404)
767+
768+
resp = client.delete(f"/api/v2/project/{project.pk}/")
769+
self.assertEqual(resp.status_code, 405)
770+
771+
resp = client.patch(f"/api/v2/project/{project.pk}/")
772+
self.assertEqual(resp.status_code, 404)
773+
702774
def test_build_read_only_endpoints_for_normal_user(self):
703775
user_normal = get(User, is_staff=False)
704776
user_admin = get(User, is_staff=True)
@@ -775,6 +847,55 @@ def test_build_read_and_write_endpoints_for_staff_user(self):
775847
resp = client.patch(f"/api/v2/build/{build.pk}/")
776848
self.assertEqual(resp.status_code, 200)
777849

850+
def test_build_read_and_write_endpoints_for_build_api_token(self):
851+
user_normal = get(User, is_staff=False)
852+
user_admin = get(User, is_staff=True)
853+
854+
project_a = get(Project, users=[user_normal], privacy_level=PUBLIC)
855+
project_b = get(Project, users=[user_admin], privacy_level=PUBLIC)
856+
project_c = get(Project, privacy_level=PUBLIC)
857+
client = APIClient()
858+
859+
_, build_api_key = BuildAPIKey.objects.create_key(project_a)
860+
client.credentials(HTTP_AUTHORIZATION=f"Token {build_api_key}")
861+
862+
# List operations without a filter aren't allowed.
863+
resp = client.get("/api/v2/build/")
864+
self.assertEqual(resp.status_code, 410)
865+
866+
# We don't allow to create builds.
867+
resp = client.post("/api/v2/build/")
868+
self.assertEqual(resp.status_code, 405)
869+
870+
Version.objects.all().update(privacy_level=PUBLIC)
871+
872+
# The key grants access to builds form project_a only.
873+
build = get(Build, project=project_a, version=project_a.versions.first())
874+
resp = client.get(f"/api/v2/build/{build.pk}/")
875+
self.assertEqual(resp.status_code, 200)
876+
877+
# We don't allow deleting builds.
878+
resp = client.delete(f"/api/v2/build/{build.pk}/")
879+
self.assertEqual(resp.status_code, 405)
880+
881+
# Update them is fine.
882+
resp = client.patch(f"/api/v2/build/{build.pk}/")
883+
self.assertEqual(resp.status_code, 200)
884+
885+
disallowed_builds = [
886+
get(Build, project=project_b, version=project_b.versions.first()),
887+
get(Build, project=project_c, version=project_c.versions.first()),
888+
]
889+
for build in disallowed_builds:
890+
resp = client.get(f"/api/v2/build/{build.pk}/")
891+
self.assertEqual(resp.status_code, 404)
892+
893+
resp = client.delete(f"/api/v2/build/{build.pk}/")
894+
self.assertEqual(resp.status_code, 405)
895+
896+
resp = client.patch(f"/api/v2/build/{build.pk}/")
897+
self.assertEqual(resp.status_code, 404)
898+
778899
def test_build_commands_read_only_endpoints_for_normal_user(self):
779900
user_normal = get(User, is_staff=False)
780901
user_admin = get(User, is_staff=True)
@@ -865,6 +986,82 @@ def test_build_commands_read_and_write_endpoints_for_staff_user(self):
865986
resp = client.patch(f"/api/v2/command/{command.pk}/")
866987
self.assertEqual(resp.status_code, 405)
867988

989+
def test_build_commands_read_and_write_endpoints_for_build_api_token(self):
990+
user_normal = get(User, is_staff=False)
991+
user_admin = get(User, is_staff=True)
992+
993+
project_a = get(Project, users=[user_normal], privacy_level=PUBLIC)
994+
project_b = get(Project, users=[user_admin], privacy_level=PUBLIC)
995+
project_c = get(Project, privacy_level=PUBLIC)
996+
client = APIClient()
997+
998+
_, build_api_key = BuildAPIKey.objects.create_key(project_a)
999+
client.credentials(HTTP_AUTHORIZATION=f"Token {build_api_key}")
1000+
1001+
# List operations without a filter aren't allowed.
1002+
resp = client.get("/api/v2/command/")
1003+
self.assertEqual(resp.status_code, 410)
1004+
1005+
Version.objects.all().update(privacy_level=PUBLIC)
1006+
1007+
build = get(Build, project=project_a, version=project_a.versions.first())
1008+
command = get(BuildCommandResult, build=build)
1009+
1010+
# We allow creating build commands.
1011+
resp = client.post(
1012+
"/api/v2/command/",
1013+
{
1014+
"build": build.pk,
1015+
"command": "test",
1016+
"output": "test",
1017+
"exit_code": 0,
1018+
"start_time": datetime.datetime.utcnow(),
1019+
"end_time": datetime.datetime.utcnow(),
1020+
},
1021+
)
1022+
self.assertEqual(resp.status_code, 201)
1023+
1024+
resp = client.get(f"/api/v2/command/{command.pk}/")
1025+
self.assertEqual(resp.status_code, 200)
1026+
1027+
# We don't allow deleting commands.
1028+
resp = client.delete(f"/api/v2/command/{command.pk}/")
1029+
self.assertEqual(resp.status_code, 405)
1030+
1031+
# Neither updating them.
1032+
resp = client.patch(f"/api/v2/command/{command.pk}/")
1033+
self.assertEqual(resp.status_code, 405)
1034+
1035+
disallowed_builds = [
1036+
get(Build, project=project_b, version=project_b.versions.first()),
1037+
get(Build, project=project_c, version=project_c.versions.first()),
1038+
]
1039+
disallowed_build_commands = [
1040+
get(BuildCommandResult, build=build) for build in disallowed_builds
1041+
]
1042+
for command in disallowed_build_commands:
1043+
resp = client.post(
1044+
"/api/v2/command/",
1045+
{
1046+
"build": command.build.pk,
1047+
"command": "test",
1048+
"output": "test",
1049+
"exit_code": 0,
1050+
"start_time": datetime.datetime.utcnow(),
1051+
"end_time": datetime.datetime.utcnow(),
1052+
},
1053+
)
1054+
self.assertEqual(resp.status_code, 403)
1055+
1056+
resp = client.get(f"/api/v2/command/{command.pk}/")
1057+
self.assertEqual(resp.status_code, 404)
1058+
1059+
resp = client.delete(f"/api/v2/command/{command.pk}/")
1060+
self.assertEqual(resp.status_code, 405)
1061+
1062+
resp = client.patch(f"/api/v2/command/{command.pk}/")
1063+
self.assertEqual(resp.status_code, 405)
1064+
8681065
def test_versions_read_only_endpoints_for_normal_user(self):
8691066
user_normal = get(User, is_staff=False)
8701067
user_admin = get(User, is_staff=True)
@@ -943,6 +1140,55 @@ def test_versions_read_and_write_endpoints_for_staff_user(self):
9431140
resp = client.patch(f"/api/v2/version/{version.pk}/")
9441141
self.assertEqual(resp.status_code, 200)
9451142

1143+
def test_versions_read_and_write_endpoints_for_build_api_token(self):
1144+
user_normal = get(User, is_staff=False)
1145+
user_admin = get(User, is_staff=True)
1146+
1147+
project_a = get(Project, users=[user_normal], privacy_level=PUBLIC)
1148+
project_b = get(Project, users=[user_admin], privacy_level=PUBLIC)
1149+
project_c = get(Project, privacy_level=PUBLIC)
1150+
Version.objects.all().update(privacy_level=PUBLIC)
1151+
1152+
client = APIClient()
1153+
_, build_api_key = BuildAPIKey.objects.create_key(project_a)
1154+
client.credentials(HTTP_AUTHORIZATION=f"Token {build_api_key}")
1155+
1156+
# List operations without a filter aren't allowed.
1157+
resp = client.get("/api/v2/version/")
1158+
self.assertEqual(resp.status_code, 410)
1159+
1160+
# We don't allow to create versions.
1161+
resp = client.post("/api/v2/version/")
1162+
self.assertEqual(resp.status_code, 405)
1163+
1164+
version = project_a.versions.first()
1165+
resp = client.get(f"/api/v2/version/{version.pk}/")
1166+
self.assertEqual(resp.status_code, 200)
1167+
1168+
# We don't allow deleting versions.
1169+
resp = client.delete(f"/api/v2/version/{version.pk}/")
1170+
self.assertEqual(resp.status_code, 405)
1171+
1172+
# Update them is fine.
1173+
resp = client.patch(f"/api/v2/version/{version.pk}/")
1174+
self.assertEqual(resp.status_code, 200)
1175+
1176+
disallowed_versions = [
1177+
project_b.versions.first(),
1178+
project_c.versions.first(),
1179+
]
1180+
for version in disallowed_versions:
1181+
resp = client.get(f"/api/v2/version/{version.pk}/")
1182+
self.assertEqual(resp.status_code, 404)
1183+
1184+
# We don't allow deleting versions.
1185+
resp = client.delete(f"/api/v2/version/{version.pk}/")
1186+
self.assertEqual(resp.status_code, 405)
1187+
1188+
# Update them is fine.
1189+
resp = client.patch(f"/api/v2/version/{version.pk}/")
1190+
self.assertEqual(resp.status_code, 404)
1191+
9461192
def test_domains_read_only_endpoints_for_normal_user(self):
9471193
user_normal = get(User, is_staff=False)
9481194
user_admin = get(User, is_staff=True)
@@ -1021,6 +1267,45 @@ def test_domains_read_and_write_endpoints_for_staff_user(self):
10211267
resp = client.patch(f"/api/v2/domain/{domain.pk}/")
10221268
self.assertEqual(resp.status_code, 405)
10231269

1270+
def test_domains_read_and_write_endpoints_for_build_api_token(self):
1271+
# Build API tokens don't grant access to the domain endpoints.
1272+
user_normal = get(User, is_staff=False)
1273+
user_admin = get(User, is_staff=True)
1274+
1275+
project_a = get(Project, users=[user_normal], privacy_level=PUBLIC)
1276+
project_b = get(Project, users=[user_admin], privacy_level=PUBLIC)
1277+
project_c = get(Project, privacy_level=PUBLIC)
1278+
Version.objects.all().update(privacy_level=PUBLIC)
1279+
1280+
client = APIClient()
1281+
_, build_api_key = BuildAPIKey.objects.create_key(project_a)
1282+
client.credentials(HTTP_AUTHORIZATION=f"Token {build_api_key}")
1283+
1284+
# List operations without a filter aren't allowed.
1285+
resp = client.get("/api/v2/domain/")
1286+
self.assertEqual(resp.status_code, 410)
1287+
1288+
# We don't allow to create domains.
1289+
resp = client.post("/api/v2/domain/")
1290+
self.assertEqual(resp.status_code, 403)
1291+
1292+
domains = [
1293+
get(Domain, project=project_a),
1294+
get(Domain, project=project_b),
1295+
get(Domain, project=project_c),
1296+
]
1297+
for domain in domains:
1298+
resp = client.get(f"/api/v2/domain/{domain.pk}/")
1299+
self.assertEqual(resp.status_code, 200)
1300+
1301+
# We don't allow deleting domains.
1302+
resp = client.delete(f"/api/v2/domain/{domain.pk}/")
1303+
self.assertEqual(resp.status_code, 403)
1304+
1305+
# Neither update them.
1306+
resp = client.patch(f"/api/v2/domain/{domain.pk}/")
1307+
self.assertEqual(resp.status_code, 403)
1308+
10241309
def test_project_features(self):
10251310
user = get(User, is_staff=True)
10261311
project = get(Project, main_language_project=None)

0 commit comments

Comments
 (0)