Skip to content

Commit

Permalink
feat(data-secrecy): Migration to Add prevent_superuser_access Bit F…
Browse files Browse the repository at this point in the history
…lag (#74700)

Created migration to add bit flag to Organization. Will need to follow
this up with a migration to add the flag so Hybrid Cloud services can
handle syncing.

**Glossary**
**Data secrecy mode:** Disallows any kind of superuser access into an
organization

**Enable/Disable Data secrecy mode:** Persistently enable/disable data
secrecy for an organization

**Waive Data secrecy mode:** Temporarily disable data secrecy for an
organizations
**Reinstate Data secrecy mode:** Re-enable data secrecy after a
temporary waiver

This flag handles the enable/disable function.


[spec](https://www.notion.so/sentry/Superuser-Data-Secrecy-Mode-b9f7fdfd8b564615ae1f91d3d981bc1a)
  • Loading branch information
iamrajjoshi authored Jul 23, 2024
1 parent bff71c4 commit 568c329
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 14 deletions.
2 changes: 1 addition & 1 deletion migrations_lockfile.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ hybridcloud: 0016_add_control_cacheversion
nodestore: 0002_nodestore_no_dictfield
remote_subscriptions: 0003_drop_remote_subscription
replays: 0004_index_together
sentry: 0744_add_dataset_source_field_to_dashboards
sentry: 0745_add_prevent_superuser_access_bitflag
social_auth: 0002_default_auto_field
uptime: 0006_projectuptimesubscription_name_owner
4 changes: 4 additions & 0 deletions src/sentry/api/endpoints/organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ class OrganizationSerializer(BaseOrganizationSerializer):
openMembership = serializers.BooleanField(required=False)
allowSharedIssues = serializers.BooleanField(required=False)
allowMemberProjectCreation = serializers.BooleanField(required=False)
allowSuperuserAccess = serializers.BooleanField(required=False)
enhancedPrivacy = serializers.BooleanField(required=False)
dataScrubber = serializers.BooleanField(required=False)
dataScrubberDefaults = serializers.BooleanField(required=False)
Expand Down Expand Up @@ -511,6 +512,8 @@ def save(self):
org.flags.require_email_verification = data["requireEmailVerification"]
if "allowMemberProjectCreation" in data:
org.flags.disable_member_project_creation = not data["allowMemberProjectCreation"]
if "allowSuperuserAccess" in data:
org.flags.prevent_superuser_access = not data["allowSuperuserAccess"]
if "name" in data:
org.name = data["name"]
if "slug" in data:
Expand All @@ -528,6 +531,7 @@ def save(self):
"require_2fa": org.flags.require_2fa.is_set,
"codecov_access": org.flags.codecov_access.is_set,
"disable_member_project_creation": org.flags.disable_member_project_creation.is_set,
"prevent_superuser_access": org.flags.prevent_superuser_access.is_set,
},
}

Expand Down
1 change: 1 addition & 0 deletions src/sentry/api/serializers/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ def serialize(
),
"avatar": avatar,
"allowMemberProjectCreation": not obj.flags.disable_member_project_creation,
"allowSuperuserAccess": not obj.flags.prevent_superuser_access,
"links": {
"organizationUrl": generate_organization_url(obj.slug),
"regionUrl": generate_region_url(),
Expand Down
48 changes: 48 additions & 0 deletions src/sentry/migrations/0745_add_prevent_superuser_access_bitflag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 5.0.6 on 2024-07-23 17:37

from django.db import migrations

import bitfield.models
from sentry.new_migrations.migrations import CheckedMigration


class Migration(CheckedMigration):
# This flag is used to mark that a migration shouldn't be automatically run in production.
# This should only be used for operations where it's safe to run the migration after your
# code has deployed. So this should not be used for most operations that alter the schema
# of a table.
# Here are some things that make sense to mark as post deployment:
# - Large data migrations. Typically we want these to be run manually so that they can be
# monitored and not block the deploy for a long period of time while they run.
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
# run this outside deployments so that we don't block them. Note that while adding an index
# is a schema change, it's completely safe to run the operation after the code has deployed.
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment

is_post_deployment = False

dependencies = [
("sentry", "0744_add_dataset_source_field_to_dashboards"),
]

operations = [
migrations.AlterField(
model_name="organization",
name="flags",
field=bitfield.models.BitField(
[
"allow_joinleave",
"enhanced_privacy",
"disable_shared_issues",
"early_adopter",
"require_2fa",
"disable_new_visibility_features",
"require_email_verification",
"codecov_access",
"disable_member_project_creation",
"prevent_superuser_access",
],
default=1,
),
),
]
5 changes: 4 additions & 1 deletion src/sentry/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ class flags(TypedClassBitField):
# Disable org-members from creating new projects
disable_member_project_creation: bool

# Prevent superuser access to an organization
prevent_superuser_access: bool

bitfield_default = 1

objects: ClassVar[OrganizationManager] = OrganizationManager(cache_fields=("pk", "slug"))
Expand Down Expand Up @@ -354,7 +357,7 @@ def _get_bulk_owner_ids(cls, organizations: Collection[Organization]) -> dict[in
organization_id__in=org_ids_to_query, role=roles.get_top_dog().id
).values_list("organization_id", "user_id")

for (org_id, user_id) in queried_owner_ids:
for org_id, user_id in queried_owner_ids:
# An org may have multiple owners. Here we mimic the behavior of
# `get_default_owner`, which is to use the first one in the query
# result's iteration order.
Expand Down
5 changes: 5 additions & 0 deletions tests/sentry/api/endpoints/test_organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ def test_various_options(self, mock_get_repositories):
"openMembership": False,
"isEarlyAdopter": True,
"codecovAccess": True,
"allowSuperuserAccess": False,
"aiSuggestedSolution": False,
"githubOpenPRBot": False,
"githubNudgeInvite": False,
Expand Down Expand Up @@ -470,6 +471,7 @@ def test_various_options(self, mock_get_repositories):

assert org.flags.early_adopter
assert org.flags.codecov_access
assert org.flags.prevent_superuser_access
assert not org.flags.allow_joinleave
assert org.flags.disable_shared_issues
assert org.flags.enhanced_privacy
Expand Down Expand Up @@ -501,6 +503,9 @@ def test_various_options(self, mock_get_repositories):
assert "to {}".format(data["openMembership"]) in log.data["allow_joinleave"]
assert "to {}".format(data["isEarlyAdopter"]) in log.data["early_adopter"]
assert "to {}".format(data["codecovAccess"]) in log.data["codecov_access"]
assert (
"to {}".format(not data["allowSuperuserAccess"]) in log.data["prevent_superuser_access"]
)
assert "to {}".format(data["enhancedPrivacy"]) in log.data["enhanced_privacy"]
assert "to {}".format(not data["allowSharedIssues"]) in log.data["disable_shared_issues"]
assert "to {}".format(data["require2FA"]) in log.data["require_2fa"]
Expand Down
34 changes: 22 additions & 12 deletions tests/sentry/models/test_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,13 @@ def test_flags_have_changed(self):
org.flags.codecov_access = True
org.flags.require_2fa = True
org.flags.disable_member_project_creation = True
org.flags.prevent_superuser_access = True
assert flag_has_changed(org, "allow_joinleave") is False
assert flag_has_changed(org, "early_adopter")
assert flag_has_changed(org, "codecov_access")
assert flag_has_changed(org, "require_2fa")
assert flag_has_changed(org, "disable_member_project_creation")
assert flag_has_changed(org, "prevent_superuser_access")

def test_has_changed(self):
org = self.create_organization()
Expand Down Expand Up @@ -228,9 +230,11 @@ def test_handle_2fa_required__compliant_and_non_compliant_members(self):
self.assert_org_member_mapping(org_member=compliant_member)
self.assert_org_member_mapping(org_member=non_compliant_member)

with self.options(
{"system.url-prefix": "http://example.com"}
), self.tasks(), outbox_runner():
with (
self.options({"system.url-prefix": "http://example.com"}),
self.tasks(),
outbox_runner(),
):
self.org.handle_2fa_required(self.request)

self.is_organization_member(compliant_user.id, compliant_member.id)
Expand Down Expand Up @@ -280,9 +284,11 @@ def test_handle_2fa_required__non_compliant_members(self):
self.assert_org_member_mapping(org_member=member)
non_compliant.append((user, member))

with self.options(
{"system.url-prefix": "http://example.com"}
), self.tasks(), outbox_runner():
with (
self.options({"system.url-prefix": "http://example.com"}),
self.tasks(),
outbox_runner(),
):
self.org.handle_2fa_required(self.request)

for user, member in non_compliant:
Expand Down Expand Up @@ -340,9 +346,11 @@ def test_handle_2fa_required__no_actor_and_api_key__ok(self, auth_log):

self.assert_org_member_mapping(org_member=member)

with self.options(
{"system.url-prefix": "http://example.com"}
), self.tasks(), outbox_runner():
with (
self.options({"system.url-prefix": "http://example.com"}),
self.tasks(),
outbox_runner(),
):
with assume_test_silo_mode(SiloMode.CONTROL):
api_key = ApiKey.objects.create(
organization_id=self.org.id,
Expand Down Expand Up @@ -373,9 +381,11 @@ def test_handle_2fa_required__no_ip_address__ok(self, auth_log):
user, member = self._create_user_and_member()
self.assert_org_member_mapping(org_member=member)

with self.options(
{"system.url-prefix": "http://example.com"}
), self.tasks(), outbox_runner():
with (
self.options({"system.url-prefix": "http://example.com"}),
self.tasks(),
outbox_runner(),
):
request = copy.deepcopy(self.request)
request.META["REMOTE_ADDR"] = None
self.org.handle_2fa_required(request)
Expand Down

0 comments on commit 568c329

Please sign in to comment.