Commit a1b120e4 authored by Mark Chao's avatar Mark Chao

Merge branch '343859-admin-membership-export-background' into 'master'

Move admin memberships CSV export to background job

See merge request gitlab-org/gitlab!80391
parents 8cdb0671 3a94d95f
...@@ -172,6 +172,7 @@ By default, impersonation is enabled. GitLab can be configured to [disable imper ...@@ -172,6 +172,7 @@ By default, impersonation is enabled. GitLab can be configured to [disable imper
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1772) in GitLab 13.8. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1772) in GitLab 13.8.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/292436) in GitLab 13.9. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/292436) in GitLab 13.9.
> - [Moved to delivery by email](https://gitlab.com/gitlab-org/gitlab/-/issues/343859) in GitLab 14.8.
An administrator can export user permissions for all users in the GitLab instance from the Admin Area's Users page. An administrator can export user permissions for all users in the GitLab instance from the Admin Area's Users page.
The export lists direct membership the users have in groups and projects. The export lists direct membership the users have in groups and projects.
...@@ -189,6 +190,11 @@ Only the first 100,000 user accounts are exported. ...@@ -189,6 +190,11 @@ Only the first 100,000 user accounts are exported.
![user permission export button](img/export_permissions_v13_11.png) ![user permission export button](img/export_permissions_v13_11.png)
GitLab creates a CSV file and:
- In GitLab 14.7 and earlier, the file is downloaded in your browser.
- In GitLab 14.8 and later, the file is sent to your primary email address.
#### Users statistics #### Users statistics
The **Users statistics** page provides an overview of user accounts by role. These statistics are The **Users statistics** page provides an overview of user accounts by role. These statistics are
......
...@@ -6,29 +6,15 @@ class Admin::UserPermissionExportsController < Admin::ApplicationController ...@@ -6,29 +6,15 @@ class Admin::UserPermissionExportsController < Admin::ApplicationController
before_action :check_user_permission_export_availability! before_action :check_user_permission_export_availability!
def index def index
response = ::UserPermissions::ExportService.new(current_user).csv_data ::Admin::MembershipsMailer.instance_memberships_export(requested_by: current_user).deliver_later
respond_to do |format| flash[:success] = _('Report is generating and will be sent to your email address.')
format.csv do
if response.success?
stream_csv_headers(csv_filename)
self.response_body = response.payload
else
flash[:alert] = _('Failed to generate report, please try again after sometime')
redirect_to admin_users_path redirect_to admin_users_path
end end
end
end
end
private private
def csv_filename
"user-permissions-export-#{Time.current.to_i}.csv"
end
def check_user_permission_export_availability! def check_user_permission_export_availability!
render_404 unless current_user.can?(:export_user_permissions) render_404 unless current_user.can?(:export_user_permissions)
end end
......
# frozen_string_literal: true
class Admin::MembershipsMailer < ApplicationMailer
helper EmailsHelper
layout 'mailer'
def instance_memberships_export(requested_by:)
filename = "gitlab_memberships_#{Date.current.iso8601}.csv"
csv_data = UserPermissions::ExportService.new.execute.payload[:csv_data]
attachments[filename] = { content: csv_data, mime_type: 'text/csv' }
mail(
to: requested_by.notification_email_or_default,
subject: _('GitLab Memberships CSV Export')
)
end
end
...@@ -2,26 +2,14 @@ ...@@ -2,26 +2,14 @@
module UserPermissions module UserPermissions
class ExportService class ExportService
def initialize(current_user) def execute
@current_user = current_user ServiceResponse.success(payload: { csv_data: csv_builder.render })
end
def csv_data
return ServiceResponse.error(message: 'Insufficient permissions') unless allowed?
ServiceResponse.success(payload: csv_builder.render)
end end
private private
attr_reader :current_user
def allowed?
current_user.can?(:export_user_permissions)
end
def csv_builder def csv_builder
@csv_builder ||= CsvBuilders::Stream.new(data, header_to_value_hash) @csv_builder ||= CsvBuilder.new(data, header_to_value_hash)
end end
def data def data
......
%p{ style: 'font-size:18px; text-align:center; line-height:30px;' }
= _('The CSV export you requested of all user memberships is attached to this email.')
The CSV export you requested of all user memberships is attached to this email.
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::MembershipsMailer do
include EmailSpec::Matchers
describe '.instance_memberships_export' do
let(:user) { create(:admin) }
subject { described_class.instance_memberships_export(requested_by: user) }
it 'contains memberships csv as an attachment' do
freeze_time do
expect(subject.attachments.size).to eq(1)
expect(subject.attachments[0].content_type).to eq('text/csv')
expect(subject.attachments[0].filename).to eq("gitlab_memberships_#{Date.current.iso8601}.csv")
end
end
it { is_expected.to have_subject 'GitLab Memberships CSV Export' }
it { is_expected.to deliver_to user.notification_email_or_default }
it { is_expected.to have_body_text('The CSV export you requested of all user memberships is attached to this email.') }
end
end
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Admin::UserPermissionExportsController do RSpec.describe Admin::UserPermissionExportsController do
let_it_be(:admin) { create(:admin) } let_it_be(:admin) { create(:admin) }
subject { get admin_user_permission_exports_path(format: :csv) } subject { get admin_user_permission_exports_path }
before do before do
allow(admin).to receive(:can?).and_call_original allow(admin).to receive(:can?).and_call_original
...@@ -14,82 +14,18 @@ RSpec.describe Admin::UserPermissionExportsController do ...@@ -14,82 +14,18 @@ RSpec.describe Admin::UserPermissionExportsController do
end end
describe '#index', :enable_admin_mode do describe '#index', :enable_admin_mode do
context 'when user is authorized' do context 'when authorized' do
let(:authorized) { true } let(:authorized) { true }
before do it 'redirects back to admin users list with notice' do
allow(UserPermissions::ExportService).to receive(:new).and_return(export_csv_service)
end
context 'when successful' do
let(:csv_data) do
<<~CSV
Username,Email,Type,Path,Access,Last Activity
alvina,alvina@test.com,Group,gitlab-org,Developer,2020-12-18
jasper,jasper@test.com,Project,gitlab-org/www,Maintainer,2020-12-16
CSV
end
let(:export_csv_service) do
instance_spy(UserPermissions::ExportService, csv_data: ServiceResponse.success(payload: csv_data))
end
it 'responds with :ok', :aggregate_failures do
subject subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8; header=present')
end
it 'invokes the Export Service' do
subject
expect(export_csv_service).to have_received(:csv_data)
end
it 'has the appropriate data' do
subject
expect(csv_response).to eq([
[
'Username',
'Email',
'Type',
'Path',
'Access',
'Last Activity'
],
%w(
alvina
alvina@test.com
Group
gitlab-org
Developer
2020-12-18
),
%w(
jasper
jasper@test.com
Project
gitlab-org/www
Maintainer
2020-12-16
)
])
end
end
context 'when Export fails' do
let(:export_csv_service) do
instance_spy(UserPermissions::ExportService, csv_data: ServiceResponse.error(message: 'Something went wrong!'))
end
it 'responds appropriately', :aggregate_failures do
subject
expect(flash[:alert]).to eq 'Failed to generate report, please try again after sometime'
expect(response).to redirect_to(admin_users_path) expect(response).to redirect_to(admin_users_path)
expect(flash[:success]).to eq('Report is generating and will be sent to your email address.')
end end
it 'enqueues a job to generate the CSV file' do
expect { subject }.to have_enqueued_mail(::Admin::MembershipsMailer, :instance_memberships_export)
end end
end end
...@@ -102,9 +38,5 @@ RSpec.describe Admin::UserPermissionExportsController do ...@@ -102,9 +38,5 @@ RSpec.describe Admin::UserPermissionExportsController do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
def csv_response
CSV.parse(response.body)
end
end end
end end
...@@ -3,59 +3,13 @@ ...@@ -3,59 +3,13 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe UserPermissions::ExportService do RSpec.describe UserPermissions::ExportService do
let(:service) { described_class.new(current_user) } let(:service) { described_class.new }
let_it_be(:admin) { create(:admin) } let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user, username: 'Jessica', email: 'jessica@test.com', last_activity_on: Date.new(2020, 12, 16)) } let_it_be(:user) { create(:user, username: 'Jessica', email: 'jessica@test.com', last_activity_on: Date.new(2020, 12, 16)) }
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 context 'data verification', :enable_admin_mode do
subject(:csv) { CSV.parse(service.csv_data.payload.to_a.join, headers: true) } subject(:csv) { CSV.parse(service.execute.payload[:csv_data], headers: true) }
let_it_be(:current_user) { admin } let_it_be(:current_user) { admin }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
......
...@@ -15032,9 +15032,6 @@ msgstr "" ...@@ -15032,9 +15032,6 @@ msgstr ""
msgid "Failed to generate export, please try again later." msgid "Failed to generate export, please try again later."
msgstr "" msgstr ""
msgid "Failed to generate report, please try again after sometime"
msgstr ""
msgid "Failed to get ref." msgid "Failed to get ref."
msgstr "" msgstr ""
...@@ -16510,6 +16507,9 @@ msgstr "" ...@@ -16510,6 +16507,9 @@ msgstr ""
msgid "GitLab KAS" msgid "GitLab KAS"
msgstr "" msgstr ""
msgid "GitLab Memberships CSV Export"
msgstr ""
msgid "GitLab Pages" msgid "GitLab Pages"
msgstr "" msgstr ""
...@@ -30678,6 +30678,9 @@ msgstr "" ...@@ -30678,6 +30678,9 @@ msgstr ""
msgid "Report abuse to admin" msgid "Report abuse to admin"
msgstr "" msgstr ""
msgid "Report is generating and will be sent to your email address."
msgstr ""
msgid "Reported %{timeAgo} by %{reportedBy}" msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr "" msgstr ""
...@@ -36346,6 +36349,9 @@ msgstr "" ...@@ -36346,6 +36349,9 @@ msgstr ""
msgid "The CSV export will be created in the background. Once finished, it will be sent to %{email} in an attachment." msgid "The CSV export will be created in the background. Once finished, it will be sent to %{email} in an attachment."
msgstr "" msgstr ""
msgid "The CSV export you requested of all user memberships is attached to this email."
msgstr ""
msgid "The GitLab subscription service (customers.gitlab.com) is currently experiencing an outage. You can monitor the status and get updates at %{linkStart}status.gitlab.com%{linkEnd}." msgid "The GitLab subscription service (customers.gitlab.com) is currently experiencing an outage. You can monitor the status and get updates at %{linkStart}status.gitlab.com%{linkEnd}."
msgstr "" msgstr ""
......
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