Commit 3ee16954 authored by Sean McGivern's avatar Sean McGivern

Merge branch '17176-add-personal-access-token-admin-api' into 'master'

Add API for admin users to create other user's PATs

See merge request gitlab-org/gitlab!45152
parents b55994ce b7bf747e
---
name: pat_creation_api_for_admin
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45152
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267553
type: development
group: group::access
default_enabled: false
......@@ -93,3 +93,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
- `204: No Content` if successfully revoked.
- `400 Bad Request` if not revoked successfully.
## Create a personal access token (admin only)
See the [Users API documentation](users.md#create-a-personal-access-token-admin-only) for information on creating a personal access token.
......@@ -1441,7 +1441,54 @@ Parameters:
| `user_id` | integer | yes | The ID of the user |
| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
### Get user activities (admin only)
## Create a personal access token (admin only)
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17176) in GitLab 13.6.
> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-an-administrators-ability-to-use-the-api-to-create-personal-access-tokens). **(CORE)**
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
> Requires admin permissions.
> Token values are returned once. Make sure you save it - you won't be able to access it again.
It creates a new personal access token.
```plaintext
POST /users/:user_id/personal_access_tokens
```
| Attribute | Type | Required | Description |
| ------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `user_id` | integer | yes | The ID of the user |
| `name` | string | yes | The name of the personal access token |
| `expires_at` | date | no | The expiration date of the personal access token in ISO format (`YYYY-MM-DD`) |
| `scopes` | array | yes | The array of scopes of the personal access token (`api`, `read_user`, `read_api`, `read_repository`, `write_repository`) |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "name=mytoken" --data "expires_at=2017-04-04" --data "scopes[]=api" "https://gitlab.example.com/api/v4/users/42/personal_access_tokens"
```
Example response:
```json
{
"id": 3,
"name": "mytoken",
"revoked": false,
"created_at": "2020-10-14T11:58:53.526Z",
"scopes": [
"api"
],
"user_id": 42,
"active": true,
"expires_at": "2020-12-31",
"token": "ggbfKkC4n-Lujy8jwCR2"
}
```
## Get user activities (admin only)
NOTE: **Note:**
This API endpoint is only available on 8.15 (EE) and 9.1 (CE) and above.
......@@ -1546,3 +1593,22 @@ Example response:
},
]
```
## Enable or disable an administrator's ability to use the API to create personal access tokens **(CORE)**
An administrator's ability to create personal access tokens through the API is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:pat_creation_api_for_admin)
```
To disable it:
```ruby
Feature.disable(:pat_creation_api_for_admin)
```
......@@ -65,9 +65,9 @@ module API
params :sort_params do
optional :order_by, type: String, values: %w[id name username created_at updated_at],
default: 'id', desc: 'Return users ordered by a field'
default: 'id', desc: 'Return users ordered by a field'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return users sorted in ascending and descending order'
desc: 'Return users sorted in ascending and descending order'
end
end
......@@ -706,6 +706,40 @@ module API
end
end
end
resource :personal_access_tokens do
helpers do
def target_user
find_user_by_id(params)
end
end
before { authenticated_as_admin! }
desc 'Create a personal access token. Available only for admins.' do
detail 'This feature was introduced in GitLab 13.6'
success Entities::PersonalAccessTokenWithToken
end
params do
requires :name, type: String, desc: 'The name of the personal access token'
requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::Gitlab::Auth.all_available_scopes.map(&:to_s),
desc: 'The array of scopes of the personal access token'
optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token'
end
post feature_category: :authentication_and_authorization do
not_found! unless Feature.enabled?(:pat_creation_api_for_admin)
response = ::PersonalAccessTokens::CreateService.new(
current_user: current_user, target_user: target_user, params: declared_params(include_missing: false)
).execute
if response.success?
present response.payload[:personal_access_token], with: Entities::PersonalAccessTokenWithToken
else
render_api_error!(response.message, response.http_status || :unprocessable_entity)
end
end
end
end
end
......
......@@ -161,7 +161,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
end
context 'accesses the profile of another admin' do
let(:admin_2) {create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com')}
let(:admin_2) { create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com') }
it 'contains the note of the user' do
get api("/user?private_token=#{admin_personal_access_token}&sudo=#{admin_2.id}")
......@@ -772,11 +772,11 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it "does not create user with invalid email" do
post api('/users', admin),
params: {
email: 'invalid email',
password: 'password',
name: 'test'
}
params: {
email: 'invalid email',
password: 'password',
name: 'test'
}
expect(response).to have_gitlab_http_status(:bad_request)
end
......@@ -811,14 +811,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it 'returns 400 error if user does not validate' do
post api('/users', admin),
params: {
password: 'pass',
email: 'test@example.com',
username: 'test!',
name: 'test',
bio: 'g' * 256,
projects_limit: -1
}
params: {
password: 'pass',
email: 'test@example.com',
username: 'test!',
name: 'test',
bio: 'g' * 256,
projects_limit: -1
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['password'])
.to eq(['is too short (minimum is 8 characters)'])
......@@ -838,23 +838,23 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
context 'with existing user' do
before do
post api('/users', admin),
params: {
email: 'test@example.com',
password: 'password',
username: 'test',
name: 'foo'
}
params: {
email: 'test@example.com',
password: 'password',
username: 'test',
name: 'foo'
}
end
it 'returns 409 conflict error if user with same email exists' do
expect do
post api('/users', admin),
params: {
name: 'foo',
email: 'test@example.com',
password: 'password',
username: 'foo'
}
params: {
name: 'foo',
email: 'test@example.com',
password: 'password',
username: 'foo'
}
end.to change { User.count }.by(0)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to eq('Email has already been taken')
......@@ -863,12 +863,12 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it 'returns 409 conflict error if same username exists' do
expect do
post api('/users', admin),
params: {
name: 'foo',
email: 'foo@example.com',
password: 'password',
username: 'test'
}
params: {
name: 'foo',
email: 'foo@example.com',
password: 'password',
username: 'test'
}
end.to change { User.count }.by(0)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to eq('Username has already been taken')
......@@ -877,12 +877,12 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it 'returns 409 conflict error if same username exists (case insensitive)' do
expect do
post api('/users', admin),
params: {
name: 'foo',
email: 'foo@example.com',
password: 'password',
username: 'TEST'
}
params: {
name: 'foo',
email: 'foo@example.com',
password: 'password',
username: 'TEST'
}
end.to change { User.count }.by(0)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to eq('Username has already been taken')
......@@ -1185,14 +1185,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
it 'returns 400 error if user does not validate' do
put api("/users/#{user.id}", admin),
params: {
password: 'pass',
email: 'test@example.com',
username: 'test!',
name: 'test',
bio: 'g' * 256,
projects_limit: -1
}
params: {
password: 'pass',
email: 'test@example.com',
username: 'test!',
name: 'test',
bio: 'g' * 256,
projects_limit: -1
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['password'])
.to eq(['is too short (minimum is 8 characters)'])
......@@ -1714,14 +1714,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
context "hard delete disabled" do
it "does not delete user" do
perform_enqueued_jobs { delete api("/users/#{user.id}", admin)}
perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
expect(response).to have_gitlab_http_status(:conflict)
end
end
context "hard delete enabled" do
it "delete user and group", :sidekiq_might_not_need_inline do
perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin)}
perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
expect(response).to have_gitlab_http_status(:no_content)
expect(Group.exists?(group.id)).to be_falsy
end
......@@ -1993,7 +1993,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
delete api("/user/keys/#{key.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.keys.count}.by(-1)
end.to change { user.keys.count }.by(-1)
end
it_behaves_like '412 response' do
......@@ -2124,7 +2124,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
post api("/user/gpg_keys/#{gpg_key.id}/revoke", user)
expect(response).to have_gitlab_http_status(:accepted)
end.to change { user.gpg_keys.count}.by(-1)
end.to change { user.gpg_keys.count }.by(-1)
end
it 'returns 404 if key ID not found' do
......@@ -2157,7 +2157,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
delete api("/user/gpg_keys/#{gpg_key.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.gpg_keys.count}.by(-1)
end.to change { user.gpg_keys.count }.by(-1)
end
it 'returns 404 if key ID not found' do
......@@ -2279,7 +2279,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
delete api("/user/emails/#{email.id}", user)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.emails.count}.by(-1)
end.to change { user.emails.count }.by(-1)
end
it_behaves_like '412 response' do
......@@ -2756,6 +2756,124 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
end
end
describe 'POST /users/:user_id/personal_access_tokens' do
let(:name) { 'new pat' }
let(:expires_at) { 3.days.from_now.to_date.to_s }
let(:scopes) { %w(api read_user) }
context 'when feature flag is enabled' do
before do
stub_feature_flags(pat_creation_api_for_admin: true)
end
it 'returns error if required attributes are missing' do
post api("/users/#{user.id}/personal_access_tokens", admin)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('name is missing, scopes is missing, scopes does not have a valid value')
end
it 'returns a 404 error if user not found' do
post api("/users/#{non_existing_record_id}/personal_access_tokens", admin),
params: {
name: name,
scopes: scopes,
expires_at: expires_at
}
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 401 error when not authenticated' do
post api("/users/#{user.id}/personal_access_tokens"),
params: {
name: name,
scopes: scopes,
expires_at: expires_at
}
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['message']).to eq('401 Unauthorized')
end
it 'returns a 403 error when authenticated as normal user' do
post api("/users/#{user.id}/personal_access_tokens", user),
params: {
name: name,
scopes: scopes,
expires_at: expires_at
}
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden')
end
it 'creates a personal access token when authenticated as admin' do
post api("/users/#{user.id}/personal_access_tokens", admin),
params: {
name: name,
expires_at: expires_at,
scopes: scopes
}
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(name)
expect(json_response['scopes']).to eq(scopes)
expect(json_response['expires_at']).to eq(expires_at)
expect(json_response['id']).to be_present
expect(json_response['created_at']).to be_present
expect(json_response['active']).to be_truthy
expect(json_response['revoked']).to be_falsey
expect(json_response['token']).to be_present
end
context 'when an error is thrown by the model' do
let!(:admin_personal_access_token) { create(:personal_access_token, user: admin) }
let(:error_message) { 'error message' }
before do
allow_next_instance_of(PersonalAccessToken) do |personal_access_token|
allow(personal_access_token).to receive_message_chain(:errors, :full_messages)
.and_return([error_message])
allow(personal_access_token).to receive(:save).and_return(false)
end
end
it 'returns the error' do
post api("/users/#{user.id}/personal_access_tokens", personal_access_token: admin_personal_access_token),
params: {
name: name,
expires_at: expires_at,
scopes: scopes
}
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq(error_message)
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(pat_creation_api_for_admin: false)
end
it 'returns a 404' do
post api("/users/#{user.id}/personal_access_tokens", admin),
params: {
name: name,
expires_at: expires_at,
scopes: scopes
}
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Not Found')
end
end
end
describe 'GET /users/:user_id/impersonation_tokens' do
let_it_be(:active_personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment