1from gitlab import cli
2from gitlab import exceptions as exc
3from gitlab import types
4from gitlab.base import RequiredOptional, RESTManager, RESTObject
5from gitlab.mixins import (
6    CreateMixin,
7    CRUDMixin,
8    DeleteMixin,
9    GetWithoutIdMixin,
10    ListMixin,
11    NoUpdateMixin,
12    ObjectDeleteMixin,
13    RetrieveMixin,
14    SaveMixin,
15    UpdateMixin,
16)
17
18from .custom_attributes import UserCustomAttributeManager  # noqa: F401
19from .events import UserEventManager  # noqa: F401
20
21__all__ = [
22    "CurrentUserEmail",
23    "CurrentUserEmailManager",
24    "CurrentUserGPGKey",
25    "CurrentUserGPGKeyManager",
26    "CurrentUserKey",
27    "CurrentUserKeyManager",
28    "CurrentUserStatus",
29    "CurrentUserStatusManager",
30    "CurrentUser",
31    "CurrentUserManager",
32    "User",
33    "UserManager",
34    "ProjectUser",
35    "ProjectUserManager",
36    "UserEmail",
37    "UserEmailManager",
38    "UserActivities",
39    "UserStatus",
40    "UserStatusManager",
41    "UserActivitiesManager",
42    "UserGPGKey",
43    "UserGPGKeyManager",
44    "UserKey",
45    "UserKeyManager",
46    "UserIdentityProviderManager",
47    "UserImpersonationToken",
48    "UserImpersonationTokenManager",
49    "UserMembership",
50    "UserMembershipManager",
51    "UserProject",
52    "UserProjectManager",
53]
54
55
56class CurrentUserEmail(ObjectDeleteMixin, RESTObject):
57    _short_print_attr = "email"
58
59
60class CurrentUserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
61    _path = "/user/emails"
62    _obj_cls = CurrentUserEmail
63    _create_attrs = RequiredOptional(required=("email",))
64
65
66class CurrentUserGPGKey(ObjectDeleteMixin, RESTObject):
67    pass
68
69
70class CurrentUserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
71    _path = "/user/gpg_keys"
72    _obj_cls = CurrentUserGPGKey
73    _create_attrs = RequiredOptional(required=("key",))
74
75
76class CurrentUserKey(ObjectDeleteMixin, RESTObject):
77    _short_print_attr = "title"
78
79
80class CurrentUserKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
81    _path = "/user/keys"
82    _obj_cls = CurrentUserKey
83    _create_attrs = RequiredOptional(required=("title", "key"))
84
85
86class CurrentUserStatus(SaveMixin, RESTObject):
87    _id_attr = None
88    _short_print_attr = "message"
89
90
91class CurrentUserStatusManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
92    _path = "/user/status"
93    _obj_cls = CurrentUserStatus
94    _update_attrs = RequiredOptional(optional=("emoji", "message"))
95
96
97class CurrentUser(RESTObject):
98    _id_attr = None
99    _short_print_attr = "username"
100    _managers = (
101        ("status", "CurrentUserStatusManager"),
102        ("emails", "CurrentUserEmailManager"),
103        ("gpgkeys", "CurrentUserGPGKeyManager"),
104        ("keys", "CurrentUserKeyManager"),
105    )
106
107
108class CurrentUserManager(GetWithoutIdMixin, RESTManager):
109    _path = "/user"
110    _obj_cls = CurrentUser
111
112
113class User(SaveMixin, ObjectDeleteMixin, RESTObject):
114    _short_print_attr = "username"
115    _managers = (
116        ("customattributes", "UserCustomAttributeManager"),
117        ("emails", "UserEmailManager"),
118        ("followers_users", "UserFollowersManager"),
119        ("following_users", "UserFollowingManager"),
120        ("events", "UserEventManager"),
121        ("gpgkeys", "UserGPGKeyManager"),
122        ("identityproviders", "UserIdentityProviderManager"),
123        ("impersonationtokens", "UserImpersonationTokenManager"),
124        ("keys", "UserKeyManager"),
125        ("memberships", "UserMembershipManager"),
126        ("projects", "UserProjectManager"),
127        ("status", "UserStatusManager"),
128    )
129
130    @cli.register_custom_action("User")
131    @exc.on_http_error(exc.GitlabBlockError)
132    def block(self, **kwargs):
133        """Block the user.
134
135        Args:
136            **kwargs: Extra options to send to the server (e.g. sudo)
137
138        Raises:
139            GitlabAuthenticationError: If authentication is not correct
140            GitlabBlockError: If the user could not be blocked
141
142        Returns:
143            bool: Whether the user status has been changed
144        """
145        path = "/users/%s/block" % self.id
146        server_data = self.manager.gitlab.http_post(path, **kwargs)
147        if server_data is True:
148            self._attrs["state"] = "blocked"
149        return server_data
150
151    @cli.register_custom_action("User")
152    @exc.on_http_error(exc.GitlabFollowError)
153    def follow(self, **kwargs):
154        """Follow the user.
155
156        Args:
157            **kwargs: Extra options to send to the server (e.g. sudo)
158
159        Raises:
160            GitlabAuthenticationError: If authentication is not correct
161            GitlabFollowError: If the user could not be followed
162
163        Returns:
164            dict: The new object data (*not* a RESTObject)
165        """
166        path = "/users/%s/follow" % self.id
167        return self.manager.gitlab.http_post(path, **kwargs)
168
169    @cli.register_custom_action("User")
170    @exc.on_http_error(exc.GitlabUnfollowError)
171    def unfollow(self, **kwargs):
172        """Unfollow the user.
173
174        Args:
175            **kwargs: Extra options to send to the server (e.g. sudo)
176
177        Raises:
178            GitlabAuthenticationError: If authentication is not correct
179            GitlabUnfollowError: If the user could not be followed
180
181        Returns:
182            dict: The new object data (*not* a RESTObject)
183        """
184        path = "/users/%s/unfollow" % self.id
185        return self.manager.gitlab.http_post(path, **kwargs)
186
187    @cli.register_custom_action("User")
188    @exc.on_http_error(exc.GitlabUnblockError)
189    def unblock(self, **kwargs):
190        """Unblock the user.
191
192        Args:
193            **kwargs: Extra options to send to the server (e.g. sudo)
194
195        Raises:
196            GitlabAuthenticationError: If authentication is not correct
197            GitlabUnblockError: If the user could not be unblocked
198
199        Returns:
200            bool: Whether the user status has been changed
201        """
202        path = "/users/%s/unblock" % self.id
203        server_data = self.manager.gitlab.http_post(path, **kwargs)
204        if server_data is True:
205            self._attrs["state"] = "active"
206        return server_data
207
208    @cli.register_custom_action("User")
209    @exc.on_http_error(exc.GitlabDeactivateError)
210    def deactivate(self, **kwargs):
211        """Deactivate the user.
212
213        Args:
214            **kwargs: Extra options to send to the server (e.g. sudo)
215
216        Raises:
217            GitlabAuthenticationError: If authentication is not correct
218            GitlabDeactivateError: If the user could not be deactivated
219
220        Returns:
221            bool: Whether the user status has been changed
222        """
223        path = "/users/%s/deactivate" % self.id
224        server_data = self.manager.gitlab.http_post(path, **kwargs)
225        if server_data:
226            self._attrs["state"] = "deactivated"
227        return server_data
228
229    @cli.register_custom_action("User")
230    @exc.on_http_error(exc.GitlabActivateError)
231    def activate(self, **kwargs):
232        """Activate the user.
233
234        Args:
235            **kwargs: Extra options to send to the server (e.g. sudo)
236
237        Raises:
238            GitlabAuthenticationError: If authentication is not correct
239            GitlabActivateError: If the user could not be activated
240
241        Returns:
242            bool: Whether the user status has been changed
243        """
244        path = "/users/%s/activate" % self.id
245        server_data = self.manager.gitlab.http_post(path, **kwargs)
246        if server_data:
247            self._attrs["state"] = "active"
248        return server_data
249
250
251class UserManager(CRUDMixin, RESTManager):
252    _path = "/users"
253    _obj_cls = User
254
255    _list_filters = (
256        "active",
257        "blocked",
258        "username",
259        "extern_uid",
260        "provider",
261        "external",
262        "search",
263        "custom_attributes",
264        "status",
265        "two_factor",
266    )
267    _create_attrs = RequiredOptional(
268        optional=(
269            "email",
270            "username",
271            "name",
272            "password",
273            "reset_password",
274            "skype",
275            "linkedin",
276            "twitter",
277            "projects_limit",
278            "extern_uid",
279            "provider",
280            "bio",
281            "admin",
282            "can_create_group",
283            "website_url",
284            "skip_confirmation",
285            "external",
286            "organization",
287            "location",
288            "avatar",
289            "public_email",
290            "private_profile",
291            "color_scheme_id",
292            "theme_id",
293        ),
294    )
295    _update_attrs = RequiredOptional(
296        required=("email", "username", "name"),
297        optional=(
298            "password",
299            "skype",
300            "linkedin",
301            "twitter",
302            "projects_limit",
303            "extern_uid",
304            "provider",
305            "bio",
306            "admin",
307            "can_create_group",
308            "website_url",
309            "skip_reconfirmation",
310            "external",
311            "organization",
312            "location",
313            "avatar",
314            "public_email",
315            "private_profile",
316            "color_scheme_id",
317            "theme_id",
318        ),
319    )
320    _types = {"confirm": types.LowercaseStringAttribute, "avatar": types.ImageAttribute}
321
322
323class ProjectUser(RESTObject):
324    pass
325
326
327class ProjectUserManager(ListMixin, RESTManager):
328    _path = "/projects/%(project_id)s/users"
329    _obj_cls = ProjectUser
330    _from_parent_attrs = {"project_id": "id"}
331    _list_filters = ("search", "skip_users")
332    _types = {"skip_users": types.ListAttribute}
333
334
335class UserEmail(ObjectDeleteMixin, RESTObject):
336    _short_print_attr = "email"
337
338
339class UserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
340    _path = "/users/%(user_id)s/emails"
341    _obj_cls = UserEmail
342    _from_parent_attrs = {"user_id": "id"}
343    _create_attrs = RequiredOptional(required=("email",))
344
345
346class UserActivities(RESTObject):
347    _id_attr = "username"
348
349
350class UserStatus(RESTObject):
351    _id_attr = None
352    _short_print_attr = "message"
353
354
355class UserStatusManager(GetWithoutIdMixin, RESTManager):
356    _path = "/users/%(user_id)s/status"
357    _obj_cls = UserStatus
358    _from_parent_attrs = {"user_id": "id"}
359
360
361class UserActivitiesManager(ListMixin, RESTManager):
362    _path = "/user/activities"
363    _obj_cls = UserActivities
364
365
366class UserGPGKey(ObjectDeleteMixin, RESTObject):
367    pass
368
369
370class UserGPGKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager):
371    _path = "/users/%(user_id)s/gpg_keys"
372    _obj_cls = UserGPGKey
373    _from_parent_attrs = {"user_id": "id"}
374    _create_attrs = RequiredOptional(required=("key",))
375
376
377class UserKey(ObjectDeleteMixin, RESTObject):
378    pass
379
380
381class UserKeyManager(ListMixin, CreateMixin, DeleteMixin, RESTManager):
382    _path = "/users/%(user_id)s/keys"
383    _obj_cls = UserKey
384    _from_parent_attrs = {"user_id": "id"}
385    _create_attrs = RequiredOptional(required=("title", "key"))
386
387
388class UserIdentityProviderManager(DeleteMixin, RESTManager):
389    """Manager for user identities.
390
391    This manager does not actually manage objects but enables
392    functionality for deletion of user identities by provider.
393    """
394
395    _path = "/users/%(user_id)s/identities"
396    _from_parent_attrs = {"user_id": "id"}
397
398
399class UserImpersonationToken(ObjectDeleteMixin, RESTObject):
400    pass
401
402
403class UserImpersonationTokenManager(NoUpdateMixin, RESTManager):
404    _path = "/users/%(user_id)s/impersonation_tokens"
405    _obj_cls = UserImpersonationToken
406    _from_parent_attrs = {"user_id": "id"}
407    _create_attrs = RequiredOptional(
408        required=("name", "scopes"), optional=("expires_at",)
409    )
410    _list_filters = ("state",)
411
412
413class UserMembership(RESTObject):
414    _id_attr = "source_id"
415
416
417class UserMembershipManager(RetrieveMixin, RESTManager):
418    _path = "/users/%(user_id)s/memberships"
419    _obj_cls = UserMembership
420    _from_parent_attrs = {"user_id": "id"}
421    _list_filters = ("type",)
422
423
424# Having this outside projects avoids circular imports due to ProjectUser
425class UserProject(RESTObject):
426    pass
427
428
429class UserProjectManager(ListMixin, CreateMixin, RESTManager):
430    _path = "/projects/user/%(user_id)s"
431    _obj_cls = UserProject
432    _from_parent_attrs = {"user_id": "id"}
433    _create_attrs = RequiredOptional(
434        required=("name",),
435        optional=(
436            "default_branch",
437            "issues_enabled",
438            "wall_enabled",
439            "merge_requests_enabled",
440            "wiki_enabled",
441            "snippets_enabled",
442            "public",
443            "visibility",
444            "description",
445            "builds_enabled",
446            "public_builds",
447            "import_url",
448            "only_allow_merge_if_build_succeeds",
449        ),
450    )
451    _list_filters = (
452        "archived",
453        "visibility",
454        "order_by",
455        "sort",
456        "search",
457        "simple",
458        "owned",
459        "membership",
460        "starred",
461        "statistics",
462        "with_issues_enabled",
463        "with_merge_requests_enabled",
464        "with_custom_attributes",
465        "with_programming_language",
466        "wiki_checksum_failed",
467        "repository_checksum_failed",
468        "min_access_level",
469        "id_after",
470        "id_before",
471    )
472
473    def list(self, **kwargs):
474        """Retrieve a list of objects.
475
476        Args:
477            all (bool): If True, return all the items, without pagination
478            per_page (int): Number of items to retrieve per request
479            page (int): ID of the page to return (starts with page 1)
480            as_list (bool): If set to False and no pagination option is
481                defined, return a generator instead of a list
482            **kwargs: Extra options to send to the server (e.g. sudo)
483
484        Returns:
485            list: The list of objects, or a generator if `as_list` is False
486
487        Raises:
488            GitlabAuthenticationError: If authentication is not correct
489            GitlabListError: If the server cannot perform the request
490        """
491        if self._parent:
492            path = "/users/%s/projects" % self._parent.id
493        else:
494            path = "/users/%s/projects" % kwargs["user_id"]
495        return ListMixin.list(self, path=path, **kwargs)
496
497
498class UserFollowersManager(ListMixin, RESTManager):
499    _path = "/users/%(user_id)s/followers"
500    _obj_cls = User
501    _from_parent_attrs = {"user_id": "id"}
502
503
504class UserFollowingManager(ListMixin, RESTManager):
505    _path = "/users/%(user_id)s/following"
506    _obj_cls = User
507    _from_parent_attrs = {"user_id": "id"}
508