|
1 |
| -# Please do not use |
2 |
| -# from __future__ import annotations |
3 |
| -# in modules such as this one where hybrid cloud data models or service classes are |
4 |
| -# defined, because we want to reflect on type annotations and avoid forward references. |
5 |
| - |
6 |
| -from collections import defaultdict |
7 |
| -from collections.abc import Iterable, MutableMapping, Sequence |
8 |
| -from enum import Enum |
9 |
| -from typing import TYPE_CHECKING, Any, Union, overload |
10 |
| - |
11 |
| -from django.core.exceptions import ObjectDoesNotExist |
12 |
| -from rest_framework import serializers |
13 |
| - |
14 |
| -from sentry.services.hybrid_cloud import RpcModel |
15 |
| -from sentry.services.hybrid_cloud.user import RpcUser |
16 |
| - |
17 |
| -if TYPE_CHECKING: |
18 |
| - from sentry.models.team import Team |
19 |
| - from sentry.models.user import User |
20 |
| - from sentry.services.hybrid_cloud.organization import RpcTeam |
21 |
| - |
22 |
| - |
23 |
| -class ActorType(str, Enum): |
24 |
| - USER = "User" |
25 |
| - TEAM = "Team" |
26 |
| - |
27 |
| - |
28 |
| -ActorTarget = Union["RpcActor", "User", "RpcUser", "Team", "RpcTeam"] |
29 |
| - |
30 |
| - |
31 |
| -class RpcActor(RpcModel): |
32 |
| - """Can represent any model object with a foreign key to Actor.""" |
33 |
| - |
34 |
| - id: int |
35 |
| - """The id of the user/team this actor represents""" |
36 |
| - |
37 |
| - actor_type: ActorType |
38 |
| - """Whether this actor is a User or Team""" |
39 |
| - |
40 |
| - slug: str | None = None |
41 |
| - |
42 |
| - class InvalidActor(ObjectDoesNotExist): |
43 |
| - """Raised when an Actor fails to resolve or be found""" |
44 |
| - |
45 |
| - pass |
46 |
| - |
47 |
| - @classmethod |
48 |
| - def resolve_many(cls, actors: Sequence["RpcActor"]) -> list["Team | RpcUser"]: |
49 |
| - """ |
50 |
| - Resolve a list of actors in a batch to the Team/User the Actor references. |
51 |
| -
|
52 |
| - Will generate more efficient queries to load actors than calling |
53 |
| - RpcActor.resolve() individually will. |
54 |
| - """ |
55 |
| - from sentry.models.team import Team |
56 |
| - from sentry.services.hybrid_cloud.user.service import user_service |
57 |
| - |
58 |
| - if not actors: |
59 |
| - return [] |
60 |
| - actors_by_type: dict[ActorType, list[RpcActor]] = defaultdict(list) |
61 |
| - for actor in actors: |
62 |
| - actors_by_type[actor.actor_type].append(actor) |
63 |
| - results: dict[tuple[ActorType, int], Team | RpcUser] = {} |
64 |
| - for actor_type, actor_list in actors_by_type.items(): |
65 |
| - if actor_type == ActorType.USER: |
66 |
| - for user in user_service.get_many(filter={"user_ids": [u.id for u in actor_list]}): |
67 |
| - results[(actor_type, user.id)] = user |
68 |
| - if actor_type == ActorType.TEAM: |
69 |
| - for team in Team.objects.filter(id__in=[t.id for t in actor_list]): |
70 |
| - results[(actor_type, team.id)] = team |
71 |
| - |
72 |
| - return list(filter(None, [results.get((actor.actor_type, actor.id)) for actor in actors])) |
73 |
| - |
74 |
| - @classmethod |
75 |
| - def many_from_object(cls, objects: Iterable[ActorTarget]) -> list["RpcActor"]: |
76 |
| - """ |
77 |
| - Create a list of RpcActor instances based on a collection of 'objects' |
78 |
| -
|
79 |
| - Objects will be grouped by the kind of actor they would be related to. |
80 |
| - Queries for actors are batched to increase efficiency. Users that are |
81 |
| - missing actors will have actors generated. |
82 |
| - """ |
83 |
| - from sentry.models.team import Team |
84 |
| - from sentry.models.user import User |
85 |
| - from sentry.services.hybrid_cloud.organization import RpcTeam |
86 |
| - |
87 |
| - result: list["RpcActor"] = [] |
88 |
| - grouped_by_type: MutableMapping[str, list[int]] = defaultdict(list) |
89 |
| - team_slugs: MutableMapping[int, str] = {} |
90 |
| - for obj in objects: |
91 |
| - if isinstance(obj, cls): |
92 |
| - result.append(obj) |
93 |
| - if isinstance(obj, (User, RpcUser)): |
94 |
| - grouped_by_type[ActorType.USER].append(obj.id) |
95 |
| - if isinstance(obj, (Team, RpcTeam)): |
96 |
| - team_slugs[obj.id] = obj.slug |
97 |
| - grouped_by_type[ActorType.TEAM].append(obj.id) |
98 |
| - |
99 |
| - if grouped_by_type[ActorType.TEAM]: |
100 |
| - team_ids = grouped_by_type[ActorType.TEAM] |
101 |
| - for team_id in team_ids: |
102 |
| - result.append( |
103 |
| - RpcActor( |
104 |
| - id=team_id, |
105 |
| - actor_type=ActorType.TEAM, |
106 |
| - slug=team_slugs.get(team_id), |
107 |
| - ) |
108 |
| - ) |
109 |
| - |
110 |
| - if grouped_by_type[ActorType.USER]: |
111 |
| - user_ids = grouped_by_type[ActorType.USER] |
112 |
| - for user_id in user_ids: |
113 |
| - result.append(RpcActor(id=user_id, actor_type=ActorType.USER)) |
114 |
| - return result |
115 |
| - |
116 |
| - @classmethod |
117 |
| - def from_object(cls, obj: ActorTarget) -> "RpcActor": |
118 |
| - """ |
119 |
| - fetch_actor: whether to make an extra query or call to fetch the actor id |
120 |
| - Without the actor_id the RpcActor acts as a tuple of id and type. |
121 |
| - """ |
122 |
| - from sentry.models.team import Team |
123 |
| - from sentry.models.user import User |
124 |
| - from sentry.services.hybrid_cloud.organization import RpcTeam |
125 |
| - |
126 |
| - if isinstance(obj, cls): |
127 |
| - return obj |
128 |
| - if isinstance(obj, User): |
129 |
| - return cls.from_orm_user(obj) |
130 |
| - if isinstance(obj, Team): |
131 |
| - return cls.from_orm_team(obj) |
132 |
| - if isinstance(obj, RpcUser): |
133 |
| - return cls.from_rpc_user(obj) |
134 |
| - if isinstance(obj, RpcTeam): |
135 |
| - return cls.from_rpc_team(obj) |
136 |
| - raise TypeError(f"Cannot build RpcActor from {type(obj)}") |
137 |
| - |
138 |
| - @classmethod |
139 |
| - def from_orm_user(cls, user: "User") -> "RpcActor": |
140 |
| - return cls( |
141 |
| - id=user.id, |
142 |
| - actor_type=ActorType.USER, |
143 |
| - ) |
144 |
| - |
145 |
| - @classmethod |
146 |
| - def from_rpc_user(cls, user: RpcUser) -> "RpcActor": |
147 |
| - return cls( |
148 |
| - id=user.id, |
149 |
| - actor_type=ActorType.USER, |
150 |
| - ) |
151 |
| - |
152 |
| - @classmethod |
153 |
| - def from_orm_team(cls, team: "Team") -> "RpcActor": |
154 |
| - return cls(id=team.id, actor_type=ActorType.TEAM, slug=team.slug) |
155 |
| - |
156 |
| - @classmethod |
157 |
| - def from_rpc_team(cls, team: "RpcTeam") -> "RpcActor": |
158 |
| - return cls(id=team.id, actor_type=ActorType.TEAM, slug=team.slug) |
159 |
| - |
160 |
| - @overload |
161 |
| - @classmethod |
162 |
| - def from_identifier(cls, id: None) -> None: |
163 |
| - ... |
164 |
| - |
165 |
| - @overload |
166 |
| - @classmethod |
167 |
| - def from_identifier(cls, id: int | str) -> "RpcActor": |
168 |
| - ... |
169 |
| - |
170 |
| - @classmethod |
171 |
| - def from_identifier(cls, id: str | int | None) -> "RpcActor | None": |
172 |
| - """ |
173 |
| - Parse an actor identifier into an RpcActor |
174 |
| -
|
175 |
| - Forms `id` can take: |
176 |
| - 1231 -> look up User by id |
177 |
| - "1231" -> look up User by id |
178 |
| - "user:1231" -> look up User by id |
179 |
| - "team:1231" -> look up Team by id |
180 |
| - "maiseythedog" -> look up User by username |
181 |
| - "maisey@dogsrule.com" -> look up User by primary email |
182 |
| - """ |
183 |
| - from sentry.services.hybrid_cloud.user.service import user_service |
184 |
| - |
185 |
| - if not id: |
186 |
| - return None |
187 |
| - # If we have an integer, fall back to assuming it's a User |
188 |
| - if isinstance(id, int): |
189 |
| - return cls(id=id, actor_type=ActorType.USER) |
190 |
| - |
191 |
| - # If the actor_identifier is a simple integer as a string, |
192 |
| - # we're also a User |
193 |
| - if id.isdigit(): |
194 |
| - return cls(id=int(id), actor_type=ActorType.USER) |
195 |
| - |
196 |
| - if id.startswith("user:"): |
197 |
| - return cls(id=int(id[5:]), actor_type=ActorType.USER) |
198 |
| - |
199 |
| - if id.startswith("team:"): |
200 |
| - return cls(id=int(id[5:]), actor_type=ActorType.TEAM) |
201 |
| - |
202 |
| - try: |
203 |
| - user = user_service.get_by_username(username=id)[0] |
204 |
| - return cls(id=user.id, actor_type=ActorType.USER) |
205 |
| - except IndexError as e: |
206 |
| - raise cls.InvalidActor(f"Unable to resolve actor identifier: {e}") |
207 |
| - |
208 |
| - @classmethod |
209 |
| - def from_id(cls, user_id: int | None = None, team_id: int | None = None) -> "RpcActor": |
210 |
| - if user_id and team_id: |
211 |
| - raise cls.InvalidActor("You can only provide one of user_id and team_id") |
212 |
| - if user_id: |
213 |
| - return cls(id=user_id, actor_type=ActorType.USER) |
214 |
| - if team_id: |
215 |
| - return cls(id=team_id, actor_type=ActorType.TEAM) |
216 |
| - raise cls.InvalidActor("You must provide one of user_id and team_id") |
217 |
| - |
218 |
| - def __post_init__(self) -> None: |
219 |
| - if not self.is_team and self.slug is not None: |
220 |
| - raise ValueError("Slugs are expected for teams only") |
221 |
| - |
222 |
| - def __hash__(self) -> int: |
223 |
| - return hash((self.id, self.actor_type)) |
224 |
| - |
225 |
| - def __eq__(self, other: Any) -> bool: |
226 |
| - return ( |
227 |
| - isinstance(other, self.__class__) |
228 |
| - and self.id == other.id |
229 |
| - and self.actor_type == other.actor_type |
230 |
| - ) |
231 |
| - |
232 |
| - def resolve(self) -> "Team | RpcUser": |
233 |
| - """ |
234 |
| - Resolve an Actor into the Team or RpcUser it represents. |
235 |
| -
|
236 |
| - Will raise Team.DoesNotExist or User.DoesNotExist when the actor is invalid |
237 |
| - """ |
238 |
| - from sentry.models.team import Team |
239 |
| - from sentry.services.hybrid_cloud.user.service import user_service |
240 |
| - |
241 |
| - if self.is_team: |
242 |
| - team = Team.objects.filter(id=self.id).first() |
243 |
| - if team: |
244 |
| - return team |
245 |
| - raise RpcActor.InvalidActor(f"Cannot find a team with id={self.id}") |
246 |
| - if self.is_user: |
247 |
| - user = user_service.get_user(user_id=self.id) |
248 |
| - if user: |
249 |
| - return user |
250 |
| - raise RpcActor.InvalidActor(f"Cannot find a User with id={self.id}") |
251 |
| - # This should be un-reachable |
252 |
| - raise RpcActor.InvalidActor("Cannot resolve an actor with an unknown type") |
253 |
| - |
254 |
| - @property |
255 |
| - def identifier(self) -> str: |
256 |
| - return f"{self.actor_type.lower()}:{self.id}" |
257 |
| - |
258 |
| - @property |
259 |
| - def is_team(self) -> bool: |
260 |
| - return self.actor_type == ActorType.TEAM |
261 |
| - |
262 |
| - @property |
263 |
| - def is_user(self) -> bool: |
264 |
| - return self.actor_type == ActorType.USER |
265 |
| - |
266 |
| - |
267 |
| -def parse_and_validate_actor(actor_identifier: str | None, organization_id: int) -> RpcActor | None: |
268 |
| - from sentry.models.organizationmember import OrganizationMember |
269 |
| - from sentry.models.team import Team |
270 |
| - |
271 |
| - if not actor_identifier: |
272 |
| - return None |
273 |
| - |
274 |
| - try: |
275 |
| - actor = RpcActor.from_identifier(actor_identifier) |
276 |
| - except Exception: |
277 |
| - raise serializers.ValidationError( |
278 |
| - "Could not parse actor. Format should be `type:id` where type is `team` or `user`." |
279 |
| - ) |
280 |
| - try: |
281 |
| - obj = actor.resolve() |
282 |
| - except RpcActor.InvalidActor: |
283 |
| - raise serializers.ValidationError(f"{actor.actor_type} does not exist") |
284 |
| - |
285 |
| - if isinstance(obj, Team): |
286 |
| - if obj.organization_id != organization_id: |
287 |
| - raise serializers.ValidationError("Team is not a member of this organization") |
288 |
| - elif isinstance(obj, RpcUser): |
289 |
| - if not OrganizationMember.objects.filter( |
290 |
| - organization_id=organization_id, user_id=obj.id |
291 |
| - ).exists(): |
292 |
| - raise serializers.ValidationError("User is not a member of this organization") |
293 |
| - |
294 |
| - return actor |
| 1 | +# Deprecated module for actor imports |
| 2 | +# Use sentry.types.actor instead. |
| 3 | +from sentry.types.actor import Actor, ActorTarget, ActorType, parse_and_validate_actor |
| 4 | + |
| 5 | +RpcActor = Actor |
| 6 | + |
| 7 | +__all__ = ( |
| 8 | + "RpcActor", |
| 9 | + "ActorType", |
| 10 | + "ActorTarget", |
| 11 | + "parse_and_validate_actor", |
| 12 | +) |
0 commit comments