Commit 538a5c67 authored by Max Woolf's avatar Max Woolf Committed by Heinrich Lee Yu

Add Project Access Tokens to credentials inventory

Adds created project access tokens to the
admin credentials inventory.

EE: true
Changelog: added
parent 458ee618
......@@ -33,6 +33,7 @@ class PersonalAccessToken < ApplicationRecord
scope :preload_users, -> { preload(:user) }
scope :order_expires_at_asc, -> { reorder(expires_at: :asc) }
scope :order_expires_at_desc, -> { reorder(expires_at: :desc) }
scope :project_access_token, -> { includes(:user).where(user: { user_type: :project_bot }) }
validates :scopes, presence: true
validate :validate_scopes
......@@ -93,6 +94,10 @@ class PersonalAccessToken < ApplicationRecord
"#{self.class.token_prefix}#{token}"
end
def project_access_token?
user&.project_bot?
end
protected
def validate_scopes
......
......@@ -13,9 +13,14 @@ GitLab administrators are responsible for the overall security of their instance
provides a Credentials inventory to keep track of all the credentials that can be used to access
their self-managed instance.
Using Credentials inventory, you can see all the personal access tokens (PAT), SSH keys, and GPG keys
that exist in your GitLab instance. In addition, you can [revoke](#revoke-a-users-personal-access-token)
and [delete](#delete-a-users-ssh-key) and see:
Use Credentials inventory to see for your GitLab instance all:
- Personal access tokens (PAT).
- Project access tokens (GitLab 14.8 and later).
- SSH keys.
- GPG keys.
You can also [revoke](#revoke-a-users-personal-access-token) and [delete](#delete-a-users-ssh-key) and see:
- Who they belong to.
- Their access scope.
......@@ -28,10 +33,6 @@ To access the Credentials inventory:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Credentials**.
The following is an example of the Credentials inventory page:
![Credentials inventory page](img/credentials_inventory_v13_10.png)
## Revoke a user's personal access token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214811) in GitLab 13.4.
......@@ -49,6 +50,15 @@ If you see a **Revoke** button, you can revoke that user's PAT. Whether you see
When a PAT is revoked from the credentials inventory, the instance notifies the user by email.
## Revoke a user's project access token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/243833) in GitLab 14.8.
The **Revoke** button next to a project access token can be selected to revoke that particular project access token. This will both:
- Revoke the token project access token.
- Enqueue a background worker to delete the project bot user.
## Delete a user's SSH key
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225248) in GitLab 13.5.
......
......@@ -6,7 +6,7 @@ class Admin::CredentialsController < Admin::ApplicationController
include RedisTracking
helper_method :credentials_inventory_path, :user_detail_path, :personal_access_token_revoke_path,
:ssh_key_delete_path, :gpg_keys_available?
:project_access_token_revoke_path, :ssh_key_delete_path, :gpg_keys_available?
before_action :check_license_credentials_inventory_available!, only: [:index, :revoke, :destroy]
......
......@@ -30,10 +30,10 @@ module CredentialsInventoryActions
end
def revoke
personal_access_token = personal_access_token_finder.find_by_id(params[:id])
personal_access_token = personal_access_token_finder.find_by_id(params[:id] || params[:credential_id])
return render_404 unless personal_access_token
result = revoke_service(personal_access_token).execute
result = revoke_service(personal_access_token, params[:project_id]).execute
if result.success?
flash[:notice] = result.message
......@@ -52,6 +52,8 @@ module CredentialsInventoryActions
::PersonalAccessTokensFinder.new({ users: users, impersonation: false, sort: 'id_desc' }).execute
elsif show_ssh_keys?
::KeysFinder.new({ users: users, key_type: 'ssh' }).execute
elsif show_project_access_tokens?
::PersonalAccessTokensFinder.new({ users: users, impersonation: false, sort: 'id_desc' }).execute.project_access_token
end
end
......@@ -78,7 +80,9 @@ module CredentialsInventoryActions
end
end
def revoke_service(token)
def revoke_service(token, project_id)
return ::ResourceAccessTokens::RevokeService.new(current_user, ::Project.find_by_id(project_id), token) if project_id
if revocable.instance_of?(Group)
::PersonalAccessTokens::RevokeService.new(current_user, token: token, group: revocable)
else
......
# frozen_string_literal: true
module CredentialsInventoryHelper
VALID_FILTERS = %w(ssh_keys personal_access_tokens gpg_keys).freeze
VALID_FILTERS = %w(ssh_keys personal_access_tokens gpg_keys project_access_tokens).freeze
def show_personal_access_tokens?
return true if params[:filter] == 'personal_access_tokens'
......@@ -17,6 +17,10 @@ module CredentialsInventoryHelper
params[:filter] == 'gpg_keys'
end
def show_project_access_tokens?
params[:filter] == 'project_access_tokens'
end
def credentials_inventory_feature_available?
License.feature_available?(:credentials_inventory)
end
......
.table-holder
.thead-white.gl-white-space-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-10{ role: 'rowheader' }= _('Name')
.table-section.section-10{ role: 'rowheader' }= _('Scopes')
.table-section.section-15{ role: 'rowheader' }= _('Project')
.table-section.section-20{ role: 'rowheader' }= _('Creator')
.table-section.section-15{ role: 'rowheader' }= _('Created on')
.table-section.section-10{ role: 'rowheader' }= _('Last used')
.table-section.section-10{ role: 'rowheader' }= _('Expires')
.table-section.section-10{ role: 'rowheader' }= _('Revoke')
= render partial: 'shared/credentials_inventory/project_access_tokens/project_access_token', collection: credentials
......@@ -8,6 +8,8 @@
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-border-0'}) do
= gl_tab_link_to s_('CredentialsInventory|Personal Access Tokens'), credentials_inventory_path(filter: 'personal_access_tokens'), { item_active: active_when(show_personal_access_tokens?), class: 'gl-border-0!' }
= gl_tab_link_to s_('CredentialsInventory|SSH Keys'), credentials_inventory_path(filter: 'ssh_keys'), { item_active: active_when(show_ssh_keys?), class: 'gl-border-0!' }
= gl_tab_link_to s_('CredentialsInventory|Project Access Tokens'), credentials_inventory_path(filter: 'project_access_tokens'), { item_active: active_when(show_project_access_tokens?), class: 'gl-border-0!' }
- if gpg_keys_available?
= gl_tab_link_to s_('CredentialsInventory|GPG Keys'), credentials_inventory_path(filter: 'gpg_keys'), { item_active: active_when(show_gpg_keys?), class: 'gl-border-0!' }
......@@ -21,5 +23,7 @@
= render 'shared/credentials_inventory/ssh_keys', credentials: @credentials
- elsif show_gpg_keys?
= render 'shared/credentials_inventory/gpg_keys', credentials: @credentials
- elsif show_project_access_tokens?
= render 'shared/credentials_inventory/project_access_tokens', credentials: @credentials
= paginate_without_count @credentials
......@@ -28,4 +28,8 @@
-# We're inferring the revoked date from the last updated_at, see https://gitlab.com/gitlab-org/gitlab/-/issues/218046#note_362875952
= personal_access_token.updated_at.to_date
- elsif personal_access_token.active?
= link_to _('Revoke'), personal_access_token_revoke_path(personal_access_token), method: :put, data: { confirm: _('Are you sure you want to revoke this personal access token? This action cannot be undone.') }, class: 'btn btn-danger btn-danger-secondary btn-md btn-secondary gl-button'
- if personal_access_token.project_access_token?
- project = personal_access_token.user.projects.first
= link_to _('Revoke'), admin_credential_project_revoke_path(credential_id: personal_access_token, project_id: project.id), method: :put, data: { confirm: _('Are you sure you want to revoke this project access token? This action cannot be undone.') }, class: 'btn btn-danger btn-danger-secondary btn-md btn-secondary gl-button'
- else
= link_to _('Revoke'), personal_access_token_revoke_path(personal_access_token), method: :put, data: { confirm: _('Are you sure you want to revoke this personal access token? This action cannot be undone.') }, class: 'btn btn-danger btn-danger-secondary btn-md btn-secondary gl-button'
- project = project_access_token.user.projects.first
.gl-responsive-table-row{ role: 'row' }
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Name')
.table-mobile-content
= project_access_token.name
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Scopes')
.table-mobile-content.gl-white-space-normal
= project_access_token.scopes.join(', ')
.table-section.section-15
.table-mobile-header{ role: 'rowheader' }
= _('Project')
.table-mobile-content
= link_to project.name, project.full_path
.table-section.section-20
.table-mobile-header{ role: 'rowheader' }
= _('Creator')
.table-mobile-content
= render 'shared/credentials_inventory/users/user_detail', user: project_access_token.user.created_by
.table-section.section-15
.table-mobile-header{ role: 'rowheader' }
= _('Created on')
.table-mobile-content
= project_access_token.created_at
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Last used')
.table-mobile-content
= project_access_token.last_used_at || _('Never')
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Expires')
.table-mobile-content
= project_access_token.expires_at || _('Never')
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Revoke')
.table-mobile-content
= link_to _('Revoke'), admin_credential_project_revoke_path(credential_id: project_access_token, project_id: project.id), method: :put, data: { confirm: _('Are you sure you want to revoke this project access token? This action cannot be undone.') }, class: 'btn btn-danger btn-danger-secondary btn-md btn-secondary gl-button'
......@@ -21,6 +21,9 @@ namespace :admin do
resources :audit_logs, controller: 'audit_logs', only: [:index]
resources :audit_log_reports, only: [:index], constraints: { format: :csv }
resources :credentials, only: [:index, :destroy] do
resources :projects, only: [] do
put :revoke, controller: :credentials
end
member do
put :revoke
end
......
......@@ -8,6 +8,9 @@ RSpec.describe Admin::CredentialsController, type: :request do
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:project_bot) { create(:user, :project_bot, created_by_id: user.id) }
let_it_be(:project_member) { create(:project_member, user: project_bot) }
let_it_be(:project_access_token) { create(:personal_access_token, user: project_member.user) }
describe 'GET #index' do
context 'admin user' do
......@@ -34,7 +37,7 @@ RSpec.describe Admin::CredentialsController, type: :request do
specify do
get admin_credentials_path(filter: filter)
expect(assigns(:credentials)).to match_array(user.personal_access_tokens)
expect(assigns(:credentials)).to match_array([user.personal_access_tokens, project_access_token].flatten)
end
end
......@@ -66,6 +69,14 @@ RSpec.describe Admin::CredentialsController, type: :request do
end
end
context 'credential type specified as `project_access_tokens`' do
it 'filters by project access tokens' do
get admin_credentials_path(filter: 'project_access_tokens')
expect(assigns(:credentials)).to contain_exactly(project_access_token)
end
end
context 'credential type specified as `gpg_keys`' do
it 'filters by gpg keys' do
gpg_key = create(:gpg_key)
......@@ -134,12 +145,23 @@ RSpec.describe Admin::CredentialsController, type: :request do
end
describe 'PUT #revoke' do
let_it_be(:project_member) { create(:project_member) }
let_it_be(:project_access_token) { create(:personal_access_token, user: project_member.user) }
let(:project) { project_member.project }
shared_examples_for 'responds with 404' do
it do
put revoke_admin_credential_path(id: token_id)
expect(response).to have_gitlab_http_status(:not_found)
end
it do
put admin_credential_project_revoke_path(credential_id: token_id, project_id: project.id)
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples_for 'displays the flash success message' do
......@@ -149,6 +171,13 @@ RSpec.describe Admin::CredentialsController, type: :request do
expect(response).to redirect_to(admin_credentials_path)
expect(flash[:notice]).to start_with 'Revoked personal access token '
end
it :aggregate_failures do
put admin_credential_project_revoke_path(credential_id: project_access_token.id, project_id: project.id)
expect(response).to redirect_to(admin_credentials_path)
expect(flash[:notice]).to eq "Access token #{project_access_token.name} has been revoked and the bot user has been scheduled for deletion."
end
end
shared_examples_for 'displays the flash error message' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe('shared/credentials_inventory/personal_access_tokens/_project_access_token.html.haml') do
let_it_be(:user) { create(:user) }
let_it_be(:project_bot) { create(:user, :project_bot, created_by_id: user.id) }
let_it_be(:project_member) { create(:project_member, user: project_bot) }
let_it_be(:project_access_token) { create(:personal_access_token, user: project_member.user, scopes: %w(read_repository api)) }
before do
allow(view).to receive(:user_detail_path).and_return('abcd')
render 'shared/credentials_inventory/project_access_tokens/project_access_token', project_access_token: project_access_token
end
it 'shows the token name' do
expect(rendered).to have_text(user.name)
end
it 'shows the token scopes' do
expect(rendered).to have_text(project_access_token.scopes.join(', '))
end
it 'shows the token project' do
expect(rendered).to have_text(project_member.project.name)
end
it 'shows the token creator', :aggregate_failures do
expect(rendered).to have_text(user.name)
expect(rendered).to have_text(user.email)
end
it 'shows the created date' do
expect(rendered).to have_text(project_access_token.created_at.to_s)
end
context 'last used date' do
context 'when token has never been used' do
let_it_be(:project_access_token) { create(:personal_access_token, user: project_member.user, scopes: %w(read_repository api), last_used_at: nil) }
it 'displays Never' do
expect(rendered).to have_text('Never')
end
end
context 'when token has been used recently' do
let_it_be(:project_access_token) { create(:personal_access_token, user: project_member.user, scopes: %w(read_repository api), last_used_at: DateTime.new(2001, 2, 3, 4, 5, 6)) }
it 'displays the time last used' do
expect(rendered).to have_text('2001-02-03 04:05:06 UTC')
end
end
end
context 'expires date' do
context 'when token has never been used' do
let_it_be(:project_access_token) { create(:personal_access_token, user: project_member.user, scopes: %w(read_repository api), expires_at: nil) }
it 'displays Never' do
expect(rendered).to have_text('Never')
end
end
context 'when token is set to expire' do
let_it_be(:project_access_token) { create(:personal_access_token, user: project_member.user, scopes: %w(read_repository api), last_used_at: DateTime.new(2004, 2, 3, 4, 5, 6)) }
it 'displays the expiration date' do
expect(rendered).to have_text('2004-02-03 04:05:06 UTC')
end
end
end
end
......@@ -4774,6 +4774,9 @@ msgstr ""
msgid "Are you sure you want to revoke this personal access token? This action cannot be undone."
msgstr ""
msgid "Are you sure you want to revoke this project access token? This action cannot be undone."
msgstr ""
msgid "Are you sure you want to stop this environment?"
msgstr ""
......@@ -10476,6 +10479,9 @@ msgstr ""
msgid "Creation date"
msgstr ""
msgid "Creator"
msgstr ""
msgid "Credentials"
msgstr ""
......@@ -10488,6 +10494,9 @@ msgstr ""
msgid "CredentialsInventory|Personal Access Tokens"
msgstr ""
msgid "CredentialsInventory|Project Access Tokens"
msgstr ""
msgid "CredentialsInventory|SSH Keys"
msgstr ""
......
......@@ -22,6 +22,16 @@ RSpec.describe PersonalAccessToken do
end
describe 'scopes' do
describe '.project_access_tokens' do
let_it_be(:user) { create(:user, :project_bot) }
let_it_be(:project_member) { create(:project_member, user: user) }
let_it_be(:project_access_token) { create(:personal_access_token, user: user) }
subject { described_class.project_access_token }
it { is_expected.to contain_exactly(project_access_token) }
end
describe '.for_user' do
it 'returns personal access tokens of specified user only' do
user_1 = create(: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