Skip to content

Commit 3763eda

Browse files
authored
Merge branch 'master' into nm/nav/issue-detail-actions
2 parents 910fa44 + 1093817 commit 3763eda

File tree

32 files changed

+633
-125
lines changed

32 files changed

+633
-125
lines changed

src/sentry/sentry_apps/api/endpoints/installation_external_issue_actions.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from django.utils.functional import empty
21
from rest_framework import serializers
32
from rest_framework.request import Request
43
from rest_framework.response import Response
@@ -12,22 +11,7 @@
1211
PlatformExternalIssueSerializer,
1312
)
1413
from sentry.sentry_apps.services.cell import sentry_app_cell_service
15-
from sentry.users.models.user import User
16-
from sentry.users.services.user.serial import serialize_rpc_user
17-
18-
19-
def _extract_lazy_object(lo):
20-
"""
21-
Unwrap a LazyObject and return the inner object. Whatever that may be.
22-
23-
ProTip: This is relying on `django.utils.functional.empty`, which may
24-
or may not be removed in the future. It's 100% undocumented.
25-
"""
26-
if not hasattr(lo, "_wrapped"):
27-
return lo
28-
if lo._wrapped is empty:
29-
lo._setup()
30-
return lo._wrapped
14+
from sentry.users.services.user.serial import serialize_generic_user
3115

3216

3317
class SentryAppInstallationExternalIssueActionsSerializer(serializers.Serializer):
@@ -57,9 +41,9 @@ def post(self, request: Request, installation) -> Response:
5741
action = data.pop("action")
5842
uri = data.pop("uri")
5943

60-
user = _extract_lazy_object(request.user)
61-
if isinstance(user, User):
62-
user = serialize_rpc_user(user)
44+
rpc_user = serialize_generic_user(request.user)
45+
if rpc_user is None:
46+
return Response({"detail": "Authentication credentials were not provided."}, status=401)
6347

6448
result = sentry_app_cell_service.create_issue_link(
6549
organization_id=installation.organization_id,
@@ -68,7 +52,7 @@ def post(self, request: Request, installation) -> Response:
6852
action=action,
6953
fields=data,
7054
uri=uri,
71-
user=user,
55+
user=rpc_user,
7256
)
7357

7458
if result.error:

src/sentry/sentry_apps/api/endpoints/installation_external_issue_details.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def delete(self, request: Request, installation, external_issue_id) -> Response:
2525
status_code=400,
2626
)
2727

28+
if not request.user.is_authenticated:
29+
return Response({"detail": "Authentication credentials were not provided."}, status=401)
30+
31+
# Do not pass `user` until cells accept the new RPC arg everywhere (deploy phase 2).
2832
result = sentry_app_cell_service.delete_external_issue(
2933
organization_id=installation.organization_id,
3034
installation=installation,

src/sentry/sentry_apps/api/endpoints/installation_external_issues.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ def post(self, request: Request, installation) -> Response:
4141
except Exception:
4242
return Response({"detail": "issueId is required, and must be an integer"}, status=400)
4343

44+
if not request.user.is_authenticated:
45+
return Response({"detail": "Authentication credentials were not provided."}, status=401)
46+
47+
# Do not pass `user` until cells accept the new RPC arg everywhere (deploy phase 2).
4448
result = sentry_app_cell_service.create_external_issue(
4549
organization_id=installation.organization_id,
4650
installation=installation,

src/sentry/sentry_apps/api/endpoints/installation_external_requests.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,23 @@ def get(self, request: Request, installation: RpcSentryAppInstallation) -> Respo
2626
if not uri:
2727
return Response({"detail": "uri query parameter is required"}, status=400)
2828

29+
if not request.user.is_authenticated:
30+
return Response({"detail": "Authentication credentials were not provided."}, status=401)
31+
32+
project_id: int | None = None
33+
project_id_raw = request.GET.get("projectId")
34+
if project_id_raw:
35+
try:
36+
project_id = int(project_id_raw)
37+
except (TypeError, ValueError):
38+
return Response({"detail": "projectId must be an integer"}, status=400)
39+
40+
# Do not pass `user` until cells accept the new RPC arg everywhere (deploy phase 2).
2941
result = sentry_app_cell_service.get_select_options(
3042
organization_id=installation.organization_id,
3143
installation=installation,
3244
uri=request.GET.get("uri"),
33-
project_id=int(request.GET["projectId"]) if request.GET.get("projectId") else None,
45+
project_id=project_id,
3446
query=request.GET.get("query"),
3547
dependent_data=request.GET.get("dependentData"),
3648
)

src/sentry/sentry_apps/services/cell/impl.py

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def get_select_options(
4444
organization_id: int,
4545
installation: RpcSentryAppInstallation,
4646
uri: str,
47+
user: RpcUser | None = None,
4748
project_id: int | None = None,
4849
query: str | None = None,
4950
dependent_data: str | None = None,
@@ -54,9 +55,32 @@ def get_select_options(
5455

5556
project_slug: str | None = None
5657
if project_id is not None:
57-
project = Project.objects.filter(id=project_id, organization_id=organization_id).first()
58-
if project:
59-
project_slug = project.slug
58+
project = (
59+
Project.objects.select_related("organization")
60+
.filter(id=project_id, organization_id=organization_id)
61+
.first()
62+
)
63+
if not project:
64+
return RpcSelectRequesterResult(
65+
error=RpcSentryAppError(
66+
message="Could not find the given project for this organization.",
67+
status_code=404,
68+
)
69+
)
70+
if user is not None:
71+
access = self._access_for_installation_user(
72+
organization=project.organization,
73+
installation=installation,
74+
user=user,
75+
)
76+
if not access.has_project_access(project):
77+
return RpcSelectRequesterResult(
78+
error=RpcSentryAppError(
79+
message="You do not have permission to access this project.",
80+
status_code=403,
81+
)
82+
)
83+
project_slug = project.slug
6084

6185
try:
6286
result = SelectRequester(
@@ -149,12 +173,13 @@ def create_external_issue(
149173
web_url: str,
150174
project: str,
151175
identifier: str,
176+
user: RpcUser | None = None,
152177
) -> RpcPlatformExternalIssueResult:
153178
"""
154179
Matches: src/sentry/sentry_apps/api/endpoints/installation_external_issues.py @ POST
155180
"""
156181
try:
157-
group = Group.objects.get(
182+
group = Group.objects.select_related("project", "project__organization").get(
158183
id=group_id,
159184
project_id__in=Project.objects.filter(organization_id=organization_id),
160185
)
@@ -166,6 +191,28 @@ def create_external_issue(
166191
)
167192
)
168193

194+
if group.project.organization_id != organization_id:
195+
return RpcPlatformExternalIssueResult(
196+
error=RpcSentryAppError(
197+
message="Could not find the corresponding issue for the given issueId",
198+
status_code=404,
199+
)
200+
)
201+
202+
if user is not None:
203+
access = self._access_for_installation_user(
204+
organization=group.project.organization,
205+
installation=installation,
206+
user=user,
207+
)
208+
if not access.has_project_access(group.project):
209+
return RpcPlatformExternalIssueResult(
210+
error=RpcSentryAppError(
211+
message="You do not have permission to create an external issue for this issue.",
212+
status_code=403,
213+
)
214+
)
215+
169216
try:
170217
external_issue = ExternalIssueCreator(
171218
install=installation,
@@ -187,14 +234,21 @@ def delete_external_issue(
187234
organization_id: int,
188235
installation: RpcSentryAppInstallation,
189236
external_issue_id: int,
237+
user: RpcUser | None = None,
190238
) -> RpcEmptyResult:
191239
"""
192240
Matches: src/sentry/sentry_apps/api/endpoints/installation_external_issue_details.py @ DELETE
193241
"""
194242
try:
195-
platform_external_issue = PlatformExternalIssue.objects.get(
243+
platform_external_issue = PlatformExternalIssue.objects.select_related(
244+
"group",
245+
"group__project",
246+
"group__project__organization",
247+
"project",
248+
"project__organization",
249+
).get(
196250
id=external_issue_id,
197-
project__organization_id=organization_id,
251+
group__project__organization_id=organization_id,
198252
service_type=installation.sentry_app.slug,
199253
)
200254
except PlatformExternalIssue.DoesNotExist:
@@ -206,6 +260,32 @@ def delete_external_issue(
206260
),
207261
)
208262

263+
issue_project = platform_external_issue.project or platform_external_issue.group.project
264+
if issue_project.organization_id != organization_id:
265+
return RpcEmptyResult(
266+
success=False,
267+
error=RpcSentryAppError(
268+
message="Could not find the corresponding external issue from given external_issue_id",
269+
status_code=404,
270+
),
271+
)
272+
organization = issue_project.organization
273+
274+
if user is not None:
275+
access = self._access_for_installation_user(
276+
organization=organization,
277+
installation=installation,
278+
user=user,
279+
)
280+
if not access.has_project_access(issue_project):
281+
return RpcEmptyResult(
282+
success=False,
283+
error=RpcSentryAppError(
284+
message="You do not have permission to delete this external issue.",
285+
status_code=403,
286+
),
287+
)
288+
209289
deletions.exec_sync(platform_external_issue)
210290

211291
return RpcEmptyResult()

src/sentry/sentry_apps/services/cell/service.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def get_select_options(
5151
organization_id: int,
5252
installation: RpcSentryAppInstallation,
5353
uri: str,
54+
user: RpcUser | None = None,
5455
project_id: int | None = None,
5556
query: str | None = None,
5657
dependent_data: str | None = None,
@@ -85,6 +86,7 @@ def create_external_issue(
8586
web_url: str,
8687
project: str,
8788
identifier: str,
89+
user: RpcUser | None = None,
8890
) -> RpcPlatformExternalIssueResult:
8991
"""Invokes ExternalIssueCreator to create an external issue."""
9092
pass
@@ -97,6 +99,7 @@ def delete_external_issue(
9799
organization_id: int,
98100
installation: RpcSentryAppInstallation,
99101
external_issue_id: int,
102+
user: RpcUser | None = None,
100103
) -> RpcEmptyResult:
101104
"""Deletes a PlatformExternalIssue."""
102105
pass

src/sentry/users/services/user/serial.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from collections.abc import Iterable
44
from typing import Any
55

6-
from django.utils.functional import LazyObject
6+
from django.utils.functional import LazyObject, empty
77

88
from sentry.db.models.manager.base_query_set import BaseQuerySet
99
from sentry.users.models.user import User
@@ -23,7 +23,10 @@ def serialize_generic_user(user: Any) -> RpcUser | None:
2323
Return None if the user is anonymous (not logged in).
2424
"""
2525
if isinstance(user, LazyObject): # from auth middleware
26-
user = getattr(user, "_wrapped")
26+
lazy: Any = user
27+
if lazy._wrapped is empty:
28+
lazy._setup()
29+
user = lazy._wrapped
2730
if user is None or user.id is None:
2831
return None
2932
if isinstance(user, RpcUser):

static/app/components/workflowEngine/layout/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ interface WorkflowEngineListLayoutProps {
1212
actions: React.ReactNode;
1313
/** The main content for this page */
1414
children: React.ReactNode;
15-
description: string;
15+
description: React.ReactNode;
1616
docsUrl: string;
1717
title: string;
1818
}

0 commit comments

Comments
 (0)