|
91 | 91 | from sentry.models.options.organization_option import OrganizationOption |
92 | 92 | from sentry.models.options.project_option import ProjectOption |
93 | 93 | from sentry.models.organization import Organization, OrganizationStatus |
| 94 | +from sentry.models.organizationmemberreplayaccess import OrganizationMemberReplayAccess |
94 | 95 | from sentry.models.project import Project |
95 | 96 | from sentry.organizations.services.organization import organization_service |
96 | 97 | from sentry.organizations.services.organization.model import ( |
@@ -338,6 +339,12 @@ class OrganizationSerializer(BaseOrganizationSerializer): |
338 | 339 | ingestThroughTrustedRelaysOnly = serializers.ChoiceField( |
339 | 340 | choices=[("enabled", "enabled"), ("disabled", "disabled")], required=False |
340 | 341 | ) |
| 342 | + replayAccessMembers = serializers.ListField( |
| 343 | + child=serializers.IntegerField(), |
| 344 | + required=False, |
| 345 | + allow_null=True, |
| 346 | + help_text="List of organization member IDs that have access to replay data. Only modifiable by owners and managers.", |
| 347 | + ) |
341 | 348 |
|
342 | 349 | def _has_sso_enabled(self): |
343 | 350 | org = self.context["organization"] |
@@ -558,6 +565,73 @@ def save(self, **kwargs): |
558 | 565 | if trusted_relay_info is not None: |
559 | 566 | self.save_trusted_relays(trusted_relay_info, changed_data, org) |
560 | 567 |
|
| 568 | + if "hasGranularReplayPermissions" in data or "replayAccessMembers" in data: |
| 569 | + if not features.has("organizations:granular-replay-permissions", org): |
| 570 | + raise serializers.ValidationError( |
| 571 | + { |
| 572 | + "hasGranularReplayPermissions": "This feature is not enabled for your organization." |
| 573 | + } |
| 574 | + ) |
| 575 | + |
| 576 | + if not self.context["request"].access.has_scope("org:admin"): |
| 577 | + raise serializers.ValidationError( |
| 578 | + { |
| 579 | + "hasGranularReplayPermissions": "You do not have permission to modify granular replay permissions." |
| 580 | + } |
| 581 | + ) |
| 582 | + |
| 583 | + if "hasGranularReplayPermissions" in data: |
| 584 | + option_key = "sentry:granular-replay-permissions" |
| 585 | + new_value = data["hasGranularReplayPermissions"] |
| 586 | + |
| 587 | + option_inst, created = OrganizationOption.objects.update_or_create( |
| 588 | + organization=org, key=option_key, defaults={"value": new_value} |
| 589 | + ) |
| 590 | + |
| 591 | + if new_value or created: |
| 592 | + changed_data["hasGranularReplayPermissions"] = f"to {new_value}" |
| 593 | + |
| 594 | + if "replayAccessMembers" in data: |
| 595 | + member_ids = data["replayAccessMembers"] |
| 596 | + |
| 597 | + if member_ids is None: |
| 598 | + member_ids = [] |
| 599 | + |
| 600 | + current_member_ids = set( |
| 601 | + OrganizationMemberReplayAccess.objects.filter(organization=org).values_list( |
| 602 | + "organizationmember_id", flat=True |
| 603 | + ) |
| 604 | + ) |
| 605 | + new_member_ids = set(member_ids) |
| 606 | + |
| 607 | + to_add = new_member_ids - current_member_ids |
| 608 | + to_remove = current_member_ids - new_member_ids |
| 609 | + |
| 610 | + if to_add: |
| 611 | + OrganizationMemberReplayAccess.objects.bulk_create( |
| 612 | + [ |
| 613 | + OrganizationMemberReplayAccess( |
| 614 | + organization=org, organizationmember_id=member_id |
| 615 | + ) |
| 616 | + for member_id in to_add |
| 617 | + ] |
| 618 | + ) |
| 619 | + |
| 620 | + if to_remove: |
| 621 | + OrganizationMemberReplayAccess.objects.filter( |
| 622 | + organization=org, organizationmember_id__in=to_remove |
| 623 | + ).delete() |
| 624 | + |
| 625 | + if to_add or to_remove: |
| 626 | + changes = [] |
| 627 | + if to_add: |
| 628 | + changes.append(f"added {len(to_add)} member(s)") |
| 629 | + if to_remove: |
| 630 | + changes.append(f"removed {len(to_remove)} member(s)") |
| 631 | + changed_data["replayAccessMembers"] = ( |
| 632 | + f"{' and '.join(changes)} (total: {len(new_member_ids)} member(s) with access)" |
| 633 | + ) |
| 634 | + |
561 | 635 | if "openMembership" in data: |
562 | 636 | org.flags.allow_joinleave = data["openMembership"] |
563 | 637 | if "allowSharedIssues" in data: |
@@ -775,6 +849,16 @@ class OrganizationDetailsPutSerializer(serializers.Serializer): |
775 | 849 | help_text="The role required to download debug information files, ProGuard mappings and source maps.", |
776 | 850 | required=False, |
777 | 851 | ) |
| 852 | + hasGranularReplayPermissions = serializers.BooleanField( |
| 853 | + help_text="Specify `true` to enable granular replay permissions, allowing per-member access control for replay data.", |
| 854 | + required=False, |
| 855 | + ) |
| 856 | + replayAccessMembers = serializers.ListField( |
| 857 | + child=serializers.IntegerField(), |
| 858 | + help_text="A list of organization member IDs who have permission to access replay data. When empty, all members have access. Requires the granular-replay-permissions feature flag.", |
| 859 | + required=False, |
| 860 | + allow_null=True, |
| 861 | + ) |
778 | 862 |
|
779 | 863 | # avatar |
780 | 864 | avatarType = serializers.ChoiceField( |
|
0 commit comments