Commit 911f2085 authored by Manoj M J's avatar Manoj M J Committed by Imre Farkas

Introduce Credentials Inventory

This change introduces new Credentials
Inventory feature, available for admins.
parent 34e8f5c6
...@@ -15,15 +15,43 @@ class KeysFinder ...@@ -15,15 +15,43 @@ class KeysFinder
def execute def execute
raise GitLabAccessDeniedError unless current_user.admin? raise GitLabAccessDeniedError unless current_user.admin?
raise InvalidFingerprint unless valid_fingerprint_param?
Key.where(fingerprint_query).first # rubocop: disable CodeReuse/ActiveRecord keys = by_key_type
keys = by_user(keys)
keys = sort(keys)
by_fingerprint(keys)
end end
private private
attr_reader :current_user, :params attr_reader :current_user, :params
def by_key_type
if params[:key_type] == 'ssh'
Key.regular_keys
else
Key.all
end
end
def sort(keys)
keys.order_last_used_at_desc
end
def by_user(keys)
return keys unless params[:user]
keys.for_user(params[:user])
end
def by_fingerprint(keys)
return keys unless params[:fingerprint].present?
raise InvalidFingerprint unless valid_fingerprint_param?
keys.where(fingerprint_query).first # rubocop: disable CodeReuse/ActiveRecord
end
def valid_fingerprint_param? def valid_fingerprint_param?
if fingerprint_type == "sha256" if fingerprint_type == "sha256"
Base64.decode64(fingerprint).length == 32 Base64.decode64(fingerprint).length == 32
......
...@@ -13,18 +13,26 @@ class PersonalAccessTokensFinder ...@@ -13,18 +13,26 @@ class PersonalAccessTokensFinder
tokens = PersonalAccessToken.all tokens = PersonalAccessToken.all
tokens = by_user(tokens) tokens = by_user(tokens)
tokens = by_impersonation(tokens) tokens = by_impersonation(tokens)
by_state(tokens) tokens = by_state(tokens)
sort(tokens)
end end
private private
# rubocop: disable CodeReuse/ActiveRecord
def by_user(tokens) def by_user(tokens)
return tokens unless @params[:user] return tokens unless @params[:user]
tokens.where(user: @params[:user]) tokens.for_user(@params[:user])
end
def sort(tokens)
available_sort_orders = PersonalAccessToken.simple_sorts.keys
return tokens unless available_sort_orders.include?(params[:sort])
tokens.order_by(params[:sort])
end end
# rubocop: enable CodeReuse/ActiveRecord
def by_impersonation(tokens) def by_impersonation(tokens)
case @params[:impersonation] case @params[:impersonation]
......
...@@ -39,6 +39,10 @@ class Key < ApplicationRecord ...@@ -39,6 +39,10 @@ class Key < ApplicationRecord
alias_attribute :fingerprint_md5, :fingerprint alias_attribute :fingerprint_md5, :fingerprint
scope :preload_users, -> { preload(:user) }
scope :for_user, -> (user) { where(user: user) }
scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
def self.regular_keys def self.regular_keys
where(type: ['Key', nil]) where(type: ['Key', nil])
end end
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class PersonalAccessToken < ApplicationRecord class PersonalAccessToken < ApplicationRecord
include Expirable include Expirable
include TokenAuthenticatable include TokenAuthenticatable
include Sortable
add_authentication_token_field :token, digest: true add_authentication_token_field :token, digest: true
...@@ -20,6 +21,8 @@ class PersonalAccessToken < ApplicationRecord ...@@ -20,6 +21,8 @@ class PersonalAccessToken < ApplicationRecord
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") } scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
scope :with_impersonation, -> { where(impersonation: true) } scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) } scope :without_impersonation, -> { where(impersonation: false) }
scope :for_user, -> (user) { where(user: user) }
scope :preload_users, -> { preload(:user) }
validates :scopes, presence: true validates :scopes, presence: true
validate :validate_scopes validate :validate_scopes
......
...@@ -182,6 +182,8 @@ ...@@ -182,6 +182,8 @@
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('Deploy Keys') = _('Deploy Keys')
= render_if_exists 'layouts/nav/sidebar/credentials_link'
= nav_link(controller: :services) do = nav_link(controller: :services) do
= link_to admin_application_settings_services_path do = link_to admin_application_settings_services_path do
.nav-icon-container .nav-icon-container
......
# frozen_string_literal: true
class AddIndexToKeys < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :keys, :last_used_at, order: { last_used_at: 'DESC NULLS LAST' }
end
def down
remove_concurrent_index :keys, :last_used_at
end
end
...@@ -2217,6 +2217,7 @@ ActiveRecord::Schema.define(version: 2019_12_14_175727) do ...@@ -2217,6 +2217,7 @@ ActiveRecord::Schema.define(version: 2019_12_14_175727) do
t.index ["fingerprint"], name: "index_keys_on_fingerprint", unique: true t.index ["fingerprint"], name: "index_keys_on_fingerprint", unique: true
t.index ["fingerprint_sha256"], name: "index_keys_on_fingerprint_sha256" t.index ["fingerprint_sha256"], name: "index_keys_on_fingerprint_sha256"
t.index ["id", "type"], name: "index_on_deploy_keys_id_and_type_and_public", unique: true, where: "(public = true)" t.index ["id", "type"], name: "index_on_deploy_keys_id_and_type_and_public", unique: true, where: "(public = true)"
t.index ["last_used_at"], name: "index_keys_on_last_used_at", order: "DESC NULLS LAST"
t.index ["user_id"], name: "index_keys_on_user_id" t.index ["user_id"], name: "index_keys_on_user_id"
end end
......
...@@ -16,3 +16,4 @@ GitLab’s [security features](../security/README.md) may also help you meet rel ...@@ -16,3 +16,4 @@ GitLab’s [security features](../security/README.md) may also help you meet rel
|**[LDAP group sync filters](auth/ldap-ee.md#group-sync)**<br>GitLab Enterprise Edition Premium gives more flexibility to synchronize with LDAP based on filters, meaning you can leverage LDAP attributes to map GitLab permissions.|Premium+|| |**[LDAP group sync filters](auth/ldap-ee.md#group-sync)**<br>GitLab Enterprise Edition Premium gives more flexibility to synchronize with LDAP based on filters, meaning you can leverage LDAP attributes to map GitLab permissions.|Premium+||
|**[Audit logs](audit_events.md)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze and track every change.|Premium+|| |**[Audit logs](audit_events.md)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze and track every change.|Premium+||
|**[Auditor users](auditor_users.md)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+|| |**[Auditor users](auditor_users.md)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+||
|**[Credentials inventory](../user/admin_area/credentials_inventory.md)**<br>With a credentials inventory, GitLab administrators can keep track of the credentials used by all of the users in their GitLab instance. |Ultimate||
...@@ -124,6 +124,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. ...@@ -124,6 +124,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
basic Postfix mail server with IMAP authentication on Ubuntu for incoming basic Postfix mail server with IMAP authentication on Ubuntu for incoming
emails. emails.
- [Abuse reports](../user/admin_area/abuse_reports.md): View and resolve abuse reports from your users. - [Abuse reports](../user/admin_area/abuse_reports.md): View and resolve abuse reports from your users.
- [Credentials Inventory](../user/admin_area/credentials_inventory.md): With Credentials inventory, GitLab administrators can keep track of the credentials used by their users in their GitLab self-managed instance. **(ULTIMATE ONLY)**
## Project settings ## Project settings
......
# Credentials inventory **(ULTIMATE ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/20912) in GitLab 12.6.
## Overview
GitLab administrators are responsible for the overall security of their instance. To assist, GitLab provides a Credentials inventory to keep track of all the credentials that can be used to access their self-managed instance.
Using Credentials inventory, GitLab administrators can see all the personal access tokens and SSH keys that exist in their instance and:
- Who they belong to.
- Their access scope.
- Their usage pattern.
To access the Credentials inventory, navigate to **Admin Area > Credentials**.
The following is an example of the Credentials inventory page:
![Credentials inventory page](img/credentials_inventory_v12_6.png)
# frozen_string_literal: true
class Admin::CredentialsController < Admin::ApplicationController
include Admin::CredentialsHelper
before_action :check_license_credentials_inventory_available!
def index
@credentials = filter_credentials.page(params[:page]).preload_users.without_count
end
private
def filter_credentials
if show_personal_access_tokens?
::PersonalAccessTokensFinder.new({ user: nil, impersonation: false, state: 'active', sort: 'id_desc' }).execute
elsif show_ssh_keys?
::KeysFinder.new(current_user, { user: nil, key_type: 'ssh' }).execute
end
end
def check_license_credentials_inventory_available!
render_404 unless credentials_inventory_feature_available?
end
end
# frozen_string_literal: true
module Admin
module CredentialsHelper
VALID_FILTERS = %w(ssh_keys personal_access_tokens).freeze
def show_personal_access_tokens?
return true if params[:filter] == 'personal_access_tokens'
VALID_FILTERS.exclude? params[:filter]
end
def show_ssh_keys?
params[:filter] == 'ssh_keys'
end
def credentials_inventory_feature_available?
License.feature_available?(:credentials_inventory)
end
end
end
...@@ -106,6 +106,7 @@ class License < ApplicationRecord ...@@ -106,6 +106,7 @@ class License < ApplicationRecord
EEU_FEATURES = EEP_FEATURES + %i[ EEU_FEATURES = EEP_FEATURES + %i[
cluster_health cluster_health
container_scanning container_scanning
credentials_inventory
dast dast
dependency_scanning dependency_scanning
epics epics
......
.table-holder
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-25{ role: 'rowheader' }= _('Owner')
.table-section.section-30{ role: 'rowheader' }= _('Scope')
.table-section.section-10{ role: 'rowheader' }= _('Created On')
.table-section.section-10{ role: 'rowheader' }= _('Expiration')
= render partial: 'admin/credentials/personal_access_tokens/personal_access_token', collection: credentials
.table-holder
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-40{ role: 'rowheader' }= _('Owner')
.table-section.section-15{ role: 'rowheader' }= _('Created On')
.table-section.section-15{ role: 'rowheader' }= _('Last Accessed On')
= render partial: 'admin/credentials/ssh_keys/ssh_key', collection: credentials
- page_title "Credentials"
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left
= icon('angle-left')
.fade-right
= icon('angle-right')
%ul.nav-links.nav.nav-tabs.scrolling-tabs
= nav_link(html_options: { class: active_when(show_personal_access_tokens?) }) do
= link_to admin_credentials_path(filter: 'personal_access_tokens') do
= s_('AdminCredentials|Personal Access Tokens')
= nav_link(html_options: { class: active_when(show_ssh_keys?) }) do
= link_to admin_credentials_path(filter: 'ssh_keys') do
= s_('AdminCredentials|SSH Keys')
- if @credentials.empty?
.nothing-here-block.border-top-0
= s_('AdminUsers|No credentials found')
- else
- if show_personal_access_tokens?
= render partial: 'admin/credentials/personal_access_tokens', locals: { credentials: @credentials }
- elsif show_ssh_keys?
= render partial: 'admin/credentials/ssh_keys', locals: { credentials: @credentials }
= paginate_without_count @credentials
.gl-responsive-table-row{ role: 'row', data: { qa_selector: 'credentials_personal_access_token_row_content' } }
.table-section.section-25
.table-mobile-header{ role: 'rowheader' }
= _('Owner')
.table-mobile-content
= render 'admin/users/user_detail', user: personal_access_token.user
.table-section.section-30
.table-mobile-header{ role: 'rowheader' }
= _('Scope')
.table-mobile-content
- scopes = personal_access_token.scopes
= scopes.present? ? scopes.join(", ") : _('No Scopes')
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Created On')
.table-mobile-content
= personal_access_token.created_at.to_date
.table-section.section-10
.table-mobile-header{ role: 'rowheader' }
= _('Expiration')
.table-mobile-content
- if personal_access_token.expires?
= personal_access_token.expires_at
- else
= _('Never')
.gl-responsive-table-row{ role: 'row', data: { qa_selector: 'credentials_ssh_key_row_content' } }
.table-section.section-40
.table-mobile-header{ role: 'rowheader' }
= _('Owner')
.table-mobile-content
= render 'admin/users/user_detail', user: ssh_key.user
.table-section.section-15
.table-mobile-header{ role: 'rowheader' }
= _('Created On')
.table-mobile-content
= ssh_key.created_at.to_date
.table-section.section-15
.table-mobile-header{ role: 'rowheader' }
= _('Last Accessed On')
.table-mobile-content
= (last_used_at = ssh_key.last_used_at).present? ? last_used_at.to_date : _('Never')
- if credentials_inventory_feature_available?
= nav_link(controller: :credentials) do
= link_to admin_credentials_path do
.nav-icon-container
= sprite_icon('lock')
%span.nav-item-name
= _('Credentials')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :credentials, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_credentials_path do
%strong.fly-out-top-item-name
= _('Credentials')
---
title: Introduce Credentials Inventory
merge_request: 20912
author:
type: added
...@@ -20,6 +20,7 @@ namespace :admin do ...@@ -20,6 +20,7 @@ namespace :admin do
resource :push_rule, only: [:show, :update] resource :push_rule, only: [:show, :update]
resource :email, only: [:show, :create] resource :email, only: [:show, :create]
resources :audit_logs, controller: 'audit_logs', only: [:index] resources :audit_logs, controller: 'audit_logs', only: [:index]
resources :credentials, only: [:index]
resource :license, only: [:show, :new, :create, :destroy] do resource :license, only: [:show, :new, :create, :destroy] do
get :download, on: :member get :download, on: :member
......
# frozen_string_literal: true
require 'spec_helper'
describe Admin::CredentialsController do
describe 'GET #index' do
context 'admin user' do
before do
sign_in(create(:admin))
end
context 'when `credentials_inventory` feature is enabled' do
before do
stub_licensed_features(credentials_inventory: true)
end
it 'responds with 200' do
get :index
expect(response).to have_gitlab_http_status(200)
end
describe 'filtering by type of credential' do
let_it_be(:personal_access_tokens) { create_list(:personal_access_token, 2) }
shared_examples_for 'filtering by `personal_access_tokens`' do
it do
get :index, params: params
expect(assigns(:credentials)).to match_array(personal_access_tokens)
end
end
context 'no credential type specified' do
let(:params) { {} }
it_behaves_like 'filtering by `personal_access_tokens`'
end
context 'non-existent credential type specified' do
let(:params) { { filter: 'non_existent_credential_type' } }
it_behaves_like 'filtering by `personal_access_tokens`'
end
context 'credential type specified as `personal_access_tokens`' do
let(:params) { { filter: 'personal_access_tokens' } }
it_behaves_like 'filtering by `personal_access_tokens`'
end
context 'credential type specified as `ssh_keys`' do
it 'filters by ssh keys' do
ssh_keys = create_list(:personal_key, 2)
get :index, params: { filter: 'ssh_keys' }
expect(assigns(:credentials)).to match_array(ssh_keys)
end
end
end
end
context 'when `credentials_inventory` feature is disabled' do
before do
stub_licensed_features(credentials_inventory: false)
end
it 'returns 404' do
get :index
expect(response).to have_gitlab_http_status(404)
end
end
end
context 'non-admin user' do
before do
sign_in(create(:user))
end
it 'returns 404' do
get :index
expect(response).to have_gitlab_http_status(404)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Admin::CredentialsHelper do
let(:filter) { nil }
before do
controller.params[:filter] = filter
end
describe '#credentials_inventory_feature_available?' do
subject { credentials_inventory_feature_available? }
context 'when credentials inventory feature is enabled' do
before do
stub_licensed_features(credentials_inventory: true)
end
it { is_expected.to be_truthy }
end
context 'when credentials inventory feature is disabled' do
before do
stub_licensed_features(credentials_inventory: false)
end
it { is_expected.to be_falsey }
end
end
describe '#show_ssh_keys?' do
subject { show_ssh_keys? }
context 'when filtering by ssh_keys' do
let(:filter) { 'ssh_keys' }
it { is_expected.to be_truthy }
end
context 'when filtering by a different, existent credential type' do
let(:filter) { 'personal_access_tokens' }
it { is_expected.to be_falsey }
end
context 'when filtering by a different, non-existent credential type' do
let(:filter) { 'non-existent-filter' }
it { is_expected.to be_falsey }
end
end
describe '#show_personal_access_tokens?' do
subject { show_personal_access_tokens? }
context 'when filtering by personal_access_tokens' do
let(:filter) { 'personal_access_tokens' }
it { is_expected.to be_truthy }
end
context 'when filtering by a different, existent credential type' do
let(:filter) { 'ssh_keys' }
it { is_expected.to be_falsey }
end
context 'when filtering by a different, non-existent credential type' do
let(:filter) { 'non-existent-filter' }
it { is_expected.to be_truthy }
end
end
end
...@@ -26,7 +26,9 @@ module API ...@@ -26,7 +26,9 @@ module API
get do get do
authenticated_with_full_private_access! authenticated_with_full_private_access!
key = KeysFinder.new(current_user, params).execute finder_params = params.merge(key_type: 'ssh')
key = KeysFinder.new(current_user, finder_params).execute
not_found!('Key') unless key not_found!('Key') unless key
present key, with: Entities::SSHKeyWithUser, current_user: current_user present key, with: Entities::SSHKeyWithUser, current_user: current_user
......
...@@ -1184,6 +1184,12 @@ msgstr "" ...@@ -1184,6 +1184,12 @@ msgstr ""
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running." msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr "" msgstr ""
msgid "AdminCredentials|Personal Access Tokens"
msgstr ""
msgid "AdminCredentials|SSH Keys"
msgstr ""
msgid "AdminDashboard|Error loading the statistics. Please try again" msgid "AdminDashboard|Error loading the statistics. Please try again"
msgstr "" msgstr ""
...@@ -1328,6 +1334,9 @@ msgstr "" ...@@ -1328,6 +1334,9 @@ msgstr ""
msgid "AdminUsers|New user" msgid "AdminUsers|New user"
msgstr "" msgstr ""
msgid "AdminUsers|No credentials found"
msgstr ""
msgid "AdminUsers|No users found" msgid "AdminUsers|No users found"
msgstr "" msgstr ""
...@@ -5117,6 +5126,9 @@ msgstr "" ...@@ -5117,6 +5126,9 @@ msgstr ""
msgid "Created At" msgid "Created At"
msgstr "" msgstr ""
msgid "Created On"
msgstr ""
msgid "Created a branch and a merge request to resolve this issue." msgid "Created a branch and a merge request to resolve this issue."
msgstr "" msgstr ""
...@@ -5165,6 +5177,9 @@ msgstr "" ...@@ -5165,6 +5177,9 @@ msgstr ""
msgid "Creation date" msgid "Creation date"
msgstr "" msgstr ""
msgid "Credentials"
msgstr ""
msgid "Critical vulnerabilities present" msgid "Critical vulnerabilities present"
msgstr "" msgstr ""
...@@ -7208,6 +7223,9 @@ msgstr "" ...@@ -7208,6 +7223,9 @@ msgstr ""
msgid "Expand up" msgid "Expand up"
msgstr "" msgstr ""
msgid "Expiration"
msgstr ""
msgid "Expiration date" msgid "Expiration date"
msgstr "" msgstr ""
...@@ -10237,6 +10255,9 @@ msgstr[1] "" ...@@ -10237,6 +10255,9 @@ msgstr[1] ""
msgid "Last %{days} days" msgid "Last %{days} days"
msgstr "" msgstr ""
msgid "Last Accessed On"
msgstr ""
msgid "Last Pipeline" msgid "Last Pipeline"
msgstr "" msgstr ""
...@@ -11721,6 +11742,9 @@ msgstr "" ...@@ -11721,6 +11742,9 @@ msgstr ""
msgid "No Milestone" msgid "No Milestone"
msgstr "" msgstr ""
msgid "No Scopes"
msgstr ""
msgid "No Tag" msgid "No Tag"
msgstr "" msgstr ""
......
...@@ -3,74 +3,145 @@ ...@@ -3,74 +3,145 @@
require 'spec_helper' require 'spec_helper'
describe KeysFinder do describe KeysFinder do
subject(:keys_finder) { described_class.new(user, params) } subject { described_class.new(user, params).execute }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:fingerprint_type) { 'md5' } let(:params) { {} }
let(:fingerprint) { 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1' }
let(:params) do let!(:key_1) do
{ create(:personal_key,
type: fingerprint_type, last_used_at: 7.days.ago,
fingerprint: fingerprint user: user,
}
end
let!(:key) do
create(:key, user: user,
key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=', key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=',
fingerprint: 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1', fingerprint: 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1',
fingerprint_sha256: 'nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg' fingerprint_sha256: 'nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg')
)
end end
let!(:key_2) { create(:personal_key, last_used_at: nil, user: user) }
let!(:key_3) { create(:personal_key, last_used_at: 2.days.ago) }
context 'with a regular user' do context 'with a regular user' do
it 'raises GitLabAccessDeniedError' do it 'raises GitLabAccessDeniedError' do
expect do expect { subject }.to raise_error(KeysFinder::GitLabAccessDeniedError)
keys_finder.execute
end.to raise_error(KeysFinder::GitLabAccessDeniedError)
end end
end end
context 'with an admin user' do context 'with an admin user' do
let(:user) {create(:admin)} let(:user) {create(:admin)}
context 'key_type' do
let!(:deploy_key) { create(:deploy_key) }
context 'when `key_type` is `ssh`' do
before do
params[:key_type] = 'ssh'
end
it 'returns only SSH keys' do
expect(subject).to contain_exactly(key_1, key_2, key_3)
end
end
context 'when `key_type` is not specified' do
it 'returns all types of keys' do
expect(subject).to contain_exactly(key_1, key_2, key_3, deploy_key)
end
end
end
context 'fingerprint' do
context 'with invalid fingerprint' do
context 'with invalid MD5 fingerprint' do context 'with invalid MD5 fingerprint' do
let(:fingerprint) { '11:11:11:11' } before do
params[:fingerprint] = '11:11:11:11'
end
it 'raises InvalidFingerprint' do it 'raises InvalidFingerprint' do
expect { keys_finder.execute } expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
.to raise_error(KeysFinder::InvalidFingerprint)
end end
end end
context 'with invalid SHA fingerprint' do context 'with invalid SHA fingerprint' do
let(:fingerprint_type) { 'sha256' } before do
let(:fingerprint) { 'nUhzNyftwAAKs7HufskYTte2g' } params[:fingerprint] = 'nUhzNyftwAAKs7HufskYTte2g'
end
it 'raises InvalidFingerprint' do it 'raises InvalidFingerprint' do
expect { keys_finder.execute } expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
.to raise_error(KeysFinder::InvalidFingerprint) end
end end
end end
context 'with valid fingerprints' do
context 'with valid MD5 params' do context 'with valid MD5 params' do
it 'returns key if the fingerprint is found' do context 'with an existent fingerprint' do
result = keys_finder.execute before do
params[:fingerprint] = 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1'
end
expect(result).to eq(key) it 'returns the key' do
expect(key.user).to eq(user) expect(subject).to eq(key_1)
expect(subject.user).to eq(user)
end
end
context 'with a non-existent fingerprint' do
before do
params[:fingerprint] = 'bb:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d2'
end
it 'returns nil' do
expect(subject).to be_nil
end
end end
end end
context 'with valid SHA256 params' do context 'with valid SHA256 params' do
let(:fingerprint) { 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1' } context 'with an existent fingerprint' do
before do
params[:fingerprint] = 'SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
end
it 'returns key' do
expect(subject).to eq(key_1)
expect(subject.user).to eq(user)
end
end
it 'returns key if the fingerprint is found' do context 'with a non-existent fingerprint' do
result = keys_finder.execute before do
params[:fingerprint] = 'SHA256:xTjuFqftwADy8AH3wFY31tAKs7HufskYTte2aXi/mNp'
end
it 'returns nil' do
expect(subject).to be_nil
end
end
end
end
end
context 'user' do
context 'without user' do
it 'contains ssh_keys of all users in the system' do
expect(subject).to contain_exactly(key_1, key_2, key_3)
end
end
context 'with user' do
before do
params[:user] = user
end
it 'contains ssh_keys of only the specified users' do
expect(subject).to contain_exactly(key_1, key_2)
end
end
end
expect(result).to eq(key) context 'sort order' do
expect(key.user).to eq(user) it 'sorts in last_used_at_desc order' do
expect(subject).to eq([key_3, key_1, key_2])
end end
end end
end end
......
...@@ -26,6 +26,16 @@ describe PersonalAccessTokensFinder do ...@@ -26,6 +26,16 @@ describe PersonalAccessTokensFinder do
revoked_impersonation_token, expired_impersonation_token) revoked_impersonation_token, expired_impersonation_token)
end end
describe 'with sort order' do
before do
params[:sort] = 'id_asc'
end
it 'sorts records as per the specified sort order' do
expect(subject).to match_array(PersonalAccessToken.all.order(id: :asc))
end
end
describe 'without impersonation' do describe 'without impersonation' do
before do before do
params[:impersonation] = false params[:impersonation] = false
......
...@@ -50,6 +50,32 @@ describe Key, :mailer do ...@@ -50,6 +50,32 @@ describe Key, :mailer do
end end
end end
describe 'scopes' do
describe '.for_user' do
let(:user_1) { create(:user) }
let(:key_of_user_1) { create(:personal_key, user: user_1) }
before do
create_list(:personal_key, 2, user: create(:user))
end
it 'returns keys of the specified user only' do
expect(described_class.for_user(user_1)).to contain_exactly(key_of_user_1)
end
end
describe '.order_last_used_at_desc' do
it 'sorts by last_used_at descending, with null values at last' do
key_1 = create(:personal_key, last_used_at: 7.days.ago)
key_2 = create(:personal_key, last_used_at: nil)
key_3 = create(:personal_key, last_used_at: 2.days.ago)
expect(described_class.order_last_used_at_desc)
.to eq([key_3, key_1, key_2])
end
end
end
context "validation of uniqueness (based on fingerprint uniqueness)" do context "validation of uniqueness (based on fingerprint uniqueness)" do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -21,6 +21,18 @@ describe PersonalAccessToken do ...@@ -21,6 +21,18 @@ describe PersonalAccessToken do
end end
end end
describe 'scopes' do
describe '.for_user' do
it 'returns personal access tokens of specified user only' do
user_1 = create(:user)
token_of_user_1 = create(:personal_access_token, user: user_1)
create_list(:personal_access_token, 2)
expect(described_class.for_user(user_1)).to contain_exactly(token_of_user_1)
end
end
end
describe ".active?" do describe ".active?" do
let(:active_personal_access_token) { build(:personal_access_token) } let(:active_personal_access_token) { build(:personal_access_token) }
let(:revoked_personal_access_token) { build(:personal_access_token, :revoked) } let(:revoked_personal_access_token) { build(:personal_access_token, :revoked) }
......
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