From 09cb65f9d6f7173d76955cf61c2aa9075ecac780 Mon Sep 17 00:00:00 2001 From: Seiji Chew <67301797+schew2381@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:42:52 -0700 Subject: [PATCH] fix(roles): Allow team admins to download debug files (#78279) There's a lot of test refactoring b/c it was super messy before, so I'll point out where the new testing happens Closes https://github.com/getsentry/sentry/issues/78229 --- src/sentry/api/endpoints/debug_files.py | 40 +- src/sentry/roles/__init__.py | 2 +- .../sentry/api/endpoints/test_debug_files.py | 536 +++++++----------- 3 files changed, 215 insertions(+), 363 deletions(-) diff --git a/src/sentry/api/endpoints/debug_files.py b/src/sentry/api/endpoints/debug_files.py index e27fd4f2efe3f3..f623e71a8e865c 100644 --- a/src/sentry/api/endpoints/debug_files.py +++ b/src/sentry/api/endpoints/debug_files.py @@ -15,7 +15,7 @@ from symbolic.debuginfo import normalize_debug_id from symbolic.exceptions import SymbolicError -from sentry import ratelimits, roles +from sentry import ratelimits from sentry.api.api_owners import ApiOwner from sentry.api.api_publish_status import ApiPublishStatus from sentry.api.base import region_silo_endpoint @@ -39,6 +39,7 @@ from sentry.models.project import Project from sentry.models.release import Release, get_artifact_counts from sentry.models.releasefile import ReleaseFile +from sentry.roles import organization_roles from sentry.tasks.assemble import ( AssembleTask, ChunkFileState, @@ -53,7 +54,7 @@ _release_suffix = re.compile(r"^(.*)\s+\(([^)]+)\)\s*$") -def upload_from_request(request, project): +def upload_from_request(request: Request, project: Project): if "file" not in request.data: return Response({"detail": "Missing uploaded file"}, status=400) fileobj = request.data["file"] @@ -61,7 +62,7 @@ def upload_from_request(request, project): return Response(serialize(files, request.user), status=201) -def has_download_permission(request, project): +def has_download_permission(request: Request, project: Project): if is_system_auth(request.auth) or is_active_superuser(request): return True @@ -72,7 +73,7 @@ def has_download_permission(request, project): required_role = organization.get_option("sentry:debug_files_role") or DEBUG_FILES_ROLE_DEFAULT if request.user.is_sentry_app: - if roles.get(required_role).priority > roles.get("member").priority: + if organization_roles.can_manage("member", required_role): return request.access.has_scope("project:write") else: return request.access.has_scope("project:read") @@ -86,7 +87,12 @@ def has_download_permission(request, project): except OrganizationMember.DoesNotExist: return False - return roles.get(current_role).priority >= roles.get(required_role).priority + if organization_roles.can_manage(current_role, required_role): + return True + + # There's an edge case where a team admin is an org member but the required + # role is org admin. In that case, the team admin should be able to download. + return required_role == "admin" and request.access.has_project_scope(project, "project:write") def _has_delete_permission(access: Access, project: Project) -> bool: @@ -104,7 +110,7 @@ class ProguardArtifactReleasesEndpoint(ProjectEndpoint): } permission_classes = (ProjectReleasePermission,) - def post(self, request: Request, project) -> Response: + def post(self, request: Request, project: Project) -> Response: release_name = request.data.get("release_name") proguard_uuid = request.data.get("proguard_uuid") @@ -153,7 +159,7 @@ def post(self, request: Request, project) -> Response: status=status.HTTP_409_CONFLICT, ) - def get(self, request: Request, project) -> Response: + def get(self, request: Request, project: Project) -> Response: """ List a Project's Proguard Associated Releases ```````````````````````````````````````` @@ -189,7 +195,7 @@ class DebugFilesEndpoint(ProjectEndpoint): } permission_classes = (ProjectReleasePermission,) - def download(self, debug_file_id, project): + def download(self, debug_file_id, project: Project): rate_limited = ratelimits.backend.is_limited( project=project, key=f"rl:DSymFilesEndpoint:download:{debug_file_id}:{project.id}", @@ -223,7 +229,7 @@ def download(self, debug_file_id, project): except OSError: raise Http404 - def get(self, request: Request, project) -> Response: + def get(self, request: Request, project: Project) -> Response: """ List a Project's Debug Information Files ```````````````````````````````````````` @@ -240,7 +246,7 @@ def get(self, request: Request, project) -> Response: :auth: required """ download_requested = request.GET.get("id") is not None - if download_requested and (has_download_permission(request, project)): + if download_requested and has_download_permission(request, project): return self.download(request.GET.get("id"), project) elif download_requested: return Response(status=403) @@ -335,7 +341,7 @@ def delete(self, request: Request, project: Project) -> Response: return Response(status=404) - def post(self, request: Request, project) -> Response: + def post(self, request: Request, project: Project) -> Response: """ Upload a New File ````````````````` @@ -367,7 +373,7 @@ class UnknownDebugFilesEndpoint(ProjectEndpoint): } permission_classes = (ProjectReleasePermission,) - def get(self, request: Request, project) -> Response: + def get(self, request: Request, project: Project) -> Response: checksums = request.GET.getlist("checksums") missing = ProjectDebugFile.objects.find_missing(checksums, project=project) return Response({"missing": missing}) @@ -382,7 +388,7 @@ class AssociateDSymFilesEndpoint(ProjectEndpoint): permission_classes = (ProjectReleasePermission,) # Legacy endpoint, kept for backwards compatibility - def post(self, request: Request, project) -> Response: + def post(self, request: Request, project: Project) -> Response: return Response({"associatedDsymFiles": []}) @@ -394,7 +400,7 @@ class DifAssembleEndpoint(ProjectEndpoint): } permission_classes = (ProjectReleasePermission,) - def post(self, request: Request, project) -> Response: + def post(self, request: Request, project: Project) -> Response: """ Assemble one or multiple chunks (FileBlob) into debug files ```````````````````````````````````````````````````````````` @@ -517,7 +523,7 @@ class SourceMapsEndpoint(ProjectEndpoint): } permission_classes = (ProjectReleasePermission,) - def get(self, request: Request, project) -> Response: + def get(self, request: Request, project: Project) -> Response: """ List a Project's Source Map Archives ```````````````````````````````````` @@ -549,7 +555,7 @@ def get(self, request: Request, project) -> Response: queryset = queryset.filter(query_q) - def expose_release(release, count): + def expose_release(release, count: int): return { "type": "release", "id": release["id"], @@ -581,7 +587,7 @@ def serialize_results(results): on_results=serialize_results, ) - def delete(self, request: Request, project) -> Response: + def delete(self, request: Request, project: Project) -> Response: """ Delete an Archive ``````````````````````````````````````````````````` diff --git a/src/sentry/roles/__init__.py b/src/sentry/roles/__init__.py index d079de62b831a6..9786b2bb157761 100644 --- a/src/sentry/roles/__init__.py +++ b/src/sentry/roles/__init__.py @@ -17,5 +17,5 @@ get_choices = default_manager.get_choices get_default = default_manager.get_default get_top_dog = default_manager.get_top_dog -with_scope = default_manager.with_scope with_any_scope = default_manager.with_any_scope +with_scope = default_manager.with_scope diff --git a/tests/sentry/api/endpoints/test_debug_files.py b/tests/sentry/api/endpoints/test_debug_files.py index 305ed82f2c6f6e..34e2a3f0dabeb6 100644 --- a/tests/sentry/api/endpoints/test_debug_files.py +++ b/tests/sentry/api/endpoints/test_debug_files.py @@ -23,7 +23,17 @@ """ -class DebugFilesUploadTest(APITestCase): +class DebugFilesTestCases(APITestCase): + def setUp(self): + self.url = reverse( + "sentry-api-0-dsym-files", + kwargs={ + "organization_id_or_slug": self.organization.slug, + "project_id_or_slug": self.project.slug, + }, + ) + self.login_as(user=self.user) + def _upload_proguard(self, url, uuid): out = BytesIO() f = zipfile.ZipFile(out, "w") @@ -40,45 +50,10 @@ def _upload_proguard(self, url, uuid): format="multipart", ) - def test_simple_proguard_upload(self): - project = self.create_project(name="foo") - - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - self.login_as(user=self.user) - - response = self._upload_proguard(url, PROGUARD_UUID) - - assert response.status_code == 201, response.content - assert len(response.data) == 1 - assert response.data[0]["headers"] == {"Content-Type": "text/x-proguard+plain"} - assert response.data[0]["sha1"] == "e6d3c5185dac63eddfdc1a5edfffa32d46103b44" - assert response.data[0]["uuid"] == PROGUARD_UUID - assert response.data[0]["objectName"] == "proguard-mapping" - assert response.data[0]["cpuName"] == "any" - assert response.data[0]["symbolType"] == "proguard" - - def test_associate_proguard_dsym(self): - project = self.create_project(name="foo") - - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - self.login_as(user=self.user) - - response = self._upload_proguard(url, PROGUARD_UUID) +class DebugFilesTest(DebugFilesTestCases): + def test_simple_proguard_upload(self): + response = self._upload_proguard(self.url, PROGUARD_UUID) assert response.status_code == 201, response.content assert len(response.data) == 1 assert response.data[0]["headers"] == {"Content-Type": "text/x-proguard+plain"} @@ -88,132 +63,68 @@ def test_associate_proguard_dsym(self): assert response.data[0]["cpuName"] == "any" assert response.data[0]["symbolType"] == "proguard" - url = reverse( - "sentry-api-0-associate-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - response = self.client.post( - url, - { - "checksums": ["e6d3c5185dac63eddfdc1a5edfffa32d46103b44"], - "platform": "android", - "name": "MyApp", - "appId": "com.example.myapp", - "version": "1.0", - "build": "1", - }, - format="json", - ) + def test_dsyms_search(self): + for i in range(25): + last_uuid = str(uuid4()) + self._upload_proguard(self.url, last_uuid) + # Test max 20 per page + response = self.client.get(self.url) assert response.status_code == 200, response.content - assert "associatedDsymFiles" in response.data - assert response.data["associatedDsymFiles"] == [] - - def test_associate_proguard_dsym_no_build(self): - project = self.create_project(name="foo") - - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) + dsyms = response.data + assert len(dsyms) == 20 - self.login_as(user=self.user) + # Test should return last + response = self.client.get(self.url + "?query=" + last_uuid) + assert response.status_code == 200, response.content + dsyms = response.data + assert len(dsyms) == 1 - response = self._upload_proguard(url, PROGUARD_UUID) + response = self.client.get(self.url + "?query=proguard") + assert response.status_code == 200, response.content + dsyms = response.data + assert len(dsyms) == 20 + def test_access_control(self): + # create a debug files such as proguard: + response = self._upload_proguard(self.url, PROGUARD_UUID) assert response.status_code == 201, response.content assert len(response.data) == 1 - assert response.data[0]["headers"] == {"Content-Type": "text/x-proguard+plain"} - assert response.data[0]["sha1"] == "e6d3c5185dac63eddfdc1a5edfffa32d46103b44" - assert response.data[0]["uuid"] == PROGUARD_UUID - assert response.data[0]["objectName"] == "proguard-mapping" - assert response.data[0]["cpuName"] == "any" - assert response.data[0]["symbolType"] == "proguard" - url = reverse( - "sentry-api-0-associate-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) + response = self.client.get(self.url) + assert response.status_code == 200, response.content - response = self.client.post( - url, - { - "checksums": ["e6d3c5185dac63eddfdc1a5edfffa32d46103b44"], - "platform": "android", - "name": "MyApp", - "appId": "com.example.myapp", - "version": "1.0", - }, - format="json", - ) + (dsym,) = response.data + download_id = dsym["id"] + # `self.user` has access to these files + response = self.client.get(f"{self.url}?id={download_id}") assert response.status_code == 200, response.content - assert "associatedDsymFiles" in response.data - assert response.data["associatedDsymFiles"] == [] - - def test_dsyms_requests(self): - project = self.create_project(name="foo") + assert PROGUARD_SOURCE == b"".join(response.streaming_content) + # with another user on a different org + other_user = self.create_user() + other_org = self.create_organization(name="other-org", owner=other_user) + other_project = self.create_project(organization=other_org) url = reverse( "sentry-api-0-dsym-files", kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, + "organization_id_or_slug": other_org.slug, + "project_id_or_slug": other_project.slug, }, ) + self.login_as(user=other_user) - self.login_as(user=self.user) - - response = self._upload_proguard(url, PROGUARD_UUID) + # accessing foreign files should not work + response = self.client.get(f"{url}?id={download_id}") + assert response.status_code == 404 + def test_dsyms_requests(self): + response = self._upload_proguard(self.url, PROGUARD_UUID) assert response.status_code == 201, response.content assert len(response.data) == 1 - url = reverse( - "sentry-api-0-associate-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - response = self.client.post( - url, - { - "checksums": ["e6d3c5185dac63eddfdc1a5edfffa32d46103b44"], - "platform": "android", - "name": "MyApp", - "appId": "com.example.myapp", - "version": "1.0", - "build": "1", - }, - format="json", - ) - - assert response.status_code == 200, response.content - assert "associatedDsymFiles" in response.data - assert response.data["associatedDsymFiles"] == [] - - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - response = self.client.get(url) - + response = self.client.get(self.url) assert response.status_code == 200, response.content (dsym,) = response.data @@ -227,10 +138,7 @@ def test_dsyms_requests(self): # Download as a user with sufficient role self.organization.update_option("sentry:debug_files_role", "admin") - user = self.create_user("baz@localhost") - self.create_member(user=user, organization=project.organization, role="owner") - self.login_as(user=user) - response = self.client.get(url + "?id=" + download_id) + response = self.client.get(self.url + "?id=" + download_id) assert response.status_code == 200, response.content assert ( response.get("Content-Disposition") @@ -241,101 +149,48 @@ def test_dsyms_requests(self): assert PROGUARD_SOURCE == b"".join(response.streaming_content) # Download as a superuser - self.login_as(user=self.user) - response = self.client.get(url + "?id=" + download_id) + superuser = self.create_user(is_superuser=True) + self.login_as(user=superuser, superuser=True) + response = self.client.get(self.url + "?id=" + download_id) assert response.get("Content-Type") == "application/octet-stream" close_streaming_response(response) # Download as a user without sufficient role self.organization.update_option("sentry:debug_files_role", "owner") - user = self.create_user("bar@localhost") - self.create_member(user=user, organization=project.organization, role="member") - self.login_as(user=user) - response = self.client.get(url + "?id=" + download_id) + member_user = self.create_user("bar@localhost") + self.create_member(user=member_user, organization=self.organization, role="member") + self.login_as(user=member_user) + response = self.client.get(self.url + "?id=" + download_id) assert response.status_code == 403, response.content # Download as a user with no permissions user_no_permission = self.create_user("baz@localhost", username="baz") self.login_as(user=user_no_permission) - response = self.client.get(url + "?id=" + download_id) + response = self.client.get(self.url + "?id=" + download_id) assert response.status_code == 403, response.content # Try to delete with no permissions - response = self.client.delete(url + "?id=" + download_id) + response = self.client.delete(self.url + "?id=" + download_id) assert response.status_code == 403, response.content # Login again with permissions self.login_as(user=self.user) - response = self.client.delete(url + "?id=888") + response = self.client.delete(self.url + "?id=888") assert response.status_code == 404, response.content - assert ProjectDebugFile.objects.count() == 1 - response = self.client.delete(url + "?id=" + download_id) + response = self.client.delete(self.url + "?id=" + download_id) assert response.status_code == 204, response.content - assert ProjectDebugFile.objects.count() == 0 - def test_dsyms_search(self): - project = self.create_project(name="foo") - - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - self.login_as(user=self.user) - - for i in range(25): - last_uuid = str(uuid4()) - self._upload_proguard(url, last_uuid) - - # Test max 20 per page - response = self.client.get(url) - assert response.status_code == 200, response.content - dsyms = response.data - assert len(dsyms) == 20 - - # Test should return last - response = self.client.get(url + "?query=" + last_uuid) - assert response.status_code == 200, response.content - dsyms = response.data - assert len(dsyms) == 1 - - response = self.client.get(url + "?query=proguard") - assert response.status_code == 200, response.content - dsyms = response.data - assert len(dsyms) == 20 - - def test_dsyms_delete_as_team_admin(self): - project = self.create_project(name="foo") - self.login_as(user=self.user) - - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - response = self._upload_proguard(url, PROGUARD_UUID) - + def test_dsyms_as_team_admin(self): + response = self._upload_proguard(self.url, PROGUARD_UUID) assert response.status_code == 201 assert len(response.data) == 1 - url = reverse( - "sentry-api-0-associate-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) response = self.client.post( - url, + self.url, { "checksums": ["e6d3c5185dac63eddfdc1a5edfffa32d46103b44"], "platform": "android", @@ -347,14 +202,7 @@ def test_dsyms_delete_as_team_admin(self): format="json", ) - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - response = self.client.get(url) + response = self.client.get(self.url) download_id = response.data[0]["id"] assert response.status_code == 200 @@ -364,12 +212,12 @@ def test_dsyms_delete_as_team_admin(self): self.create_member( user=team_admin, - organization=project.organization, + organization=self.organization, role="member", ) self.create_member( user=team_admin_without_access, - organization=project.organization, + organization=self.organization, role="member", ) self.create_team_membership(user=team_admin, team=self.team, role="admin") @@ -377,61 +225,136 @@ def test_dsyms_delete_as_team_admin(self): user=team_admin_without_access, team=self.create_team(), role="admin" ) - # Team admin without project access can't delete self.login_as(team_admin_without_access) - response = self.client.delete(url + "?id=" + download_id) + # Team admin without project access can't download + response = self.client.get(self.url + "?id=" + download_id) + assert response.status_code == 403, response.content + # Team admin without project access can't delete + response = self.client.delete(self.url + "?id=" + download_id) assert response.status_code == 404, response.content assert ProjectDebugFile.objects.count() == 1 - # Team admin with project access can delete self.login_as(team_admin) - response = self.client.delete(url + "?id=" + download_id) + # Team admin with project access can download + response = self.client.get(self.url + "?id=" + download_id) + assert response.status_code == 200, response.content + assert response.get("Content-Type") == "application/octet-stream" + close_streaming_response(response) + # Team admin with project access can delete + response = self.client.delete(self.url + "?id=" + download_id) assert response.status_code == 204, response.content assert ProjectDebugFile.objects.count() == 0 - def test_source_maps(self): - project = self.create_project(name="foo") - release = Release.objects.create(organization_id=project.organization_id, version="1") - release2 = Release.objects.create(organization_id=project.organization_id, version="2") - release3 = Release.objects.create(organization_id=project.organization_id, version="3") - release.add_project(project) - release2.add_project(project) - release3.add_project(project) +class AssociateDebugFilesTest(DebugFilesTestCases): + def setUp(self): + super().setUp() + self.associate_url = reverse( + "sentry-api-0-associate-dsym-files", + kwargs={ + "organization_id_or_slug": self.organization.slug, + "project_id_or_slug": self.project.slug, + }, + ) + + def test_associate_proguard_dsym(self): + response = self._upload_proguard(self.url, PROGUARD_UUID) + assert response.status_code == 201, response.content + assert len(response.data) == 1 + assert response.data[0]["headers"] == {"Content-Type": "text/x-proguard+plain"} + assert response.data[0]["sha1"] == "e6d3c5185dac63eddfdc1a5edfffa32d46103b44" + assert response.data[0]["uuid"] == PROGUARD_UUID + assert response.data[0]["objectName"] == "proguard-mapping" + assert response.data[0]["cpuName"] == "any" + assert response.data[0]["symbolType"] == "proguard" + + response = self.client.post( + self.associate_url, + { + "checksums": ["e6d3c5185dac63eddfdc1a5edfffa32d46103b44"], + "platform": "android", + "name": "MyApp", + "appId": "com.example.myapp", + "version": "1.0", + "build": "1", + }, + format="json", + ) + + assert response.status_code == 200, response.content + assert "associatedDsymFiles" in response.data + assert response.data["associatedDsymFiles"] == [] + + def test_associate_proguard_dsym_no_build(self): + response = self._upload_proguard(self.url, PROGUARD_UUID) + assert response.status_code == 201, response.content + assert len(response.data) == 1 + assert response.data[0]["headers"] == {"Content-Type": "text/x-proguard+plain"} + assert response.data[0]["sha1"] == "e6d3c5185dac63eddfdc1a5edfffa32d46103b44" + assert response.data[0]["uuid"] == PROGUARD_UUID + assert response.data[0]["objectName"] == "proguard-mapping" + assert response.data[0]["cpuName"] == "any" + assert response.data[0]["symbolType"] == "proguard" + + response = self.client.post( + self.associate_url, + { + "checksums": ["e6d3c5185dac63eddfdc1a5edfffa32d46103b44"], + "platform": "android", + "name": "MyApp", + "appId": "com.example.myapp", + "version": "1.0", + }, + format="json", + ) + + assert response.status_code == 200, response.content + assert "associatedDsymFiles" in response.data + assert response.data["associatedDsymFiles"] == [] + + +class SourceMapsEndpointTest(APITestCase): + def setUp(self): + self.url = reverse( + "sentry-api-0-source-maps", + kwargs={ + "organization_id_or_slug": self.organization.slug, + "project_id_or_slug": self.project.slug, + }, + ) + self.login_as(user=self.user) + + def test_source_maps(self): + release = Release.objects.create(organization_id=self.project.organization_id, version="1") + release2 = Release.objects.create(organization_id=self.project.organization_id, version="2") + release3 = Release.objects.create(organization_id=self.project.organization_id, version="3") + release.add_project(self.project) + release2.add_project(self.project) + release3.add_project(self.project) ReleaseFile.objects.create( - organization_id=project.organization_id, + organization_id=self.project.organization_id, release_id=release.id, file=File.objects.create(name="application.js", type="release.file"), name="http://example.com/application.js", ) ReleaseFile.objects.create( - organization_id=project.organization_id, + organization_id=self.project.organization_id, release_id=release.id, file=File.objects.create(name="application2.js", type="release.file"), name="http://example.com/application2.js", ) ReleaseFile.objects.create( - organization_id=project.organization_id, + organization_id=self.project.organization_id, release_id=release2.id, file=File.objects.create(name="application3.js", type="release.file"), name="http://example.com/application2.js", artifact_count=0, ) - url = reverse( - "sentry-api-0-source-maps", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - self.login_as(user=self.user) - - response = self.client.get(url) + response = self.client.get(self.url) assert response.status_code == 200, response.content assert len(response.data) == 3 @@ -446,141 +369,64 @@ def test_source_maps(self): assert response.data[2]["fileCount"] == 2 def test_source_maps_sorting(self): - project = self.create_project(name="foo") - - release = Release.objects.create(organization_id=project.organization_id, version="1") - release2 = Release.objects.create(organization_id=project.organization_id, version="2") - release.add_project(project) - release2.add_project(project) + release = Release.objects.create(organization_id=self.project.organization_id, version="1") + release2 = Release.objects.create(organization_id=self.project.organization_id, version="2") + release.add_project(self.project) + release2.add_project(self.project) ReleaseFile.objects.create( - organization_id=project.organization_id, + organization_id=self.project.organization_id, release_id=release.id, file=File.objects.create(name="application.js", type="release.file"), name="http://example.com/application.js", ) ReleaseFile.objects.create( - organization_id=project.organization_id, + organization_id=self.project.organization_id, release_id=release.id, file=File.objects.create(name="application2.js", type="release.file"), name="http://example.com/application2.js", ) - url = reverse( - "sentry-api-0-source-maps", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - release_ids = [release.id, release2.id] - self.login_as(user=self.user) - response = self.client.get(url + "?sortBy=date_added") + response = self.client.get(self.url + "?sortBy=date_added") assert response.status_code == 200, response.content assert list(map(lambda value: value["id"], response.data)) == release_ids - response = self.client.get(url + "?sortBy=-date_added") + response = self.client.get(self.url + "?sortBy=-date_added") assert response.status_code == 200, response.content assert list(map(lambda value: value["id"], response.data)) == release_ids[::-1] - response = self.client.get(url + "?sortBy=invalid") + response = self.client.get(self.url + "?sortBy=invalid") assert response.status_code == 400 assert response.data["error"] == "You can either sort via 'date_added' or '-date_added'" def test_source_maps_delete_archive(self): - project = self.create_project(name="foo") - - release = Release.objects.create(organization_id=project.organization_id, version="1", id=1) - release.add_project(project) + release = Release.objects.create( + organization_id=self.project.organization_id, version="1", id=1 + ) + release.add_project(self.project) ReleaseFile.objects.create( - organization_id=project.organization_id, + organization_id=self.project.organization_id, release_id=release.id, file=File.objects.create(name="application.js", type="release.file"), name="http://example.com/application.js", ) - url = reverse( - "sentry-api-0-source-maps", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - self.login_as(user=self.user) - - response = self.client.delete(url + "?name=1") + response = self.client.delete(self.url + "?name=1") assert response.status_code == 204 assert not ReleaseFile.objects.filter(release_id=release.id).exists() def test_source_maps_release_archive(self): - project = self.create_project(name="foo") - - release = Release.objects.create(organization_id=project.organization_id, version="1") - release.add_project(project) + release = Release.objects.create(organization_id=self.project.organization_id, version="1") + release.add_project(self.project) self.create_release_archive(release=release.version) - url = reverse( - "sentry-api-0-source-maps", - kwargs={ - "organization_id_or_slug": project.organization.slug, - "project_id_or_slug": project.slug, - }, - ) - - self.login_as(user=self.user) - - response = self.client.get(url) + response = self.client.get(self.url) assert response.status_code == 200, response.content assert len(response.data) == 1 assert response.data[0]["name"] == str(release.version) assert response.data[0]["fileCount"] == 2 - - def test_access_control(self): - # create a debug files such as proguard: - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": self.project.organization.slug, - "project_id_or_slug": self.project.slug, - }, - ) - self.login_as(user=self.user) - - response = self._upload_proguard(url, PROGUARD_UUID) - - assert response.status_code == 201, response.content - assert len(response.data) == 1 - - response = self.client.get(url) - assert response.status_code == 200, response.content - - (dsym,) = response.data - download_id = dsym["id"] - - # `self.user` has access to these files - response = self.client.get(f"{url}?id={download_id}") - assert response.status_code == 200, response.content - assert PROGUARD_SOURCE == b"".join(response.streaming_content) - - # with another user on a different org - other_user = self.create_user() - other_org = self.create_organization(name="other-org", owner=other_user) - other_project = self.create_project(organization=other_org) - url = reverse( - "sentry-api-0-dsym-files", - kwargs={ - "organization_id_or_slug": other_org.slug, - "project_id_or_slug": other_project.slug, - }, - ) - self.login_as(user=other_user) - - # accessing foreign files should not work - response = self.client.get(f"{url}?id={download_id}") - assert response.status_code == 404