diff --git a/docs/gl_objects/groups.rst b/docs/gl_objects/groups.rst index 98f0beeb6..fafa40a86 100644 --- a/docs/gl_objects/groups.rst +++ b/docs/gl_objects/groups.rst @@ -358,6 +358,28 @@ You can use the ``ldapgroups`` manager to list available LDAP groups:: # list the groups for a specific LDAP provider ldap_groups = gl.ldapgroups.list(search='foo', provider='ldapmain') +SAML group links +================ + +Add a SAML group link to an existing GitLab group:: + + saml_link = group.saml_group_links.create({ + "saml_group_name": "", + "access_level": + }) + +List a group's SAML group links:: + + group.saml_group_links.list() + +Get a SAML group link:: + + group.saml_group_links.get("") + +Remove a link:: + + saml_link.delete() + Groups hooks ============ diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py index 4111496ed..d03eb38c8 100644 --- a/gitlab/v4/objects/groups.py +++ b/gitlab/v4/objects/groups.py @@ -12,6 +12,7 @@ CRUDMixin, DeleteMixin, ListMixin, + NoUpdateMixin, ObjectDeleteMixin, SaveMixin, ) @@ -58,6 +59,8 @@ "GroupLDAPGroupLinkManager", "GroupSubgroup", "GroupSubgroupManager", + "GroupSAMLGroupLink", + "GroupSAMLGroupLinkManager", ] @@ -98,6 +101,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject): subgroups: "GroupSubgroupManager" variables: GroupVariableManager wikis: GroupWikiManager + saml_group_links: "GroupSAMLGroupLinkManager" @cli.register_custom_action("Group", ("project_id",)) @exc.on_http_error(exc.GitlabTransferProjectError) @@ -464,3 +468,20 @@ class GroupLDAPGroupLinkManager(ListMixin, CreateMixin, DeleteMixin, RESTManager _create_attrs = RequiredOptional( required=("provider", "group_access"), exclusive=("cn", "filter") ) + + +class GroupSAMLGroupLink(ObjectDeleteMixin, RESTObject): + _id_attr = "name" + _repr_attr = "name" + + +class GroupSAMLGroupLinkManager(NoUpdateMixin, RESTManager): + _path = "/groups/{group_id}/saml_group_links" + _obj_cls: Type[GroupSAMLGroupLink] = GroupSAMLGroupLink + _from_parent_attrs = {"group_id": "id"} + _create_attrs = RequiredOptional(required=("saml_group_name", "access_level")) + + def get( + self, id: Union[str, int], lazy: bool = False, **kwargs: Any + ) -> GroupSAMLGroupLink: + return cast(GroupSAMLGroupLink, super().get(id=id, lazy=lazy, **kwargs)) diff --git a/tests/functional/api/test_groups.py b/tests/functional/api/test_groups.py index 51fbe3272..ec381d594 100644 --- a/tests/functional/api/test_groups.py +++ b/tests/functional/api/test_groups.py @@ -304,3 +304,11 @@ def test_group_transfer(gl, group): transferred_group = gl.groups.get(transfer_group.id) assert transferred_group.path == transferred_group.full_path + + +@pytest.mark.gitlab_premium +@pytest.mark.xfail(reason="need to setup an identity provider or it's mock") +def test_group_saml_group_links(group): + group.saml_group_links.create( + {"saml_group_name": "saml-group-1", "access_level": 10} + ) diff --git a/tests/unit/objects/test_groups.py b/tests/unit/objects/test_groups.py index 29e3c1a77..58c350827 100644 --- a/tests/unit/objects/test_groups.py +++ b/tests/unit/objects/test_groups.py @@ -8,7 +8,12 @@ import responses import gitlab -from gitlab.v4.objects import GroupDescendantGroup, GroupLDAPGroupLink, GroupSubgroup +from gitlab.v4.objects import ( + GroupDescendantGroup, + GroupLDAPGroupLink, + GroupSAMLGroupLink, + GroupSubgroup, +) from gitlab.v4.objects.projects import GroupProject, SharedProject content = {"name": "name", "id": 1, "path": "path"} @@ -20,6 +25,11 @@ "filter": "(memberOf=cn=some_group,ou=groups,ou=fake_ou,dc=sub_dc,dc=example,dc=tld)", } ] +saml_group_links_content = [{"name": "saml-group-1", "access_level": 10}] +create_saml_group_link_request_body = { + "saml_group_name": "saml-group-1", + "access_level": 10, +} projects_content = [ { "id": 9, @@ -237,6 +247,75 @@ def resp_list_ldap_group_links(no_content): yield rsps +@pytest.fixture +def resp_list_saml_group_links(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1/saml_group_links", + json=saml_group_links_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_get_saml_group_link(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1/saml_group_links/saml-group-1", + json=saml_group_links_content[0], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_saml_group_link(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/groups/1/saml_group_links", + match=[ + responses.matchers.json_params_matcher( + create_saml_group_link_request_body + ) + ], + json=saml_group_links_content[0], + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_delete_saml_group_link(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/groups/1/saml_group_links", + match=[ + responses.matchers.json_params_matcher( + create_saml_group_link_request_body + ) + ], + json=saml_group_links_content[0], + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/groups/1/saml_group_links/saml-group-1", + json=no_content, + content_type="application/json", + status=204, + ) + yield rsps + + def test_get_group(gl, resp_groups): data = gl.groups.get(1) assert isinstance(data, gitlab.v4.objects.Group) @@ -341,3 +420,36 @@ def test_update_group_push_rule( def test_delete_group_push_rule(group, resp_delete_push_rules_group): pr = group.pushrules.get() pr.delete() + + +def test_list_saml_group_links(group, resp_list_saml_group_links): + saml_group_links = group.saml_group_links.list() + assert isinstance(saml_group_links[0], GroupSAMLGroupLink) + assert saml_group_links[0].name == saml_group_links_content[0]["name"] + assert ( + saml_group_links[0].access_level == saml_group_links_content[0]["access_level"] + ) + + +def test_get_saml_group_link(group, resp_get_saml_group_link): + saml_group_link = group.saml_group_links.get("saml-group-1") + assert isinstance(saml_group_link, GroupSAMLGroupLink) + assert saml_group_link.name == saml_group_links_content[0]["name"] + assert saml_group_link.access_level == saml_group_links_content[0]["access_level"] + + +def test_create_saml_group_link(group, resp_create_saml_group_link): + saml_group_link = group.saml_group_links.create(create_saml_group_link_request_body) + assert isinstance(saml_group_link, GroupSAMLGroupLink) + assert ( + saml_group_link.name == create_saml_group_link_request_body["saml_group_name"] + ) + assert ( + saml_group_link.access_level + == create_saml_group_link_request_body["access_level"] + ) + + +def test_delete_saml_group_link(group, resp_delete_saml_group_link): + saml_group_link = group.saml_group_links.create(create_saml_group_link_request_body) + saml_group_link.delete()