Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 15 additions & 27 deletions src/sentry/api/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from sentry.api.exceptions import StaffRequired, SuperuserRequired
from sentry.apidocs.hooks import HTTP_METHOD_NAME
from sentry.auth import access
from sentry.auth.staff import has_staff_option
from sentry.middleware import is_frontend_request
from sentry.organizations.absolute_url import generate_organization_url
from sentry.ratelimits.config import DEFAULT_RATE_LIMIT_CONFIG, RateLimitConfig
Expand Down Expand Up @@ -60,12 +59,7 @@
update_token_access_record,
)
from .paginator import BadPaginationError, MissingPaginationError, Paginator
from .permissions import (
NoPermission,
StaffPermission,
SuperuserOrStaffFeatureFlaggedPermission,
SuperuserPermission,
)
from .permissions import NoPermission, StaffPermission, SuperuserPermission

__all__ = [
"Endpoint",
Expand Down Expand Up @@ -261,33 +255,27 @@ def convert_args(self, request: Request, *args, **kwargs):

def permission_denied(self, request, message=None, code=None):
"""
Raise a specific superuser exception if the user can become superuser
and the only permission class is SuperuserPermission. Otherwise, raises
Raise a specific staff/superuser exception if the user can become staff/superuser
and the only permission class is StaffPermission/SuperuserPermission. Otherwise, raises
the appropriate exception according to parent DRF function.
"""
permissions = self.get_permissions()
if request.user.is_authenticated and len(permissions) == 1:

# User with staff permission should raise StaffRequired error
permission_cls = permissions[0]
enforce_staff_permission = has_staff_option(request.user)

# TODO(schew2381): Remove SuperuserOrStaffFeatureFlaggedPermission
# from isinstance checks once feature flag is removed.
if enforce_staff_permission:
is_staff_user = request.user.is_staff
has_only_staff_permission = isinstance(
permission_cls, (StaffPermission, SuperuserOrStaffFeatureFlaggedPermission)
)
is_staff_user = request.user.is_staff
has_only_staff_permission = isinstance(permission_cls, StaffPermission)

if is_staff_user and has_only_staff_permission:
raise StaffRequired()
else:
is_superuser_user = request.user.is_superuser
has_only_superuser_permission = isinstance(
permission_cls, (SuperuserPermission, SuperuserOrStaffFeatureFlaggedPermission)
)
if is_staff_user and has_only_staff_permission:
raise StaffRequired()

# User with superuser permission should raise SuperuserRequired error
is_superuser_user = request.user.is_superuser
has_only_superuser_permission = isinstance(permission_cls, SuperuserPermission)

if is_superuser_user and has_only_superuser_permission:
raise SuperuserRequired()
if is_superuser_user and has_only_superuser_permission:
raise SuperuserRequired()

super().permission_denied(request, message, code)

Expand Down
4 changes: 2 additions & 2 deletions src/sentry/api/endpoints/admin_project_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission
from sentry.api.permissions import StaffPermission
from sentry.models.project import Project
from sentry.models.projectkey import ProjectKey
from sentry.relay import projectconfig_cache
Expand All @@ -23,7 +23,7 @@ class AdminRelayProjectConfigsEndpoint(Endpoint):
"GET": ApiPublishStatus.PRIVATE,
"POST": ApiPublishStatus.PRIVATE,
}
permission_classes = (SuperuserOrStaffFeatureFlaggedPermission,)
permission_classes = (StaffPermission,)

def get(self, request: Request) -> Response:
"""The GET endpoint retrieves the project configs for a specific project_id
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/api/endpoints/organization_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission
from sentry.api.permissions import StaffPermission
from sentry.api.serializers import serialize
from sentry.hybridcloud.services.organization_mapping import organization_mapping_service
from sentry.models.organization import OrganizationStatus
Expand Down Expand Up @@ -51,7 +51,7 @@ class OrganizationForkEndpoint(Endpoint):
publish_status = {
"POST": ApiPublishStatus.EXPERIMENTAL,
}
permission_classes = (SuperuserOrStaffFeatureFlaggedPermission,)
permission_classes = (StaffPermission,)

def post(self, request: Request, organization_id_or_slug) -> Response:
"""
Expand Down
18 changes: 1 addition & 17 deletions src/sentry/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
TwoFactorRequired,
)
from sentry.auth import access
from sentry.auth.staff import has_staff_option, is_active_staff
from sentry.auth.staff import is_active_staff
from sentry.auth.superuser import SUPERUSER_ORG_ID, is_active_superuser
from sentry.auth.system import is_system_auth
from sentry.demo_mode.utils import get_readonly_scopes, is_demo_mode_enabled, is_demo_user
Expand Down Expand Up @@ -67,22 +67,6 @@ def has_permission(self, request: Request, view: object) -> bool:
return is_active_staff(request)


# NOTE(schew2381): This is a temporary permission that does NOT perform an OR
# between SuperuserPermission and StaffPermission. Instead, it uses StaffPermission
# if the option is enabled for the user, and otherwise checks SuperuserPermission. We
# need this to handle the transition for endpoints that will only be accessible to
# staff but not superuser, that currently use SuperuserPermission. Once staff is
# released to the everyone, we can delete this permission and use StaffPermission
class SuperuserOrStaffFeatureFlaggedPermission(BasePermission):
def has_permission(self, request: Request, view: object) -> bool:
enforce_staff_permission = has_staff_option(request.user)

if enforce_staff_permission:
return StaffPermission().has_permission(request, view)

return SuperuserPermission().has_permission(request, view)


class ScopedPermission(BasePermission):
"""
Permissions work depending on the type of authentication:
Expand Down
13 changes: 3 additions & 10 deletions src/sentry/auth/elevated_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,13 @@ def on_response(cls, response: HttpResponse) -> None:
pass


# TODO(schew2381): Delete this method after the option is removed
def has_elevated_mode(request: HttpRequest) -> bool:
"""
This is a temporary helper method that checks if the user on the request has
the staff option enabled. If so, it checks is_active_staff and otherwise
defaults to checking is_active_superuser.
Checks if the user on the request has active staff mode.
"""
from sentry.auth.staff import has_staff_option, is_active_staff
from sentry.auth.superuser import is_active_superuser
from sentry.auth.staff import is_active_staff

if isinstance(request.user, AnonymousUser):
return False

if has_staff_option(request.user):
return is_active_staff(request)

return is_active_superuser(request)
return is_active_staff(request)
17 changes: 0 additions & 17 deletions src/sentry/auth/staff.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
from typing import Any, Final

from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.signing import BadSignature
from django.http import HttpRequest, HttpResponse
from django.utils import timezone as django_timezone
from django.utils.crypto import constant_time_compare, get_random_string

from sentry import options
from sentry.auth.elevated_mode import ElevatedMode, InactiveReason
from sentry.auth.system import is_system_auth
from sentry.users.models.user import User
Expand Down Expand Up @@ -54,21 +52,6 @@ def is_active_staff(request: HttpRequest) -> bool:
return staff.is_active


# TODO(schew2381): Delete after staff is GA'd and the options are removed
def has_staff_option(user: User | AnonymousUser) -> bool:
"""
This checks two options, the first being whether or not staff has been GA'd.
If not, it falls back to checking the second option which by email specifies which
users staff is enabled for.
"""
if options.get("staff.ga-rollout"):
return True

if (email := getattr(user, "email", None)) is None:
return False
return email in options.get("staff.user-email-allowlist")


def _seconds_to_timestamp(seconds: str) -> datetime:
return datetime.fromtimestamp(float(seconds), timezone.utc)

Expand Down
13 changes: 1 addition & 12 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,18 +288,7 @@
)

# Staff
register(
"staff.ga-rollout",
type=Bool,
default=False,
flags=FLAG_MODIFIABLE_BOOL | FLAG_AUTOMATOR_MODIFIABLE,
)
register(
"staff.user-email-allowlist",
type=Sequence,
default=[],
flags=FLAG_ALLOW_EMPTY | FLAG_AUTOMATOR_MODIFIABLE,
)
# (No longer needed - staff is now GA)
# Superuser read/write
register(
"superuser.read-write.ga-rollout",
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/relocation/api/endpoints/abort.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission
from sentry.api.permissions import StaffPermission
from sentry.api.serializers import serialize
from sentry.relocation.models.relocation import Relocation

Expand All @@ -25,7 +25,7 @@ class RelocationAbortEndpoint(Endpoint):
# TODO(getsentry/team-ospo#214): Stabilize before GA.
"PUT": ApiPublishStatus.EXPERIMENTAL,
}
permission_classes = (SuperuserOrStaffFeatureFlaggedPermission,)
permission_classes = (StaffPermission,)

def put(self, request: Request, relocation_uuid: str) -> Response:
"""
Expand Down
14 changes: 5 additions & 9 deletions src/sentry/relocation/api/endpoints/artifacts/details.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.exceptions import ResourceDoesNotExist, StaffRequired, SuperuserRequired
from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission
from sentry.api.exceptions import ResourceDoesNotExist, StaffRequired
from sentry.api.permissions import StaffPermission
from sentry.auth.elevated_mode import has_elevated_mode
from sentry.auth.staff import has_staff_option
from sentry.backup.crypto import (
CryptoKeyVersion,
GCPKMSDecryptor,
Expand Down Expand Up @@ -43,7 +42,7 @@ class RelocationArtifactDetailsEndpoint(Endpoint):
# TODO(getsentry/team-ospo#214): Stabilize before GA.
"GET": ApiPublishStatus.EXPERIMENTAL,
}
permission_classes = (SuperuserOrStaffFeatureFlaggedPermission,)
permission_classes = (StaffPermission,)

def get(
self, request: Request, relocation_uuid: str, artifact_kind: str, file_name: str
Expand All @@ -61,12 +60,9 @@ def get(

logger.info("relocations.artifact.details.get.start", extra={"caller": request.user.id})

# TODO(schew2381): Remove the superuser reference below after feature flag is removed.
# Must be superuser/staff AND have a `UserPermission` of `relocation.admin` to see access!
# Must be staff AND have a `UserPermission` of `relocation.admin` to see access!
if not has_elevated_mode(request):
if has_staff_option(request.user):
raise StaffRequired
raise SuperuserRequired
raise StaffRequired

if not request.access.has_permission("relocation.admin"):
raise PermissionDenied(
Expand Down
14 changes: 5 additions & 9 deletions src/sentry/relocation/api/endpoints/artifacts/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.exceptions import ResourceDoesNotExist, StaffRequired, SuperuserRequired
from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission
from sentry.api.exceptions import ResourceDoesNotExist, StaffRequired
from sentry.api.permissions import StaffPermission
from sentry.auth.elevated_mode import has_elevated_mode
from sentry.auth.staff import has_staff_option
from sentry.models.files.utils import get_relocation_storage
from sentry.relocation.models.relocation import Relocation

Expand All @@ -28,7 +27,7 @@ class RelocationArtifactIndexEndpoint(Endpoint):
# TODO(getsentry/team-ospo#214): Stabilize before GA.
"GET": ApiPublishStatus.EXPERIMENTAL,
}
permission_classes = (SuperuserOrStaffFeatureFlaggedPermission,)
permission_classes = (StaffPermission,)

def get(self, request: Request, relocation_uuid: str) -> Response:
"""
Expand All @@ -42,12 +41,9 @@ def get(self, request: Request, relocation_uuid: str) -> Response:

logger.info("relocations.artifact.index.get.start", extra={"caller": request.user.id})

# TODO(schew2381): Remove the superuser reference below after feature flag is removed.
# Must be superuser/staff AND have a `UserPermission` of `relocation.admin` to see access!
# Must be staff AND have a `UserPermission` of `relocation.admin` to see access!
if not has_elevated_mode(request):
if has_staff_option(request.user):
raise StaffRequired
raise SuperuserRequired
raise StaffRequired

if not request.access.has_permission("relocation.admin"):
raise PermissionDenied(ERR_NEED_RELOCATION_ADMIN)
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/relocation/api/endpoints/cancel.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission
from sentry.api.permissions import StaffPermission
from sentry.api.serializers import serialize
from sentry.relocation.api.endpoints import ERR_UNKNOWN_RELOCATION_STEP
from sentry.relocation.models.relocation import Relocation
Expand All @@ -36,7 +36,7 @@ class RelocationCancelEndpoint(Endpoint):
# TODO(getsentry/team-ospo#214): Stabilize before GA.
"PUT": ApiPublishStatus.EXPERIMENTAL,
}
permission_classes = (SuperuserOrStaffFeatureFlaggedPermission,)
permission_classes = (StaffPermission,)

def put(self, request: Request, relocation_uuid: str) -> Response:
"""
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/relocation/api/endpoints/details.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import Endpoint, region_silo_endpoint
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.permissions import SuperuserOrStaffFeatureFlaggedPermission
from sentry.api.permissions import StaffPermission
from sentry.api.serializers import serialize
from sentry.relocation.models.relocation import Relocation

Expand All @@ -21,7 +21,7 @@ class RelocationDetailsEndpoint(Endpoint):
# TODO(getsentry/team-ospo#214): Stabilize before GA.
"GET": ApiPublishStatus.EXPERIMENTAL,
}
permission_classes = (SuperuserOrStaffFeatureFlaggedPermission,)
permission_classes = (StaffPermission,)

def get(self, request: Request, relocation_uuid: str) -> Response:
"""
Expand Down
11 changes: 4 additions & 7 deletions src/sentry/relocation/api/endpoints/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,15 @@ class RelocationsPostSerializer(serializers.Serializer):
def validate_new_relocation_request(
request: Request, owner_username: str, org_slugs: list[str], file_size: int
) -> Response | None:
# TODO(schew2381): Remove all superuser references in function after feature flag is removed.

# We only honor the `relocation.enabled` flag for non-superusers/staff.
# We only honor the `relocation.enabled` flag for non-staff.
elevated_user = has_elevated_mode(request)
if not options.get("relocation.enabled") and not elevated_user:
return Response({"detail": ERR_FEATURE_DISABLED}, status=status.HTTP_403_FORBIDDEN)

if not request.user.is_authenticated:
return Response(status=status.HTTP_400_BAD_REQUEST)

# Only superuser/staff can start relocations for other users.
# Only staff can start relocations for other users.
creator = user_service.get_user(user_id=request.user.id)
if creator is None:
raise RuntimeError("Could not ascertain request user's username")
Expand All @@ -121,7 +119,7 @@ def validate_new_relocation_request(
status=status.HTTP_400_BAD_REQUEST,
)

# Regular users may be throttled, but superuser/staff never are.
# Regular users may be throttled, but staff never are.
relocation_size_category = get_relocation_size_category(file_size)
if not elevated_user and should_throttle_relocation(relocation_size_category):
return Response(
Expand Down Expand Up @@ -185,8 +183,7 @@ def get(self, request: Request) -> Response:

queryset = Relocation.objects.all()

# TODO(schew2381): Remove the superuser reference below after feature flag is removed.
# Non-superuser/staff can only see their own relocations.
# Non-staff can only see their own relocations.
if not has_elevated_mode(request):
queryset = queryset.filter(owner_id=request.user.id)

Expand Down
Loading
Loading