Skip to content

Commit 60a336c

Browse files
authoredFeb 14, 2024
chore(staff): Let staff access user details endpoint (#64631)
This endpoint is open to people viewing themselves, superuser, and staff There is logic inside PUT and delete that changes depending on which mode you are. Right now: 1. PUT - Superuser can change users to be superuser or staff if they have `users.admin` 2. DELETE - Superusers can hard delete users if they have `users.admin` We want to prevent superusers in the future from being able to do this, but allow staff because these actions are only performed through the _admin portal When checking for `users.admin`, once the feature flag is removed we'll include an explicit check for `is_active_staff` to achieve this.
1 parent cc97408 commit 60a336c

File tree

4 files changed

+265
-71
lines changed

4 files changed

+265
-71
lines changed
 

‎src/sentry/api/endpoints/user_details.py

+21-9
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
from sentry import roles
1414
from sentry.api.api_publish_status import ApiPublishStatus
1515
from sentry.api.base import control_silo_endpoint
16-
from sentry.api.bases.user import UserEndpoint
16+
from sentry.api.bases.user import UserAndStaffPermission, UserEndpoint
1717
from sentry.api.decorators import sudo_required
1818
from sentry.api.endpoints.organization_details import post_org_pending_deletion
1919
from sentry.api.serializers import serialize
2020
from sentry.api.serializers.models.user import DetailedSelfUserSerializer
2121
from sentry.api.serializers.rest_framework import CamelSnakeModelSerializer
22-
from sentry.auth.superuser import is_active_superuser
22+
from sentry.auth.elevated_mode import has_elevated_mode
2323
from sentry.constants import LANGUAGES
2424
from sentry.models.options.user_option import UserOption
2525
from sentry.models.organization import OrganizationStatus
@@ -154,6 +154,8 @@ class UserDetailsEndpoint(UserEndpoint):
154154
"PUT": ApiPublishStatus.UNKNOWN,
155155
}
156156

157+
permission_classes = (UserAndStaffPermission,)
158+
157159
def get(self, request: Request, user) -> Response:
158160
"""
159161
Retrieve User Details
@@ -182,7 +184,13 @@ def put(self, request: Request, user) -> Response:
182184
:param string default_issue_event: Event displayed by default, "recommended", "latest" or "oldest"
183185
:auth: required
184186
"""
185-
if not request.access.has_permission("users.admin"):
187+
# We want to prevent superusers from setting users to superuser or staff
188+
# because this is only done through _admin. This will always be enforced
189+
# once the feature flag is removed.
190+
can_elevate_user = has_elevated_mode(request) and request.access.has_permission(
191+
"users.admin"
192+
)
193+
if not can_elevate_user:
186194
if not user.is_superuser and request.data.get("isSuperuser"):
187195
return Response(
188196
{"detail": "Missing required permission to add superuser."},
@@ -194,11 +202,13 @@ def put(self, request: Request, user) -> Response:
194202
status=status.HTTP_403_FORBIDDEN,
195203
)
196204

197-
if request.access.has_permission("users.admin"):
205+
if can_elevate_user:
198206
serializer_cls = PrivilegedUserSerializer
199-
# with superuser read write separation, superuser read cannot hit this endpoint
200-
# so we can keep this as is_active_superuser
201-
elif is_active_superuser(request):
207+
# With superuser read/write separation, superuser read cannot hit this endpoint
208+
# so we can keep this as is_active_superuser. Once the feature flag is
209+
# removed and we only check is_active_staff, we can remove this comment.
210+
elif has_elevated_mode(request):
211+
# TODO(schew2381): Rename to staff serializer
202212
serializer_cls = SuperuserUserSerializer
203213
else:
204214
serializer_cls = UserSerializer
@@ -257,7 +267,6 @@ def delete(self, request: Request, user) -> Response:
257267
:param list organizations: List of organization ids to remove
258268
:auth required:
259269
"""
260-
261270
serializer = DeleteUserSerializer(data=request.data)
262271

263272
if not serializer.is_valid():
@@ -328,9 +337,12 @@ def delete(self, request: Request, user) -> Response:
328337
}
329338

330339
hard_delete = serializer.validated_data.get("hardDelete", False)
340+
can_delete = has_elevated_mode(request) and request.access.has_permission("users.admin")
331341

332342
# Only active superusers can hard delete accounts
333-
if hard_delete and not request.access.has_permission("users.admin"):
343+
# This will be changed to only active staff can delete accounts once the
344+
# staff feature flag is removed.
345+
if hard_delete and not can_delete:
334346
return Response(
335347
{"detail": "Missing required permission to hard delete account."},
336348
status=status.HTTP_403_FORBIDDEN,

‎src/sentry/api/serializers/models/user.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from sentry.api.serializers import Serializer, register
1616
from sentry.api.serializers.types import SerializedAvatarFields
1717
from sentry.app import env
18-
from sentry.auth.superuser import is_active_superuser
18+
from sentry.auth.elevated_mode import has_elevated_mode
1919
from sentry.models.authenticator import Authenticator
2020
from sentry.models.authidentity import AuthIdentity
2121
from sentry.models.avatars.user_avatar import UserAvatar
@@ -122,7 +122,8 @@ def _user_is_requester(self, obj: User, requester: User | AnonymousUser | RpcUse
122122
def _get_identities(
123123
self, item_list: Sequence[User], user: User
124124
) -> dict[int, list[AuthIdentity]]:
125-
if not (env.request and is_active_superuser(env.request)):
125+
126+
if not (env.request and has_elevated_mode(env.request)):
126127
item_list = [x for x in item_list if x.id == user.id]
127128

128129
queryset = AuthIdentity.objects.filter(
@@ -290,8 +291,10 @@ def serialize(
290291
) -> DetailedUserSerializerResponse:
291292
d = cast(DetailedUserSerializerResponse, super().serialize(obj, attrs, user))
292293

293-
# XXX(dcramer): we don't use is_active_superuser here as we simply
294-
# want to tell the UI that we're an authenticated superuser, and
294+
# TODO(schew2381): Remove mention of superuser below once the staff feature flag is removed
295+
296+
# XXX(dcramer): we don't check for active superuser/staff here as we simply
297+
# want to tell the UI that we're an authenticated superuser/staff, and
295298
# for requests that require an *active* session, they should prompt
296299
# on-demand. This ensures things like links to the Sentry admin can
297300
# still easily be rendered.
@@ -357,9 +360,10 @@ def serialize(
357360
d = cast(DetailedSelfUserSerializerResponse, super().serialize(obj, attrs, user))
358361

359362
# safety check to never return this information if the acting user is not 1) this user, 2) an admin
360-
if user.id == obj.id or user.is_superuser:
361-
# XXX(dcramer): we don't use is_active_superuser here as we simply
362-
# want to tell the UI that we're an authenticated superuser, and
363+
# TODO(schew2381): Remove user.is_superuser once the staff feature flag is removed
364+
if user.id == obj.id or user.is_superuser or user.is_staff:
365+
# XXX(dcramer): we don't check for active superuser/staff here as we simply
366+
# want to tell the UI that we're an authenticated superuser/staff, and
363367
# for requests that require an *active* session, they should prompt
364368
# on-demand. This ensures things like links to the Sentry admin can
365369
# still easily be rendered.

0 commit comments

Comments
 (0)