mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
487 lines
20 KiB
Python
487 lines
20 KiB
Python
import copy
|
|
from unittest import mock
|
|
|
|
from django.contrib.auth.models import AnonymousUser
|
|
from django.core import mail
|
|
from django.db import models
|
|
|
|
from sentry import audit_log
|
|
from sentry.api.endpoints.organization_details import (
|
|
flag_has_changed,
|
|
has_changed,
|
|
old_value,
|
|
update_tracked_data,
|
|
)
|
|
from sentry.auth.authenticators.totp import TotpInterface
|
|
from sentry.models.apikey import ApiKey
|
|
from sentry.models.auditlogentry import AuditLogEntry
|
|
from sentry.models.notificationsettingoption import NotificationSettingOption
|
|
from sentry.models.notificationsettingprovider import NotificationSettingProvider
|
|
from sentry.models.options.organization_option import OrganizationOption
|
|
from sentry.models.options.user_option import UserOption
|
|
from sentry.models.organization import Organization
|
|
from sentry.models.organizationmember import OrganizationMember
|
|
from sentry.models.user import User
|
|
from sentry.silo import SiloMode
|
|
from sentry.tasks.deletion.hybrid_cloud import (
|
|
schedule_hybrid_cloud_foreign_key_jobs,
|
|
schedule_hybrid_cloud_foreign_key_jobs_control,
|
|
)
|
|
from sentry.testutils.cases import TestCase
|
|
from sentry.testutils.helpers.features import with_feature
|
|
from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
|
|
from sentry.testutils.outbox import outbox_runner
|
|
from sentry.testutils.silo import assume_test_silo_mode, region_silo_test
|
|
|
|
|
|
@region_silo_test
|
|
class OrganizationTest(TestCase, HybridCloudTestMixin):
|
|
def test_slugify_on_new_orgs(self):
|
|
org = Organization.objects.create(name="name", slug="---downtown_canada---")
|
|
assert org.slug == "downtown-canada"
|
|
|
|
# Only slugify on new instances of Organization
|
|
org.slug = "---downtown_canada---"
|
|
org.save()
|
|
org.refresh_from_db()
|
|
assert org.slug == "---downtown_canada---"
|
|
|
|
org = Organization.objects.create(name="---foo_bar---")
|
|
assert org.slug == "foo-bar"
|
|
|
|
def test_slugify_long_org_names(self):
|
|
# Org name is longer than allowed org slug, and should be trimmed when slugified.
|
|
org = Organization.objects.create(name="Stove, Electrical, and Catering Stuff")
|
|
assert org.slug == "stove-electrical-and-catering"
|
|
|
|
# Ensure org slugs are unique
|
|
org2 = Organization.objects.create(name="Stove, Electrical, and Catering Stuff")
|
|
assert org2.slug.startswith("stove-electrical-and-cateri-")
|
|
assert len(org2.slug) > len("stove-electrical-and-cateri-")
|
|
assert org.slug != org2.slug
|
|
|
|
def test_get_default_owner(self):
|
|
user = self.create_user("[email protected]")
|
|
org = self.create_organization(owner=user)
|
|
assert org.get_default_owner().id == user.id
|
|
|
|
def test_default_owner_id(self):
|
|
user = self.create_user("[email protected]")
|
|
org = self.create_organization(owner=user)
|
|
assert org.default_owner_id == user.id
|
|
|
|
def test_default_owner_id_no_owner(self):
|
|
org = self.create_organization()
|
|
assert org.default_owner_id is None
|
|
|
|
def test_default_owner_id_only_owner_through_team(self):
|
|
user = self.create_user("[email protected]")
|
|
org = self.create_organization()
|
|
owner_team = self.create_team(organization=org, org_role="owner")
|
|
self.create_member(organization=org, user=user, teams=[owner_team])
|
|
assert org.default_owner_id == user.id
|
|
|
|
@mock.patch.object(
|
|
Organization, "get_owners", side_effect=Organization.get_owners, autospec=True
|
|
)
|
|
def test_default_owner_id_cached(self, mock_get_owners):
|
|
user = self.create_user("[email protected]")
|
|
org = self.create_organization(owner=user)
|
|
assert org.default_owner_id == user.id
|
|
assert mock_get_owners.call_count == 1
|
|
assert org.default_owner_id == user.id
|
|
assert mock_get_owners.call_count == 1
|
|
|
|
def test_flags_have_changed(self):
|
|
org = self.create_organization()
|
|
update_tracked_data(org)
|
|
org.flags.allow_joinleave = True # Only flag that defaults to True
|
|
org.flags.early_adopter = True
|
|
org.flags.codecov_access = True
|
|
org.flags.require_2fa = True
|
|
org.flags.disable_member_project_creation = 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")
|
|
|
|
def test_has_changed(self):
|
|
org = self.create_organization()
|
|
update_tracked_data(org)
|
|
|
|
org.name = "Bizzy"
|
|
assert has_changed(org, "name") is True
|
|
|
|
OrganizationOption.objects.create(
|
|
organization=org, key="sentry:require_scrub_ip_address", value=False
|
|
)
|
|
o = OrganizationOption.objects.get(organization=org, key="sentry:require_scrub_ip_address")
|
|
update_tracked_data(o)
|
|
o.value = True
|
|
assert has_changed(o, "value") is True
|
|
|
|
OrganizationOption.objects.create(
|
|
organization=org, key="sentry:account-rate-limit", value=0
|
|
)
|
|
p = OrganizationOption.objects.get(organization=org, key="sentry:account-rate-limit")
|
|
update_tracked_data(p)
|
|
p.value = 50000
|
|
assert has_changed(p, "value") is True
|
|
|
|
OrganizationOption.objects.create(
|
|
organization=org, key="sentry:project-rate-limit", value=85
|
|
)
|
|
r = OrganizationOption.objects.get(organization=org, key="sentry:project-rate-limit")
|
|
update_tracked_data(r)
|
|
r.value = 85
|
|
assert has_changed(r, "value") is False
|
|
|
|
OrganizationOption.objects.create(organization=org, key="sentry:sensitive_fields", value=[])
|
|
s = OrganizationOption.objects.get(organization=org, key="sentry:sensitive_fields")
|
|
update_tracked_data(s)
|
|
s.value = ["email"]
|
|
assert has_changed(s, "value") is True
|
|
|
|
OrganizationOption.objects.create(
|
|
organization=org, key="sentry:safe_fields", value=["email"]
|
|
)
|
|
f = OrganizationOption.objects.get(organization=org, key="sentry:safe_fields")
|
|
update_tracked_data(f)
|
|
f.value = ["email"]
|
|
assert has_changed(f, "value") is False
|
|
|
|
OrganizationOption.objects.create(
|
|
organization=org, key="sentry:store_crash_reports", value=0
|
|
)
|
|
p = OrganizationOption.objects.get(organization=org, key="sentry:store_crash_reports")
|
|
update_tracked_data(p)
|
|
p.value = 10
|
|
assert has_changed(p, "value") is True
|
|
|
|
def test_name_hasnt_changed_on_init(self):
|
|
inst = Organization(id=1, name="bar")
|
|
update_tracked_data(inst)
|
|
self.assertFalse(has_changed(inst, "name"))
|
|
|
|
def test_name_has_changes_before_save(self):
|
|
inst = Organization(id=1, name="bar")
|
|
update_tracked_data(inst)
|
|
inst.name = "baz"
|
|
self.assertTrue(has_changed(inst, "name"))
|
|
self.assertEqual(old_value(inst, "name"), "bar")
|
|
|
|
def test_name_hasnt_changed_after_save(self):
|
|
inst = Organization(id=1, name="bar")
|
|
update_tracked_data(inst)
|
|
inst.name = "baz"
|
|
self.assertTrue(has_changed(inst, "name"))
|
|
self.assertEqual(old_value(inst, "name"), "bar")
|
|
update_tracked_data(inst)
|
|
models.signals.post_save.send(instance=inst, sender=type(inst), created=False)
|
|
self.assertFalse(has_changed(inst, "name"))
|
|
|
|
|
|
@region_silo_test
|
|
class Require2fa(TestCase, HybridCloudTestMixin):
|
|
def setUp(self):
|
|
self.owner = self.create_user("[email protected]")
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
TotpInterface().enroll(self.owner)
|
|
self.org = self.create_organization(owner=self.owner)
|
|
self.request = self.make_request(user=self.owner)
|
|
|
|
def _create_user(self, has_email=True):
|
|
if not has_email:
|
|
return self.create_user("")
|
|
return self.create_user()
|
|
|
|
def _create_user_and_member(self, has_2fa=False, has_user_email=True):
|
|
user = self._create_user(has_email=has_user_email)
|
|
if has_2fa:
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
TotpInterface().enroll(user)
|
|
member = self.create_member(organization=self.org, user=user)
|
|
return user, member
|
|
|
|
def is_organization_member(self, user_id, member_id):
|
|
member = OrganizationMember.objects.get(id=member_id)
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
user = User.objects.get(id=user_id)
|
|
assert not member.is_pending
|
|
assert not member.email
|
|
assert member.user_id == user.id
|
|
|
|
def is_pending_organization_member(self, user_id, member_id, was_booted=True):
|
|
member = OrganizationMember.objects.get(id=member_id)
|
|
if user_id:
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert User.objects.filter(id=user_id).exists()
|
|
assert member.is_pending
|
|
assert member.email
|
|
if was_booted:
|
|
assert member.token
|
|
assert member.token_expires_at
|
|
else:
|
|
assert member.token is None
|
|
assert member.token_expires_at is None
|
|
|
|
def test_handle_2fa_required__compliant_and_non_compliant_members(self):
|
|
compliant_user, compliant_member = self._create_user_and_member(has_2fa=True)
|
|
non_compliant_user, non_compliant_member = self._create_user_and_member()
|
|
|
|
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():
|
|
self.org.handle_2fa_required(self.request)
|
|
|
|
self.is_organization_member(compliant_user.id, compliant_member.id)
|
|
self.is_pending_organization_member(non_compliant_user.id, non_compliant_member.id)
|
|
|
|
self.assert_org_member_mapping(org_member=compliant_member)
|
|
self.assert_org_member_mapping(org_member=non_compliant_member)
|
|
|
|
assert len(mail.outbox) == 1
|
|
assert mail.outbox[0].to == [non_compliant_user.email]
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
audit_logs = AuditLogEntry.objects.filter(
|
|
event=audit_log.get_event_id("MEMBER_PENDING"),
|
|
organization_id=self.org.id,
|
|
actor=self.owner,
|
|
)
|
|
assert audit_logs.count() == 1
|
|
assert audit_logs[0].data["email"] == non_compliant_user.email
|
|
assert audit_logs[0].target_user_id == non_compliant_user.id
|
|
|
|
def test_handle_2fa_required__compliant_members(self):
|
|
compliant = []
|
|
for num in range(0, 4):
|
|
user, member = self._create_user_and_member(has_2fa=True)
|
|
compliant.append((user, member))
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.org.handle_2fa_required(self.request)
|
|
|
|
for user, member in compliant:
|
|
self.is_organization_member(user.id, member.id)
|
|
|
|
assert len(mail.outbox) == 0
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert not AuditLogEntry.objects.filter(
|
|
event=audit_log.get_event_id("MEMBER_PENDING"),
|
|
organization_id=self.org.id,
|
|
actor=self.owner,
|
|
).exists()
|
|
|
|
def test_handle_2fa_required__non_compliant_members(self):
|
|
non_compliant = []
|
|
for num in range(0, 4):
|
|
user, member = self._create_user_and_member()
|
|
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():
|
|
self.org.handle_2fa_required(self.request)
|
|
|
|
for user, member in non_compliant:
|
|
self.is_pending_organization_member(user.id, member.id)
|
|
self.assert_org_member_mapping(org_member=member)
|
|
|
|
assert len(mail.outbox) == len(non_compliant)
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert AuditLogEntry.objects.filter(
|
|
event=audit_log.get_event_id("MEMBER_PENDING"),
|
|
organization_id=self.org.id,
|
|
actor=self.owner,
|
|
).count() == len(non_compliant)
|
|
|
|
def test_handle_2fa_required__pending_member__ok(self):
|
|
member = self.create_member(organization=self.org, email="[email protected]")
|
|
assert not member.user_id
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.org.handle_2fa_required(self.request)
|
|
self.is_pending_organization_member(user_id=None, member_id=member.id, was_booted=False)
|
|
|
|
assert len(mail.outbox) == 0
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert not AuditLogEntry.objects.filter(
|
|
event=audit_log.get_event_id("MEMBER_PENDING"),
|
|
organization_id=self.org.id,
|
|
actor=self.owner,
|
|
).exists()
|
|
|
|
@mock.patch("sentry.tasks.auth.logger")
|
|
def test_handle_2fa_required__no_email__warning(self, auth_log):
|
|
user, member = self._create_user_and_member(has_user_email=False)
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert not user.has_2fa()
|
|
|
|
assert not user.email
|
|
assert not member.email
|
|
|
|
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
|
|
self.org.handle_2fa_required(self.request)
|
|
self.is_organization_member(user.id, member.id)
|
|
|
|
auth_log.warning.assert_called_with(
|
|
"Could not remove %s noncompliant user from org",
|
|
"2FA",
|
|
extra={"organization_id": self.org.id, "user_id": user.id, "member_id": member.id},
|
|
)
|
|
|
|
@mock.patch("sentry.tasks.auth.logger")
|
|
def test_handle_2fa_required__no_actor_and_api_key__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 assume_test_silo_mode(SiloMode.CONTROL):
|
|
api_key = ApiKey.objects.create(
|
|
organization_id=self.org.id,
|
|
scope_list=["org:read", "org:write", "member:read", "member:write"],
|
|
)
|
|
request = copy.deepcopy(self.request)
|
|
request.user = AnonymousUser()
|
|
request.auth = api_key
|
|
self.org.handle_2fa_required(request)
|
|
self.is_pending_organization_member(user.id, member.id)
|
|
self.assert_org_member_mapping(org_member=member)
|
|
|
|
assert len(mail.outbox) == 1
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert (
|
|
AuditLogEntry.objects.filter(
|
|
event=audit_log.get_event_id("MEMBER_PENDING"),
|
|
organization_id=self.org.id,
|
|
actor=None,
|
|
actor_key=api_key,
|
|
).count()
|
|
== 1
|
|
)
|
|
|
|
@mock.patch("sentry.tasks.auth.logger")
|
|
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():
|
|
request = copy.deepcopy(self.request)
|
|
request.META["REMOTE_ADDR"] = None
|
|
self.org.handle_2fa_required(request)
|
|
self.is_pending_organization_member(user.id, member.id)
|
|
self.assert_org_member_mapping(org_member=member)
|
|
|
|
assert len(mail.outbox) == 1
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
assert (
|
|
AuditLogEntry.objects.filter(
|
|
event=audit_log.get_event_id("MEMBER_PENDING"),
|
|
organization_id=self.org.id,
|
|
actor=self.owner,
|
|
actor_key=None,
|
|
ip_address=None,
|
|
).count()
|
|
== 1
|
|
)
|
|
|
|
def test_get_audit_log_data(self):
|
|
org = self.create_organization()
|
|
result = org.get_audit_log_data()
|
|
assert result["flags"] == int(org.flags)
|
|
|
|
def test_absolute_url_no_customer_domain(self):
|
|
org = self.create_organization(owner=self.user, slug="acme")
|
|
url = org.absolute_url("/organizations/acme/restore/")
|
|
assert url == "http://testserver/organizations/acme/restore/"
|
|
|
|
url = org.absolute_url("/organizations/acme/issues/", query="project=123", fragment="ref")
|
|
assert url == "http://testserver/organizations/acme/issues/?project=123#ref"
|
|
|
|
@with_feature("organizations:customer-domains")
|
|
def test_absolute_url_with_customer_domain(self):
|
|
org = self.create_organization(owner=self.user, slug="acme")
|
|
url = org.absolute_url("/organizations/acme/restore/")
|
|
assert url == "http://acme.testserver/restore/"
|
|
|
|
url = org.absolute_url("/organizations/acme/issues/", query="project=123", fragment="ref")
|
|
assert url == "http://acme.testserver/issues/?project=123#ref"
|
|
|
|
url = org.absolute_url("/organizations/acme/issues/", query="?project=123", fragment="#ref")
|
|
assert url == "http://acme.testserver/issues/?project=123#ref"
|
|
|
|
|
|
@region_silo_test
|
|
class OrganizationDeletionTest(TestCase):
|
|
def add_org_notification_settings(self, org: Organization, user: User):
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
args = {
|
|
"scope_type": "organization",
|
|
"scope_identifier": org.id,
|
|
"type": "deploy",
|
|
"user_id": user.id,
|
|
"value": "never",
|
|
}
|
|
NotificationSettingOption.objects.create(**args)
|
|
NotificationSettingProvider.objects.create(**args, provider="slack")
|
|
|
|
def test_hybrid_cloud_deletion(self):
|
|
org = self.create_organization()
|
|
user = self.create_user()
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
UserOption.objects.set_value(user, "cool_key", "Hello!", organization_id=org.id)
|
|
org_id = org.id
|
|
|
|
self.add_org_notification_settings(org, user)
|
|
|
|
# Set up another org + notification settings to validate that this is unaffected by org deletion
|
|
unaffected_user = self.create_user()
|
|
unaffected_org = self.create_organization(owner=unaffected_user)
|
|
self.add_org_notification_settings(unaffected_org, unaffected_user)
|
|
|
|
with outbox_runner():
|
|
org.delete()
|
|
|
|
assert not Organization.objects.filter(id=org_id).exists()
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
# cascade is asynchronous, ensure there is still related search,
|
|
assert UserOption.objects.filter(organization_id=org_id).exists()
|
|
|
|
# Run cascades in the region, and then in control
|
|
with self.tasks(), assume_test_silo_mode(SiloMode.REGION):
|
|
schedule_hybrid_cloud_foreign_key_jobs()
|
|
with self.tasks(), assume_test_silo_mode(SiloMode.CONTROL):
|
|
schedule_hybrid_cloud_foreign_key_jobs_control()
|
|
|
|
with assume_test_silo_mode(SiloMode.CONTROL):
|
|
# Ensure they are all now gone.
|
|
assert not UserOption.objects.filter(organization_id=org_id).exists()
|
|
|
|
assert NotificationSettingOption.objects.filter(
|
|
scope_type="organization",
|
|
scope_identifier=unaffected_org.id,
|
|
).exists()
|
|
|
|
assert not NotificationSettingOption.objects.filter(
|
|
scope_type="organization",
|
|
scope_identifier=org_id,
|
|
).exists()
|