From 48b696720ed268f40cc9ff04ed853a36758f5aaf Mon Sep 17 00:00:00 2001 From: Victor Rodrigues Date: Tue, 28 May 2024 22:04:23 +1000 Subject: [PATCH] Implemented ability to create service accounts which was introduced in GitLab v16.1. This type of user does not take up a license seat and can only be created through the API. Also implemented the ability to create, rotate and revoke personal access tokens as these can also not be created through the GitLab web UI. --- lib/gitlab/client/users.rb | 68 ++++++++++++++ spec/fixtures/personal_access_create.json | 13 +++ spec/fixtures/personal_access_get_all.json | 28 ++++++ spec/fixtures/personal_access_rotate.json | 13 +++ spec/fixtures/service_account.json | 1 + spec/gitlab/client/users_spec.rb | 100 +++++++++++++++++++++ 6 files changed, 223 insertions(+) create mode 100644 spec/fixtures/personal_access_create.json create mode 100644 spec/fixtures/personal_access_get_all.json create mode 100644 spec/fixtures/personal_access_rotate.json create mode 100644 spec/fixtures/service_account.json diff --git a/lib/gitlab/client/users.rb b/lib/gitlab/client/users.rb index 31dd23886..1e681c6db 100644 --- a/lib/gitlab/client/users.rb +++ b/lib/gitlab/client/users.rb @@ -58,6 +58,22 @@ def create_user(*args) post('/users', body: body) end + # Creates a service account. + # Requires authentication from an admin account. + # + # @example + # Gitlab.create_service_account('service_account_6018816a18e515214e0c34c2b33523fc', 'Service account user') + # + # @param [String] name (required) The email of the service account. + # @param [String] username (required) The username of the service account. + # @return [Gitlab::ObjectifiedHash] Information about created service account. + def create_service_account(*args) + raise ArgumentError, 'Missing required parameters' unless args[1] + + body = { name: args[0], username: args[1] } + post('/service_accounts', body: body) + end + # Updates a user. # # @example @@ -404,6 +420,47 @@ def create_user_impersonation_token(user_id, name, scopes, expires_at = nil) post("/users/#{user_id}/impersonation_tokens", body: body) end + # Get all personal access tokens for a user + # + # @example + # Gitlab.user_personal_access_tokens(1) + # + # @param [Integer] user_id The ID of the user. + # @return [Array] + def user_personal_access_tokens(user_id) + get("/personal_access_tokens?user_id=#{user_id}") + end + + # Create personal access token + # + # @example + # Gitlab.create_personal_access_token(2, "token", ["api", "read_user"]) + # Gitlab.create_personal_access_token(2, "token", ["api", "read_user"], "1970-01-01") + # + # @param [Integer] user_id The ID of the user. + # @param [String] name Name of the personal access token. + # @param [Array] scopes Array of scopes for the impersonation token + # @param [String] expires_at Date for impersonation token expiration in ISO format. + # @return [Gitlab::ObjectifiedHash] + def create_personal_access_token(user_id, name, scopes, expires_at = nil) + body = { name: name, scopes: scopes } + body[:expires_at] = expires_at if expires_at + post("/users/#{user_id}/personal_access_tokens", body: body) + end + + # Rotate a personal access token + # + # @example + # Gitlab.rotate_personal_access_token(1) + # + # @param [Integer] personal_access_token_id ID of the personal access token. + # @return [Gitlab::ObjectifiedHash] + def rotate_personal_access_token(personal_access_token_id, expires_at = nil) + body = {} + body[:expires_at] = expires_at if expires_at + post("/personal_access_tokens/#{personal_access_token_id}/rotate", body: body) + end + # Revoke an impersonation token # # @example @@ -416,6 +473,17 @@ def revoke_user_impersonation_token(user_id, impersonation_token_id) delete("/users/#{user_id}/impersonation_tokens/#{impersonation_token_id}") end + # Revoke a personal access token + # + # @example + # Gitlab.revoke_personal_access_token(1) + # + # @param [Integer] personal_access_token_id ID of the personal access token. + # @return [Gitlab::ObjectifiedHash] + def revoke_personal_access_token(personal_access_token_id) + delete("/personal_access_tokens/#{personal_access_token_id}") + end + # Disables two factor authentication (2FA) for the specified user. # # @example diff --git a/spec/fixtures/personal_access_create.json b/spec/fixtures/personal_access_create.json new file mode 100644 index 000000000..a8937c2c4 --- /dev/null +++ b/spec/fixtures/personal_access_create.json @@ -0,0 +1,13 @@ +{ + "id": 2, + "name": "service_account_token", + "revoked": false, + "created_at": "2024-05-24T06:46:43.160Z", + "scopes" : [ + "api" + ], + "user_id": 2, + "active": true, + "expires_at": "2025-05-24", + "token": "glpat-3Hm21_tY3sn4Wafwq39p" +} diff --git a/spec/fixtures/personal_access_get_all.json b/spec/fixtures/personal_access_get_all.json new file mode 100644 index 000000000..8f5fef5a5 --- /dev/null +++ b/spec/fixtures/personal_access_get_all.json @@ -0,0 +1,28 @@ +[ + { + "id": 2, + "name": "service_account_token_1", + "revoked": false, + "created_at": "2024-05-24T06:46:43.160Z", + "scopes" : [ + "api" + ], + "user_id": 2, + "last_used_at": "2024-05-24T16:00:00.000Z", + "active": true, + "expires_at": "2025-05-24" + }, + { + "id": 3, + "name": "service_account_token_2", + "revoked": false, + "created_at": "2024-05-25T06:46:43.160Z", + "scopes" : [ + "api" + ], + "user_id": 2, + "last_used_at": "2024-05-24T16:00:00.100Z", + "active": true, + "expires_at": "2025-05-24" + } +] \ No newline at end of file diff --git a/spec/fixtures/personal_access_rotate.json b/spec/fixtures/personal_access_rotate.json new file mode 100644 index 000000000..8320c4265 --- /dev/null +++ b/spec/fixtures/personal_access_rotate.json @@ -0,0 +1,13 @@ +{ + "id": 4, + "name": "service_account_token", + "revoked": false, + "created_at": "2024-05-24T06:46:44.000Z", + "scopes" : [ + "api" + ], + "user_id": 2, + "active": true, + "expires_at": "2025-05-24", + "token": "glpat--xSo18jU2MPtQ576ZYnp" +} \ No newline at end of file diff --git a/spec/fixtures/service_account.json b/spec/fixtures/service_account.json new file mode 100644 index 000000000..4a8ffe247 --- /dev/null +++ b/spec/fixtures/service_account.json @@ -0,0 +1 @@ +{"id":2,"name":"Service Account 2","username":"service_account_2","state":"active","locked":false} \ No newline at end of file diff --git a/spec/gitlab/client/users_spec.rb b/spec/gitlab/client/users_spec.rb index b680518c1..aaaa9cc8c 100644 --- a/spec/gitlab/client/users_spec.rb +++ b/spec/gitlab/client/users_spec.rb @@ -113,6 +113,33 @@ end end + describe '.create_service_account' do + context 'when successful request' do + before do + stub_post('/service_accounts', 'service_account') + @user = Gitlab.create_service_account('name', 'username') + end + + it 'gets the correct resource' do + body = { name: 'name', username: 'username' } + expect(a_post('/service_accounts').with(body: body)).to have_been_made + end + + it 'returns information about a created user' do + expect(@user.username).to eq('service_account_2') + end + end + + context 'when bad request' do + it 'throws an exception' do + stub_post('/service_accounts', 'error_already_exists', 409) + expect do + Gitlab.create_service_account('name', 'username') + end.to raise_error(Gitlab::Error::Conflict, "Server responded with code 409, message: 409 Already exists. Request URI: #{Gitlab.endpoint}/service_accounts") + end + end + end + describe '.edit_user' do before do @options = { name: 'Roberto' } @@ -663,6 +690,79 @@ end end + describe 'get all personal access tokens' do + describe 'get all' do + before do + stub_get('/user_personal_access_tokens?user_id=2', 'personal_access_get_all') + @token = Gitlab.user_personal_access_tokens(2) + end + + it 'gets the correct resource' do + expect(a_get('/personal_access_tokens?user_id=2')).to have_been_made + end + + it 'gets an array of user impersonation tokens' do + expect(@tokens.first.id).to eq(2) + expect(@tokens.last.id).to eq(3) + expect(@tokens.first.active).to be_truthy + expect(@tokens.last.active).to be_truthy + end + end + end + + describe 'create personal access token' do + before do + stub_post('/user/personal_access_tokens/', 'personal_access_create') + @token = Gitlab.create_personal_access_token(2, 'service_account_2', ['api']) + end + + it 'gets the correct resource' do + expect(a_post('/user/personal_access_tokens').with(body: 'name=service_account_2&scopes%5B%5D=api')).to have_been_made + end + + it 'returns a valid personal access token' do + expect(@token.name).to eq('service_account_token') + expect(@token.user_id).to eq(2) + expect(@token.id).to eq(2) + expect(@token.active).to be_truthy + expect(@token.token).to eq('glpat-3Hm21_tY3sn4Wafwq39p') + end + end + + describe 'rotate personal access token' do + before do + stub_post('/personal_access_tokens/2/rotate', 'personal_access_rotate') + @token = Gitlab.rotate_personal_access_token(2, '2025-05-24') + end + + it 'gets the correct resource' do + body = { expires_at: '2025-05-24' } + expect(a_post('/personal_access_tokens/2/rotate').with(body: body)).to have_been_made + end + + it 'returns a valid personal access token' do + expect(@token.user_id).to eq(2) + expect(@token.id).to eq(4) + expect(@token.active).to be_truthy + expect(@token.expires_at).to eq('2025-05-24') + expect(@token.token).to eq('glpat--xSo18jU2MPtQ576ZYnp') + end + end + + describe 'revoke personal accees token' do + before do + stub_request(:delete, "#{Gitlab.endpoint}/personal_access_tokens/2") + .with(headers: { 'PRIVATE-TOKEN' => Gitlab.private_token }) + .to_return(status: 204) + @token = Gitlab.revoke_user_impersonation_token(2) + end + + it 'revokes a personal access token' do + expect(a_delete('/personal_access_tokens/2')).to have_been_made + expect(@token.to_hash).to be_empty + end + end + describe '.disable_two_factor' do before do stub_request(:patch, "#{Gitlab.endpoint}/users/1/disable_two_factor")