|
14 | 14 | from rest_framework import status
|
15 | 15 | from rest_framework.test import APIClient
|
16 | 16 |
|
| 17 | +from readthedocs.api.v2.models import BuildAPIKey |
17 | 18 | from readthedocs.api.v2.views.integrations import (
|
18 | 19 | GITHUB_CREATE,
|
19 | 20 | GITHUB_DELETE,
|
@@ -604,6 +605,31 @@ def test_build_filter_by_commit(self):
|
604 | 605 | class APITests(TestCase):
|
605 | 606 | fixtures = ['eric.json', 'test_data.json']
|
606 | 607 |
|
| 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 | + |
607 | 633 | def test_user_doesnt_get_full_api_return(self):
|
608 | 634 | user_normal = get(User, is_staff=False)
|
609 | 635 | user_admin = get(User, is_staff=True)
|
@@ -699,6 +725,52 @@ def test_project_read_and_write_endpoints_for_staff_user(self):
|
699 | 725 | resp = client.patch(f"/api/v2/project/{project.pk}/")
|
700 | 726 | self.assertEqual(resp.status_code, 200)
|
701 | 727 |
|
| 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 | + |
702 | 774 | def test_build_read_only_endpoints_for_normal_user(self):
|
703 | 775 | user_normal = get(User, is_staff=False)
|
704 | 776 | user_admin = get(User, is_staff=True)
|
@@ -775,6 +847,55 @@ def test_build_read_and_write_endpoints_for_staff_user(self):
|
775 | 847 | resp = client.patch(f"/api/v2/build/{build.pk}/")
|
776 | 848 | self.assertEqual(resp.status_code, 200)
|
777 | 849 |
|
| 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 | + |
778 | 899 | def test_build_commands_read_only_endpoints_for_normal_user(self):
|
779 | 900 | user_normal = get(User, is_staff=False)
|
780 | 901 | user_admin = get(User, is_staff=True)
|
@@ -865,6 +986,82 @@ def test_build_commands_read_and_write_endpoints_for_staff_user(self):
|
865 | 986 | resp = client.patch(f"/api/v2/command/{command.pk}/")
|
866 | 987 | self.assertEqual(resp.status_code, 405)
|
867 | 988 |
|
| 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 | + |
868 | 1065 | def test_versions_read_only_endpoints_for_normal_user(self):
|
869 | 1066 | user_normal = get(User, is_staff=False)
|
870 | 1067 | user_admin = get(User, is_staff=True)
|
@@ -943,6 +1140,55 @@ def test_versions_read_and_write_endpoints_for_staff_user(self):
|
943 | 1140 | resp = client.patch(f"/api/v2/version/{version.pk}/")
|
944 | 1141 | self.assertEqual(resp.status_code, 200)
|
945 | 1142 |
|
| 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 | + |
946 | 1192 | def test_domains_read_only_endpoints_for_normal_user(self):
|
947 | 1193 | user_normal = get(User, is_staff=False)
|
948 | 1194 | user_admin = get(User, is_staff=True)
|
@@ -1021,6 +1267,45 @@ def test_domains_read_and_write_endpoints_for_staff_user(self):
|
1021 | 1267 | resp = client.patch(f"/api/v2/domain/{domain.pk}/")
|
1022 | 1268 | self.assertEqual(resp.status_code, 405)
|
1023 | 1269 |
|
| 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 | + |
1024 | 1309 | def test_project_features(self):
|
1025 | 1310 | user = get(User, is_staff=True)
|
1026 | 1311 | project = get(Project, main_language_project=None)
|
|
0 commit comments