feat(api): add support for token self-rotation · python-gitlab/python-gitlab@da40e09

7 files changed

lines changed

Original file line numberDiff line numberDiff line change

@@ -46,3 +46,9 @@ Rotate a group access token and retrieve its new value::

4646

# or directly using a token ID

4747

new_token = group.access_tokens.rotate(42)

4848

print(new_token.token)

49+
50+

Self-Rotate the group access token you are using to authenticate the request and retrieve its new value::

51+
52+

token = group.access_tokens.get(42, lazy=True)

53+

token.rotate(self_rotate=True)

54+

print(token.token)

Original file line numberDiff line numberDiff line change

@@ -61,6 +61,12 @@ Rotate a personal access token and retrieve its new value::

6161

new_token_dict = gl.personal_access_tokens.rotate(42)

6262

print(new_token_dict)

6363
64+

Self-Rotate the personal access token you are using to authenticate the request and retrieve its new value::

65+
66+

token = gl.personal_access_tokens.get(42, lazy=True)

67+

token.rotate(self_rotate=True)

68+

print(token.token)

69+
6470

Create a personal access token for a user (admin only)::

6571
6672

user = gl.users.get(25, lazy=True)

Original file line numberDiff line numberDiff line change

@@ -46,3 +46,9 @@ Rotate a project access token and retrieve its new value::

4646

# or directly using a token ID

4747

new_token = project.access_tokens.rotate(42)

4848

print(new_token.token)

49+
50+

Self-Rotate the project access token you are using to authenticate the request and retrieve its new value::

51+
52+

token = project.access_tokens.get(42, lazy=True)

53+

token.rotate(self_rotate=True)

54+

print(new_token.token)

Original file line numberDiff line numberDiff line change

@@ -660,10 +660,11 @@ class ObjectRotateMixin(_RestObjectBase):

660660

optional=("expires_at",),

661661

)

662662

@exc.on_http_error(exc.GitlabRotateError)

663-

def rotate(self, **kwargs: Any) -> dict[str, Any]:

663+

def rotate(self, *, self_rotate: bool = False, **kwargs: Any) -> dict[str, Any]:

664664

"""Rotate the current access token object.

665665
666666

Args:

667+

self_rotate: If True, the current access token object will be rotated.

667668

**kwargs: Extra options to send to the server (e.g. sudo)

668669
669670

Raises:

@@ -673,7 +674,8 @@ def rotate(self, **kwargs: Any) -> dict[str, Any]:

673674

if TYPE_CHECKING:

674675

assert isinstance(self.manager, RotateMixin)

675676

assert self.encoded_id is not None

676-

server_data = self.manager.rotate(self.encoded_id, **kwargs)

677+

token_id = "self" if self_rotate else self.encoded_id

678+

server_data = self.manager.rotate(token_id, **kwargs)

677679

self._update_attrs(server_data)

678680

return server_data

679681
Original file line numberDiff line numberDiff line change

@@ -91,6 +91,19 @@ def resp_rotate_group_access_token(token_content):

9191

yield rsps

9292
9393
94+

@pytest.fixture

95+

def resp_self_rotate_group_access_token(token_content):

96+

with responses.RequestsMock() as rsps:

97+

rsps.add(

98+

method=responses.POST,

99+

url="http://localhost/api/v4/groups/1/access_tokens/self/rotate",

100+

json=token_content,

101+

content_type="application/json",

102+

status=200,

103+

)

104+

yield rsps

105+
106+
94107

def test_list_group_access_tokens(gl, resp_list_group_access_token):

95108

access_tokens = gl.groups.get(1, lazy=True).access_tokens.list()

96109

assert len(access_tokens) == 1

@@ -127,3 +140,15 @@ def test_rotate_group_access_token(group, resp_rotate_group_access_token):

127140

access_token.rotate()

128141

assert isinstance(access_token, GroupAccessToken)

129142

assert access_token.token == "s3cr3t"

143+
144+
145+

def test_self_rotate_group_access_token(group, resp_self_rotate_group_access_token):

146+

access_token = group.access_tokens.get(1, lazy=True)

147+

access_token.rotate(self_rotate=True)

148+

assert isinstance(access_token, GroupAccessToken)

149+

assert access_token.token == "s3cr3t"

150+
151+

# Verify that the url contains "self"

152+

rotation_calls = resp_self_rotate_group_access_token.calls

153+

assert len(rotation_calls) == 1

154+

assert "self/rotate" in rotation_calls[0].request.url

Original file line numberDiff line numberDiff line change

@@ -102,6 +102,19 @@ def resp_rotate_personal_access_token(token_content):

102102

yield rsps

103103
104104
105+

@pytest.fixture

106+

def resp_self_rotate_personal_access_token(token_content):

107+

with responses.RequestsMock() as rsps:

108+

rsps.add(

109+

method=responses.POST,

110+

url="http://localhost/api/v4/personal_access_tokens/self/rotate",

111+

json=token_content,

112+

content_type="application/json",

113+

status=200,

114+

)

115+

yield rsps

116+
117+
105118

def test_create_personal_access_token(gl, resp_create_user_personal_access_token):

106119

user = gl.users.get(1, lazy=True)

107120

access_token = user.personal_access_tokens.create(

@@ -148,8 +161,20 @@ def test_revoke_personal_access_token_by_id(gl, resp_delete_personal_access_toke

148161

gl.personal_access_tokens.delete(token_id)

149162
150163
151-

def test_rotate_project_access_token(gl, resp_rotate_personal_access_token):

164+

def test_rotate_personal_access_token(gl, resp_rotate_personal_access_token):

152165

access_token = gl.personal_access_tokens.get(1, lazy=True)

153166

access_token.rotate()

154167

assert isinstance(access_token, PersonalAccessToken)

155168

assert access_token.token == "s3cr3t"

169+
170+
171+

def test_self_rotate_personal_access_token(gl, resp_self_rotate_personal_access_token):

172+

access_token = gl.personal_access_tokens.get(1, lazy=True)

173+

access_token.rotate(self_rotate=True)

174+

assert isinstance(access_token, PersonalAccessToken)

175+

assert access_token.token == "s3cr3t"

176+
177+

# Verify that the url contains "self"

178+

rotation_calls = resp_self_rotate_personal_access_token.calls

179+

assert len(rotation_calls) == 1

180+

assert "self/rotate" in rotation_calls[0].request.url

Original file line numberDiff line numberDiff line change

@@ -91,6 +91,19 @@ def resp_rotate_project_access_token(token_content):

9191

yield rsps

9292
9393
94+

@pytest.fixture

95+

def resp_self_rotate_project_access_token(token_content):

96+

with responses.RequestsMock() as rsps:

97+

rsps.add(

98+

method=responses.POST,

99+

url="http://localhost/api/v4/projects/1/access_tokens/self/rotate",

100+

json=token_content,

101+

content_type="application/json",

102+

status=200,

103+

)

104+

yield rsps

105+
106+
94107

def test_list_project_access_tokens(gl, resp_list_project_access_token):

95108

access_tokens = gl.projects.get(1, lazy=True).access_tokens.list()

96109

assert len(access_tokens) == 1

@@ -127,3 +140,17 @@ def test_rotate_project_access_token(project, resp_rotate_project_access_token):

127140

access_token.rotate()

128141

assert isinstance(access_token, ProjectAccessToken)

129142

assert access_token.token == "s3cr3t"

143+
144+
145+

def test_self_rotate_project_access_token(

146+

project, resp_self_rotate_project_access_token

147+

):

148+

access_token = project.access_tokens.get(1, lazy=True)

149+

access_token.rotate(self_rotate=True)

150+

assert isinstance(access_token, ProjectAccessToken)

151+

assert access_token.token == "s3cr3t"

152+
153+

# Verify that the url contains "self"

154+

rotation_calls = resp_self_rotate_project_access_token.calls

155+

assert len(rotation_calls) == 1

156+

assert "self/rotate" in rotation_calls[0].request.url