Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/sentry/core/endpoints/organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,7 @@ def post_org_pending_deletion(
capture_exception(e)

delete_confirmation_args: DeleteConfirmationArgs = {
"username": request.user.get_username(),
"username": require_user(request).get_username(),
"ip_address": entry.ip_address,
"deletion_datetime": entry.datetime,
"countdown": ONE_DAY,
Expand Down
25 changes: 12 additions & 13 deletions src/sentry/core/endpoints/organization_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from sentry.apidocs.examples.user_examples import UserExamples
from sentry.apidocs.parameters import CursorQueryParam, OrganizationParams, VisibilityParams
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.auth.principal import has_user
from sentry.auth.principal import has_user, is_owned_by_acting_user, require_user
from sentry.auth.staff import is_active_staff
from sentry.auth.superuser import is_active_superuser
from sentry.db.models.query import in_iexact
Expand Down Expand Up @@ -184,7 +184,7 @@ def _get_from_cell(self, request: Request) -> Response:

# also fetches organizations in which you are a member of an owner team
queryset = Organization.objects.get_organizations_where_user_is_owner(
user_id=request.user.id
user_id=require_user(request).id
)
if agent_organization_id is not None:
queryset = queryset.filter(id=agent_organization_id)
Expand Down Expand Up @@ -213,9 +213,9 @@ def _get_from_cell(self, request: Request) -> Response:
queryset = queryset.none()
elif not (is_active_superuser(request) and request.GET.get("show") == "all"):
queryset = queryset.filter(
id__in=OrganizationMember.objects.filter(user_id=request.user.id).values(
"organization"
)
id__in=OrganizationMember.objects.filter(
is_owned_by_acting_user(request.user)
).values("organization")
)
if request.auth and request.auth.organization_id is not None and queryset.count() > 1:
# If a token is limited to one organization, this endpoint should only return that one organization
Expand Down Expand Up @@ -325,10 +325,9 @@ def _get_from_control(self, request: Request) -> Response:
else:
queryset = queryset.none()
elif not (is_active_superuser(request) and request.GET.get("show") == "all"):
assert request.user.id is not None
queryset = queryset.filter(
organization_id__in=OrganizationMemberMapping.objects.filter(
user_id=request.user.id
is_owned_by_acting_user(request.user)
).values("organization_id")
)
if request.auth and request.auth.organization_id is not None and queryset.count() > 1:
Expand Down Expand Up @@ -420,11 +419,10 @@ def _get_from_control(self, request: Request) -> Response:
def _get_owned_from_control(
self, request: Request, *, organization_id: int | None = None
) -> Response:
assert request.user.id is not None
owner_role = roles.get_top_dog().id

owner_org_ids = OrganizationMemberMapping.objects.filter(
user_id=request.user.id,
user_id=require_user(request).id,
role=owner_role,
).values_list("organization_id", flat=True)

Expand Down Expand Up @@ -504,9 +502,10 @@ def post(self, request: Request) -> Response:
{"detail": "Organizations are not allowed to be created by this user."}, status=401
)

user = require_user(request)
limit = options.get("api.rate-limit.org-create")
if limit and ratelimiter.backend.is_limited(
f"org-create:{request.user.id}", limit=limit, window=3600
f"org-create:{user.id}", limit=limit, window=3600
):
return Response(
{"detail": "You are attempting to create too many organizations too quickly."},
Expand Down Expand Up @@ -536,21 +535,21 @@ def post(self, request: Request) -> Response:
"type": 0,
},
"ip_address": request.META["REMOTE_ADDR"],
"provisioning_user_id": request.user.id,
"provisioning_user_id": user.id,
"sender": "in-app",
"referrer": "in-app",
}

try:
create_default_team = bool(result.get("defaultTeam"))
rpc_user = serialize_generic_user(request.user)
rpc_user = serialize_generic_user(user)
assert rpc_user # narrow type

provision_args = OrganizationProvisioningOptions(
provision_options=OrganizationOptions(
name=result["name"],
slug=result.get("slug") or result["name"],
owning_user_id=request.user.id,
owning_user_id=user.id,
owner=rpc_user,
create_default_team=create_default_team,
ip_address=request.META["REMOTE_ADDR"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from sentry.apidocs.parameters import GlobalParams
from sentry.apidocs.response_types import DetailResponse
from sentry.auth.access import Access
from sentry.auth.principal import require_user
from sentry.auth.principal import acting_user_id, is_acting_user, require_user
from sentry.auth.superuser import superuser_has_permission
from sentry.core.endpoints.organization_member_utils import can_admin_team, can_set_team_role
from sentry.models.groupassignee import GroupAssignee
Expand Down Expand Up @@ -205,7 +205,7 @@ def _can_delete(
if not request.user.is_authenticated:
return False

if request.user.id == member.user_id:
if is_acting_user(request.user, member.user_id):
return True

# There is an edge case where org owners/managers cannot remove a member from a team they
Expand All @@ -224,8 +224,8 @@ def _create_access_request(
if not created:
return

requester = request.user.id if request.user.id != member.user_id else None
if requester:
requester = acting_user_id(request.user)
if requester and requester != member.user_id:
omt.update(requester_id=requester)

omt.send_request_email()
Expand Down
13 changes: 4 additions & 9 deletions src/sentry/core/endpoints/organization_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from email.headerregistry import Address
from typing import Any, TypedDict, TypeIs

from django.contrib.auth.models import AnonymousUser
from django.db import IntegrityError, router, transaction
from django.db.models import Q
from django.db.models.query import QuerySet
Expand Down Expand Up @@ -47,7 +46,7 @@
)
from sentry.apidocs.response_types import DetailResponse
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.auth.principal import has_user, require_user
from sentry.auth.principal import RequestPrincipal, has_user, is_owned_by_acting_user, require_user
from sentry.constants import ObjectStatus
from sentry.core.endpoints.team_projects import (
AuditData,
Expand Down Expand Up @@ -249,11 +248,8 @@ def get(
team_list = list(Team.objects.filter(organization=organization, slug__in=value))
queryset = queryset.exclude(teams__in=team_list)
elif key == "is_member":
# isnull=False: a caller with no user (id None) would otherwise match
# teamless projects and teams with pending invites.
queryset = queryset.filter(
teams__organizationmember__user_id=request.user.id,
teams__organizationmember__user_id__isnull=False,
is_owned_by_acting_user(request.user, "teams__organizationmember__user_id")
)
else:
queryset = queryset.none()
Expand Down Expand Up @@ -303,7 +299,7 @@ def serialize_on_result(result: Sequence[Project]) -> list[object]:
paginator_cls=OffsetPaginator,
)

def should_add_creator_to_team(self, user: User | AnonymousUser) -> TypeIs[User]:
def should_add_creator_to_team(self, user: RequestPrincipal) -> TypeIs[User]:
return has_user(user)

@extend_schema(
Expand Down Expand Up @@ -487,8 +483,7 @@ def get(self, request: Request, organization: Organization) -> Response:

all_projects = queryset.count()
my_projects = queryset.filter(
teams__organizationmember__user_id=request.user.id,
teams__organizationmember__user_id__isnull=False,
is_owned_by_acting_user(request.user, "teams__organizationmember__user_id")
).count()

return Response({"allProjects": all_projects, "myProjects": my_projects})
4 changes: 2 additions & 2 deletions src/sentry/core/endpoints/organization_teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from sentry.apidocs.response_types import ValidationErrorResponse, as_validation_errors
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.auth.principal import has_user
from sentry.auth.principal import has_user, is_owned_by_acting_user
from sentry.db.models.fields.slug import DEFAULT_SLUG_MAX_LENGTH
from sentry.integrations.models.external_actor import ExternalActor
from sentry.models.organization import Organization
Expand Down Expand Up @@ -231,7 +231,7 @@ def post(
if self.should_add_creator_to_team(request):
try:
member = OrganizationMember.objects.get(
user_id=request.user.id, organization=organization
is_owned_by_acting_user(request.user), organization=organization
)
except OrganizationMember.DoesNotExist:
pass
Expand Down
24 changes: 16 additions & 8 deletions src/sentry/core/endpoints/project_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ValidationErrorResponse,
as_validation_errors,
)
from sentry.auth.principal import acting_user_id
from sentry.constants import (
PROJECT_SLUG_MAX_LENGTH,
RESERVED_PROJECT_SLUGS,
Expand Down Expand Up @@ -825,14 +826,21 @@ def put(
if old_slug:
ProjectRedirect.record(project, old_slug)

if result.get("isBookmarked"):
try:
with transaction.atomic(router.db_for_write(ProjectBookmark)):
ProjectBookmark.objects.create(project_id=project.id, user_id=request.user.id)
except IntegrityError:
pass
elif result.get("isBookmarked") is False:
ProjectBookmark.objects.filter(project_id=project.id, user_id=request.user.id).delete()
# Bookmarks are per user: skipped without one, like token callers always were.
bookmark_user_id = acting_user_id(request.user)
if bookmark_user_id is not None:
if result.get("isBookmarked"):
try:
with transaction.atomic(router.db_for_write(ProjectBookmark)):
ProjectBookmark.objects.create(
project_id=project.id, user_id=bookmark_user_id
)
except IntegrityError:
pass
elif result.get("isBookmarked") is False:
ProjectBookmark.objects.filter(
project_id=project.id, user_id=bookmark_user_id
).delete()

if result.get("digestsMinDelay"):
project.update_option("digests:mail:minimum_delay", result["digestsMinDelay"])
Expand Down
6 changes: 4 additions & 2 deletions src/sentry/core/endpoints/project_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from sentry.api.paginator import DateTimePaginator
from sentry.api.serializers import ProjectWithOrganizationSerializer, serialize
from sentry.auth import access
from sentry.auth.principal import has_user
from sentry.auth.principal import has_user, is_owned_by_acting_user
from sentry.auth.superuser import is_active_superuser
from sentry.constants import CELL_API_DEPRECATION_DATE, ObjectStatus
from sentry.db.models.query import in_iexact
Expand Down Expand Up @@ -88,7 +88,9 @@ def get(self, request: Request) -> Response:
if isinstance(queryset, EmptyQuerySet):
raise AuthenticationFailed("Token not found")
else:
queryset = queryset.filter(teams__organizationmember__user_id=request.user.id)
queryset = queryset.filter(
is_owned_by_acting_user(request.user, "teams__organizationmember__user_id")
)

org_id_filter = request.GET.get("organizationId", None)
if org_id_filter:
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/core/endpoints/project_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from sentry.apidocs.parameters import GlobalParams
from sentry.apidocs.response_types import DetailResponse
from sentry.auth.principal import acting_user_id
from sentry.models.options.project_option import ProjectOption
from sentry.models.organizationmember import OrganizationMember
from sentry.models.project import Project
Expand Down Expand Up @@ -139,7 +140,7 @@ def post(
transaction_id = uuid4().hex
url_data = sign(
salt=SALT,
actor_id=request.user.id,
actor_id=acting_user_id(request.user),
from_organization_id=project.organization_id,
project_id=project.id,
user_id=unique_user_ids.pop(),
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/core/endpoints/scim/members.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from sentry.apidocs.parameters import GlobalParams
from sentry.apidocs.response_types import DetailResponse
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.auth.principal import acting_user_id
from sentry.auth.providers.saml2.activedirectory.apps import ACTIVE_DIRECTORY_PROVIDER_NAME
from sentry.auth.services.auth import auth_service
from sentry.conf.types.sentry_config import SentryMode
Expand Down Expand Up @@ -672,7 +673,7 @@ def post(
organization=organization,
email=result["email"],
role=result["role"],
inviter_id=request.user.id,
inviter_id=acting_user_id(request.user),
)

# TODO: are invite tokens needed for SAML orgs?
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/core/endpoints/team_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
as_validation_errors,
)
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.auth.principal import is_owned_by_acting_user
from sentry.constants import PROJECT_SLUG_MAX_LENGTH, RESERVED_PROJECT_SLUGS, ObjectStatus
from sentry.issue_detection.detectors.disable_detectors import set_default_disabled_detectors
from sentry.models.organization import Organization
Expand Down Expand Up @@ -166,8 +167,8 @@ def _has_team_admin_membership(request: Request, team: Team) -> bool:
):
return False
return OrganizationMemberTeam.objects.filter(
is_owned_by_acting_user(request.user, "organizationmember__user_id"),
team=team,
organizationmember__user_id=request.user.id,
is_active=True,
role__in=[role.id for role in team_roles.with_scope("team:admin")],
).exists()
Expand Down
2 changes: 1 addition & 1 deletion tests/sentry/auth/test_principal.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def _is_request_user_type_check(node: ast.AST) -> bool:


# Lower these as callers move off. Exact, so the baseline can't drift back up.
REQUEST_USER_ID_BASELINE = 299
REQUEST_USER_ID_BASELINE = 278
REQUEST_USER_TYPE_CHECK_BASELINE = 31


Expand Down
49 changes: 49 additions & 0 deletions tests/sentry/core/endpoints/test_organization_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ def test_service_account_sees_projects_without_a_team(self) -> None:
assert response.status_code == 200, response.content
assert teamless.id in [int(project["id"]) for project in response.data]

@override_settings(SEER_API_SHARED_SECRET="service-account-secret")
@with_feature("organizations:service-accounts")
def test_service_account_is_member_of_no_projects(self) -> None:
project = self.create_project(teams=[self.team])
service_account = self.create_service_account(
organization=self.organization,
scope_list=["org:read", "project:read"],
project_ids=[project.id],
)
viewer_context = encode_viewer_context(
ViewerContext(
actor=Actor(type=ActorType.SERVICE_ACCOUNT, actor_id=service_account.id),
organization_id=self.organization.id,
),
key="service-account-secret",
)

response = self.client.get(
reverse(self.endpoint, args=[self.organization.slug]),
{"query": "is_member:1"},
HTTP_X_VIEWER_CONTEXT=viewer_context,
)

assert response.status_code == 200, response.content
assert response.data == []


class OrganizationProjectsTest(OrganizationProjectsTestBase):
def setUp(self) -> None:
Expand Down Expand Up @@ -440,3 +466,26 @@ def test_project_count(self) -> None:

response = self.get_success_response(self.organization.slug, qs_params={"get_counts": "1"})
assert response.data == {"allProjects": 6, "myProjects": 4}

@override_settings(SEER_API_SHARED_SECRET="service-account-secret")
@with_feature("organizations:service-accounts")
def test_service_account_has_no_projects_of_its_own(self) -> None:
project = self.create_project(teams=[self.team])
service_account = self.create_service_account(
organization=self.organization, scope_list=["org:read"], project_ids=[project.id]
)
viewer_context = encode_viewer_context(
ViewerContext(
actor=Actor(type=ActorType.SERVICE_ACCOUNT, actor_id=service_account.id),
organization_id=self.organization.id,
),
key="service-account-secret",
)

response = self.client.get(
reverse(self.endpoint, args=[self.organization.slug]),
HTTP_X_VIEWER_CONTEXT=viewer_context,
)

assert response.status_code == 200, response.content
assert response.data == {"allProjects": 1, "myProjects": 0}
Loading
Loading