Skip to content

Service account improvements #3109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gitlab/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 45 additions & 4 deletions gitlab/v4/objects/service_accounts.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
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__ = ["GroupServiceAccount", "GroupServiceAccountManager"]
__all__ = [
"ServiceAccount",
"ServiceAccountManager",
"GroupServiceAccount",
"GroupServiceAccountManager",
"GroupServiceAccountAccessToken",
"GroupServiceAccountAccessTokenManager",
]


class GroupServiceAccount(ObjectDeleteMixin, RESTObject):
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


class ServiceAccountManager(CreateMixin[ServiceAccount], ListMixin[ServiceAccount]):
Comment on lines +39 to +43
Copy link
Preview

Copilot AI Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ServiceAccount currently has no delete support; add ObjectDeleteMixin to the class and DeleteMixin to ServiceAccountManager so that instance-level service accounts can be deleted.

Suggested change
class ServiceAccount(RESTObject):
pass
class ServiceAccountManager(CreateMixin[ServiceAccount], ListMixin[ServiceAccount]):
class ServiceAccount(ObjectDeleteMixin, RESTObject):
pass
class ServiceAccountManager(CreateMixin[ServiceAccount], DeleteMixin[ServiceAccount], ListMixin[ServiceAccount]):

Copilot uses AI. Check for mistakes.

_path = "/service_accounts"
_obj_cls = ServiceAccount
_create_attrs = RequiredOptional(optional=("name", "username", "email"))


class GroupServiceAccount(ObjectDeleteMixin, RESTObject):
access_tokens: GroupServiceAccountAccessTokenManager


class GroupServiceAccountManager(
CreateMixin[GroupServiceAccount],
DeleteMixin[GroupServiceAccount],
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/objects/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,23 @@
}

service_account_content = {
"id": 42,
"name": "gitlab-service-account",
"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():
Expand Down Expand Up @@ -343,6 +356,44 @@ 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


@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)
Expand Down Expand Up @@ -489,3 +540,24 @@ 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()


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"]
76 changes: 76 additions & 0 deletions tests/unit/objects/test_service_accounts.py
Original file line number Diff line number Diff line change
@@ -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"