Browse Source

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
Seiji Chew 5 months ago
parent
commit
09cb65f9d6

+ 23 - 17
src/sentry/api/endpoints/debug_files.py

@@ -15,7 +15,7 @@ from rest_framework.response import Response
 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.organizationmember import OrganizationMember
 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 @@ DIF_MIMETYPES = {v: k for k, v in KNOWN_DIF_FORMATS.items()}
 _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 @@ class ProguardArtifactReleasesEndpoint(ProjectEndpoint):
                 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 @@ class DebugFilesEndpoint(ProjectEndpoint):
         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 @@ class DebugFilesEndpoint(ProjectEndpoint):
         :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 @@ class DebugFilesEndpoint(ProjectEndpoint):
 
         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 @@ class SourceMapsEndpoint(ProjectEndpoint):
 
             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 @@ class SourceMapsEndpoint(ProjectEndpoint):
             on_results=serialize_results,
         )
 
-    def delete(self, request: Request, project) -> Response:
+    def delete(self, request: Request, project: Project) -> Response:
         """
         Delete an Archive
         ```````````````````````````````````````````````````

+ 1 - 1
src/sentry/roles/__init__.py

@@ -17,5 +17,5 @@ get_all = default_manager.get_all
 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

+ 191 - 345
tests/sentry/api/endpoints/test_debug_files.py

@@ -23,7 +23,17 @@ org.slf4j.helpers.Util$ClassContextSecurityManager -> org.a.b.g$a:
 """
 
 
-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 @@ class DebugFilesUploadTest(APITestCase):
             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 @@ class DebugFilesUploadTest(APITestCase):
         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 @@ class DebugFilesUploadTest(APITestCase):
 
         # 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 @@ class DebugFilesUploadTest(APITestCase):
         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 @@ class DebugFilesUploadTest(APITestCase):
             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 @@ class DebugFilesUploadTest(APITestCase):
 
         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 @@ class DebugFilesUploadTest(APITestCase):
             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 @@ class DebugFilesUploadTest(APITestCase):
         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