Commit f9606f19 authored by Max Woolf's avatar Max Woolf Committed by Mayra Cabrera

Add personal_access_tokens API endpoint

Adds /personal_access_tokens API for auditing
PATs and allowing API access to list of
PATs.
parent d1a865d5
......@@ -5,12 +5,14 @@ class PersonalAccessTokensFinder
delegate :build, :find, :find_by_id, :find_by_token, to: :execute
def initialize(params = {})
def initialize(params = {}, current_user = nil)
@params = params
@current_user = current_user
end
def execute
tokens = PersonalAccessToken.all
tokens = by_current_user(tokens)
tokens = by_user(tokens)
tokens = by_impersonation(tokens)
tokens = by_state(tokens)
......@@ -20,6 +22,15 @@ class PersonalAccessTokensFinder
private
attr_reader :current_user
def by_current_user(tokens)
return tokens if current_user.nil? || current_user.admin?
return PersonalAccessToken.none unless Ability.allowed?(current_user, :read_user_personal_access_tokens, params[:user])
tokens
end
def by_user(tokens)
return tokens unless @params[:user]
......
......@@ -20,6 +20,7 @@ class UserPolicy < BasePolicy
enable :destroy_user
enable :update_user
enable :update_user_status
enable :read_user_personal_access_tokens
end
rule { default }.enable :read_user_profile
......
---
title: Add personal_access_tokens list to REST API
merge_request: 37806
author:
type: added
......@@ -140,6 +140,7 @@ The following API resources are available outside of project and group contexts
| [Namespaces](namespaces.md) | `/namespaces` |
| [Notification settings](notification_settings.md) | `/notification_settings` (also available for groups and projects) |
| [Pages domains](pages_domains.md) | `/pages/domains` (also available for projects) |
| [Personal access tokens](personal_access_tokens.md) | `/personal_access_tokens` |
| [Projects](projects.md) | `/users/:id/projects` (also available for projects) |
| [Project repository storage moves](project_repository_storage_moves.md) **(CORE ONLY)** | `/project_repository_storage_moves` |
| [Runners](runners.md) | `/runners` (also available for projects) |
......
# Personal access tokens API **(ULTIMATE)**
You can read more about [personal access tokens](../user/profile/personal_access_tokens.md#personal-access-tokens).
## List personal access tokens
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22726) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
Get a list of personal access tokens.
```plaintext
GET /personal_access_tokens
```
| Attribute | Type | required | Description |
|-----------|---------|----------|---------------------|
| `user_id` | integer/string | no | The ID of the user to filter by |
NOTE: **Note:**
Administrators can use the `user_id` parameter to filter by a user. Non-administrators cannot filter by any user except themselves. Attempting to do so will result in a `401 Unauthorized` response.
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/personal_access_tokens"
```
```json
[
{
"id": 4,
"name": "Test Token",
"revoked": false,
"created_at": "2020-07-23T14:31:47.729Z",
"scopes": [
"api"
],
"active": true,
"user_id": 24,
"expires_at": null
}
]
```
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/personal_access_tokens?user_id=3"
```
```json
[
{
"id": 4,
"name": "Test Token",
"revoked": false,
"created_at": "2020-07-23T14:31:47.729Z",
"scopes": [
"api"
],
"active": true,
"user_id": 3,
"expires_at": null
}
]
```
......@@ -1293,6 +1293,7 @@ Example response:
[
{
"active" : true,
"user_id" : 2,
"scopes" : [
"api"
],
......@@ -1305,6 +1306,7 @@ Example response:
},
{
"active" : false,
"user_id" : 2,
"scopes" : [
"read_user"
],
......@@ -1344,6 +1346,7 @@ Example response:
```json
{
"active" : true,
"user_id" : 2,
"scopes" : [
"api"
],
......@@ -1387,6 +1390,7 @@ Example response:
{
"id" : 2,
"revoked" : false,
"user_id" : 2,
"scopes" : [
"api"
],
......
......@@ -126,6 +126,7 @@ class License < ApplicationRecord
insights
issuable_health_status
license_scanning
personal_access_token_api_management
personal_access_token_expiration_policy
enforce_pat_expiration
prometheus_alerts
......
# frozen_string_literal: true
module API
class PersonalAccessTokens < Grape::API::Instance
include ::API::PaginationParams
desc 'Get all Personal Access Tokens' do
detail 'This feature was added in GitLab 13.3'
success Entities::PersonalAccessToken
end
params do
optional :user_id, type: Integer, desc: 'User ID'
use :pagination
end
before do
authenticate!
restrict_non_admins! unless current_user.admin?
end
helpers do
def finder_params(current_user)
current_user.admin? ? { user: user(params[:user_id]) } : { user: current_user }
end
def user(user_id)
UserFinder.new(user_id).find_by_id
end
def restrict_non_admins!
return if params[:user_id].blank?
unauthorized! unless Ability.allowed?(current_user, :read_user_personal_access_tokens, user(params[:user_id]))
end
def authenticate!
unauthorized! unless ::License.feature_available?(:personal_access_token_api_management)
super
end
end
get :personal_access_tokens do
tokens = PersonalAccessTokensFinder.new(finder_params(current_user), current_user).execute
present paginate(tokens), with: Entities::PersonalAccessToken
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::PersonalAccessTokens do
let_it_be(:path) { '/personal_access_tokens' }
let_it_be(:token1) { create(:personal_access_token) }
let_it_be(:token2) { create(:personal_access_token) }
let_it_be(:current_user) { create(:user) }
context 'when unlicensed' do
before do
stub_licensed_features(personal_access_token_api_management: false)
end
it 'responds with unauthorized' do
get api(path, current_user)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when licensed' do
before do
stub_licensed_features(personal_access_token_api_management: true)
end
context 'logged in as an Administrator' do
let_it_be(:current_user) { create(:admin) }
it 'returns all PATs by default' do
get api(path, current_user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(PersonalAccessToken.all.count)
end
context 'filtered with user_id parameter' do
it 'returns only PATs belonging to that user' do
get api(path, current_user), params: { user_id: token1.user.id }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.first['user_id']).to eq(token1.user.id)
end
end
end
context 'logged in as a non-Administrator' do
let_it_be(:current_user) { create(:user) }
let_it_be(:user) { create(:user) }
let_it_be(:token) { create(:personal_access_token, user: current_user)}
let_it_be(:other_token) { create(:personal_access_token, user: user) }
it 'returns all PATs belonging to the signed-in user' do
get api(path, current_user, personal_access_token: token)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.map { |r| r['user_id'] }.uniq).to contain_exactly(current_user.id)
end
context 'filtered with user_id parameter' do
it 'returns PATs belonging to the specific user' do
get api(path, current_user, personal_access_token: token), params: { user_id: current_user.id }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.map { |r| r['user_id'] }.uniq).to contain_exactly(current_user.id)
end
it 'is unauthorized if filtered by a user other than current_user' do
get api(path, current_user, personal_access_token: token), params: { user_id: user.id }
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
context 'not authenticated' do
it 'is forbidden' do
get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
......@@ -194,6 +194,7 @@ module API
mount ::API::GoProxy
mount ::API::Pages
mount ::API::PagesDomains
mount ::API::PersonalAccessTokens
mount ::API::ProjectClusters
mount ::API::ProjectContainerRepositories
mount ::API::ProjectEvents
......
......@@ -3,7 +3,7 @@
module API
module Entities
class PersonalAccessToken < Grape::Entity
expose :id, :name, :revoked, :created_at, :scopes
expose :id, :name, :revoked, :created_at, :scopes, :user_id
expose :active?, as: :active
expose :expires_at do |personal_access_token|
personal_access_token.expires_at ? personal_access_token.expires_at.strftime("%Y-%m-%d") : nil
......
......@@ -3,13 +3,14 @@
require 'spec_helper'
RSpec.describe PersonalAccessTokensFinder do
def finder(options = {})
described_class.new(options)
def finder(options = {}, current_user = nil)
described_class.new(options, current_user)
end
describe '#execute' do
let(:user) { create(:user) }
let(:params) { {} }
let(:current_user) { nil }
let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
let!(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) }
let!(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) }
......@@ -17,7 +18,42 @@ RSpec.describe PersonalAccessTokensFinder do
let!(:expired_impersonation_token) { create(:personal_access_token, :expired, :impersonation, user: user) }
let!(:revoked_impersonation_token) { create(:personal_access_token, :revoked, :impersonation, user: user) }
subject { finder(params).execute }
subject { finder(params, current_user).execute }
context 'when current_user is defined' do
let(:current_user) { create(:admin) }
let(:params) { { user: user } }
context 'current_user is allowed to read PATs' do
it do
is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
revoked_personal_access_token, expired_personal_access_token,
revoked_impersonation_token, expired_impersonation_token)
end
end
context 'current_user is not allowed to read PATs' do
let(:current_user) { create(:user) }
it { is_expected.to be_empty }
end
context 'when user param is not set' do
let(:params) { {} }
it do
is_expected.to contain_exactly(active_personal_access_token, active_impersonation_token,
revoked_personal_access_token, expired_personal_access_token,
revoked_impersonation_token, expired_impersonation_token)
end
context 'when current_user is not an administrator' do
let(:current_user) { create(:user) }
it { is_expected.to be_empty }
end
end
end
describe 'without user' do
it do
......
......@@ -12,6 +12,34 @@ RSpec.describe UserPolicy do
it { is_expected.to be_allowed(:read_user) }
end
describe "reading a different user's Personal Access Tokens" do
let(:token) { create(:personal_access_token, user: user) }
context 'when user is admin' do
let(:current_user) { create(:user, :admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:read_user_personal_access_tokens) }
end
context 'when admin mode is disabled' do
it { is_expected.not_to be_allowed(:read_user_personal_access_tokens) }
end
end
context 'when user is not an admin' do
context 'requesting their own personal access tokens' do
subject { described_class.new(current_user, current_user) }
it { is_expected.to be_allowed(:read_user_personal_access_tokens) }
end
context "requesting a different user's personal access tokens" do
it { is_expected.not_to be_allowed(:read_user_personal_access_tokens) }
end
end
end
shared_examples 'changing a user' do |ability|
context "when a regular user tries to destroy another regular user" do
it { is_expected.not_to be_allowed(ability) }
......
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