test_organizationmember.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. from datetime import timedelta
  2. from unittest.mock import patch
  3. import pytest
  4. from django.core import mail
  5. from django.db import router
  6. from django.utils import timezone
  7. from rest_framework.serializers import ValidationError
  8. from sentry import roles
  9. from sentry.auth import manager
  10. from sentry.exceptions import UnableToAcceptMemberInvitationException
  11. from sentry.models.authidentity import AuthIdentity
  12. from sentry.models.authprovider import AuthProvider
  13. from sentry.models.options.organization_option import OrganizationOption
  14. from sentry.models.organizationmember import INVITE_DAYS_VALID, InviteStatus, OrganizationMember
  15. from sentry.silo.base import SiloMode
  16. from sentry.silo.safety import unguarded_write
  17. from sentry.testutils.cases import TestCase
  18. from sentry.testutils.helpers import with_feature
  19. from sentry.testutils.hybrid_cloud import HybridCloudTestMixin
  20. from sentry.testutils.outbox import outbox_runner
  21. from sentry.testutils.silo import assume_test_silo_mode
  22. from sentry.users.services.user.service import user_service
  23. class MockOrganizationRoles:
  24. TEST_ORG_ROLES = [
  25. {
  26. "id": "alice",
  27. "name": "Alice",
  28. "desc": "In Wonderland",
  29. "scopes": ["project:read", "project:write"],
  30. },
  31. {"id": "bob", "name": "Bob", "desc": "The builder", "scopes": ["project:read"]},
  32. {"id": "carol", "name": "Carol", "desc": "A nanny?", "scopes": ["project:write"]},
  33. ]
  34. TEST_TEAM_ROLES = [
  35. {"id": "alice", "name": "Alice", "desc": "In Wonderland"},
  36. {"id": "bob", "name": "Bob", "desc": "The builder"},
  37. {"id": "carol", "name": "Carol", "desc": "A nanny?"},
  38. ]
  39. def __init__(self):
  40. from sentry.roles.manager import RoleManager
  41. self.default_manager = RoleManager(self.TEST_ORG_ROLES, self.TEST_TEAM_ROLES)
  42. self.organization_roles = self.default_manager.organization_roles
  43. def get_all(self):
  44. return self.organization_roles.get_all()
  45. def get(self, x):
  46. return self.organization_roles.get(x)
  47. class OrganizationMemberTest(TestCase, HybridCloudTestMixin):
  48. def test_legacy_token_generation(self):
  49. member = OrganizationMember(id=1, organization_id=1, email="foo@example.com")
  50. with self.settings(SECRET_KEY="a"):
  51. assert member.legacy_token == "f3f2aa3e57f4b936dfd4f42c38db003e"
  52. def test_legacy_token_generation_no_email(self):
  53. """
  54. We include membership tokens in RPC memberships so it needs to not error
  55. for accepted invites.
  56. """
  57. member = OrganizationMember(organization_id=1, user_id=self.user.id)
  58. assert member.legacy_token
  59. def test_legacy_token_generation_unicode_key(self):
  60. member = OrganizationMember(id=1, organization_id=1, email="foo@example.com")
  61. with self.settings(
  62. SECRET_KEY=(
  63. b"\xfc]C\x8a\xd2\x93\x04\x00\x81\xeak\x94\x02H"
  64. b"\x1d\xcc&P'q\x12\xa2\xc0\xf2v\x7f\xbb*lX"
  65. )
  66. ):
  67. assert member.legacy_token == "df41d9dfd4ba25d745321e654e15b5d0"
  68. def test_get_invite_link_with_referrer(self):
  69. member = OrganizationMember(id=1, organization=self.organization, email="foo@example.com")
  70. link = member.get_invite_link(referrer="test_referrer")
  71. assert "?referrer=test_referrer" in link
  72. def test_send_invite_email(self):
  73. member = OrganizationMember(id=1, organization=self.organization, email="foo@example.com")
  74. with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
  75. member.send_invite_email()
  76. assert len(mail.outbox) == 1
  77. msg = mail.outbox[0]
  78. assert msg.to == ["foo@example.com"]
  79. @with_feature("system:multi-region")
  80. def test_send_invite_email_customer_domains(self):
  81. member = OrganizationMember(id=1, organization=self.organization, email="admin@example.com")
  82. with self.tasks():
  83. member.send_invite_email()
  84. assert len(mail.outbox) == 1
  85. assert self.organization.absolute_url("/accept/") in mail.outbox[0].body
  86. def test_send_sso_link_email(self):
  87. organization = self.create_organization()
  88. member = OrganizationMember(id=1, organization=organization, email="foo@example.com")
  89. provider = manager.get("dummy")
  90. with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
  91. member.send_sso_link_email("sender@example.com", provider)
  92. assert len(mail.outbox) == 1
  93. msg = mail.outbox[0]
  94. assert msg.to == ["foo@example.com"]
  95. assert msg.subject == f"Action Required for {organization.name}"
  96. @patch("sentry.utils.email.MessageBuilder")
  97. def test_send_sso_unlink_email(self, builder):
  98. with assume_test_silo_mode(SiloMode.CONTROL):
  99. user = self.create_user(email="foo@example.com")
  100. user.password = ""
  101. user.save()
  102. member = self.create_member(user=user, organization=self.organization)
  103. provider = manager.get("dummy")
  104. with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
  105. rpc_user = user_service.get_user(user_id=user.id)
  106. member.send_sso_unlink_email(rpc_user, provider)
  107. context = builder.call_args[1]["context"]
  108. assert context["organization"] == self.organization
  109. assert context["provider"] == provider
  110. assert context["actor_email"] == user.email
  111. assert not context["has_password"]
  112. assert "set_password_url" in context
  113. @patch("sentry.utils.email.MessageBuilder")
  114. def test_send_sso_unlink_email_str_sender(self, builder):
  115. with assume_test_silo_mode(SiloMode.CONTROL):
  116. user = self.create_user(email="foo@example.com")
  117. user.password = ""
  118. user.save()
  119. member = self.create_member(user=user, organization=self.organization)
  120. provider = manager.get("dummy")
  121. with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
  122. member.send_sso_unlink_email(user.email, provider)
  123. context = builder.call_args[1]["context"]
  124. assert context["organization"] == self.organization
  125. assert context["provider"] == provider
  126. assert context["actor_email"] == user.email
  127. assert not context["has_password"]
  128. assert "set_password_url" in context
  129. def test_token_expires_at_set_on_save(self):
  130. with outbox_runner():
  131. member = OrganizationMember(organization=self.organization, email="foo@example.com")
  132. member.token = member.generate_token()
  133. member.save()
  134. self.assert_org_member_mapping(org_member=member)
  135. expires_at = timezone.now() + timedelta(days=INVITE_DAYS_VALID)
  136. assert member.token_expires_at
  137. assert member.token_expires_at.date() == expires_at.date()
  138. def test_token_expiration(self):
  139. with outbox_runner():
  140. member = OrganizationMember(organization=self.organization, email="foo@example.com")
  141. member.token = member.generate_token()
  142. member.save()
  143. self.assert_org_member_mapping(org_member=member)
  144. assert member.is_pending
  145. assert member.token_expired is False
  146. member.token_expires_at = timezone.now() - timedelta(minutes=1)
  147. assert member.token_expired
  148. def test_set_user(self):
  149. with outbox_runner():
  150. member = OrganizationMember(organization=self.organization, email="foo@example.com")
  151. member.token = member.generate_token()
  152. member.save()
  153. self.assert_org_member_mapping(org_member=member)
  154. with outbox_runner():
  155. user = self.create_user(email="foo@example.com")
  156. member.set_user(user.id)
  157. member.save()
  158. assert member.is_pending is False
  159. assert member.token_expires_at is None
  160. assert member.token is None
  161. assert member.email is None
  162. member.refresh_from_db()
  163. self.assert_org_member_mapping(org_member=member)
  164. def test_regenerate_token(self):
  165. member = OrganizationMember(organization=self.organization, email="foo@example.com")
  166. assert (member.token, member.token_expires_at) == (None, None)
  167. member.regenerate_token()
  168. assert member.token
  169. assert member.token_expires_at
  170. expires_at = timezone.now() + timedelta(days=INVITE_DAYS_VALID)
  171. assert member.token_expires_at.date() == expires_at.date()
  172. def test_delete_expired_clear(self):
  173. ninety_one_days = timezone.now() - timedelta(days=1)
  174. member = self.create_member(
  175. organization=self.organization,
  176. role="member",
  177. email="test@example.com",
  178. token="abc-def",
  179. token_expires_at=ninety_one_days,
  180. )
  181. with outbox_runner():
  182. OrganizationMember.objects.delete_expired(timezone.now())
  183. assert OrganizationMember.objects.filter(id=member.id).first() is None
  184. self.assert_org_member_mapping_not_exists(org_member=member)
  185. def test_delete_identities(self):
  186. org = self.create_organization()
  187. user = self.create_user()
  188. member = self.create_member(user_id=user.id, organization_id=org.id)
  189. self.assert_org_member_mapping(org_member=member)
  190. with assume_test_silo_mode(SiloMode.CONTROL):
  191. ap = AuthProvider.objects.create(
  192. organization_id=org.id, provider="sentry_auth_provider", config={}
  193. )
  194. AuthIdentity.objects.create(user=user, auth_provider=ap)
  195. qs = AuthIdentity.objects.filter(auth_provider__organization_id=org.id, user_id=user.id)
  196. assert qs.exists()
  197. with outbox_runner():
  198. member.outbox_for_update().save()
  199. # ensure that even if the outbox sends a general, non delete update, it doesn't cascade
  200. # the delete to auth identity objects.
  201. with assume_test_silo_mode(SiloMode.CONTROL):
  202. assert qs.exists()
  203. with outbox_runner():
  204. member.delete()
  205. with assume_test_silo_mode(SiloMode.CONTROL):
  206. assert not qs.exists()
  207. self.assert_org_member_mapping_not_exists(org_member=member)
  208. def test_delete_expired_SCIM_enabled(self):
  209. organization = self.create_organization()
  210. org3 = self.create_organization()
  211. with assume_test_silo_mode(SiloMode.CONTROL):
  212. AuthProvider.objects.create(
  213. provider="saml2",
  214. organization_id=organization.id,
  215. flags=AuthProvider.flags.scim_enabled,
  216. )
  217. AuthProvider.objects.create(
  218. provider="saml2",
  219. organization_id=org3.id,
  220. flags=AuthProvider.flags.allow_unlinked,
  221. )
  222. ninety_one_days = timezone.now() - timedelta(days=91)
  223. member = self.create_member(
  224. organization=organization,
  225. role="member",
  226. email="test@example.com",
  227. token="abc-def",
  228. token_expires_at=ninety_one_days,
  229. )
  230. member2 = self.create_member(
  231. organization=org3,
  232. role="member",
  233. email="test2@example.com",
  234. token="abc-defg",
  235. token_expires_at=ninety_one_days,
  236. )
  237. with outbox_runner():
  238. OrganizationMember.objects.delete_expired(timezone.now())
  239. assert OrganizationMember.objects.filter(id=member.id).exists()
  240. assert not OrganizationMember.objects.filter(id=member2.id).exists()
  241. self.assert_org_member_mapping_not_exists(org_member=member2)
  242. def test_delete_expired_miss(self):
  243. tomorrow = timezone.now() + timedelta(days=1)
  244. member = self.create_member(
  245. organization=self.organization,
  246. role="member",
  247. email="test@example.com",
  248. token="abc-def",
  249. token_expires_at=tomorrow,
  250. )
  251. with outbox_runner():
  252. OrganizationMember.objects.delete_expired(timezone.now())
  253. assert OrganizationMember.objects.filter(id=member.id).exists()
  254. self.assert_org_member_mapping(org_member=member)
  255. def test_delete_expired_leave_claimed(self):
  256. user = self.create_user()
  257. member = self.create_member(
  258. organization=self.organization,
  259. role="member",
  260. user=user,
  261. token="abc-def",
  262. token_expires_at="2018-01-01 10:00:00+00:00",
  263. )
  264. with outbox_runner():
  265. OrganizationMember.objects.delete_expired(timezone.now())
  266. assert OrganizationMember.objects.filter(id=member.id).exists()
  267. self.assert_org_member_mapping(org_member=member)
  268. def test_delete_expired_leave_null_expires(self):
  269. member = self.create_member(
  270. organization=self.organization,
  271. role="member",
  272. email="test@example.com",
  273. token="abc-def",
  274. token_expires_at=None,
  275. )
  276. with outbox_runner():
  277. OrganizationMember.objects.delete_expired(timezone.now())
  278. assert OrganizationMember.objects.get(id=member.id)
  279. self.assert_org_member_mapping(org_member=member)
  280. def test_approve_invite(self):
  281. member = self.create_member(
  282. organization=self.organization,
  283. role="member",
  284. email="test@example.com",
  285. invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
  286. )
  287. assert not member.invite_approved
  288. member.approve_invite()
  289. member.save()
  290. member = OrganizationMember.objects.get(id=member.id)
  291. assert member.invite_approved
  292. assert member.invite_status == InviteStatus.APPROVED.value
  293. self.assert_org_member_mapping(org_member=member)
  294. def test_scopes_with_member_admin_config(self):
  295. member = OrganizationMember.objects.create(
  296. organization=self.organization,
  297. role="member",
  298. email="test@example.com",
  299. )
  300. assert "event:admin" in member.get_scopes()
  301. self.organization.update_option("sentry:events_member_admin", True)
  302. assert "event:admin" in member.get_scopes()
  303. self.organization.update_option("sentry:events_member_admin", False)
  304. assert "event:admin" not in member.get_scopes()
  305. def test_scopes_with_member_alert_write(self):
  306. member = OrganizationMember.objects.create(
  307. organization=self.organization,
  308. role="member",
  309. email="test@example.com",
  310. )
  311. admin = OrganizationMember.objects.create(
  312. organization=self.organization,
  313. role="admin",
  314. email="admin@example.com",
  315. )
  316. assert "alerts:write" in member.get_scopes()
  317. assert "alerts:write" in admin.get_scopes()
  318. self.organization.update_option("sentry:alerts_member_write", True)
  319. assert "alerts:write" in member.get_scopes()
  320. assert "alerts:write" in admin.get_scopes()
  321. self.organization.update_option("sentry:alerts_member_write", False)
  322. assert "alerts:write" not in member.get_scopes()
  323. assert "alerts:write" in admin.get_scopes()
  324. def test_get_contactable_members_for_org(self):
  325. organization = self.create_organization()
  326. user1 = self.create_user()
  327. user2 = self.create_user()
  328. member = self.create_member(organization=organization, user=user1)
  329. self.create_member(
  330. organization=organization,
  331. user=user2,
  332. invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
  333. )
  334. self.create_member(organization=organization, email="hi@example.com")
  335. assert OrganizationMember.objects.filter(organization=organization).count() == 3
  336. results = OrganizationMember.objects.get_contactable_members_for_org(organization.id)
  337. assert results.count() == 1
  338. assert results[0].user_id == member.user_id
  339. def test_validate_invitation_success(self):
  340. member = self.create_member(
  341. organization=self.organization,
  342. invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
  343. email="hello@sentry.io",
  344. role="member",
  345. )
  346. user = self.create_user()
  347. assert member.validate_invitation(user, [roles.get("member")])
  348. @with_feature({"organizations:invite-members": False})
  349. def test_validate_invitation_lack_feature(self):
  350. member = self.create_member(
  351. organization=self.organization,
  352. invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
  353. email="hello@sentry.io",
  354. role="member",
  355. )
  356. user = self.create_user()
  357. with pytest.raises(
  358. UnableToAcceptMemberInvitationException,
  359. match="Your organization is not allowed to invite members.",
  360. ):
  361. member.validate_invitation(user, [roles.get("member")])
  362. def test_validate_invitation_no_join_requests(self):
  363. OrganizationOption.objects.create(
  364. organization_id=self.organization.id, key="sentry:join_requests", value=False
  365. )
  366. member = self.create_member(
  367. organization=self.organization,
  368. invite_status=InviteStatus.REQUESTED_TO_JOIN.value,
  369. email="hello@sentry.io",
  370. role="member",
  371. )
  372. user = self.create_user()
  373. with pytest.raises(
  374. UnableToAcceptMemberInvitationException,
  375. match="Your organization does not allow requests to join.",
  376. ):
  377. member.validate_invitation(user, [roles.get("member")])
  378. def test_validate_invitation_outside_allowed_role(self):
  379. member = self.create_member(
  380. organization=self.organization,
  381. invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
  382. email="hello@sentry.io",
  383. role="admin",
  384. )
  385. user = self.create_user()
  386. with pytest.raises(
  387. UnableToAcceptMemberInvitationException,
  388. match="You do not have permission to approve a member invitation with the role admin.",
  389. ):
  390. member.validate_invitation(user, [roles.get("member")])
  391. def test_approve_member_invitation(self):
  392. member = self.create_member(
  393. organization=self.organization,
  394. invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
  395. email="hello@sentry.io",
  396. role="member",
  397. )
  398. user = self.create_user()
  399. member.approve_member_invitation(user)
  400. self.assert_org_member_mapping(org_member=member)
  401. assert member.invite_status == InviteStatus.APPROVED.value
  402. def test_reject_member_invitation(self):
  403. member = self.create_member(
  404. organization=self.organization,
  405. invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value,
  406. email="hello@sentry.io",
  407. role="member",
  408. )
  409. user = self.create_user()
  410. member.reject_member_invitation(user)
  411. assert not OrganizationMember.objects.filter(id=member.id).exists()
  412. self.assert_org_member_mapping_not_exists(org_member=member)
  413. def test_invalid_reject_member_invitation(self):
  414. user = self.create_user(email="hello@sentry.io")
  415. member = self.create_member(
  416. organization=self.organization,
  417. invite_status=InviteStatus.APPROVED.value,
  418. user=user,
  419. role="member",
  420. )
  421. user = self.create_user()
  422. member.reject_member_invitation(user)
  423. self.assert_org_member_mapping(org_member=member)
  424. assert OrganizationMember.objects.filter(id=member.id).exists()
  425. def test_get_allowed_org_roles_to_invite(self):
  426. member = OrganizationMember.objects.get(
  427. user_id=self.user.id, organization=self.organization
  428. )
  429. with unguarded_write(using=router.db_for_write(OrganizationMember)):
  430. member.update(role="manager")
  431. assert member.get_allowed_org_roles_to_invite() == [
  432. roles.get("member"),
  433. roles.get("admin"),
  434. roles.get("manager"),
  435. ]
  436. def test_get_allowed_org_roles_to_invite_subset_logic(self):
  437. mock_org_roles = MockOrganizationRoles()
  438. with (
  439. patch("sentry.roles.organization_roles.get", mock_org_roles.get),
  440. patch("sentry.roles.organization_roles.get_all", mock_org_roles.get_all),
  441. ):
  442. alice = self.create_member(
  443. user=self.create_user(), organization=self.organization, role="alice"
  444. )
  445. assert alice.get_allowed_org_roles_to_invite() == [
  446. roles.get("alice"),
  447. roles.get("bob"),
  448. roles.get("carol"),
  449. ]
  450. bob = self.create_member(
  451. user=self.create_user(), organization=self.organization, role="bob"
  452. )
  453. assert bob.get_allowed_org_roles_to_invite() == [
  454. roles.get("bob"),
  455. ]
  456. carol = self.create_member(
  457. user=self.create_user(), organization=self.organization, role="carol"
  458. )
  459. assert carol.get_allowed_org_roles_to_invite() == [
  460. roles.get("carol"),
  461. ]
  462. def test_cannot_demote_last_owner(self):
  463. org = self.create_organization()
  464. with pytest.raises(ValidationError):
  465. member = self.create_member(organization=org, role="owner", user=self.create_user())
  466. member.role = "manager"
  467. member.save()