-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat(scim): Manage privileges via SCIM Groups #107709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
5817726 to
53727cd
Compare
53727cd to
94b13e2
Compare
94b13e2 to
ae266ed
Compare
ae266ed to
f2b5d5a
Compare
| return ( | ||
| settings.SENTRY_MODE == SentryMode.SAAS | ||
| and team.organization.id == settings.SENTRY_DEFAULT_ORGANIZATION_ID | ||
| and team.slug in ("snty-staff", "snty-superuser") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Jeffreyhung Are these team names OK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we break superuser down to read and write? or is this just prefix and we can append read write at the end?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah good point about superuser read and write! Hmm... So users who are superuser.write would be a subset of users who are superuser.read. How would you like to model this in the Idp? We could either have 2 distinct groups, which permissions read and write separately (i.e. User Y would need to be both in read and write groups), or we can have 2 distinct groups that are mutually exclusive (i.e. User Y who has superuser write, will be given both read and write permissions, but will only be in the write group in Okta). @Jeffreyhung What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will make things easier to manage
we can have 2 distinct groups that are mutually exclusive (i.e. User Y who has superuser write, will be given both read and write permissions, but will only be in the write group in Okta
f2b5d5a to
60b2f7c
Compare
| ) | ||
| members_to_revoke.append((member, team)) | ||
| except OrganizationMember.DoesNotExist: | ||
| pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
REMOVE pre-scan revokes privileges without verifying team membership
High Severity
The REMOVE pre-scan only checks that the OrganizationMember exists in the organization, but never verifies the member is actually in the team before scheduling privilege revocation. In contrast, the REPLACE pre-scan correctly queries OrganizationMemberTeam.objects.filter(team_id=team.id) to only collect actual team members. Additionally, _remove_members_operation itself correctly handles the "not in team" case by catching OrganizationMemberTeam.DoesNotExist and returning — but by then, the privilege has already been irrevocably revoked via the RPC call. This means a SCIM REMOVE operation targeting any org member (even one never in snty-staff or snty-superuser) would incorrectly strip their is_staff or is_superuser flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... this might be OK because it lets us remove the flags from users who might not be added to the team yet.
| "member_id": member.id, | ||
| }, | ||
| ) | ||
| failed_grants.append(member.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grant phase doesn't verify final team membership state
Low Severity
members_to_grant_privileges accumulates members from all ADD and REPLACE operations across the entire PATCH, but the post-transaction grant phase never re-verifies that those members are still in the team after all operations complete. If a later operation (e.g., REPLACE or REMOVE) undoes an earlier ADD within the same PATCH, the member ends up with is_staff or is_superuser granted despite no longer being in the privileged team. The grant phase at line 588 iterates all accumulated members without checking OrganizationMemberTeam existence.
Additional Locations (1)
When members are added/removed from "staff" or "superuser" teams in the default SaaS organization, automatically grant or revoke the corresponding is_staff/is_superuser flags. Key changes: - Grant is_staff when adding to "staff" team, is_superuser to "superuser" team - Revoke only the matching privilege when removing from privileged teams - Security-first two-pass architecture: revoke before delete, grant after add - Handle transaction constraints (RPC calls outside transactions) - Add error handling and logging for privilege update failures - Add comprehensive test coverage (8 new tests)
When a snty-staff or snty-superuser team is deleted, automatically revoke the corresponding privileges from all team members. Key changes: - Revoke is_staff when deleting snty-staff team - Revoke is_superuser when deleting snty-superuser team - Fail with 500 error if privilege revocation fails during deletion - Add 4 new tests for team deletion privilege management Co-Authored-By: Claude <noreply@anthropic.com>
60b2f7c to
38fcbb4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| def _remove_members_operation(self, request: Request, member_id, team): | ||
| added_members.append(member) | ||
|
|
||
| return added_members |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ADD retry after grant failure silently skips privilege
Medium Severity
If _grant_privilege fails (RPC error) and returns a 500, the team membership transaction has already committed. When the SCIM provider retries the same ADD operation, _add_members_operation sees the member already exists in the team (line 411) and continues, returning an empty added_members list. The grant loop has nothing to process, and the endpoint returns 204 success — but the privilege was never granted. The operation is not idempotent for privileged teams, leaving a member in the team without the corresponding is_staff/is_superuser flag.
Additional Locations (1)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this happens, then we can just remove then re-add the user. There's going to be inconsistencies here because it's over 2 different silos and there's no real "transaction" we can enforce on this.
Add RPC service methods to manage UserPermission records across silos: - `add_permission(user_id, permission)` - Creates a permission for a user - `remove_permission(user_id, permission)` - Removes a permission from a user These methods enable region silo endpoints to manage control silo UserPermission records through proper RPC calls instead of direct database access. Both methods use proper transaction handling with `router.db_for_write(UserPermission)` and return boolean values indicating whether the operation created/removed a record. Co-Authored-By: Claude <noreply@anthropic.com>
Rename snty-superuser to snty-superuser-read and add new snty-superuser-write team for granular superuser privilege management: - **snty-superuser-read**: Grants `is_superuser` flag only - **snty-superuser-write**: Grants `is_superuser` flag + "superuser.write" permission Changes: - Update `_should_manage_privileges()` to recognize three privileged teams - Update `_grant_privilege()` to use `user_service.add_permission()` RPC for managing UserPermission records across silos - Update `_revoke_privileges()` to use `user_service.remove_permission()` RPC - Fix tests to wrap UserPermission access with `assume_test_silo_mode(SiloMode.CONTROL)` since UserPermission is a control silo model This enables proper cross-silo permission management where region silo SCIM endpoints can manage control silo UserPermission records through RPC instead of direct database access. Tests: 34 passing Co-Authored-By: Claude <noreply@anthropic.com>
92c132d to
19452e9
Compare
| "scim.privilege_grant_failed", | ||
| extra={ | ||
| "organization_id": team.organization.id, | ||
| "team_id": team.id, | ||
| "member_id": member.id, | ||
| }, | ||
| ) | ||
| return Response( | ||
| {"detail": "Failed to grant user privileges"}, | ||
| status=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: A failure during privilege granting, after team membership is committed, leads to a permanent inconsistent state because idempotent retries will not re-attempt the grant.
Severity: HIGH
Suggested Fix
To ensure atomicity, the privilege granting logic should be moved inside the main database transaction. Alternatively, implement a compensation mechanism, such as rolling back the team membership if the privilege grant fails, or create a background job to reconcile users who are in this inconsistent state.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/sentry/core/endpoints/scim/teams.py#L608-L623
Potential issue: The SCIM team update logic separates team membership changes from
privilege granting. Team membership is saved in a database transaction, and then
privilege-granting RPC calls are made afterward. If a privilege grant fails, the user is
left in an inconsistent state: they are a member of the team but lack the necessary
privileges. Because the operation is idempotent, subsequent retries by the SCIM client
will detect that the user is already on the team and will not re-attempt the failed
privilege grant, making the inconsistent state permanent.


The main purpose of this PR is to allow our SaaS instance to manage elevated privileges in a secondary IdP system. It is done by specific team names (i.e.
snty-staffandsnty-superuser) created on the default org.When members are added/removed from "staff" or "superuser" teams in the default SaaS organization, automatically grant or revoke the corresponding is_staff/is_superuser flags.
Key changes: