CVE-2023-36826: Incorrect Authorization (CWE-863)
value: Sentry is an error tracking and performance monitoring platform. Starting in version 8.21.0 and prior to version 23.5.2, an authenticated user can download a debug or artifact bundle from arbitrary organizations and projects with a known bundle ID. The user does not need to be a member of the organization or have permissions on the project. A patch was issued in version 23.5.2 to ensure authorization checks are properly scoped on requests to retrieve debug or artifact bundles. Authenticated users who do not have the necessary permissions on the particular project are no longer able to download them. Sentry SaaS users do not need to take any action. Self-Hosted Sentry users should upgrade to version 23.5.2 or higher.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 6.5
Published: 2023-07-25T19:15Z
Repository: https://github.com/getsentry/sentry
Language: Python
Files changed: src/sentry/api/endpoints/artifact_lookup.py, src/sentry/api/endpoints/debug_files.py, tests/sentry/api/endpoints/test_project_artifact_lookup.py
Commit date: 2023-05-24 16:47:20 +0200
Commit message: Restrict file downloads to Project (#49680)

The `debug_files` and `artifact_lookup` endpoint are now restricting
file downloads to the specific project according to the route
parameters.
Diff: diff --git a/src/sentry/api/endpoints/artifact_lookup.py b/src/sentry/api/endpoints/artifact_lookup.py
index c381f6863e..25f8199081 100644
--- a/src/sentry/api/endpoints/artifact_lookup.py
+++ b/src/sentry/api/endpoints/artifact_lookup.py
@@ -18,11 +18,10 @@ from sentry.auth.system import is_system_auth
 from sentry.lang.native.sources import get_internal_artifact_lookup_source_url
 from sentry.models import (
     ArtifactBundle,
     DebugIdArtifactBundle,
     Distribution,
-    File,
     Project,
     ProjectArtifactBundle,
     Release,
     ReleaseArtifactBundle,
     ReleaseFile,
@@ -43,27 +42,47 @@ AVAILABLE_FOR_RENEWAL_DAYS = 30
 
 @region_silo_endpoint
 class ProjectArtifactLookupEndpoint(ProjectEndpoint):
     permission_classes = (ProjectReleasePermission,)
 
-    def download_file(self, file_id, project: Project):
+    def download_file(self, download_id, project: Project):
+        ty, ty_id = download_id.split("/")
+
         rate_limited = ratelimits.is_limited(
             project=project,
-            key=f"rl:ArtifactLookupEndpoint:download:{file_id}:{project.id}",
+            key=f"rl:ArtifactLookupEndpoint:download:{download_id}:{project.id}",
             limit=10,
         )
         if rate_limited:
             logger.info(
                 "notification.rate_limited",
-                extra={"project_id": project.id, "file_id": file_id},
+                extra={"project_id": project.id, "file_id": download_id},
             )
             return HttpResponse({"Too many download requests"}, status=429)
 
-        file = File.objects.filter(id=file_id).first()
+        file = None
+        if ty == "artifact_bundle":
+            file = (
+                ArtifactBundle.objects.filter(
+                    id=ty_id,
+                    projectartifactbundle__project_id=project.id,
+                )
+                .select_related("file")
+                .first()
+            )
+        elif ty == "release_file":
+            # NOTE: `ReleaseFile` does have a `project_id`, but that seems to
+            # be always empty, so using the `organization_id` instead.
+            file = (
+                ReleaseFile.objects.filter(id=ty_id, organization_id=project.organization.id)
+                .select_related("file")
+                .first()
+            )
 
         if file is None:
             raise Http404
+        file = file.file
 
         try:
             fp = file.getfile()
             response = StreamingHttpResponse(
                 iter(lambda: fp.read(4096), b""), content_type="application/octet-stream"
@@ -91,13 +110,13 @@ class ProjectArtifactLookupEndpoint(ProjectEndpoint):
         :qparam string release: used in conjunction with `url`.
         :qparam string dist: used in conjunction with `url`.
 
         :auth: required
         """
-        if request.GET.get("download") is not None:
+        if (download_id := request.GET.get("download")) is not None:
             if has_download_permission(request, project):
-                return self.download_file(request.GET.get("download"), project)
+                return self.download_file(download_id, project)
             else:
                 return Response(status=403)
 
         debug_id = request.GET.get("debug_id")
         try:
@@ -112,11 +131,11 @@ class ProjectArtifactLookupEndpoint(ProjectEndpoint):
         bundle_file_ids = set()
 
         def update_bundles(inner_bundles: Set[Tuple[int, datetime, int]]):
             for (bundle_id, date_added, file_id) in inner_bundles:
                 used_artifact_bundles[bundle_id] = date_added
-                bundle_file_ids.add(file_id)
+                bundle_file_ids.add(("artifact_bundle", bundle_id, file_id))
 
         if debug_id:
             bundles = get_artifact_bundles_containing_debug_id(debug_id, project)
             update_bundles(bundles)
 
@@ -130,11 +149,12 @@ class ProjectArtifactLookupEndpoint(ProjectEndpoint):
             bundles = get_release_artifacts(project, release_name, dist_name)
             update_bundles(bundles)
 
             release, dist = try_resolve_release_dist(project, release_name, dist_name)
             if release:
-                bundle_file_ids |= get_legacy_release_bundles(release, dist)
+                for (releasefile_id, file_id) in get_legacy_release_bundles(release, dist):
+                    bundle_file_ids.add(("release_file", releasefile_id, file_id))
                 individual_files = get_legacy_releasefile_by_file_url(release, dist, url)
 
         if options.get("sourcemaps.artifact-bundles.enable-renewal") == 1.0:
             with metrics.timer("artifact_lookup.get.renew_artifact_bundles"):
                 # Before constructing the response, we want to update the artifact bundles renewal date.
@@ -142,33 +162,40 @@ class ProjectArtifactLookupEndpoint(ProjectEndpoint):
 
         # Then: Construct our response
         url_constructor = UrlConstructor(request, project)
 
         found_artifacts = []
-        for file_id in bundle_file_ids:
+        # NOTE: the reason we use the `file_id` as the `id` we return is because
+        # downstream symbolicator relies on that for its internal caching.
+        # We do not want to hard-refresh those caches quite yet, and the `id`
+        # should also be as unique as possible, which the `file_id` is.
+        for (ty, ty_id, file_id) in bundle_file_ids:
             found_artifacts.append(
                 {
                     "id": str(file_id),
                     "type": "bundle",
-                    "url": url_constructor.url_for_file_id(file_id),
+                    "url": url_constructor.url_for_file_id(ty, ty_id),
                 }
             )
 
         for release_file in individual_files:
             found_artifacts.append(
                 {
                     "id": str(release_file.file.id),
                     "type": "file",
-                    "url": url_constructor.url_for_file_id(release_file.file.id),
+                    "url": url_constructor.url_for_file_id("release_file", release_file.id),
                     # The `name` is the url/abs_path of the file,
                     # as in: `"~/path/to/file.min.js"`.
                     "abs_path": release_file.name,
                     # These headers should ideally include the `Sourcemap` reference
                     "headers": release_file.file.headers,
                 }
             )
 
+        # make sure we have a stable sort order for tests
+        found_artifacts.sort(key=lambda x: int(x["id"]))
+
         # NOTE: We do not paginate this response, as we have very tight limits on all the individual queries.
         return Response(serialize(found_artifacts, request.user))
 
 
 def renew_artifact_bundles(used_artifact_bundles: Mapping[int, datetime]):
@@ -257,23 +284,25 @@ def try_resolve_release_dist(
         logger.error("Failed to read", exc_info=exc)
 
     return release, dist
 
 
-def get_legacy_release_bundles(release: Release, dist: Optional[Distribution]):
+def get_legacy_release_bundles(
+    release: Release, dist: Optional[Distribution]
+) -> Set[Tuple[int, int]]:
     return set(
-        ReleaseFile.objects.select_related("file")
-        .filter(
+        ReleaseFile.objects.filter(
             release_id=release.id,
             dist_id=dist.id if dist else None,
             # a `ReleaseFile` with `0` artifacts represents a release archive,
             # see the comment above the definition of `artifact_count`.
             artifact_count=0,
             # similarly the special `type` is also used for release archives.
             file__type=RELEASE_BUNDLE_TYPE,
         )
-        .values_list("file_id", flat=True)
+        .select_related("file")
+        .values_list("id", "file_id")
         # TODO: this `order_by` might be incredibly slow
         # we want to have a hard limit on the returned bundles here. and we would
         # want to pick the most recently uploaded ones. that should mostly be
         # relevant for customers that upload multiple bundles, or are uploading
         # newer files for existing releases. In that case the symbolication is
@@ -302,13 +331,13 @@ class UrlConstructor:
         if is_system_auth(request.auth):
             self.base_url = get_internal_artifact_lookup_source_url(project)
         else:
             self.base_url = request.build_absolute_uri(request.path)
 
-    def url_for_file_id(self, file_id: int) -> str:
+    def url_for_file_id(self, ty: str, file_id: int) -> str:
         # NOTE: Returning a self-route that requires authentication (via Bearer token)
         # is not really forward compatible with a pre-signed URL that does not
         # require any authentication or headers whatsoever.
         # This also requires a workaround in Symbolicator, as its generic http
         # downloader blocks "internal" IPs, whereas the internal Sentry downloader
         # is explicitly exempt.
-        return f"{self.base_url}?download={file_id}"
+        return f"{self.base_url}?download={ty}/{file_id}"
diff --git a/src/sentry/api/endpoints/debug_files.py b/src/sentry/api/endpoints/debug_files.py
index eecd3c6463..ef3ee5e247 100644
--- a/src/sentry/api/endpoints/debug_files.py
+++ b/src/sentry/api/endpoints/debug_files.py
@@ -93,13 +93,15 @@ class DebugFilesEndpoint(ProjectEndpoint):
         if rate_limited:
             logger.info(
                 "notification.rate_limited",
                 extra={"project_id": project.id, "project_debug_file_id": debug_file_id},
             )
-            return HttpResponse({"Too many download requests"}, status=403)
+            return HttpResponse({"Too many download requests"}, status=429)
 
-        debug_file = ProjectDebugFile.objects.filter(id=debug_file_id).first()
+        debug_file = ProjectDebugFile.objects.filter(
+            id=debug_file_id, project_id=project.id
+        ).first()
 
         if debug_file is None:
             raise Http404
 
         try:
diff --git a/tests/sentry/api/endpoints/test_project_artifact_lookup.py b/tests/sentry/api/endpoints/test_project_artifact_lookup.py
index 116d0b1ef0..75e3c96579 100644
--- a/tests/sentry/api/endpoints/test_project_artifact_lookup.py
+++ b/tests/sentry/api/endpoints/test_project_artifact_lookup.py
@@ -90,19 +90,19 @@ class ArtifactLookupTest(APITestCase):
             "bundle_ab.zip",
             {
                 "path/in/zip/a": {
                     "url": "~/path/to/app.js",
                     "type": "source_map",
-                    "content": b"foo",
+                    "content": b"foo_id",
                     "headers": {
                         "debug-id": debug_id_a,
                     },
                 },
                 "path/in/zip/b": {
                     "url": "~/path/to/app.js",
                     "type": "source_map",
-                    "content": b"bar",
+                    "content": b"bar_id",
                     "headers": {
                         "debug-id": debug_id_b,
                     },
                 },
             },
@@ -113,11 +113,15 @@ class ArtifactLookupTest(APITestCase):
             organization_id=self.organization.id,
             bundle_id=bundle_id_ab,
             file=file_ab,
             artifact_count=2,
         )
-
+        ProjectArtifactBundle.objects.create(
+            organization_id=self.organization.id,
+            project_id=self.project.id,
+            artifact_bundle=artifact_bundle_ab,
+        )
         DebugIdArtifactBundle.objects.create(
             organization_id=self.organization.id,
             debug_id=debug_id_a,
             artifact_bundle=artifact_bundle_ab,
             source_file_type=SourceFileType.SOURCE_MAP.value,
@@ -134,11 +138,11 @@ class ArtifactLookupTest(APITestCase):
             "bundle_c.zip",
             {
                 "path/in/zip/c": {
                     "url": "~/path/to/app.js",
                     "type": "source_map",
-                    "content": b"baz",
+                    "content": b"baz_id",
                     "headers": {
                         "debug-id": debug_id_c,
                     },
                 },
             },
@@ -149,11 +153,15 @@ class ArtifactLookupTest(APITestCase):
             organization_id=self.organization.id,
             bundle_id=bundle_id_c,
             file=file_c,
             artifact_count=1,
         )
-
+        ProjectArtifactBundle.objects.create(
+            organization_id=self.organization.id,
+            project_id=self.project.id,
+            artifact_bundle=artifact_bundle_c,
+        )
         DebugIdArtifactBundle.objects.create(
             organization_id=self.organization.id,
             debug_id=debug_id_c,
             artifact_bundle=artifact_bundle_c,
             source_file_type=SourceFileType.SOURCE_MAP.value,
@@ -196,11 +204,11 @@ class ArtifactLookupTest(APITestCase):
             "bundle_a.zip",
             {
                 "path/in/zip": {
                     "url": "~/path/to/app.js",
                     "type": "source_map",
-                    "content": b"foo",
+                    "content": b"foo_url",
                     "headers": {
                         "debug-id": debug_id_a,
                     },
                 },
             },
@@ -209,24 +217,23 @@ class ArtifactLookupTest(APITestCase):
             "bundle_b.zip",
             {
                 "path/in/zip_a": {
                     "url": "~/path/to/app.js",
                     "type": "source_map",
-                    "content": b"foo",
+                    "content": b"foo_url",
                 },
                 "path/in/zip_b": {
                     "url": "~/path/to/other/app.js",
                     "type": "source_map",
-                    "content": b"bar",
+                    "content": b"bar_url",
                 },
             },
         )
 
         artifact_bundle_a = ArtifactBundle.objects.create(
             organization_id=self.organization.id, bundle_id=uuid4(), file=file_a, artifact_count=1
         )
-
         DebugIdArtifactBundle.objects.create(
             organization_id=self.organization.id,
             debug_id=debug_id_a,
             artifact_bundle=artifact_bundle_a,
             source_file_type=SourceFileType.SOURCE_MAP.value,
@@ -335,30 +342,30 @@ class ArtifactLookupTest(APITestCase):
         assert read_artifact_index(self.release, None) is None
 
         archive1, archive1_file = self.create_archive(
             fields={},
             files={
-                "foo": "foo",
-                "bar": "bar",
+                "foo": "foo1",
+                "bar": "bar1",
             },
         )
 
         assert read_artifact_index(self.release, None) == {
             "files": {
                 "fake://foo": {
                     "archive_ident": archive1.ident,
                     "date_created": "2021-06-11T09:13:01.317902Z",
                     "filename": "foo",
-                    "sha1": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
-                    "size": 3,
+                    "sha1": "18a16d4530763ef43321d306c9f6c59ffed33072",
+                    "size": 4,
                 },
                 "fake://bar": {
                     "archive_ident": archive1.ident,
                     "date_created": "2021-06-11T09:13:01.317902Z",
                     "filename": "bar",
-                    "sha1": "62cdb7020ff920e5aa642c3d4066950dd1f01f4d",
-                    "size": 3,
+                    "sha1": "763675d6a1d8d0a3a28deca62bb68abd8baf86f3",
+                    "size": 4,
                 },
             },
         }
 
         # Should download 1 archives as both files are within a single archive
@@ -370,29 +377,29 @@ class ArtifactLookupTest(APITestCase):
 
         # Override `bar` file inside the index. It will now have different `sha1`` and different `archive_ident` as it comes from other archive.
         archive2, archive2_file = self.create_archive(
             fields={},
             files={
-                "bar": "BAR",
+                "bar": "BAR1",
             },
         )
 
         assert read_artifact_index(self.release, None) == {
             "files": {
                 "fake://foo": {
                     "archive_ident": archive1.ident,
                     "date_created": "2021-06-11T09:13:01.317902Z",
                     "filename": "foo",
-                    "sha1": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
-                    "size": 3,
+                    "sha1": "18a16d4530763ef43321d306c9f6c59ffed33072",
+                    "size": 4,
                 },
                 "fake://bar": {
                     "archive_ident": archive2.ident,
                     "date_created": "2021-06-11T09:13:01.317902Z",
                     "filename": "bar",
-                    "sha1": "a5d5c1bba91fdb6c669e1ae0413820885bbfc455",
-                    "size": 3,
+                    "sha1": "7f9353c7b307875542883ba558a1692706fcad33",
+                    "size": 4,
                 },
             },
         }
 
         response = self.client.get(f"{url}?release={self.release.version}&url=foo").json()
@@ -425,12 +432,12 @@ class ArtifactLookupTest(APITestCase):
         dist = self.release.add_dist("foo")
 
         archive1, archive1_file = self.create_archive(
             fields={},
             files={
-                "foo": "foo",
-                "bar": "bar",
+                "foo": "foo2",
+                "bar": "bar2",
             },
             dist=dist,
         )
 
         # No index for dist-less requests.
@@ -440,19 +447,19 @@ class ArtifactLookupTest(APITestCase):
             "files": {
                 "fake://foo": {
                     "archive_ident": archive1.ident,
                     "date_created": "2021-06-11T09:13:01.317902Z",
                     "filename": "foo",
-                    "sha1": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
-                    "size": 3,
+                    "sha1": "aaadd94977b8fbf3f6fb09fc3bbbc9edbdfa8427",
+                    "size": 4,
                 },
                 "fake://bar": {
                     "archive_ident": archive1.ident,
                     "date_created": "2021-06-11T09:13:01.317902Z",
                     "filename": "bar",
-                    "sha1": "62cdb7020ff920e5aa642c3d4066950dd1f01f4d",
-                    "size": 3,
+                    "sha1": "033c4846b506a4a48e32cdf54515c91d3499adb3",
+                    "size": 4,
                 },
             },
         }
 
         # Should download 1 archives as both files are within a single archive
@@ -466,30 +473,30 @@ class ArtifactLookupTest(APITestCase):
 
         # Override `bar` file inside the index. It will now have different `sha1`` and different `archive_ident` as it comes from other archive.
         archive2, archive2_file = self.create_archive(
             fields={},
             files={
-                "bar": "BAR",
+                "bar": "BAR2",
             },
             dist=dist,
         )
 
         assert read_artifact_index(self.release, dist) == {
             "files": {
                 "fake://foo": {
                     "archive_ident": archive1.ident,
                     "date_created": "2021-06-11T09:13:01.317902Z",
                     "filename": "foo",
-                    "sha1": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
-                    "size": 3,
+                    "sha1": "aaadd94977b8fbf3f6fb09fc3bbbc9edbdfa8427",
+                    "size": 4,
                 },
                 "fake://bar": {
                     "archive_ident": archive2.ident,
                     "date_created": "2021-06-11T09:13:01.317902Z",
                     "filename": "bar",
-                    "sha1": "a5d5c1bba91fdb6c669e1ae0413820885bbfc455",
-                    "size": 3,
+                    "sha1": "528c5563f06a1e98954d17d365a219b68dd93baf",
+                    "size": 4,
                 },
             },
         }
 
         response = self.client.get(
@@ -528,11 +535,11 @@ class ArtifactLookupTest(APITestCase):
                     "bundle_c.zip",
                     {
                         "path/in/zip/c": {
                             "url": "~/path/to/app.js",
                             "type": "source_map",
-                            "content": b"baz",
+                            "content": b"baz_renew",
                             "headers": {
                                 "debug-id": debug_id,
                             },
                         },
                     },
@@ -598,11 +605,11 @@ class ArtifactLookupTest(APITestCase):
                 "bundle_c.zip",
                 {
                     "path/in/zip/c": {
                         "url": "~/path/to/app.js",
                         "type": "source_map",
-                        "content": b"baz",
+                        "content": b"baz_renew",
                     },
                 },
             )
 
             for days_before, expected_date_added, release in (

Vulnerable code:     File,
    def download_file(self, file_id, project: Project):
            key=f"rl:ArtifactLookupEndpoint:download:{file_id}:{project.id}",
                extra={"project_id": project.id, "file_id": file_id},
        file = File.objects.filter(id=file_id).first()
        if request.GET.get("download") is not None:
                return self.download_file(request.GET.get("download"), project)
                bundle_file_ids.add(file_id)
                bundle_file_ids |= get_legacy_release_bundles(release, dist)
        for file_id in bundle_file_ids:
                    "url": url_constructor.url_for_file_id(file_id),
                    "url": url_constructor.url_for_file_id(release_file.file.id),
def get_legacy_release_bundles(release: Release, dist: Optional[Distribution]):
        ReleaseFile.objects.select_related("file")
        .filter(
        .values_list("file_id", flat=True)
    def url_for_file_id(self, file_id: int) -> str:
        return f"{self.base_url}?download={file_id}"
            return HttpResponse({"Too many download requests"}, status=403)
        debug_file = ProjectDebugFile.objects.filter(id=debug_file_id).first()
                    "content": b"foo",
                    "content": b"bar",

                    "content": b"baz",

                    "content": b"foo",
                    "content": b"foo",
                    "content": b"bar",

                "foo": "foo",
                "bar": "bar",
                    "sha1": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
                    "size": 3,
                    "sha1": "62cdb7020ff920e5aa642c3d4066950dd1f01f4d",
                    "size": 3,
                "bar": "BAR",
                    "sha1": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
                    "size": 3,
                    "sha1": "a5d5c1bba91fdb6c669e1ae0413820885bbfc455",
                    "size": 3,
                "foo": "foo",
                "bar": "bar",
                    "sha1": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
                    "size": 3,
                    "sha1": "62cdb7020ff920e5aa642c3d4066950dd1f01f4d",
                    "size": 3,
                "bar": "BAR",
                    "sha1": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
                    "size": 3,
                    "sha1": "a5d5c1bba91fdb6c669e1ae0413820885bbfc455",
                    "size": 3,
                            "content": b"baz",
                        "content": b"baz",
Fixed code:     def download_file(self, download_id, project: Project):
        ty, ty_id = download_id.split("/")

            key=f"rl:ArtifactLookupEndpoint:download:{download_id}:{project.id}",
                extra={"project_id": project.id, "file_id": download_id},
        file = None
        if ty == "artifact_bundle":
            file = (
                ArtifactBundle.objects.filter(
                    id=ty_id,
                    projectartifactbundle__project_id=project.id,
                )
                .select_related("file")
                .first()
            )
        elif ty == "release_file":
            # NOTE: `ReleaseFile` does have a `project_id`, but that seems to
            # be always empty, so using the `organization_id` instead.
            file = (
                ReleaseFile.objects.filter(id=ty_id, organization_id=project.organization.id)
                .select_related("file")
                .first()
            )
        file = file.file
        if (download_id := request.GET.get("download")) is not None:
                return self.download_file(download_id, project)
                bundle_file_ids.add(("artifact_bundle", bundle_id, file_id))
                for (releasefile_id, file_id) in get_legacy_release_bundles(release, dist):
                    bundle_file_ids.add(("release_file", releasefile_id, file_id))
        # NOTE: the reason we use the `file_id` as the `id` we return is because
        # downstream symbolicator relies on that for its internal caching.
        # We do not want to hard-refresh those caches quite yet, and the `id`
        # should also be as unique as possible, which the `file_id` is.
        for (ty, ty_id, file_id) in bundle_file_ids:
                    "url": url_constructor.url_for_file_id(ty, ty_id),
                    "url": url_constructor.url_for_file_id("release_file", release_file.id),
        # make sure we have a stable sort order for tests
        found_artifacts.sort(key=lambda x: int(x["id"]))

def get_legacy_release_bundles(
    release: Release, dist: Optional[Distribution]
) -> Set[Tuple[int, int]]:
        ReleaseFile.objects.filter(
        .select_related("file")
        .values_list("id", "file_id")
    def url_for_file_id(self, ty: str, file_id: int) -> str:
        return f"{self.base_url}?download={ty}/{file_id}"
            return HttpResponse({"Too many download requests"}, status=429)
        debug_file = ProjectDebugFile.objects.filter(
            id=debug_file_id, project_id=project.id
        ).first()
                    "content": b"foo_id",
                    "content": b"bar_id",
        ProjectArtifactBundle.objects.create(
            organization_id=self.organization.id,
            project_id=self.project.id,
            artifact_bundle=artifact_bundle_ab,
        )
                    "content": b"baz_id",
        ProjectArtifactBundle.objects.create(
            organization_id=self.organization.id,
            project_id=self.project.id,
            artifact_bundle=artifact_bundle_c,
        )
                    "content": b"foo_url",
                    "content": b"foo_url",
                    "content": b"bar_url",
                "foo": "foo1",
                "bar": "bar1",
                    "sha1": "18a16d4530763ef43321d306c9f6c59ffed33072",
                    "size": 4,
                    "sha1": "763675d6a1d8d0a3a28deca62bb68abd8baf86f3",
                    "size": 4,
                "bar": "BAR1",
                    "sha1": "18a16d4530763ef43321d306c9f6c59ffed33072",
                    "size": 4,
                    "sha1": "7f9353c7b307875542883ba558a1692706fcad33",
                    "size": 4,
                "foo": "foo2",
                "bar": "bar2",
                    "sha1": "aaadd94977b8fbf3f6fb09fc3bbbc9edbdfa8427",
                    "size": 4,
                    "sha1": "033c4846b506a4a48e32cdf54515c91d3499adb3",
                    "size": 4,
                "bar": "BAR2",
                    "sha1": "aaadd94977b8fbf3f6fb09fc3bbbc9edbdfa8427",
                    "size": 4,
                    "sha1": "528c5563f06a1e98954d17d365a219b68dd93baf",
                    "size": 4,
                            "content": b"baz_renew",
                        "content": b"baz_renew",