Skip to content

Commit 69098d2

Browse files
committed
fix(preprod): Fix permission check to respect org-level roles
The permission check was using has_project_membership which only checks team membership, rejecting users with org-level roles (owner, manager) who lack direct team membership. Fixed by: - Using get_projects() with explicit project_ids to leverage has_project_access (respects org-level permissions) - Overriding check_object_permissions to skip org scope checks - Converting PermissionDenied (403) to 404 for security - Checking feature flags early to return 404 when disabled
1 parent 472e43d commit 69098d2

File tree

1 file changed

+35
-6
lines changed

1 file changed

+35
-6
lines changed

src/sentry/preprod/api/bases/preprod_artifact_endpoint.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44

55
import sentry_sdk
66
from rest_framework import status
7-
from rest_framework.exceptions import APIException
7+
from rest_framework.exceptions import APIException, PermissionDenied
88
from rest_framework.permissions import BasePermission
99
from rest_framework.request import Request
1010

11-
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationEventPermission
11+
from sentry import features
12+
from sentry.api.bases.organization import (
13+
OrganizationEndpoint,
14+
OrganizationEventPermission,
15+
OrganizationPermission,
16+
)
1217
from sentry.constants import ObjectStatus
1318
from sentry.models.project import Project
1419
from sentry.preprod.authentication import LaunchpadRpcSignatureAuthentication
@@ -42,7 +47,15 @@ class ProjectPreprodArtifactPermission(OrganizationEventPermission):
4247

4348

4449
class PreprodArtifactEndpoint(OrganizationEndpoint):
45-
permission_classes: tuple[type[BasePermission], ...] = (OrganizationEventPermission,)
50+
# Override permission_classes to skip org-level scope checks in the parent class.
51+
# We do project-level permission checks in convert_args and return 404 (not 403)
52+
# for security when users lack project access
53+
permission_classes: tuple[type[BasePermission], ...] = ()
54+
55+
def check_object_permissions(self, request: Request, obj: Any) -> None:
56+
# Set up request.access but skip org-level scope checks.
57+
# Project-level permissions are checked in convert_args with 404 (not 403) on denial.
58+
OrganizationPermission().determine_access(request, obj)
4659

4760
def convert_args(
4861
self,
@@ -79,12 +92,28 @@ def convert_args(
7992
if project.status != ObjectStatus.ACTIVE:
8093
raise HeadPreprodArtifactResourceDoesNotExist
8194

82-
# Skip project access check for service-to-service RPC authenticated requests
95+
# Return 404 (not 403) when feature is disabled to avoid information leakage
96+
if not features.has(
97+
"organizations:preprod-frontend-routes", project.organization, actor=request.user
98+
):
99+
raise HeadPreprodArtifactResourceDoesNotExist
100+
101+
# Skip project access check for RPC-authenticated requests
83102
is_rpc_authenticated = hasattr(request, "successful_authenticator") and isinstance(
84103
request.successful_authenticator, LaunchpadRpcSignatureAuthentication
85104
)
86-
if not is_rpc_authenticated and project not in self.get_projects(request, organization):
87-
raise HeadPreprodArtifactResourceDoesNotExist
105+
106+
if not is_rpc_authenticated:
107+
# Check project access using has_project_access (respects org roles) not
108+
# has_project_membership (team-only). Convert PermissionDenied to 404 for security.
109+
try:
110+
accessible_projects = self.get_projects(
111+
request, organization, project_ids={project.id}
112+
)
113+
if project not in accessible_projects:
114+
raise HeadPreprodArtifactResourceDoesNotExist
115+
except PermissionDenied:
116+
raise HeadPreprodArtifactResourceDoesNotExist
88117

89118
# If project_id_or_slug is provided, validate it matches the artifact's project
90119
if project_id_or_slug is not None:

0 commit comments

Comments
 (0)