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 CRUDMixin, ListMixin, ObjectDeleteMixin, SaveMixin
6
7from .access_requests import GroupAccessRequestManager  # noqa: F401
8from .audit_events import GroupAuditEventManager  # noqa: F401
9from .badges import GroupBadgeManager  # noqa: F401
10from .boards import GroupBoardManager  # noqa: F401
11from .clusters import GroupClusterManager  # noqa: F401
12from .custom_attributes import GroupCustomAttributeManager  # noqa: F401
13from .deploy_tokens import GroupDeployTokenManager  # noqa: F401
14from .epics import GroupEpicManager  # noqa: F401
15from .export_import import GroupExportManager, GroupImportManager  # noqa: F401
16from .hooks import GroupHookManager  # noqa: F401
17from .issues import GroupIssueManager  # noqa: F401
18from .labels import GroupLabelManager  # noqa: F401
19from .members import (  # noqa: F401
20    GroupBillableMemberManager,
21    GroupMemberAllManager,
22    GroupMemberManager,
23)
24from .merge_requests import GroupMergeRequestManager  # noqa: F401
25from .milestones import GroupMilestoneManager  # noqa: F401
26from .notification_settings import GroupNotificationSettingsManager  # noqa: F401
27from .packages import GroupPackageManager  # noqa: F401
28from .projects import GroupProjectManager  # noqa: F401
29from .runners import GroupRunnerManager  # noqa: F401
30from .statistics import GroupIssuesStatisticsManager  # noqa: F401
31from .variables import GroupVariableManager  # noqa: F401
32from .wikis import GroupWikiManager  # noqa: F401
33
34__all__ = [
35    "Group",
36    "GroupManager",
37    "GroupDescendantGroup",
38    "GroupDescendantGroupManager",
39    "GroupSubgroup",
40    "GroupSubgroupManager",
41]
42
43
44class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
45    _short_print_attr = "name"
46    _managers = (
47        ("accessrequests", "GroupAccessRequestManager"),
48        ("audit_events", "GroupAuditEventManager"),
49        ("badges", "GroupBadgeManager"),
50        ("billable_members", "GroupBillableMemberManager"),
51        ("boards", "GroupBoardManager"),
52        ("customattributes", "GroupCustomAttributeManager"),
53        ("descendant_groups", "GroupDescendantGroupManager"),
54        ("exports", "GroupExportManager"),
55        ("epics", "GroupEpicManager"),
56        ("hooks", "GroupHookManager"),
57        ("imports", "GroupImportManager"),
58        ("issues", "GroupIssueManager"),
59        ("issues_statistics", "GroupIssuesStatisticsManager"),
60        ("labels", "GroupLabelManager"),
61        ("members", "GroupMemberManager"),
62        ("members_all", "GroupMemberAllManager"),
63        ("mergerequests", "GroupMergeRequestManager"),
64        ("milestones", "GroupMilestoneManager"),
65        ("notificationsettings", "GroupNotificationSettingsManager"),
66        ("packages", "GroupPackageManager"),
67        ("projects", "GroupProjectManager"),
68        ("runners", "GroupRunnerManager"),
69        ("subgroups", "GroupSubgroupManager"),
70        ("variables", "GroupVariableManager"),
71        ("clusters", "GroupClusterManager"),
72        ("deploytokens", "GroupDeployTokenManager"),
73        ("wikis", "GroupWikiManager"),
74    )
75
76    @cli.register_custom_action("Group", ("to_project_id",))
77    @exc.on_http_error(exc.GitlabTransferProjectError)
78    def transfer_project(self, to_project_id, **kwargs):
79        """Transfer a project to this group.
80
81        Args:
82            to_project_id (int): ID of the project to transfer
83            **kwargs: Extra options to send to the server (e.g. sudo)
84
85        Raises:
86            GitlabAuthenticationError: If authentication is not correct
87            GitlabTransferProjectError: If the project could not be transfered
88        """
89        path = "/groups/%s/projects/%s" % (self.id, to_project_id)
90        self.manager.gitlab.http_post(path, **kwargs)
91
92    @cli.register_custom_action("Group", ("scope", "search"))
93    @exc.on_http_error(exc.GitlabSearchError)
94    def search(self, scope, search, **kwargs):
95        """Search the group resources matching the provided string.'
96
97        Args:
98            scope (str): Scope of the search
99            search (str): Search string
100            **kwargs: Extra options to send to the server (e.g. sudo)
101
102        Raises:
103            GitlabAuthenticationError: If authentication is not correct
104            GitlabSearchError: If the server failed to perform the request
105
106        Returns:
107            GitlabList: A list of dicts describing the resources found.
108        """
109        data = {"scope": scope, "search": search}
110        path = "/groups/%s/search" % self.get_id()
111        return self.manager.gitlab.http_list(path, query_data=data, **kwargs)
112
113    @cli.register_custom_action("Group", ("cn", "group_access", "provider"))
114    @exc.on_http_error(exc.GitlabCreateError)
115    def add_ldap_group_link(self, cn, group_access, provider, **kwargs):
116        """Add an LDAP group link.
117
118        Args:
119            cn (str): CN of the LDAP group
120            group_access (int): Minimum access level for members of the LDAP
121                group
122            provider (str): LDAP provider for the LDAP group
123            **kwargs: Extra options to send to the server (e.g. sudo)
124
125        Raises:
126            GitlabAuthenticationError: If authentication is not correct
127            GitlabCreateError: If the server cannot perform the request
128        """
129        path = "/groups/%s/ldap_group_links" % self.get_id()
130        data = {"cn": cn, "group_access": group_access, "provider": provider}
131        self.manager.gitlab.http_post(path, post_data=data, **kwargs)
132
133    @cli.register_custom_action("Group", ("cn",), ("provider",))
134    @exc.on_http_error(exc.GitlabDeleteError)
135    def delete_ldap_group_link(self, cn, provider=None, **kwargs):
136        """Delete an LDAP group link.
137
138        Args:
139            cn (str): CN of the LDAP group
140            provider (str): LDAP provider for the LDAP group
141            **kwargs: Extra options to send to the server (e.g. sudo)
142
143        Raises:
144            GitlabAuthenticationError: If authentication is not correct
145            GitlabDeleteError: If the server cannot perform the request
146        """
147        path = "/groups/%s/ldap_group_links" % self.get_id()
148        if provider is not None:
149            path += "/%s" % provider
150        path += "/%s" % cn
151        self.manager.gitlab.http_delete(path)
152
153    @cli.register_custom_action("Group")
154    @exc.on_http_error(exc.GitlabCreateError)
155    def ldap_sync(self, **kwargs):
156        """Sync LDAP groups.
157
158        Args:
159            **kwargs: Extra options to send to the server (e.g. sudo)
160
161        Raises:
162            GitlabAuthenticationError: If authentication is not correct
163            GitlabCreateError: If the server cannot perform the request
164        """
165        path = "/groups/%s/ldap_sync" % self.get_id()
166        self.manager.gitlab.http_post(path, **kwargs)
167
168    @cli.register_custom_action("Group", ("group_id", "group_access"), ("expires_at",))
169    @exc.on_http_error(exc.GitlabCreateError)
170    def share(self, group_id, group_access, expires_at=None, **kwargs):
171        """Share the group with a group.
172
173        Args:
174            group_id (int): ID of the group.
175            group_access (int): Access level for the group.
176            **kwargs: Extra options to send to the server (e.g. sudo)
177
178        Raises:
179            GitlabAuthenticationError: If authentication is not correct
180            GitlabCreateError: If the server failed to perform the request
181        """
182        path = "/groups/%s/share" % self.get_id()
183        data = {
184            "group_id": group_id,
185            "group_access": group_access,
186            "expires_at": expires_at,
187        }
188        self.manager.gitlab.http_post(path, post_data=data, **kwargs)
189
190    @cli.register_custom_action("Group", ("group_id",))
191    @exc.on_http_error(exc.GitlabDeleteError)
192    def unshare(self, group_id, **kwargs):
193        """Delete a shared group link within a group.
194
195        Args:
196            group_id (int): ID of the group.
197            **kwargs: Extra options to send to the server (e.g. sudo)
198
199        Raises:
200            GitlabAuthenticationError: If authentication is not correct
201            GitlabDeleteError: If the server failed to perform the request
202        """
203        path = "/groups/%s/share/%s" % (self.get_id(), group_id)
204        self.manager.gitlab.http_delete(path, **kwargs)
205
206
207class GroupManager(CRUDMixin, RESTManager):
208    _path = "/groups"
209    _obj_cls = Group
210    _list_filters = (
211        "skip_groups",
212        "all_available",
213        "search",
214        "order_by",
215        "sort",
216        "statistics",
217        "owned",
218        "with_custom_attributes",
219        "min_access_level",
220        "top_level_only",
221    )
222    _create_attrs = RequiredOptional(
223        required=("name", "path"),
224        optional=(
225            "description",
226            "membership_lock",
227            "visibility",
228            "share_with_group_lock",
229            "require_two_factor_authentication",
230            "two_factor_grace_period",
231            "project_creation_level",
232            "auto_devops_enabled",
233            "subgroup_creation_level",
234            "emails_disabled",
235            "avatar",
236            "mentions_disabled",
237            "lfs_enabled",
238            "request_access_enabled",
239            "parent_id",
240            "default_branch_protection",
241            "shared_runners_minutes_limit",
242            "extra_shared_runners_minutes_limit",
243        ),
244    )
245    _update_attrs = RequiredOptional(
246        optional=(
247            "name",
248            "path",
249            "description",
250            "membership_lock",
251            "share_with_group_lock",
252            "visibility",
253            "require_two_factor_authentication",
254            "two_factor_grace_period",
255            "project_creation_level",
256            "auto_devops_enabled",
257            "subgroup_creation_level",
258            "emails_disabled",
259            "avatar",
260            "mentions_disabled",
261            "lfs_enabled",
262            "request_access_enabled",
263            "default_branch_protection",
264            "file_template_project_id",
265            "shared_runners_minutes_limit",
266            "extra_shared_runners_minutes_limit",
267            "prevent_forking_outside_group",
268            "shared_runners_setting",
269        ),
270    )
271    _types = {"avatar": types.ImageAttribute, "skip_groups": types.ListAttribute}
272
273    @exc.on_http_error(exc.GitlabImportError)
274    def import_group(self, file, path, name, parent_id=None, **kwargs):
275        """Import a group from an archive file.
276
277        Args:
278            file: Data or file object containing the group
279            path (str): The path for the new group to be imported.
280            name (str): The name for the new group.
281            parent_id (str): ID of a parent group that the group will
282                be imported into.
283            **kwargs: Extra options to send to the server (e.g. sudo)
284
285        Raises:
286            GitlabAuthenticationError: If authentication is not correct
287            GitlabImportError: If the server failed to perform the request
288
289        Returns:
290            dict: A representation of the import status.
291        """
292        files = {"file": ("file.tar.gz", file, "application/octet-stream")}
293        data = {"path": path, "name": name}
294        if parent_id is not None:
295            data["parent_id"] = parent_id
296
297        return self.gitlab.http_post(
298            "/groups/import", post_data=data, files=files, **kwargs
299        )
300
301
302class GroupSubgroup(RESTObject):
303    pass
304
305
306class GroupSubgroupManager(ListMixin, RESTManager):
307    _path = "/groups/%(group_id)s/subgroups"
308    _obj_cls = GroupSubgroup
309    _from_parent_attrs = {"group_id": "id"}
310    _list_filters = (
311        "skip_groups",
312        "all_available",
313        "search",
314        "order_by",
315        "sort",
316        "statistics",
317        "owned",
318        "with_custom_attributes",
319        "min_access_level",
320    )
321    _types = {"skip_groups": types.ListAttribute}
322
323
324class GroupDescendantGroup(RESTObject):
325    pass
326
327
328class GroupDescendantGroupManager(GroupSubgroupManager):
329    """
330    This manager inherits from GroupSubgroupManager as descendant groups
331    share all attributes with subgroups, except the path and object class.
332    """
333
334    _path = "/groups/%(group_id)s/descendant_groups"
335    _obj_cls = GroupDescendantGroup
336