Commit 7b9e13ba authored by manojmj's avatar manojmj

Introduce Credentials Inventory for Groups that enforce managed accounts

This change introduces Introduce Credentials Inventory
at the Group level for Groups that enforce Group Managed Accounts
parent de7e4aa7
......@@ -8,16 +8,13 @@ class KeysFinder
'md5' => 'fingerprint'
}.freeze
def initialize(current_user, params)
@current_user = current_user
def initialize(params)
@params = params
end
def execute
raise GitLabAccessDeniedError unless current_user.admin?
keys = by_key_type
keys = by_user(keys)
keys = by_users(keys)
keys = sort(keys)
by_fingerprint(keys)
......@@ -25,7 +22,7 @@ class KeysFinder
private
attr_reader :current_user, :params
attr_reader :params
def by_key_type
if params[:key_type] == 'ssh'
......@@ -39,10 +36,10 @@ class KeysFinder
keys.order_last_used_at_desc
end
def by_user(keys)
return keys unless params[:user]
def by_users(keys)
return keys unless params[:users]
keys.for_user(params[:user])
keys.for_user(params[:users])
end
def by_fingerprint(keys)
......
......@@ -81,6 +81,20 @@ Since use of the group-managed account requires the use of SSO, users of group-m
- The user will be unable to access the group (their credentials will no longer work on the identity provider when prompted to SSO).
- Contributions in the group (e.g. issues, merge requests) will remain intact.
##### Credentials inventory for Group-managed accounts **(ULTIMATE)**
> [Introduced in GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/38133)
Owners who manage user accounts in a group can view the following details of personal access tokens and SSH keys:
- Owners
- Scopes
- Usage patterns
To access the Credentials inventory of a group, navigate to **{shield}** **Security & Compliance > Credentials** in your group's sidebar.
This feature is similar to the [Credentials inventory for self-managed instances](../../admin_area/credentials_inventory.md).
#### Assertions
When using group-managed accounts, the following user details need to be passed to GitLab as SAML
......
# frozen_string_literal: true
class Admin::CredentialsController < Admin::ApplicationController
extend ::Gitlab::Utils::Override
include CredentialsInventoryActions
helper_method :credentials_inventory_path, :user_detail_path
before_action :check_license_credentials_inventory_available!, only: [:index]
private
def check_license_credentials_inventory_available!
render_404 unless credentials_inventory_feature_available?
end
override :credentials_inventory_path
def credentials_inventory_path(args)
admin_credentials_path(args)
end
override :user_detail_path
def user_detail_path(user)
admin_user_path(user)
end
def user
override :users
def users
nil
end
end
......@@ -4,10 +4,6 @@ module CredentialsInventoryActions
extend ActiveSupport::Concern
include CredentialsInventoryHelper
included do
before_action :check_license_credentials_inventory_available!, only: [:index]
end
def index
@credentials = filter_credentials.page(params[:page]).preload_users.without_count # rubocop:disable Gitlab/ModuleWithInstanceVariables
......@@ -18,17 +14,13 @@ module CredentialsInventoryActions
def filter_credentials
if show_personal_access_tokens?
::PersonalAccessTokensFinder.new({ user: user, impersonation: false, state: 'active', sort: 'id_desc' }).execute
::PersonalAccessTokensFinder.new({ user: users, impersonation: false, state: 'active', sort: 'id_desc' }).execute
elsif show_ssh_keys?
::KeysFinder.new(current_user, { user: user, key_type: 'ssh' }).execute
::KeysFinder.new({ users: users, key_type: 'ssh' }).execute
end
end
def check_license_credentials_inventory_available!
render_404 unless credentials_inventory_feature_available?
end
def user
def users
raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
end
# frozen_string_literal: true
class Groups::Security::ComplianceDashboardsController < Groups::ApplicationController
include Groups::SecurityFeaturesHelper
layout 'group'
before_action :authorize_compliance_dashboard!
......@@ -13,7 +15,6 @@ class Groups::Security::ComplianceDashboardsController < Groups::ApplicationCont
private
def authorize_compliance_dashboard!
render_404 unless group.feature_available?(:group_level_compliance_dashboard) &&
can?(current_user, :read_group_compliance_dashboard, group)
render_404 unless group_level_compliance_dashboard_available?(group)
end
end
# frozen_string_literal: true
class Groups::Security::CredentialsController < Groups::ApplicationController
layout 'group'
extend ::Gitlab::Utils::Override
include CredentialsInventoryActions
include Groups::SecurityFeaturesHelper
helper_method :credentials_inventory_path, :user_detail_path
before_action :validate_group_level_credentials_inventory_available!, only: [:index]
private
def validate_group_level_credentials_inventory_available!
render_404 unless group_level_credentials_inventory_available?(group)
end
override :credentials_inventory_path
def credentials_inventory_path(args)
group_security_credentials_path(args)
end
override :user_detail_path
def user_detail_path(user)
user_path(user)
end
override :users
def users
group.managed_users
end
end
# frozen_string_literal: true
module Groups::SecurityFeaturesHelper
def group_level_security_dashboard_available?(group)
group.feature_available?(:security_dashboard)
end
def group_level_compliance_dashboard_available?(group)
group.feature_available?(:group_level_compliance_dashboard) &&
can?(current_user, :read_group_compliance_dashboard, group)
end
def group_level_credentials_inventory_available?(group)
can?(current_user, :read_group_credentials_inventory, group) &&
group.feature_available?(:credentials_inventory) &&
group.enforced_group_managed_accounts?
end
def primary_group_level_security_feature_path(group)
if group_level_security_dashboard_available?(group)
group_security_dashboard_path(group)
elsif group_level_compliance_dashboard_available?(group)
group_security_compliance_dashboard_path(group)
elsif group_level_credentials_inventory_available?(group)
group_security_credentials_path(group)
end
end
end
......@@ -45,6 +45,7 @@ module EE
has_one :deletion_schedule, class_name: 'GroupDeletionSchedule'
delegate :deleting_user, :marked_for_deletion_on, to: :deletion_schedule, allow_nil: true
delegate :enforced_group_managed_accounts?, to: :saml_provider, allow_nil: true
belongs_to :file_template_project, class_name: "Project"
......
......@@ -135,6 +135,7 @@ module EE
rule { admin | owner }.policy do
enable :read_group_compliance_dashboard
enable :read_group_credentials_inventory
end
rule { needs_new_sso_session }.policy do
......
- compliance_dashboard_available = @group.feature_available?(:group_level_compliance_dashboard)
- security_dashboard_available = @group.feature_available?(:security_dashboard)
- if compliance_dashboard_available || security_dashboard_available
- main_path = compliance_dashboard_available ? group_security_compliance_dashboard_path(@group) : group_security_dashboard_path(@group)
= nav_link(path: %w[groups/security/compliance_dashboards#show groups/security/dashboard#show]) do
- main_path = primary_group_level_security_feature_path(@group)
- if main_path.present?
= nav_link(path: %w[dashboard#show compliance_dashboards#show credentials#index]) do
= link_to main_path, data: { qa_selector: 'security_compliance_link' } do
.nav-icon-container
= sprite_icon('shield')
%span.nav-item-name
= _('Security & Compliance')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'group_secure_submenu' } }
- if security_dashboard_available
= nav_link(path: 'groups/security/dashboard#show') do
- if group_level_security_dashboard_available?(@group)
= nav_link(path: 'dashboard#show') do
= link_to group_security_dashboard_path(@group), title: _('Security'), data: { qa_selector: 'security_dashboard_link' } do
%span= _('Security')
- if compliance_dashboard_available
= nav_link(path: 'groups/security/compliance_dashboards#show') do
- if group_level_compliance_dashboard_available?(@group)
= nav_link(path: 'compliance_dashboards#show') do
= link_to group_security_compliance_dashboard_path(@group), title: _('Compliance') do
%span= _('Compliance')
- if group_level_credentials_inventory_available?(@group)
= nav_link(path: 'credentials#index') do
= link_to group_security_credentials_path(@group), title: _('Credentials') do
%span= _('Credentials')
- elsif show_discover_group_security?(@group)
= nav_link(path: group_security_discover_path(@group)) do
= link_to group_security_discover_path(@group) do
......
---
title: Introduce Credentials Inventory for Groups that enforce Group Managed Accounts
merge_request: 23944
author:
type: added
......@@ -119,6 +119,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resource :compliance_dashboard, only: [:show]
resources :vulnerable_projects, only: [:index]
resource :discover, only: [:show], controller: :discover
resources :credentials, only: [:index]
resources :vulnerability_findings, only: [:index] do
collection do
......
# frozen_string_literal: true
require 'spec_helper'
describe Groups::Security::CredentialsController do
let_it_be(:group_with_managed_accounts) { create(:group_with_managed_accounts, :private) }
let_it_be(:managed_users) { create_list(:user, 2, :group_managed, managing_group: group_with_managed_accounts) }
before do
allow_next_instance_of(Gitlab::Auth::GroupSaml::SsoEnforcer) do |sso_enforcer|
allow(sso_enforcer).to receive(:active_session?).and_return(true)
end
owner = managed_users.first
group_with_managed_accounts.add_owner(owner)
sign_in(owner)
end
describe 'GET #index' do
let(:filter) {}
let(:group_id) { group_with_managed_accounts.to_param }
subject { get :index, params: { group_id: group_id.to_param, filter: filter } }
context 'when `credentials_inventory` feature is enabled' do
before do
stub_licensed_features(credentials_inventory: true)
end
context 'for a group that enforces group managed accounts' do
context 'for a user with access to view credentials inventory' do
it 'responds with 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
end
context 'filtering by type of credential' do
before do
managed_users.each do |user|
create(:personal_access_token, user: user)
end
end
shared_examples_for 'filtering by `personal_access_tokens`' do
it do
subject
expect(assigns(:credentials)).to match_array(PersonalAccessToken.where(user: managed_users))
end
end
context 'no credential type specified' do
let(:filter) { nil }
it_behaves_like 'filtering by `personal_access_tokens`'
end
context 'non-existent credential type specified' do
let(:filter) { 'non_existent_credential_type' }
it_behaves_like 'filtering by `personal_access_tokens`'
end
context 'credential type specified as `personal_access_tokens`' do
let(:filter) { 'personal_access_tokens' }
it_behaves_like 'filtering by `personal_access_tokens`'
end
context 'user scope' do
it 'does not show the credentials of a user outside the group' do
personal_access_token = create(:personal_access_token, user: create(:user))
subject
expect(assigns(:credentials)).not_to include(personal_access_token)
end
end
context 'credential type specified as `ssh_keys`' do
let(:filter) { 'ssh_keys' }
before do
managed_users.each do |user|
create(:personal_key, user: user)
end
end
it 'filters by ssh keys' do
subject
expect(assigns(:credentials)).to match_array(Key.regular_keys.where(user: managed_users))
end
end
end
context 'for a user without access to view credentials inventory' do
before do
maintainer = managed_users.last
group_with_managed_accounts.add_maintainer(maintainer)
sign_in(maintainer)
end
it 'responds with 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
context 'for a group that does not enforce group managed accounts' do
let(:group_id) { create(:group).to_param }
it 'responds with 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when `credentials_inventory` feature is disabled' do
before do
stub_licensed_features(credentials_inventory: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
......@@ -52,4 +52,12 @@ FactoryBot.define do
)
end
end
factory :group_with_managed_accounts, parent: :group do
after(:create) do |group, evaluator|
create(:saml_provider,
:enforced_group_managed_accounts,
group: group)
end
end
end
......@@ -7,7 +7,14 @@ FactoryBot.modify do
end
trait :group_managed do
association :managing_group, factory: :group
association :managing_group, factory: :group_with_managed_accounts
after(:create) do |user, evaluator|
create(:group_saml_identity,
user: user,
saml_provider: user.managing_group.saml_provider
)
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Groups::Security::Credentials' do
include Spec::Support::Helpers::Features::ResponsiveTableHelpers
let_it_be(:group_with_managed_accounts) { create(:group_with_managed_accounts, :private) }
let_it_be(:managed_user) { create(:user, :group_managed, managing_group: group_with_managed_accounts, name: 'David') }
let(:group_id) { group_with_managed_accounts.to_param }
before do
allow_next_instance_of(Gitlab::Auth::GroupSaml::SsoEnforcer) do |sso_enforcer|
allow(sso_enforcer).to receive(:active_session?).and_return(true)
end
group_with_managed_accounts.add_owner(managed_user)
sign_in(managed_user)
end
context 'licensed' do
before do
stub_licensed_features(credentials_inventory: true)
end
context 'links' do
before do
visit group_security_credentials_path(group_id: group_id)
end
it 'has Credentials Inventory link in sidebar' do
expect(page).to have_link('Credentials', href: group_security_credentials_path(group_id: group_id))
end
context 'tabs' do
it 'contains the relevant filter tabs' do
expect(page).to have_link('Personal Access Tokens', href: group_security_credentials_path(group_id: group_id, filter: 'personal_access_tokens'))
expect(page).to have_link('SSH Keys', href: group_security_credentials_path(group_id: group_id, filter: 'ssh_keys'))
end
end
end
context 'filtering' do
context 'by Personal Access Tokens' do
before do
create(:personal_access_token,
user: managed_user,
created_at: '2019-12-10',
expires_at: nil)
visit group_security_credentials_path(group_id: group_id, filter: 'personal_access_tokens')
end
it 'shows details of personal access tokens' do
expect(first_row.text).to include('David')
expect(first_row.text).to include('api')
expect(first_row.text).to include('2019-12-10')
expect(first_row.text).to include('Never')
end
end
context 'by SSH Keys' do
before do
create(:personal_key,
user: managed_user,
created_at: '2019-12-09',
last_used_at: '2019-12-10')
visit group_security_credentials_path(group_id: group_id, filter: 'ssh_keys')
end
it 'shows details of ssh keys' do
expect(first_row.text).to include('David')
expect(first_row.text).to include('2019-12-09')
expect(first_row.text).to include('2019-12-10')
end
end
end
end
context 'unlicensed' do
before do
stub_licensed_features(credentials_inventory: false)
end
it 'returns 400' do
visit group_security_credentials_path(group_id: group_id)
expect(page.status_code).to eq(404)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Groups::SecurityFeaturesHelper do
using RSpec::Parameterized::TableSyntax
let_it_be(:group, refind: true) { create(:group) }
let_it_be(:user, refind: true) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
describe '#group_level_security_dashboard_available?' do
where(:security_dashboard_feature_enabled, :result) do
true | true
false | false
end
with_them do
before do
stub_licensed_features(security_dashboard: security_dashboard_feature_enabled)
end
it 'returns the expected result' do
expect(helper.group_level_security_dashboard_available?(group)).to eq(result)
end
end
end
describe '#group_level_security_dashboard_available?' do
where(:group_level_compliance_dashboard_enabled, :read_group_compliance_dashboard_permission, :result) do
false | false | false
true | false | false
false | true | false
true | true | true
end
with_them do
before do
stub_licensed_features(group_level_compliance_dashboard: group_level_compliance_dashboard_enabled)
allow(helper).to receive(:can?).with(user, :read_group_compliance_dashboard, group).and_return(read_group_compliance_dashboard_permission)
end
it 'returns the expected result' do
expect(helper.group_level_compliance_dashboard_available?(group)).to eq(result)
end
end
end
describe '#group_level_credentials_inventory_available?' do
where(:credentials_inventory_feature_enabled, :enforced_group_managed_accounts, :read_group_credentials_inventory_permission, :result) do
true | false | false | false
true | true | false | false
true | false | true | false
true | true | true | true
false | false | false | false
false | false | false | false
false | false | true | false
false | true | true | false
end
with_them do
before do
stub_licensed_features(credentials_inventory: credentials_inventory_feature_enabled)
allow(group).to receive(:enforced_group_managed_accounts?).and_return(enforced_group_managed_accounts)
allow(helper).to receive(:can?).with(user, :read_group_credentials_inventory, group).and_return(read_group_credentials_inventory_permission)
end
it 'returns the expected result' do
expect(helper.group_level_credentials_inventory_available?(group)).to eq(result)
end
end
end
describe '#primary_group_level_security_feature_path' do
subject { helper.primary_group_level_security_feature_path(group) }
context 'group_level_security_dashboard is available' do
before do
allow(helper).to receive(:group_level_security_dashboard_available?).with(group).and_return(true)
end
it 'returns path to security dashboard' do
expect(subject).to eq(group_security_dashboard_path(group))
end
end
context 'group_level_compliance_dashboard is available' do
before do
allow(helper).to receive(:group_level_compliance_dashboard_available?).with(group).and_return(true)
end
it 'returns path to compliance dashboard' do
expect(subject).to eq(group_security_compliance_dashboard_path(group))
end
end
context 'group_level_credentials_inventory is available' do
before do
allow(helper).to receive(:group_level_credentials_inventory_available?).with(group).and_return(true)
end
it 'returns path to credentials inventory dashboard' do
expect(subject).to eq(group_security_credentials_path(group))
end
end
context 'when no security features are available' do
before do
allow(helper).to receive(:group_level_security_dashboard_available?).with(group).and_return(false)
allow(helper).to receive(:group_level_compliance_dashboard_available?).with(group).and_return(false)
allow(helper).to receive(:group_level_credentials_inventory_available?).with(group).and_return(false)
end
it 'returns nil' do
expect(subject).to be_nil
end
end
end
end
......@@ -371,6 +371,64 @@ describe GroupPolicy do
end
end
describe 'read_group_credentials_inventory' do
context 'with admin' do
let(:current_user) { admin }
it { is_expected.to be_allowed(:read_group_credentials_inventory) }
end
context 'with owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:read_group_credentials_inventory) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
it { is_expected.to be_disallowed(:read_group_credentials_inventory) }
end
context 'with developer' do
let(:current_user) { developer }
it { is_expected.to be_disallowed(:read_group_credentials_inventory) }
context 'when security dashboard features is not available' do
before do
stub_licensed_features(security_dashboard: false)
end
it { is_expected.to be_disallowed(:read_group_credentials_inventory) }
end
end
context 'with reporter' do
let(:current_user) { reporter }
it { is_expected.to be_disallowed(:read_group_credentials_inventory) }
end
context 'with guest' do
let(:current_user) { guest }
it { is_expected.to be_disallowed(:read_group_credentials_inventory) }
end
context 'with non member' do
let(:current_user) { create(:user) }
it { is_expected.to be_disallowed(:read_group_credentials_inventory) }
end
context 'with anonymous' do
let(:current_user) { nil }
it { is_expected.to be_disallowed(:read_group_credentials_inventory) }
end
end
describe 'read_group_security_dashboard' do
before do
stub_licensed_features(security_dashboard: true)
......
......@@ -8,6 +8,7 @@ describe 'layouts/nav/sidebar/_group' do
end
let(:group) { create(:group) }
let(:user) { create(:user) }
describe 'contribution analytics tab' do
it 'is not visible when there is no valid license and we dont show promotions' do
......@@ -96,6 +97,21 @@ describe 'layouts/nav/sidebar/_group' do
stub_licensed_features(group_level_compliance_dashboard: true)
end
context 'when the user does not have access to Compliance dashboard' do
it 'is not visible' do
render
expect(rendered).not_to have_link 'Security & Compliance'
expect(rendered).not_to have_link 'Compliance'
end
end
context 'when the user has access to Compliance dashboard' do
before do
group.add_owner(user)
allow(view).to receive(:current_user).and_return(user)
end
it 'is visible' do
render
......@@ -103,6 +119,50 @@ describe 'layouts/nav/sidebar/_group' do
expect(rendered).to have_link 'Compliance'
end
end
end
context 'when credentials inventory feature is enabled' do
shared_examples_for 'Credentials tab is not visible' do
it 'does not show the `Credentials` tab' do
render
expect(rendered).not_to have_link 'Security & Compliance'
expect(rendered).not_to have_link 'Credentials'
end
end
before do
stub_licensed_features(credentials_inventory: true)
end
context 'when the group does not enforce managed accounts' do
it_behaves_like 'Credentials tab is not visible'
end
context 'when the group enforces managed accounts' do
before do
allow(group).to receive(:enforced_group_managed_accounts?).and_return(true)
end
context 'when the user has privileges to view Credentials' do
before do
group.add_owner(user)
allow(view).to receive(:current_user).and_return(user)
end
it 'is visible' do
render
expect(rendered).to have_link 'Security & Compliance'
expect(rendered).to have_link 'Credentials'
end
end
context 'when the user does not have privileges to view Credentials' do
it_behaves_like 'Credentials tab is not visible'
end
end
end
context 'when security dashboard feature is disabled' do
let(:group) { create(:group, plan: :bronze_plan) }
......
......@@ -26,7 +26,7 @@ module API
get do
authenticated_with_can_read_all_resources!
key = KeysFinder.new(current_user, params).execute
key = KeysFinder.new(params).execute
not_found!('Key') unless key
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe KeysFinder do
subject { described_class.new(user, params).execute }
subject { described_class.new(params).execute }
let(:user) { create(:user) }
let(:params) { {} }
......@@ -20,15 +20,6 @@ describe KeysFinder do
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
it 'raises GitLabAccessDeniedError' do
expect { subject }.to raise_error(KeysFinder::GitLabAccessDeniedError)
end
end
context 'with an admin user' do
let(:user) {create(:admin)}
context 'key_type' do
let!(:deploy_key) { create(:deploy_key) }
......@@ -160,7 +151,7 @@ describe KeysFinder do
context 'with user' do
before do
params[:user] = user
params[:users] = user
end
it 'contains ssh_keys of only the specified users' do
......@@ -174,5 +165,4 @@ describe KeysFinder do
expect(subject).to eq([key_3, key_1, key_2])
end
end
end
end
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