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