models.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. from django.conf import settings
  2. from django.db import models
  3. from django.db.models import F, OuterRef, Q
  4. from django.db.models.functions import Coalesce
  5. from django.utils.text import slugify
  6. from django.utils.translation import gettext_lazy as _
  7. from organizations.abstract import SharedBaseModel
  8. from organizations.base import (
  9. OrganizationBase,
  10. OrganizationInvitationBase,
  11. OrganizationOwnerBase,
  12. OrganizationUserBase,
  13. )
  14. from organizations.managers import OrgManager
  15. from organizations.signals import owner_changed, user_added
  16. from sql_util.utils import SubqueryCount, SubquerySum
  17. from observability.metrics import clear_metrics_cache
  18. from .fields import OrganizationSlugField
  19. # Defines which scopes belong to which role
  20. # Credit to sentry/conf/server.py
  21. ROLES = (
  22. {
  23. "id": "member",
  24. "name": "Member",
  25. "desc": "Members can view and act on events, as well as view most other data within the organization.",
  26. "scopes": set(
  27. [
  28. "event:read",
  29. "event:write",
  30. "event:admin",
  31. "project:releases",
  32. "project:read",
  33. "org:read",
  34. "member:read",
  35. "team:read",
  36. ]
  37. ),
  38. },
  39. {
  40. "id": "admin",
  41. "name": "Admin",
  42. "desc": "Admin privileges on any teams of which they're a member. They can create new teams and projects, as well as remove teams and projects which they already hold membership on (or all teams, if open membership is on). Additionally, they can manage memberships of teams that they are members of.",
  43. "scopes": set(
  44. [
  45. "event:read",
  46. "event:write",
  47. "event:admin",
  48. "org:read",
  49. "member:read",
  50. "project:read",
  51. "project:write",
  52. "project:admin",
  53. "project:releases",
  54. "team:read",
  55. "team:write",
  56. "team:admin",
  57. "org:integrations",
  58. ]
  59. ),
  60. },
  61. {
  62. "id": "manager",
  63. "name": "Manager",
  64. "desc": "Gains admin access on all teams as well as the ability to add and remove members.",
  65. "is_global": True,
  66. "scopes": set(
  67. [
  68. "event:read",
  69. "event:write",
  70. "event:admin",
  71. "member:read",
  72. "member:write",
  73. "member:admin",
  74. "project:read",
  75. "project:write",
  76. "project:admin",
  77. "project:releases",
  78. "team:read",
  79. "team:write",
  80. "team:admin",
  81. "org:read",
  82. "org:write",
  83. "org:integrations",
  84. ]
  85. ),
  86. },
  87. {
  88. "id": "owner",
  89. "name": "Organization Owner",
  90. "desc": "Unrestricted access to the organization, its data, and its settings. Can add, modify, and delete projects and members, as well as make billing and plan changes.",
  91. "is_global": True,
  92. "scopes": set(
  93. [
  94. "org:read",
  95. "org:write",
  96. "org:admin",
  97. "org:integrations",
  98. "member:read",
  99. "member:write",
  100. "member:admin",
  101. "team:read",
  102. "team:write",
  103. "team:admin",
  104. "project:read",
  105. "project:write",
  106. "project:admin",
  107. "project:releases",
  108. "event:read",
  109. "event:write",
  110. "event:admin",
  111. ]
  112. ),
  113. },
  114. )
  115. class OrganizationUserRole(models.IntegerChoices):
  116. MEMBER = 0, "Member"
  117. ADMIN = 1, "Admin"
  118. MANAGER = 2, "Manager"
  119. OWNER = 3, "Owner" # Many users can be owner but only one primary owner
  120. @classmethod
  121. def from_string(cls, string: str):
  122. for status in cls:
  123. if status.label.lower() == string.lower():
  124. return status
  125. @classmethod
  126. def get_role(cls, role: int):
  127. return ROLES[role]
  128. class OrganizationManager(OrgManager):
  129. def with_event_counts(self, current_period=True):
  130. subscription_filter = Q()
  131. event_subscription_filter = Q()
  132. checks_subscription_filter = Q()
  133. if current_period and settings.BILLING_ENABLED:
  134. subscription_filter = Q(
  135. created__gte=OuterRef(
  136. "djstripe_customers__subscriptions__current_period_start"
  137. ),
  138. created__lt=OuterRef(
  139. "djstripe_customers__subscriptions__current_period_end"
  140. ),
  141. )
  142. event_subscription_filter = Q(
  143. date__gte=OuterRef(
  144. "djstripe_customers__subscriptions__current_period_start"
  145. ),
  146. date__lt=OuterRef(
  147. "djstripe_customers__subscriptions__current_period_end"
  148. ),
  149. )
  150. checks_subscription_filter = Q(
  151. start_check__gte=OuterRef(
  152. "djstripe_customers__subscriptions__current_period_start"
  153. ),
  154. start_check__lt=OuterRef(
  155. "djstripe_customers__subscriptions__current_period_end"
  156. ),
  157. )
  158. queryset = self.annotate(
  159. issue_event_count=Coalesce(
  160. SubquerySum(
  161. "projects__eventprojecthourlystatistic__count",
  162. filter=event_subscription_filter,
  163. ),
  164. 0,
  165. ),
  166. transaction_count=Coalesce(
  167. SubquerySum(
  168. "projects__transactioneventprojecthourlystatistic__count",
  169. filter=event_subscription_filter,
  170. ),
  171. 0,
  172. ),
  173. uptime_check_event_count=SubqueryCount(
  174. "monitor__checks", filter=checks_subscription_filter
  175. ),
  176. file_size=(
  177. Coalesce(
  178. SubquerySum(
  179. "release__releasefile__file__blob__size",
  180. filter=subscription_filter,
  181. ),
  182. 0,
  183. )
  184. + Coalesce(
  185. SubquerySum(
  186. "projects__debuginformationfile__file__blob__size",
  187. filter=subscription_filter,
  188. ),
  189. 0,
  190. )
  191. )
  192. / 1000000,
  193. total_event_count=F("issue_event_count")
  194. + F("transaction_count")
  195. + F("uptime_check_event_count")
  196. + F("file_size"),
  197. )
  198. return queryset.distinct("pk")
  199. class Organization(SharedBaseModel, OrganizationBase):
  200. slug = OrganizationSlugField(
  201. max_length=200,
  202. blank=False,
  203. editable=True,
  204. populate_from="name",
  205. unique=True,
  206. help_text=_("The name in all lowercase, suitable for URL identification"),
  207. )
  208. is_accepting_events = models.BooleanField(
  209. default=True, help_text="Used for throttling at org level"
  210. )
  211. open_membership = models.BooleanField(
  212. default=True, help_text="Allow any organization member to join any team"
  213. )
  214. scrub_ip_addresses = models.BooleanField(
  215. default=True,
  216. help_text="Default for whether projects should script IP Addresses",
  217. )
  218. objects = OrganizationManager()
  219. def save(self, *args, **kwargs):
  220. new = False
  221. if not self.pk:
  222. new = True
  223. super().save(*args, **kwargs)
  224. if new:
  225. clear_metrics_cache()
  226. def delete(self, *args, **kwargs):
  227. super().delete(*args, **kwargs)
  228. clear_metrics_cache()
  229. def slugify_function(self, content):
  230. reserved_words = [
  231. "login",
  232. "register",
  233. "app",
  234. "profile",
  235. "organizations",
  236. "settings",
  237. "issues",
  238. "performance",
  239. "_health",
  240. "rest-auth",
  241. "api",
  242. "accept",
  243. "stripe",
  244. "admin",
  245. "status_page",
  246. "__debug__",
  247. ]
  248. slug = slugify(content)
  249. if slug in reserved_words:
  250. return slug + "-1"
  251. return slug
  252. def add_user(self, user, role=OrganizationUserRole.MEMBER):
  253. """
  254. Adds a new user and if the first user makes the user an admin and
  255. the owner.
  256. """
  257. users_count = self.users.all().count()
  258. if users_count == 0:
  259. role = OrganizationUserRole.OWNER
  260. org_user = self._org_user_model.objects.create(
  261. user=user, organization=self, role=role
  262. )
  263. if users_count == 0:
  264. self._org_owner_model.objects.create(
  265. organization=self, organization_user=org_user
  266. )
  267. # User added signal
  268. user_added.send(sender=self, user=user)
  269. return org_user
  270. @property
  271. def owners(self):
  272. return self.users.filter(
  273. organizations_ext_organizationuser__role=OrganizationUserRole.OWNER
  274. )
  275. @property
  276. def email(self):
  277. """Used to identify billing contact for stripe."""
  278. billing_contact = self.owner.organization_user.user
  279. return billing_contact.email
  280. def get_user_scopes(self, user):
  281. org_user = self.organization_users.get(user=user)
  282. return org_user.get_scopes()
  283. def change_owner(self, new_owner):
  284. """
  285. Changes ownership of an organization.
  286. """
  287. old_owner = self.owner.organization_user
  288. self.owner.organization_user = new_owner
  289. self.owner.save()
  290. owner_changed.send(sender=self, old=old_owner, new=new_owner)
  291. def is_owner(self, user):
  292. """
  293. Returns True is user is the organization's owner, otherwise false
  294. """
  295. return self.owner.organization_user.user == user
  296. class OrganizationUser(SharedBaseModel, OrganizationUserBase):
  297. user = models.ForeignKey(
  298. "users.User",
  299. blank=True,
  300. null=True,
  301. on_delete=models.CASCADE,
  302. related_name="organizations_ext_organizationuser",
  303. )
  304. role = models.PositiveSmallIntegerField(choices=OrganizationUserRole.choices)
  305. email = models.EmailField(
  306. blank=True, null=True, help_text="Email for pending invite"
  307. )
  308. class Meta(OrganizationOwnerBase.Meta):
  309. unique_together = (("user", "organization"), ("email", "organization"))
  310. def __str__(self, *args, **kwargs):
  311. if self.user:
  312. return super().__str__(*args, **kwargs)
  313. return self.email
  314. def get_email(self):
  315. if self.user:
  316. return self.user.email
  317. return self.email
  318. def get_role(self):
  319. return self.get_role_display().lower()
  320. def get_scopes(self):
  321. role = OrganizationUserRole.get_role(self.role)
  322. return role["scopes"]
  323. def accept_invite(self, user):
  324. self.user = user
  325. self.email = None
  326. self.save()
  327. @property
  328. def pending(self):
  329. return self.user_id is None
  330. @property
  331. def is_active(self):
  332. """Non pending means active"""
  333. return not self.pending
  334. class OrganizationOwner(OrganizationOwnerBase):
  335. """Only usage is for billing contact currently"""
  336. class OrganizationInvitation(OrganizationInvitationBase):
  337. """Required to exist for django-organizations"""