Commit b7bf747e authored by Sanad Liaquat's avatar Sanad Liaquat Committed by Sean McGivern

Create PAT creation API for admins

Allow admin users to create PAT for other users.
This feature is guarded by a feature flag.
parent 5c7183e5
---
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