Files
sentinel/tests/sentry/models/test_organizationmember.py
2024-02-15 17:01:50 +00:00

582 lines
22 KiB
Python

from datetime import timedelta
from unittest.mock import patch
import pytest
from django.core import mail
from django.db import router
from django.utils import timezone
from rest_framework.serializers import ValidationError
from sentry import roles
from sentry.auth import manager
from sentry.exceptions import UnableToAcceptMemberInvitationException
from sentry.models.authidentity import AuthIdentity
from sentry.models.authprovider import AuthProvider
from sentry.models.options.organization_option import OrganizationOption
from sentry.models.organizationmember import INVITE_DAYS_VALID, InviteStatus, OrganizationMember
from sentry.models.organizationmemberteam import OrganizationMemberTeam
from sentry.services.hybrid_cloud.user.service import user_service
from sentry.silo import SiloMode, unguarded_write
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers 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
class MockOrganizationRoles:
TEST_ORG_ROLES = [
{"id": "alice", "name": "Alice", "scopes": ["project:read", "project:write"]},
{"id": "bob", "name": "Bob", "scopes": ["project:read"]},
{"id": "carol", "name": "Carol", "scopes": ["project:write"]},
]
TEST_TEAM_ROLES = [
{"id": "alice", "name": "Alice"},
{"id": "bob", "name": "Bob"},
{"id": "carol", "name": "Carol"},
]
def __init__(self):
from sentry.roles.manager import RoleManager
self.default_manager = RoleManager(self.TEST_ORG_ROLES, self.TEST_TEAM_ROLES)
self.organization_roles = self.default_manager.organization_roles
def get_all(self):
return self.organization_roles.get_all()
def get(self, x):
return self.organization_roles.get(x)
@region_silo_test
class OrganizationMemberTest(TestCase, HybridCloudTestMixin):
def test_legacy_token_generation(self):
member = OrganizationMember(id=1, organization_id=1, email="[email protected]")
with self.settings(SECRET_KEY="a"):
assert member.legacy_token == "f3f2aa3e57f4b936dfd4f42c38db003e"
def test_legacy_token_generation_no_email(self):
"""
We include membership tokens in RPC memberships so it needs to not error
for accepted invites.
"""
member = OrganizationMember(organization_id=1, user_id=self.user.id)
assert member.legacy_token
def test_legacy_token_generation_unicode_key(self):
member = OrganizationMember(id=1, organization_id=1, email="[email protected]")
with self.settings(
SECRET_KEY=(
b"\xfc]C\x8a\xd2\x93\x04\x00\x81\xeak\x94\x02H"
b"\x1d\xcc&P'q\x12\xa2\xc0\xf2v\x7f\xbb*lX"
)
):
assert member.legacy_token == "df41d9dfd4ba25d745321e654e15b5d0"
def test_get_invite_link_with_referrer(self):
member = OrganizationMember(id=1, organization=self.organization, email="[email protected]")
link = member.get_invite_link(referrer="test_referrer")
assert "?referrer=test_referrer" in link
def test_send_invite_email(self):
member = OrganizationMember(id=1, organization=self.organization, email="[email protected]")
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
member.send_invite_email()
assert len(mail.outbox) == 1
msg = mail.outbox[0]
assert msg.to == ["[email protected]"]
@with_feature("organizations:customer-domains")
def test_send_invite_email_customer_domains(self):
member = OrganizationMember(id=1, organization=self.organization, email="[email protected]")
with self.tasks():
member.send_invite_email()
assert len(mail.outbox) == 1
assert self.organization.absolute_url("/accept/") in mail.outbox[0].body
def test_send_sso_link_email(self):
organization = self.create_organization()
member = OrganizationMember(id=1, organization=organization, email="[email protected]")
provider = manager.get("dummy")
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
member.send_sso_link_email("[email protected]", provider)
assert len(mail.outbox) == 1
msg = mail.outbox[0]
assert msg.to == ["[email protected]"]
assert msg.subject == f"Action Required for {organization.name}"
@patch("sentry.utils.email.MessageBuilder")
def test_send_sso_unlink_email(self, builder):
with assume_test_silo_mode(SiloMode.CONTROL):
user = self.create_user(email="[email protected]")
user.password = ""
user.save()
member = self.create_member(user=user, organization=self.organization)
provider = manager.get("dummy")
with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
rpc_user = user_service.get_user(user_id=user.id)
member.send_sso_unlink_email(rpc_user, provider)
context = builder.call_args[1]["context"]
assert context["organization"] == self.organization
assert context["provider"] == provider
assert not context["has_password"]
assert "set_password_url" in context
def test_token_expires_at_set_on_save(self):
with outbox_runner():
member = OrganizationMember(organization=self.organization, email="[email protected]")
member.token = member.generate_token()
member.save()
self.assert_org_member_mapping(org_member=member)
expires_at = timezone.now() + timedelta(days=INVITE_DAYS_VALID)
assert member.token_expires_at
assert member.token_expires_at.date() == expires_at.date()
def test_token_expiration(self):
with outbox_runner():
member = OrganizationMember(organization=self.organization, email="[email protected]")
member.token = member.generate_token()
member.save()
self.assert_org_member_mapping(org_member=member)
assert member.is_pending
assert member.token_expired is False
member.token_expires_at = timezone.now() - timedelta(minutes=1)
assert member.token_expired
def test_set_user(self):
with outbox_runner():
member = OrganizationMember(organization=self.organization, email="[email protected]")
member.token = member.generate_token()
member.save()
self.assert_org_member_mapping(org_member=member)
with outbox_runner():
user = self.create_user(email="[email protected]")
member.set_user(user.id)
member.save()
assert member.is_pending is False
assert member.token_expires_at is None
assert member.token is None
assert member.email is None
member.refresh_from_db()
self.assert_org_member_mapping(org_member=member)
def test_regenerate_token(self):
member = OrganizationMember(organization=self.organization, email="[email protected]")
assert member.token is None
assert member.token_expires_at is None
member.regenerate_token()
assert member.token
assert member.token_expires_at
expires_at = timezone.now() + timedelta(days=INVITE_DAYS_VALID)
assert member.token_expires_at.date() == expires_at.date()
def test_delete_expired_clear(self):
ninety_one_days = timezone.now() - timedelta(days=1)
member = self.create_member(
organization=self.organization,
role="member",
email="[email protected]",
token="abc-def",
token_expires_at=ninety_one_days,
)
with outbox_runner():
OrganizationMember.objects.delete_expired(timezone.now())
assert OrganizationMember.objects.filter(id=member.id).first() is None
self.assert_org_member_mapping_not_exists(org_member=member)
def test_delete_identities(self):
org = self.create_organization()
user = self.create_user()
member = self.create_member(user_id=user.id, organization_id=org.id)
self.assert_org_member_mapping(org_member=member)
with assume_test_silo_mode(SiloMode.CONTROL):
ap = AuthProvider.objects.create(
organization_id=org.id, provider="sentry_auth_provider", config={}
)
AuthIdentity.objects.create(user=user, auth_provider=ap)
qs = AuthIdentity.objects.filter(auth_provider__organization_id=org.id, user_id=user.id)
assert qs.exists()
with outbox_runner():
member.outbox_for_update().save()
# ensure that even if the outbox sends a general, non delete update, it doesn't cascade
# the delete to auth identity objects.
with assume_test_silo_mode(SiloMode.CONTROL):
assert qs.exists()
with outbox_runner():
member.delete()
with assume_test_silo_mode(SiloMode.CONTROL):
assert not qs.exists()
self.assert_org_member_mapping_not_exists(org_member=member)
def test_delete_expired_SCIM_enabled(self):
organization = self.create_organization()
org3 = self.create_organization()
with assume_test_silo_mode(SiloMode.CONTROL):
AuthProvider.objects.create(
provider="saml2",
organization_id=organization.id,
flags=AuthProvider.flags.scim_enabled,
)
AuthProvider.objects.create(
provider="saml2",
organization_id=org3.id,
flags=AuthProvider.flags.allow_unlinked,
)
ninety_one_days = timezone.now() - timedelta(days=91)
member = self.create_member(
organization=organization,
role="member",
email="[email protected]",
token="abc-def",
token_expires_at=ninety_one_days,
)
member2 = self.create_member(
organization=org3,
role="member",
email="[email protected]",
token="abc-defg",
token_expires_at=ninety_one_days,
)
with outbox_runner():
OrganizationMember.objects.delete_expired(timezone.now())
assert OrganizationMember.objects.filter(id=member.id).exists()
assert not OrganizationMember.objects.filter(id=member2.id).exists()
self.assert_org_member_mapping_not_exists(org_member=member2)
def test_delete_expired_miss(self):
tomorrow = timezone.now() + timedelta(days=1)
member = self.create_member(
organization=self.organization,
role="member",
email="[email protected]",
token="abc-def",
token_expires_at=tomorrow,
)
with outbox_runner():
OrganizationMember.objects.delete_expired(timezone.now())
assert OrganizationMember.objects.filter(id=member.id).exists()
self.assert_org_member_mapping(org_member=member)
def test_delete_expired_leave_claimed(self):
user = self.create_user()
member = self.create_member(
organization=self.organization,
role="member",
user=user,
token="abc-def",
token_expires_at="2018-01-01 10:00:00",
)
with outbox_runner():
OrganizationMember.objects.delete_expired(timezone.now())
assert OrganizationMember.objects.filter(id=member.id).exists()
self.assert_org_member_mapping(org_member=member)
def test_delete_expired_leave_null_expires(self):
member = self.create_member(
organization=self.organization,
role="member",
email="[email protected]",
token="abc-def",
token_expires_at=None,
)
with outbox_runner():
OrganizationMember.objects.delete_expired(timezone.now())
assert OrganizationMember.objects.get(id=member.id)
self.assert_org_member_mapping(org_member=member)
def test_approve_invite(self):
member = self.create_member(
organization=self.organization,
role="member",
email="[email protected]",
invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
)
assert not member.invite_approved
member.approve_invite()
member.save()
member = OrganizationMember.objects.get(id=member.id)
assert member.invite_approved
assert member.invite_status == InviteStatus.APPROVED.value
self.assert_org_member_mapping(org_member=member)
def test_scopes_with_member_admin_config(self):
member = OrganizationMember.objects.create(
organization=self.organization,
role="member",
email="[email protected]",
)
assert "event:admin" in member.get_scopes()
self.organization.update_option("sentry:events_member_admin", True)
assert "event:admin" in member.get_scopes()
self.organization.update_option("sentry:events_member_admin", False)
assert "event:admin" not in member.get_scopes()
def test_scopes_with_member_alert_write(self):
member = OrganizationMember.objects.create(
organization=self.organization,
role="member",
email="[email protected]",
)
admin = OrganizationMember.objects.create(
organization=self.organization,
role="admin",
email="[email protected]",
)
assert "alerts:write" in member.get_scopes()
assert "alerts:write" in admin.get_scopes()
self.organization.update_option("sentry:alerts_member_write", True)
assert "alerts:write" in member.get_scopes()
assert "alerts:write" in admin.get_scopes()
self.organization.update_option("sentry:alerts_member_write", False)
assert "alerts:write" not in member.get_scopes()
assert "alerts:write" in admin.get_scopes()
def test_scopes_with_team_org_role(self):
member = OrganizationMember.objects.create(
organization=self.organization,
role="member",
email="[email protected]",
)
owner = OrganizationMember.objects.create(
organization=self.organization,
role="owner",
email="[email protected]",
)
owner_member_scopes = member.get_scopes() | owner.get_scopes()
team = self.create_team(organization=self.organization, org_role="owner")
OrganizationMemberTeam.objects.create(organizationmember=member, team=team)
member.refresh_from_db()
assert member.get_scopes() == owner_member_scopes
def test_get_contactable_members_for_org(self):
organization = self.create_organization()
user1 = self.create_user()
user2 = self.create_user()
member = self.create_member(organization=organization, user=user1)
self.create_member(
organization=organization,
user=user2,
invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
)
self.create_member(organization=organization, email="[email protected]")
assert OrganizationMember.objects.filter(organization=organization).count() == 3
results = OrganizationMember.objects.get_contactable_members_for_org(organization.id)
assert results.count() == 1
assert results[0].user_id == member.user_id
def test_validate_invitation_success(self):
member = self.create_member(
organization=self.organization,
invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
email="[email protected]",
role="member",
)
user = self.create_user()
assert member.validate_invitation(user, [roles.get("member")])
@with_feature({"organizations:invite-members": False})
def test_validate_invitation_lack_feature(self):
member = self.create_member(
organization=self.organization,
invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
email="[email protected]",
role="member",
)
user = self.create_user()
with pytest.raises(
UnableToAcceptMemberInvitationException,
match="Your organization is not allowed to invite members.",
):
member.validate_invitation(user, [roles.get("member")])
def test_validate_invitation_no_join_requests(self):
OrganizationOption.objects.create(
organization_id=self.organization.id, key="sentry:join_requests", value=False
)
member = self.create_member(
organization=self.organization,
invite_status=InviteStatus.REQUESTED_TO_JOIN.value,
email="[email protected]",
role="member",
)
user = self.create_user()
with pytest.raises(
UnableToAcceptMemberInvitationException,
match="Your organization does not allow requests to join.",
):
member.validate_invitation(user, [roles.get("member")])
def test_validate_invitation_outside_allowed_role(self):
member = self.create_member(
organization=self.organization,
invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
email="[email protected]",
role="admin",
)
user = self.create_user()
with pytest.raises(
UnableToAcceptMemberInvitationException,
match="You do not have permission to approve a member invitation with the role admin.",
):
member.validate_invitation(user, [roles.get("member")])
def test_validate_invitation_with_org_role_from_team(self):
team = self.create_team(org_role="admin")
member = self.create_member(
organization=self.organization,
invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
email="[email protected]",
role="member",
teams=[team],
)
user = self.create_user()
assert member.validate_invitation(user, [roles.get("admin"), roles.get("member")])
with pytest.raises(
UnableToAcceptMemberInvitationException,
match="You do not have permission to approve a member invitation with the role admin.",
):
member.validate_invitation(user, [roles.get("manager")])
def test_approve_member_invitation(self):
member = self.create_member(
organization=self.organization,
invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
email="[email protected]",
role="member",
)
user = self.create_user()
member.approve_member_invitation(user)
self.assert_org_member_mapping(org_member=member)
assert member.invite_status == InviteStatus.APPROVED.value
def test_reject_member_invitation(self):
member = self.create_member(
organization=self.organization,
invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
email="[email protected]",
role="member",
)
user = self.create_user()
member.reject_member_invitation(user)
assert not OrganizationMember.objects.filter(id=member.id).exists()
self.assert_org_member_mapping_not_exists(org_member=member)
def test_invalid_reject_member_invitation(self):
user = self.create_user(email="[email protected]")
member = self.create_member(
organization=self.organization,
invite_status=InviteStatus.APPROVED.value,
user=user,
role="member",
)
user = self.create_user()
member.reject_member_invitation(user)
self.assert_org_member_mapping(org_member=member)
assert OrganizationMember.objects.filter(id=member.id).exists()
def test_get_allowed_org_roles_to_invite(self):
member = OrganizationMember.objects.get(
user_id=self.user.id, organization=self.organization
)
with unguarded_write(using=router.db_for_write(OrganizationMember)):
member.update(role="manager")
assert member.get_allowed_org_roles_to_invite() == [
roles.get("member"),
roles.get("admin"),
roles.get("manager"),
]
def test_get_allowed_org_roles_to_invite_subset_logic(self):
mock_org_roles = MockOrganizationRoles()
with patch("sentry.roles.organization_roles.get", mock_org_roles.get), patch(
"sentry.roles.organization_roles.get_all", mock_org_roles.get_all
):
alice = self.create_member(
user=self.create_user(), organization=self.organization, role="alice"
)
assert alice.get_allowed_org_roles_to_invite() == [
roles.get("alice"),
roles.get("bob"),
roles.get("carol"),
]
bob = self.create_member(
user=self.create_user(), organization=self.organization, role="bob"
)
assert bob.get_allowed_org_roles_to_invite() == [
roles.get("bob"),
]
carol = self.create_member(
user=self.create_user(), organization=self.organization, role="carol"
)
assert carol.get_allowed_org_roles_to_invite() == [
roles.get("carol"),
]
def test_org_roles_by_source(self):
manager_team = self.create_team(organization=self.organization, org_role="manager")
owner_team = self.create_team(organization=self.organization, org_role="owner")
owner_team2 = self.create_team(organization=self.organization, org_role="owner")
member = self.create_member(
organization=self.organization,
teams=[manager_team, owner_team, owner_team2],
user=self.create_user(),
role="member",
)
roles = member.get_org_roles_from_teams_by_source()
assert roles[0][1].id == "owner"
assert roles[-1][0] == manager_team.slug
assert roles[-1][1].id == "manager"
def test_cannot_demote_last_owner(self):
org = self.create_organization()
with pytest.raises(ValidationError):
member = self.create_member(organization=org, role="owner", user=self.create_user())
member.role = "manager"
member.save()