Skip to content

Commit 358da71

Browse files
committed
feat(api): add full service account support at instance, group, and project level
1 parent 1282712 commit 358da71

File tree

6 files changed

+890
-5
lines changed

6 files changed

+890
-5
lines changed

docs/api-objects.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ API examples
6262
gl_objects/resource_groups
6363
gl_objects/search
6464
gl_objects/secure_files
65+
gl_objects/service_accounts
6566
gl_objects/settings
6667
gl_objects/snippets
6768
gl_objects/statistics
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
################
2+
Service Accounts
3+
################
4+
5+
References
6+
----------
7+
8+
* v4 API:
9+
10+
+ :class:`gitlab.v4.objects.ServiceAccount`
11+
+ :class:`gitlab.v4.objects.ServiceAccountManager`
12+
+ :class:`gitlab.v4.objects.GroupServiceAccount`
13+
+ :class:`gitlab.v4.objects.GroupServiceAccountManager`
14+
+ :class:`gitlab.v4.objects.GroupServiceAccountAccessToken`
15+
+ :class:`gitlab.v4.objects.GroupServiceAccountAccessTokenManager`
16+
+ :class:`gitlab.v4.objects.ProjectServiceAccount`
17+
+ :class:`gitlab.v4.objects.ProjectServiceAccountManager`
18+
+ :class:`gitlab.v4.objects.ProjectServiceAccountAccessToken`
19+
+ :class:`gitlab.v4.objects.ProjectServiceAccountAccessTokenManager`
20+
21+
* GitLab API: https://docs.gitlab.com/api/service_accounts/
22+
23+
Instance service accounts
24+
-------------------------
25+
26+
List instance service accounts::
27+
28+
accounts = gl.service_accounts.list()
29+
30+
Create an instance service account::
31+
32+
sa = gl.service_accounts.create({})
33+
# with optional attributes
34+
sa = gl.service_accounts.create({"name": "my-bot", "username": "my-bot", "email": "my-bot@example.com"})
35+
36+
Update an instance service account::
37+
38+
gl.service_accounts.update(sa.id, {"name": "renamed-bot"})
39+
# or via the object
40+
sa.name = "renamed-bot"
41+
sa.save()
42+
43+
Group service accounts
44+
----------------------
45+
46+
List group service accounts::
47+
48+
accounts = group.service_accounts.list()
49+
50+
Create a group service account::
51+
52+
sa = group.service_accounts.create({})
53+
# with optional attributes
54+
sa = group.service_accounts.create({"name": "ci-bot", "username": "ci-bot"})
55+
56+
Update a group service account::
57+
58+
group.service_accounts.update(sa.id, {"name": "renamed-bot"})
59+
# or via the object
60+
sa.name = "renamed-bot"
61+
sa.save()
62+
63+
Delete a group service account::
64+
65+
group.service_accounts.delete(sa.id)
66+
# or via the object
67+
sa.delete()
68+
69+
Group service account personal access tokens
70+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
71+
72+
List tokens for a group service account::
73+
74+
tokens = sa.access_tokens.list()
75+
76+
Create a token for a group service account::
77+
78+
token = sa.access_tokens.create({
79+
"name": "ci-token",
80+
"scopes": ["api"],
81+
"expires_at": "2026-01-01",
82+
})
83+
print(token.token)
84+
85+
Rotate a token::
86+
87+
token.rotate()
88+
print(token.token)
89+
# or directly using a token ID
90+
new_token = sa.access_tokens.rotate(token.id)
91+
print(new_token["token"])
92+
93+
Revoke a token::
94+
95+
sa.access_tokens.delete(token.id)
96+
# or via the object
97+
token.delete()
98+
99+
Project service accounts
100+
------------------------
101+
102+
List project service accounts::
103+
104+
accounts = project.service_accounts.list()
105+
106+
Create a project service account::
107+
108+
sa = project.service_accounts.create({})
109+
# with optional attributes
110+
sa = project.service_accounts.create({"name": "ci-bot", "username": "ci-bot"})
111+
112+
Update a project service account::
113+
114+
project.service_accounts.update(sa.id, {"name": "renamed-bot"})
115+
# or via the object
116+
sa.name = "renamed-bot"
117+
sa.save()
118+
119+
Delete a project service account::
120+
121+
project.service_accounts.delete(sa.id)
122+
# or via the object
123+
sa.delete()
124+
125+
Project service account personal access tokens
126+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
127+
128+
List tokens for a project service account::
129+
130+
tokens = sa.access_tokens.list()
131+
132+
Create a token for a project service account::
133+
134+
token = sa.access_tokens.create({
135+
"name": "ci-token",
136+
"scopes": ["read_repository"],
137+
"expires_at": "2026-01-01",
138+
})
139+
print(token.token)
140+
141+
Rotate a token::
142+
143+
token.rotate()
144+
print(token.token)
145+
# or directly using a token ID
146+
new_token = sa.access_tokens.rotate(token.id)
147+
print(new_token["token"])
148+
149+
Revoke a token::
150+
151+
sa.access_tokens.delete(token.id)
152+
# or via the object
153+
token.delete()

gitlab/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ def __init__(
211211
"""See :class:`~gitlab.v4.objects.PersonalAccessTokenManager`"""
212212
self.topics = objects.TopicManager(self)
213213
"""See :class:`~gitlab.v4.objects.TopicManager`"""
214+
self.service_accounts = objects.ServiceAccountManager(self)
215+
"""See :class:`~gitlab.v4.objects.ServiceAccountManager`"""
214216
self.statistics = objects.ApplicationStatisticsManager(self)
215217
"""See :class:`~gitlab.v4.objects.ApplicationStatisticsManager`"""
216218

gitlab/v4/objects/projects.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
from .resource_groups import ProjectResourceGroupManager
9090
from .runners import ProjectRunnerManager # noqa: F401
9191
from .secure_files import ProjectSecureFileManager # noqa: F401
92+
from .service_accounts import ProjectServiceAccountManager # noqa: F401
9293
from .snippets import ProjectSnippetManager # noqa: F401
9394
from .statistics import ( # noqa: F401
9495
ProjectAdditionalStatisticsManager,
@@ -247,6 +248,7 @@ class Project(
247248
repositories: ProjectRegistryRepositoryManager
248249
runners: ProjectRunnerManager
249250
secure_files: ProjectSecureFileManager
251+
service_accounts: ProjectServiceAccountManager
250252
services: ProjectServiceManager
251253
snippets: ProjectSnippetManager
252254
external_status_checks: ProjectExternalStatusCheckManager
Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,155 @@
1+
"""
2+
GitLab API: https://docs.gitlab.com/api/service_accounts/
3+
"""
4+
15
from gitlab.base import RESTObject
2-
from gitlab.mixins import CreateMixin, DeleteMixin, ListMixin, ObjectDeleteMixin
3-
from gitlab.types import RequiredOptional
6+
from gitlab.mixins import (
7+
CreateMixin,
8+
DeleteMixin,
9+
ListMixin,
10+
ObjectDeleteMixin,
11+
ObjectRotateMixin,
12+
RotateMixin,
13+
SaveMixin,
14+
UpdateMethod,
15+
UpdateMixin,
16+
)
17+
from gitlab.types import ArrayAttribute, RequiredOptional
18+
19+
__all__ = [
20+
"ServiceAccount",
21+
"ServiceAccountManager",
22+
"GroupServiceAccount",
23+
"GroupServiceAccountManager",
24+
"GroupServiceAccountAccessToken",
25+
"GroupServiceAccountAccessTokenManager",
26+
"ProjectServiceAccount",
27+
"ProjectServiceAccountManager",
28+
"ProjectServiceAccountAccessToken",
29+
"ProjectServiceAccountAccessTokenManager",
30+
]
31+
32+
_SA_ACCOUNT_ATTRS = RequiredOptional(optional=("name", "username", "email"))
33+
34+
_SA_TOKEN_CREATE_ATTRS = RequiredOptional(
35+
required=("name", "scopes"), optional=("description", "expires_at")
36+
)
37+
38+
_SA_TOKEN_LIST_FILTERS = (
39+
"created_after",
40+
"created_before",
41+
"expires_after",
42+
"expires_before",
43+
"last_used_after",
44+
"last_used_before",
45+
"revoked",
46+
"search",
47+
"sort",
48+
"state",
49+
)
450

5-
__all__ = ["GroupServiceAccount", "GroupServiceAccountManager"]
651

52+
# ---------------------------------------------------------------------------
53+
# Instance-level service accounts
54+
# ---------------------------------------------------------------------------
755

8-
class GroupServiceAccount(ObjectDeleteMixin, RESTObject):
56+
57+
class ServiceAccount(SaveMixin, RESTObject):
958
pass
1059

1160

61+
class ServiceAccountManager(
62+
CreateMixin[ServiceAccount], ListMixin[ServiceAccount], UpdateMixin[ServiceAccount]
63+
):
64+
_path = "/service_accounts"
65+
_obj_cls = ServiceAccount
66+
_create_attrs = _SA_ACCOUNT_ATTRS
67+
_update_attrs = _SA_ACCOUNT_ATTRS
68+
_update_method = UpdateMethod.PATCH
69+
_list_filters = ("order_by", "sort")
70+
71+
72+
# ---------------------------------------------------------------------------
73+
# Group-level service accounts
74+
# ---------------------------------------------------------------------------
75+
76+
77+
class GroupServiceAccountAccessToken(ObjectDeleteMixin, ObjectRotateMixin, RESTObject):
78+
pass
79+
80+
81+
class GroupServiceAccountAccessTokenManager(
82+
CreateMixin[GroupServiceAccountAccessToken],
83+
DeleteMixin[GroupServiceAccountAccessToken],
84+
ListMixin[GroupServiceAccountAccessToken],
85+
RotateMixin[GroupServiceAccountAccessToken],
86+
):
87+
_path = "/groups/{group_id}/service_accounts/{user_id}/personal_access_tokens"
88+
_obj_cls = GroupServiceAccountAccessToken
89+
_from_parent_attrs = {"group_id": "group_id", "user_id": "id"}
90+
_create_attrs = _SA_TOKEN_CREATE_ATTRS
91+
_types = {"scopes": ArrayAttribute}
92+
_list_filters = _SA_TOKEN_LIST_FILTERS
93+
94+
95+
class GroupServiceAccount(SaveMixin, ObjectDeleteMixin, RESTObject):
96+
access_tokens: GroupServiceAccountAccessTokenManager
97+
98+
1299
class GroupServiceAccountManager(
13100
CreateMixin[GroupServiceAccount],
14101
DeleteMixin[GroupServiceAccount],
15102
ListMixin[GroupServiceAccount],
103+
UpdateMixin[GroupServiceAccount],
16104
):
17105
_path = "/groups/{group_id}/service_accounts"
18106
_obj_cls = GroupServiceAccount
19107
_from_parent_attrs = {"group_id": "id"}
20-
_create_attrs = RequiredOptional(optional=("name", "username"))
108+
_create_attrs = _SA_ACCOUNT_ATTRS
109+
_update_attrs = _SA_ACCOUNT_ATTRS
110+
_update_method = UpdateMethod.PATCH
111+
_list_filters = ("order_by", "sort")
112+
113+
114+
# ---------------------------------------------------------------------------
115+
# Project-level service accounts
116+
# ---------------------------------------------------------------------------
117+
118+
119+
class ProjectServiceAccountAccessToken(
120+
ObjectDeleteMixin, ObjectRotateMixin, RESTObject
121+
):
122+
pass
123+
124+
125+
class ProjectServiceAccountAccessTokenManager(
126+
CreateMixin[ProjectServiceAccountAccessToken],
127+
DeleteMixin[ProjectServiceAccountAccessToken],
128+
ListMixin[ProjectServiceAccountAccessToken],
129+
RotateMixin[ProjectServiceAccountAccessToken],
130+
):
131+
_path = "/projects/{project_id}/service_accounts/{user_id}/personal_access_tokens"
132+
_obj_cls = ProjectServiceAccountAccessToken
133+
_from_parent_attrs = {"project_id": "project_id", "user_id": "id"}
134+
_create_attrs = _SA_TOKEN_CREATE_ATTRS
135+
_types = {"scopes": ArrayAttribute}
136+
_list_filters = _SA_TOKEN_LIST_FILTERS
137+
138+
139+
class ProjectServiceAccount(SaveMixin, ObjectDeleteMixin, RESTObject):
140+
access_tokens: ProjectServiceAccountAccessTokenManager
141+
142+
143+
class ProjectServiceAccountManager(
144+
CreateMixin[ProjectServiceAccount],
145+
DeleteMixin[ProjectServiceAccount],
146+
ListMixin[ProjectServiceAccount],
147+
UpdateMixin[ProjectServiceAccount],
148+
):
149+
_path = "/projects/{project_id}/service_accounts"
150+
_obj_cls = ProjectServiceAccount
151+
_from_parent_attrs = {"project_id": "id"}
152+
_create_attrs = _SA_ACCOUNT_ATTRS
153+
_update_attrs = _SA_ACCOUNT_ATTRS
154+
_update_method = UpdateMethod.PATCH
155+
_list_filters = ("order_by", "sort")

0 commit comments

Comments
 (0)