Skip to content

Commit 1020ce9

Browse files
feat: add support for SAML group links (#2367)
1 parent 65abb85 commit 1020ce9

File tree

4 files changed

+164
-1
lines changed

4 files changed

+164
-1
lines changed

docs/gl_objects/groups.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,28 @@ You can use the ``ldapgroups`` manager to list available LDAP groups::
358358
# list the groups for a specific LDAP provider
359359
ldap_groups = gl.ldapgroups.list(search='foo', provider='ldapmain')
360360

361+
SAML group links
362+
================
363+
364+
Add a SAML group link to an existing GitLab group::
365+
366+
saml_link = group.saml_group_links.create({
367+
"saml_group_name": "<your_saml_group_name>",
368+
"access_level": <chosen_access_level>
369+
})
370+
371+
List a group's SAML group links::
372+
373+
group.saml_group_links.list()
374+
375+
Get a SAML group link::
376+
377+
group.saml_group_links.get("<your_saml_group_name>")
378+
379+
Remove a link::
380+
381+
saml_link.delete()
382+
361383
Groups hooks
362384
============
363385

gitlab/v4/objects/groups.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
CRUDMixin,
1313
DeleteMixin,
1414
ListMixin,
15+
NoUpdateMixin,
1516
ObjectDeleteMixin,
1617
SaveMixin,
1718
)
@@ -58,6 +59,8 @@
5859
"GroupLDAPGroupLinkManager",
5960
"GroupSubgroup",
6061
"GroupSubgroupManager",
62+
"GroupSAMLGroupLink",
63+
"GroupSAMLGroupLinkManager",
6164
]
6265

6366

@@ -98,6 +101,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
98101
subgroups: "GroupSubgroupManager"
99102
variables: GroupVariableManager
100103
wikis: GroupWikiManager
104+
saml_group_links: "GroupSAMLGroupLinkManager"
101105

102106
@cli.register_custom_action("Group", ("project_id",))
103107
@exc.on_http_error(exc.GitlabTransferProjectError)
@@ -464,3 +468,20 @@ class GroupLDAPGroupLinkManager(ListMixin, CreateMixin, DeleteMixin, RESTManager
464468
_create_attrs = RequiredOptional(
465469
required=("provider", "group_access"), exclusive=("cn", "filter")
466470
)
471+
472+
473+
class GroupSAMLGroupLink(ObjectDeleteMixin, RESTObject):
474+
_id_attr = "name"
475+
_repr_attr = "name"
476+
477+
478+
class GroupSAMLGroupLinkManager(NoUpdateMixin, RESTManager):
479+
_path = "/groups/{group_id}/saml_group_links"
480+
_obj_cls: Type[GroupSAMLGroupLink] = GroupSAMLGroupLink
481+
_from_parent_attrs = {"group_id": "id"}
482+
_create_attrs = RequiredOptional(required=("saml_group_name", "access_level"))
483+
484+
def get(
485+
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
486+
) -> GroupSAMLGroupLink:
487+
return cast(GroupSAMLGroupLink, super().get(id=id, lazy=lazy, **kwargs))

tests/functional/api/test_groups.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,11 @@ def test_group_transfer(gl, group):
304304

305305
transferred_group = gl.groups.get(transfer_group.id)
306306
assert transferred_group.path == transferred_group.full_path
307+
308+
309+
@pytest.mark.gitlab_premium
310+
@pytest.mark.xfail(reason="need to setup an identity provider or it's mock")
311+
def test_group_saml_group_links(group):
312+
group.saml_group_links.create(
313+
{"saml_group_name": "saml-group-1", "access_level": 10}
314+
)

tests/unit/objects/test_groups.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
import responses
99

1010
import gitlab
11-
from gitlab.v4.objects import GroupDescendantGroup, GroupLDAPGroupLink, GroupSubgroup
11+
from gitlab.v4.objects import (
12+
GroupDescendantGroup,
13+
GroupLDAPGroupLink,
14+
GroupSAMLGroupLink,
15+
GroupSubgroup,
16+
)
1217
from gitlab.v4.objects.projects import GroupProject, SharedProject
1318

1419
content = {"name": "name", "id": 1, "path": "path"}
@@ -20,6 +25,11 @@
2025
"filter": "(memberOf=cn=some_group,ou=groups,ou=fake_ou,dc=sub_dc,dc=example,dc=tld)",
2126
}
2227
]
28+
saml_group_links_content = [{"name": "saml-group-1", "access_level": 10}]
29+
create_saml_group_link_request_body = {
30+
"saml_group_name": "saml-group-1",
31+
"access_level": 10,
32+
}
2333
projects_content = [
2434
{
2535
"id": 9,
@@ -237,6 +247,75 @@ def resp_list_ldap_group_links(no_content):
237247
yield rsps
238248

239249

250+
@pytest.fixture
251+
def resp_list_saml_group_links():
252+
with responses.RequestsMock() as rsps:
253+
rsps.add(
254+
method=responses.GET,
255+
url="http://localhost/api/v4/groups/1/saml_group_links",
256+
json=saml_group_links_content,
257+
content_type="application/json",
258+
status=200,
259+
)
260+
yield rsps
261+
262+
263+
@pytest.fixture
264+
def resp_get_saml_group_link():
265+
with responses.RequestsMock() as rsps:
266+
rsps.add(
267+
method=responses.GET,
268+
url="http://localhost/api/v4/groups/1/saml_group_links/saml-group-1",
269+
json=saml_group_links_content[0],
270+
content_type="application/json",
271+
status=200,
272+
)
273+
yield rsps
274+
275+
276+
@pytest.fixture
277+
def resp_create_saml_group_link():
278+
with responses.RequestsMock() as rsps:
279+
rsps.add(
280+
method=responses.POST,
281+
url="http://localhost/api/v4/groups/1/saml_group_links",
282+
match=[
283+
responses.matchers.json_params_matcher(
284+
create_saml_group_link_request_body
285+
)
286+
],
287+
json=saml_group_links_content[0],
288+
content_type="application/json",
289+
status=200,
290+
)
291+
yield rsps
292+
293+
294+
@pytest.fixture
295+
def resp_delete_saml_group_link(no_content):
296+
with responses.RequestsMock() as rsps:
297+
rsps.add(
298+
method=responses.POST,
299+
url="http://localhost/api/v4/groups/1/saml_group_links",
300+
match=[
301+
responses.matchers.json_params_matcher(
302+
create_saml_group_link_request_body
303+
)
304+
],
305+
json=saml_group_links_content[0],
306+
content_type="application/json",
307+
status=200,
308+
)
309+
rsps.add(
310+
method=responses.DELETE,
311+
url="http://localhost/api/v4/groups/1/saml_group_links/saml-group-1",
312+
json=no_content,
313+
content_type="application/json",
314+
status=204,
315+
)
316+
yield rsps
317+
318+
240319
def test_get_group(gl, resp_groups):
241320
data = gl.groups.get(1)
242321
assert isinstance(data, gitlab.v4.objects.Group)
@@ -341,3 +420,36 @@ def test_update_group_push_rule(
341420
def test_delete_group_push_rule(group, resp_delete_push_rules_group):
342421
pr = group.pushrules.get()
343422
pr.delete()
423+
424+
425+
def test_list_saml_group_links(group, resp_list_saml_group_links):
426+
saml_group_links = group.saml_group_links.list()
427+
assert isinstance(saml_group_links[0], GroupSAMLGroupLink)
428+
assert saml_group_links[0].name == saml_group_links_content[0]["name"]
429+
assert (
430+
saml_group_links[0].access_level == saml_group_links_content[0]["access_level"]
431+
)
432+
433+
434+
def test_get_saml_group_link(group, resp_get_saml_group_link):
435+
saml_group_link = group.saml_group_links.get("saml-group-1")
436+
assert isinstance(saml_group_link, GroupSAMLGroupLink)
437+
assert saml_group_link.name == saml_group_links_content[0]["name"]
438+
assert saml_group_link.access_level == saml_group_links_content[0]["access_level"]
439+
440+
441+
def test_create_saml_group_link(group, resp_create_saml_group_link):
442+
saml_group_link = group.saml_group_links.create(create_saml_group_link_request_body)
443+
assert isinstance(saml_group_link, GroupSAMLGroupLink)
444+
assert (
445+
saml_group_link.name == create_saml_group_link_request_body["saml_group_name"]
446+
)
447+
assert (
448+
saml_group_link.access_level
449+
== create_saml_group_link_request_body["access_level"]
450+
)
451+
452+
453+
def test_delete_saml_group_link(group, resp_delete_saml_group_link):
454+
saml_group_link = group.saml_group_links.create(create_saml_group_link_request_body)
455+
saml_group_link.delete()

0 commit comments

Comments
 (0)