From ec818f37ef0511831543e19907c4d0a4353b4a8d Mon Sep 17 00:00:00 2001 From: Rolf Offermanns Date: Wed, 29 Jan 2025 09:46:36 +0100 Subject: [PATCH 1/6] feat(api): Add instance level service accounts (EE) Fixes #2812. --- gitlab/client.py | 2 ++ gitlab/v4/objects/service_accounts.py | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/gitlab/client.py b/gitlab/client.py index 37dd4c2e6..a313b7dbc 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -214,6 +214,8 @@ def __init__( """See :class:`~gitlab.v4.objects.TopicManager`""" self.statistics = objects.ApplicationStatisticsManager(self) """See :class:`~gitlab.v4.objects.ApplicationStatisticsManager`""" + self.service_accounts = objects.ServiceAccountManager(self) + """See :class:`~gitlab.v4.objects.ServiceAccountManager`""" def __enter__(self) -> Gitlab: return self diff --git a/gitlab/v4/objects/service_accounts.py b/gitlab/v4/objects/service_accounts.py index bf6f53d4f..a17e8e37c 100644 --- a/gitlab/v4/objects/service_accounts.py +++ b/gitlab/v4/objects/service_accounts.py @@ -2,7 +2,24 @@ from gitlab.mixins import CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin from gitlab.types import RequiredOptional -__all__ = ["GroupServiceAccount", "GroupServiceAccountManager"] +__all__ = [ + "ServiceAccount", + "ServiceAccountManager", + "GroupServiceAccount", + "GroupServiceAccountManager", +] + + +class ServiceAccount(RESTObject): + pass + + +class ServiceAccountManager(CreateMixin, ListMixin, RESTManager): + _path = "/service_accounts" + _obj_cls = ServiceAccount + _create_attrs = RequiredOptional( + optional=("name", "username", "email"), + ) class GroupServiceAccount(ObjectDeleteMixin, RESTObject): From fc185a76ca33eb19f892ef4be6f5813258c36375 Mon Sep 17 00:00:00 2001 From: Rolf Offermanns Date: Wed, 29 Jan 2025 09:54:17 +0100 Subject: [PATCH 2/6] feat(api): Add support for group service account access tokens. Fixes #2946 --- gitlab/v4/objects/service_accounts.py | 36 ++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/gitlab/v4/objects/service_accounts.py b/gitlab/v4/objects/service_accounts.py index a17e8e37c..9347bc7a9 100644 --- a/gitlab/v4/objects/service_accounts.py +++ b/gitlab/v4/objects/service_accounts.py @@ -1,12 +1,21 @@ from gitlab.base import RESTObject -from gitlab.mixins import CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin -from gitlab.types import RequiredOptional +from gitlab.mixins import ( + CreateMixin, + DeleteMixin, + ListMixin, + ObjectDeleteMixin, + ObjectRotateMixin, + RotateMixin, +) +from gitlab.types import ArrayAttribute, RequiredOptional __all__ = [ "ServiceAccount", "ServiceAccountManager", "GroupServiceAccount", "GroupServiceAccountManager", + "GroupServiceAccountAccessToken", + "GroupServiceAccountAccessTokenManager", ] @@ -14,12 +23,10 @@ class ServiceAccount(RESTObject): pass -class ServiceAccountManager(CreateMixin, ListMixin, RESTManager): +class ServiceAccountManager(CreateMixin[ServiceAccount], ListMixin[ServiceAccount]): _path = "/service_accounts" _obj_cls = ServiceAccount - _create_attrs = RequiredOptional( - optional=("name", "username", "email"), - ) + _create_attrs = RequiredOptional(optional=("name", "username", "email")) class GroupServiceAccount(ObjectDeleteMixin, RESTObject): @@ -35,3 +42,20 @@ class GroupServiceAccountManager( _obj_cls = GroupServiceAccount _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional(optional=("name", "username")) + + +class GroupServiceAccountAccessToken(ObjectRotateMixin, RESTObject): + pass + + +class GroupServiceAccountAccessTokenManager( + CreateMixin[GroupServiceAccountAccessToken], + RotateMixin[GroupServiceAccountAccessToken], +): + _path = "/groups/{group_id}/service_accounts/{user_id}/personal_access_tokens" + _obj_cls = GroupServiceAccountAccessToken + _from_parent_attrs = {"group_id": "id", "user_id": "user_id"} + _create_attrs = RequiredOptional( + required=("name", "scopes"), optional=("expires_at",) + ) + _types = {"scopes": ArrayAttribute} From 8d817a2e2b6e37ad7416eeda2cc83a6134989442 Mon Sep 17 00:00:00 2001 From: Rolf Offermanns Date: Sat, 8 Feb 2025 21:56:55 +0100 Subject: [PATCH 3/6] feat(unit): Add unit test for group_service_account delete --- tests/unit/objects/test_groups.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/objects/test_groups.py b/tests/unit/objects/test_groups.py index 7d1510c8d..c93cf6ba1 100644 --- a/tests/unit/objects/test_groups.py +++ b/tests/unit/objects/test_groups.py @@ -84,6 +84,7 @@ } service_account_content = { + "id": 42, "name": "gitlab-service-account", "username": "gitlab-service-account", } @@ -343,6 +344,24 @@ def resp_create_group_service_account(): yield rsps +@pytest.fixture +def resp_delete_group_service_account(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/groups/1/service_accounts", + json=service_account_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/groups/1/service_accounts/42", + status=204, + ) + yield rsps + + def test_get_group(gl, resp_groups): data = gl.groups.get(1) assert isinstance(data, gitlab.v4.objects.Group) @@ -489,3 +508,10 @@ def test_create_group_service_account(group, resp_create_group_service_account): ) assert service_account.name == "gitlab-service-account" assert service_account.username == "gitlab-service-account" + + +def test_delete_group_service_account(group, resp_delete_group_service_account): + service_account = group.service_accounts.create( + {"name": "gitlab-service-account", "username": "gitlab-service-account"} + ) + service_account.delete() From 44058901145b81dfc056ad485dafacb7f74b0217 Mon Sep 17 00:00:00 2001 From: Rolf Offermanns Date: Sat, 8 Feb 2025 21:57:18 +0100 Subject: [PATCH 4/6] feat(unit): Add unit tests for service accounts --- tests/unit/objects/test_service_accounts.py | 76 +++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/unit/objects/test_service_accounts.py diff --git a/tests/unit/objects/test_service_accounts.py b/tests/unit/objects/test_service_accounts.py new file mode 100644 index 000000000..664539f75 --- /dev/null +++ b/tests/unit/objects/test_service_accounts.py @@ -0,0 +1,76 @@ +""" +GitLab API: https://docs.gitlab.com/ee/api/user_service_accounts.html +""" + +import pytest +import responses + +create_service_account_defaults_content = { + "id": 57, + "username": "service_account_6018816a18e515214e0c34c2b33523fc", + "name": "Service account user", + "email": "service_account_6018816a18e515214e0c34c2b33523fc@noreply.gitlab.example.com", +} + + +create_service_account_content = { + "id": 42, + "username": "my_service_account", + "name": "My Service account user", + "email": "servicebot@example.com", +} + + +@pytest.fixture +def resp_create_service_account_defaults(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/service_accounts", + json=create_service_account_defaults_content, + content_type="application/json", + status=200, + ) + + yield rsps + + +@pytest.fixture +def resp_create_service_account(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/service_accounts", + json=create_service_account_content, + content_type="application/json", + status=200, + ) + + yield rsps + + +def test_create_service_account_defaults(gl, resp_create_service_account_defaults): + service_account = gl.service_accounts.create() + assert service_account.id == 57 + assert ( + service_account.username == "service_account_6018816a18e515214e0c34c2b33523fc" + ) + assert service_account.name == "Service account user" + assert ( + service_account.email + == "service_account_6018816a18e515214e0c34c2b33523fc@noreply.gitlab.example.com" + ) + + +def test_create_service_account(gl, resp_create_service_account): + service_account = gl.service_accounts.create( + { + "name": "My Service account user", + "username": "my_service_account", + "email": "servicebot@example.com", + } + ) + assert service_account.id == 42 + assert service_account.username == "my_service_account" + assert service_account.name == "My Service account user" + assert service_account.email == "servicebot@example.com" From 454b3ea8a23713f946216145522a2602bb2a7b0b Mon Sep 17 00:00:00 2001 From: Rolf Offermanns Date: Mon, 10 Feb 2025 08:58:09 +0100 Subject: [PATCH 5/6] feat(api): Add API integration for group service account access tokens --- gitlab/v4/objects/service_accounts.py | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/gitlab/v4/objects/service_accounts.py b/gitlab/v4/objects/service_accounts.py index 9347bc7a9..493c91d8d 100644 --- a/gitlab/v4/objects/service_accounts.py +++ b/gitlab/v4/objects/service_accounts.py @@ -19,6 +19,23 @@ ] +class GroupServiceAccountAccessToken(ObjectRotateMixin, RESTObject): + pass + + +class GroupServiceAccountAccessTokenManager( + CreateMixin[GroupServiceAccountAccessToken], + RotateMixin[GroupServiceAccountAccessToken], +): + _path = "/groups/{group_id}/service_accounts/{user_id}/personal_access_tokens" + _obj_cls = GroupServiceAccountAccessToken + _from_parent_attrs = {"group_id": "group_id", "user_id": "id"} + _create_attrs = RequiredOptional( + required=("name", "scopes"), optional=("expires_at",) + ) + _types = {"scopes": ArrayAttribute} + + class ServiceAccount(RESTObject): pass @@ -30,7 +47,7 @@ class ServiceAccountManager(CreateMixin[ServiceAccount], ListMixin[ServiceAccoun class GroupServiceAccount(ObjectDeleteMixin, RESTObject): - pass + access_tokens: GroupServiceAccountAccessTokenManager class GroupServiceAccountManager( @@ -42,20 +59,3 @@ class GroupServiceAccountManager( _obj_cls = GroupServiceAccount _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional(optional=("name", "username")) - - -class GroupServiceAccountAccessToken(ObjectRotateMixin, RESTObject): - pass - - -class GroupServiceAccountAccessTokenManager( - CreateMixin[GroupServiceAccountAccessToken], - RotateMixin[GroupServiceAccountAccessToken], -): - _path = "/groups/{group_id}/service_accounts/{user_id}/personal_access_tokens" - _obj_cls = GroupServiceAccountAccessToken - _from_parent_attrs = {"group_id": "id", "user_id": "user_id"} - _create_attrs = RequiredOptional( - required=("name", "scopes"), optional=("expires_at",) - ) - _types = {"scopes": ArrayAttribute} From cd9cfa822f881a61335455400a8bf6183eae2a52 Mon Sep 17 00:00:00 2001 From: Rolf Offermanns Date: Mon, 10 Feb 2025 08:58:30 +0100 Subject: [PATCH 6/6] feat(unit): Add unit test for group service account access tokens --- tests/unit/objects/test_groups.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/unit/objects/test_groups.py b/tests/unit/objects/test_groups.py index c93cf6ba1..1904cc98e 100644 --- a/tests/unit/objects/test_groups.py +++ b/tests/unit/objects/test_groups.py @@ -89,6 +89,18 @@ "username": "gitlab-service-account", } +service_account_access_token_content = { + "id": 1, + "name": "service_account_access_token", + "revoked": False, + "scopes": ["api"], + "user_id": 42, + "last_used": None, + "active": True, + "expires_at": None, + "token": "abcdefg12345", +} + @pytest.fixture def resp_groups(): @@ -362,6 +374,26 @@ def resp_delete_group_service_account(): yield rsps +@pytest.fixture +def resp_create_group_service_account_access_token(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/groups/1/service_accounts", + json=service_account_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/groups/1/service_accounts/42/personal_access_tokens", + json=service_account_access_token_content, + content_type="application/json", + status=200, + ) + yield rsps + + def test_get_group(gl, resp_groups): data = gl.groups.get(1) assert isinstance(data, gitlab.v4.objects.Group) @@ -515,3 +547,17 @@ def test_delete_group_service_account(group, resp_delete_group_service_account): {"name": "gitlab-service-account", "username": "gitlab-service-account"} ) service_account.delete() + + +def test_create_group_service_account_access_token( + group, resp_create_group_service_account_access_token +): + service_account = group.service_accounts.create( + {"name": "gitlab-service-account", "username": "gitlab-service-account"} + ) + access_token = service_account.access_tokens.create( + {"name": "service_account_access_token", "scopes": ["api"]} + ) + assert service_account.id == 42 + assert access_token.name == "service_account_access_token" + assert access_token.scopes == ["api"]