Skip to content
Closed
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
6 changes: 3 additions & 3 deletions src/sentry/api/bases/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ class IncidentPermission(OrganizationPermission):
"project:write",
"project:admin",
],
"POST": ["org:write", "org:admin", "project:read", "project:write", "project:admin"],
"PUT": ["org:write", "org:admin", "project:read", "project:write", "project:admin"],
"DELETE": ["org:write", "org:admin", "project:read", "project:write", "project:admin"],
"POST": ["org:write", "org:admin", "project:write", "project:admin"],
"PUT": ["org:write", "org:admin", "project:write", "project:admin"],
"DELETE": ["org:write", "org:admin", "project:write", "project:admin"],
}


Expand Down
84 changes: 62 additions & 22 deletions src/sentry/api/bases/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from sentry.models.project import Project
from sentry.models.release import Release
from sentry.models.releases.release_project import ReleaseProject
from sentry.models.team import Team
from sentry.organizations.services.organization import (
RpcOrganization,
RpcUserOrganizationContext,
Expand Down Expand Up @@ -158,16 +159,16 @@ class OrganizationIntegrationsPermission(OrganizationPermission):
class OrganizationIntegrationsLoosePermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin", "org:integrations", "org:ci"],
"POST": ["org:read", "org:write", "org:admin", "org:integrations"],
"PUT": ["org:read", "org:write", "org:admin", "org:integrations"],
"POST": ["org:write", "org:admin", "org:integrations"],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless I missed it, but this is going to restrict code mappings creation only to admins/owners. We might want to introduce a new scope org:codemappings or something like that

"PUT": ["org:write", "org:admin", "org:integrations"],
"DELETE": ["org:admin", "org:integrations"],
}


class OrganizationCodeMappingsBulkPermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin", "org:integrations", "org:ci"],
"POST": ["org:read", "org:write", "org:admin", "org:integrations", "org:ci"],
"POST": ["org:write", "org:admin", "org:integrations", "org:ci"],
}


Expand Down Expand Up @@ -195,63 +196,102 @@ class OrganizationUserReportsPermission(OrganizationPermission):

class OrganizationPinnedSearchPermission(OrganizationPermission):
scope_map = {
"PUT": ["org:read", "org:write", "org:admin"],
"DELETE": ["org:read", "org:write", "org:admin"],
"PUT": ["user:preferences"],
"DELETE": ["user:preferences"],
}


class OrganizationSearchPermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin"],
"POST": ["org:read", "org:write", "org:admin"],
"PUT": ["org:read", "org:write", "org:admin"],
"DELETE": ["org:read", "org:write", "org:admin"],
"GET": ["org:read", "org:searches"],
"POST": ["org:searches"],
"PUT": ["org:searches"],
"DELETE": ["org:searches"],
}


class OrganizationPreferencePermission(OrganizationPermission):
scope_map = {
"GET": ["user:preferences"],
"POST": ["user:preferences"],
"PUT": ["user:preferences"],
"DELETE": ["user:preferences"],
}


class OrganizationDataExportPermission(OrganizationPermission):
scope_map = {
"GET": ["event:read", "event:write", "event:admin"],
"POST": ["event:read", "event:write", "event:admin"],
"POST": ["event:write", "event:admin"],
}


def _has_any_team_scope(request: Request, scope: str) -> bool:
if not request.access.team_ids_with_membership:
return False

teams = Team.objects.filter(id__in=request.access.team_ids_with_membership)
return any(request.access.has_team_scope(team, scope) for team in teams)


class OrganizationAlertRulePermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin", "alerts:read"],
# grant org:read permission, but raise permission denied if the members aren't allowed
# to create alerts and the user isn't a team admin
"POST": ["org:read", "org:write", "org:admin", "alerts:write"],
"POST": ["org:write", "org:admin", "alerts:write"],
"PUT": ["org:write", "org:admin", "alerts:write"],
"DELETE": ["org:write", "org:admin", "alerts:write"],
}

def has_object_permission(
self,
request: Request,
view: APIView,
organization: Organization | RpcOrganization | RpcUserOrganizationContext,
) -> bool:
if super().has_object_permission(request, view, organization):
return True

return request.method in {"POST", "PUT", "DELETE"} and _has_any_team_scope(
request, "alerts:write"
)


class OrganizationDetectorPermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin", "alerts:read"],
# grant org:read permission, but raise permission denied if the members aren't allowed
# to create alerts and the user isn't a team admin
"POST": ["org:read", "org:write", "org:admin", "alerts:write"],
"PUT": ["org:read", "org:write", "org:admin", "alerts:write"],
"DELETE": ["org:read", "org:write", "org:admin", "alerts:write"],
"POST": ["org:write", "org:admin", "alerts:write"],
"PUT": ["org:write", "org:admin", "alerts:write"],
"DELETE": ["org:write", "org:admin", "alerts:write"],
}

def has_object_permission(
self,
request: Request,
view: APIView,
organization: Organization | RpcOrganization | RpcUserOrganizationContext,
) -> bool:
if super().has_object_permission(request, view, organization):
return True

return request.method in {"POST", "PUT", "DELETE"} and _has_any_team_scope(
request, "alerts:write"
)


class OrgAuthTokenPermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin"],
"POST": ["org:read", "org:write", "org:admin"],
"PUT": ["org:read", "org:write", "org:admin"],
"POST": ["org:write", "org:admin"],
"PUT": ["org:write", "org:admin"],
"DELETE": ["org:write", "org:admin"],
}


class OrganizationFlagWebHookSigningSecretPermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin"],
"POST": ["org:read", "org:write", "org:admin"],
"DELETE": ["org:write", "org:admin"],
"POST": ["flags:write"],
"DELETE": ["flags:write"],
}


Expand Down
3 changes: 3 additions & 0 deletions src/sentry/api/bases/organization_request_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@


class OrganizationRequestChangeEndpointPermission(OrganizationPermission):
readonly_mutation_scope_exceptions = {
"POST": "This endpoint only files a request for an organization change; members use it without organization write access.",
}
# just requesting so read permission is enough
scope_map = {
"POST": ["org:read"],
Expand Down
13 changes: 11 additions & 2 deletions src/sentry/api/bases/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,22 @@ class ProjectAlertRulePermission(ProjectPermission):

class ProjectOwnershipPermission(ProjectPermission):
scope_map = {
"GET": ["project:read", "project:write", "project:admin"],
"GET": ["project:codeowners", "project:read"],
"POST": ["project:write", "project:admin"],
"PUT": ["project:read", "project:write", "project:admin"],
"PUT": ["project:codeowners"],
"DELETE": ["project:admin"],
}


class ProjectCodeOwnersPermission(ProjectPermission):
scope_map = {
"GET": ["project:codeowners", "project:read"],
"POST": ["project:codeowners"],
"PUT": ["project:codeowners"],
"DELETE": ["project:codeowners"],
}


class ProjectEndpoint(Endpoint):
permission_classes: tuple[type[BasePermission], ...] = (ProjectPermission,)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import cell_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPreferencePermission
from sentry.api.serializers.rest_framework.base import CamelSnakeSerializer
from sentry.models.organization import Organization
from sentry.users.models.user import User
Expand Down Expand Up @@ -41,17 +41,13 @@ def get_request_builder_args(user: User, organization: Organization, platforms:
}


class OnboardingContinuationPermission(OrganizationPermission):
scope_map = {"POST": ["org:read", "org:write", "org:admin"]}


@cell_silo_endpoint
class OrganizationOnboardingContinuationEmail(OrganizationEndpoint):
publish_status = {
"POST": ApiPublishStatus.PRIVATE,
}
owner = ApiOwner.VALUE_DISCOVERY
permission_classes = (OnboardingContinuationPermission,)
permission_classes = (OrganizationPreferencePermission,)

def post(self, request: Request, organization: Organization):
serializer = OnboardingContinuationSerializer(data=request.data)
Expand Down
8 changes: 2 additions & 6 deletions src/sentry/api/endpoints/organization_onboarding_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,20 @@
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import cell_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPreferencePermission
from sentry.api.serializers import serialize
from sentry.models.organization import Organization
from sentry.models.organizationonboardingtask import OnboardingTask, OnboardingTaskStatus


class OnboardingTaskPermission(OrganizationPermission):
scope_map = {"POST": ["org:read"], "GET": ["org:read"]}


@cell_silo_endpoint
class OrganizationOnboardingTaskEndpoint(OrganizationEndpoint):
publish_status = {
"POST": ApiPublishStatus.PRIVATE,
"GET": ApiPublishStatus.PRIVATE,
}
owner = ApiOwner.VALUE_DISCOVERY
permission_classes = (OnboardingTaskPermission,)
permission_classes = (OrganizationPreferencePermission,)

def post(self, request: Request, organization) -> Response:
task_id = onboarding_tasks.get_task_lookup_by_key(request.data["task"])
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/api/endpoints/organization_recent_searches.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class RecentSearchSerializer(serializers.Serializer):

class OrganizationRecentSearchPermission(OrganizationPermission):
scope_map = {
"GET": ["org:read", "org:write", "org:admin"],
"POST": ["org:read", "org:write", "org:admin"],
"GET": ["user:preferences"],
"POST": ["user:preferences"],
}


Expand Down
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/project_repo_path_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class ProjectRepoPathParsingEndpointLoosePermission(ProjectPermission):
"""

scope_map = {
"POST": ["org:read", "project:write", "project:admin"],
"POST": ["project:write"],
}


Expand Down
3 changes: 2 additions & 1 deletion src/sentry/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from sentry.auth.system import is_system_auth
from sentry.demo_mode.utils import get_readonly_scopes, is_demo_mode_enabled, is_demo_user
from sentry.hybridcloud.rpc import extract_id_from
from sentry.models.apiscopes import add_scope_hierarchy
from sentry.models.orgauthtoken import is_org_auth_token_auth, update_org_auth_token_last_used
from sentry.organizations.services.organization import (
RpcOrganization,
Expand Down Expand Up @@ -120,7 +121,7 @@ def has_permission(self, request: Request, view: APIView) -> bool:

assert request.method is not None
allowed_scopes = set(self.scope_map.get(request.method, []))
current_scopes = request.auth.get_scopes()
current_scopes = add_scope_hierarchy(request.auth.get_scopes())
return any(s in allowed_scopes for s in current_scopes)

def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool:
Expand Down
4 changes: 3 additions & 1 deletion src/sentry/auth/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,9 @@ def has_any_project_scope(self, project: Project, scopes: Collection[str]) -> bo

def _wrap_scopes(scopes_upper_bound: Iterable[str] | None) -> frozenset[str] | None:
if scopes_upper_bound is not None:
return frozenset(scopes_upper_bound)
from sentry.models.apiscopes import add_scope_hierarchy

return frozenset(add_scope_hierarchy(list(scopes_upper_bound)))
return None


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@


class RepositoryTokenRegeneratePermission(OrganizationPermission):
readonly_mutation_scope_exceptions = {
"POST": "Codecov token regeneration preserves read-only token access for now.",
}
scope_map = {
"GET": ["org:read", "org:write", "org:admin"],
"POST": ["org:read", "org:write", "org:admin"],
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/codecov/endpoints/sync_repos/sync_repos.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@


class SyncReposPermission(OrganizationPermission):
readonly_mutation_scope_exceptions = {
"POST": "Codecov sync preserves read-only token access pending integration-scope cleanup.",
}
scope_map = {
"GET": ["org:read", "org:write", "org:admin"],
"POST": ["org:read", "org:write", "org:admin"],
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/conduit/endpoints/organization_conduit_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class OrganizationConduitDemoPermission(OrganizationPermission):
This is a demo-only feature and doesn't modify organization state.
"""

readonly_mutation_scope_exceptions = {
"POST": "Demo credential generation preserves read-only token access for now.",
}
scope_map = {
"POST": ["org:read", "org:write", "org:admin"],
}
Expand Down
Loading
Loading