Commit 2be1a2a2 authored by Aishwarya Subramanian's avatar Aishwarya Subramanian Committed by Imre Farkas

Adds query to export User Permissions

Adds a service that generates a CSV export of User
Permissions in an instance.
The service is available to only admins in tiers Premium
and above.
parent f9041604
......@@ -5,6 +5,12 @@ module EE
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do
scope :with_csv_entity_associations, -> do
includes(:user, source: [:route, :parent])
end
end
class_methods do
extend ::Gitlab::Utils::Override
......@@ -44,5 +50,9 @@ module EE
user.using_license_seat?
end
# rubocop: enable Naming/PredicateName
def source_kind
source.is_a?(Group) && source.parent.present? ? 'Sub group' : source.class.to_s
end
end
end
......@@ -128,6 +128,7 @@ class License < ApplicationRecord
ci_project_subscriptions
incident_timeline_view
oncall_schedules
export_user_permissions
]
EEP_FEATURES.freeze
......
......@@ -17,6 +17,10 @@ module EE
License.feature_available?(:adjourned_deletion_for_projects_and_groups)
end
condition(:export_user_permissions_available) do
::License.feature_available?(:export_user_permissions) && ::Feature.enabled?(:export_user_permissions_feature_flag)
end
rule { ~anonymous & operations_dashboard_available }.enable :read_operations_dashboard
rule { admin }.policy do
......@@ -39,6 +43,8 @@ module EE
rule { admin & adjourned_project_deletion_available }.policy do
enable :list_removable_projects
end
rule { export_user_permissions_available & admin }.enable :export_user_permissions
end
end
end
# frozen_string_literal: true
module Members
class PermissionsExportService
def initialize(current_user)
@current_user = current_user
end
def csv_data
return ServiceResponse.error(message: 'Insufficient permissions') unless allowed?
ServiceResponse.success(payload: csv_builder.render)
end
private
attr_reader :current_user
def allowed?
current_user.can?(:export_user_permissions)
end
def csv_builder
@csv_builder ||= CsvBuilders::Stream.new(data, header_to_value_hash)
end
def data
Member
.active_without_invites_and_requests
.with_csv_entity_associations
end
def header_to_value_hash
{
'Username' => 'user_username',
'Email' => 'user_email',
'Type' => 'source_kind',
'Path' => -> (member) { member.source&.full_path },
'Access Level' => 'human_access'
}
end
end
end
---
name: export_user_permissions_feature_flag
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49399
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292436
milestone: '13.7'
type: development
group: group::compliance
default_enabled: false
......@@ -3,6 +3,14 @@
require 'spec_helper'
RSpec.describe Member, type: :model do
let_it_be(:user) { build :user }
let_it_be(:group) { create :group }
let_it_be(:member) { build :group_member, group: group, user: user }
let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:sub_group_member) { build(:group_member, group: sub_group, user: user) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:project_member) { build(:project_member, project: project, user: user) }
describe '#notification_service' do
it 'returns a NullNotificationService instance for LDAP users' do
member = described_class.new
......@@ -15,10 +23,6 @@ RSpec.describe Member, type: :model do
end
describe '#is_using_seat', :aggregate_failures do
let(:user) { build :user }
let(:group) { create :group }
let(:member) { build :group_member, group: group, user: user }
context 'when hosted on GL.com' do
before do
allow(Gitlab).to receive(:com?).and_return true
......@@ -43,4 +47,24 @@ RSpec.describe Member, type: :model do
end
end
end
describe '#source_kind' do
subject { member.source_kind }
context 'when source is of Group kind' do
it { is_expected.to eq('Group') }
end
context 'when source is of Sub group kind' do
let(:member) { sub_group_member }
it { is_expected.to eq('Sub group') }
end
context 'when source is of Project kind' do
let(:member) { project_member }
it { is_expected.to eq('Project') }
end
end
end
......@@ -237,4 +237,35 @@ RSpec.describe GlobalPolicy do
end
end
end
describe ':export_user_permissions', :enable_admin_mode do
using RSpec::Parameterized::TableSyntax
let(:policy) { :export_user_permissions }
let_it_be(:admin) { build_stubbed(:admin) }
let_it_be(:guest) { build_stubbed(:user) }
where(:role, :flag_enabled, :licensed, :allowed) do
:admin | true | true | true
:admin | true | false | false
:admin | false | true | false
:admin | false | false | false
:guest | true | true | false
:guest | true | false | false
:guest | false | true | false
:guest | false | false | false
end
with_them do
let(:current_user) { public_send(role) }
before do
stub_licensed_features(export_user_permissions: licensed)
stub_feature_flags(export_user_permissions_feature_flag: flag_enabled)
end
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::PermissionsExportService do
let(:service) { described_class.new(current_user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user, username: 'Jessica', email: 'jessica@test.com') }
context 'access' do
shared_examples 'allowed to export user permissions' do
it { expect(service.csv_data).to be_success }
end
shared_examples 'not allowed to export user permissions' do
it { expect(service.csv_data).not_to be_success }
end
before do
stub_licensed_features(export_user_permissions: licensed)
end
context 'when user is an admin', :enable_admin_mode do
let(:current_user) { admin }
context 'when licensed' do
let(:licensed) { true }
it_behaves_like 'allowed to export user permissions'
end
context 'when not licensed' do
let(:licensed) { false }
it_behaves_like 'not allowed to export user permissions'
end
end
context 'when user is not an admin' do
let(:current_user) { user }
context 'when licensed' do
let(:licensed) { true }
it_behaves_like 'not allowed to export user permissions'
end
context 'when not licensed' do
let(:licensed) { false }
it_behaves_like 'not allowed to export user permissions'
end
end
end
context 'data verification', :enable_admin_mode do
subject(:csv) { CSV.parse(service.csv_data.payload.to_a.join, headers: true) }
let_it_be(:current_user) { admin }
let_it_be(:group) { create(:group) }
let_it_be(:group_owner) { create(:group_member, :owner, group: group, user: user) }
before do
stub_licensed_features(export_user_permissions: true)
end
it 'includes the appropriate headers' do
expect(csv.headers).to eq([
'Username', 'Email', 'Type', 'Path', 'Access Level'
])
end
specify 'Username' do
expect(csv[0]['Username']).to eq('Jessica')
end
specify 'Email' do
expect(csv[0]['Email']).to eq('jessica@test.com')
end
specify 'Type' do
expect(csv[0]['Type']).to eq('Group')
end
specify 'Path' do
expect(csv[0]['Path']).to eq(group.full_path)
end
specify 'Access Level' do
expect(csv[0]['Access Level']).to eq('Owner')
end
context 'when user is member of a sub group' do
let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:sub_group_user) { create(:user, username: 'Oliver', email: 'oliver@test.com') }
let_it_be(:sub_group_maintainer) { create(:group_member, :maintainer, group: sub_group, user: sub_group_user) }
it 'displays attributes correctly', :aggregate_failures do
row = csv.find { |row| row['Username'] == 'Oliver' }
expect(row['Path']).to eq(sub_group.full_path)
expect(row['Type']).to eq('Sub group')
expect(row['Access Level']).to eq('Maintainer')
end
end
context 'when user is member of a project' do
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:project_user) { create(:user, username: 'Theo', email: 'theo@test.com') }
let_it_be(:project_developer) { create(:project_member, :developer, project: project, user: project_user) }
it 'displays attributes correctly', :aggregate_failures do
row = csv.find { |row| row['Username'] == 'Theo' }
expect(row['Path']).to eq(project.full_path)
expect(row['Type']).to eq('Project')
expect(row['Access Level']).to eq('Developer')
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