Skip to content
代码片段 群组 项目
未验证 提交 bf327dd1 编辑于 作者: Carla Drago's avatar Carla Drago 提交者: GitLab
浏览文件

Merge branch 'jnutt/513794-csv-upload' into 'master'

Allow bulk reassignment by API endpoint

See merge request https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183690



Merged-by: default avatarCarla Drago <cdrago@gitlab.com>
Approved-by: default avatarRez <rmarandi@gitlab.com>
Approved-by: default avatarAshraf Khamis <akhamis@gitlab.com>
Approved-by: default avatarAlessio Caiazza <code.git@caiazza.info>
Approved-by: default avatarCarla Drago <cdrago@gitlab.com>
Reviewed-by: default avatarAshraf Khamis <akhamis@gitlab.com>
Co-authored-by: default avatarJames Nutt <jnutt@gitlab.com>
No related branches found
No related tags found
2 合并请求!3031Merge per-main-jh to main-jh by luzhiyuan,!3030Merge per-main-jh to main-jh
......@@ -30,7 +30,7 @@ Prerequisites:
Use the following endpoints to perform [bulk placeholder reassignment](../user/project/import/_index.md#request-reassignment-by-using-a-csv-file) without using the UI.
## Download the CSV reassignment spreadsheet
## Download the CSV file
Download a CSV file of pending reassignments.
......@@ -59,3 +59,31 @@ Source host,Import type,Source user identifier,Source user name,Source username,
http://gitlab.example,gitlab_migration,11,Bob,bob,"",""
http://gitlab.example,gitlab_migration,9,Alice,alice,"",""
```
## Reassign placeholders
Complete the [CSV file](#download-the-csv-file) and upload it to reassign placeholder users.
```plaintext
POST /groups/:id/placeholder_reassignments
```
Supported attributes:
| Attribute | Type | Required | Description |
| --------- | ----------------- | -------- | ----------- |
| `id` | integer or string | yes | ID of the group or [URL-encoded path of the group](rest/_index.md#namespaced-paths). |
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
--form "file=@placeholder_reassignments_for_group_2_1741253695.csv" \
"http://gdk.test:3000/api/v4/groups/2/placeholder_reassignments"
```
Example response:
```json
{"message":"The file is being processed and you will receive an email when completed."}
```
......@@ -2,6 +2,12 @@
module API
class GroupPlaceholderReassignments < ::API::Base
helpers do
def csv_upload_params
declared_params(include_missing: false)
end
end
before do
authenticate!
not_found! unless Feature.enabled?(:importer_user_mapping_reassignment_csv, current_user)
......@@ -33,6 +39,53 @@ class GroupPlaceholderReassignments < ::API::Base
unprocessable_entity!(csv_response.message)
end
end
desc 'Workhorse authorization for the reassignment CSV file' do
detail 'This feature was introduced in GitLab 17.10'
end
post ':id/placeholder_reassignments/authorize' do
require_gitlab_workhorse!
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
::Import::PlaceholderReassignmentsUploader.workhorse_authorize(
has_length: false,
maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes
)
end
params do
requires :file,
type: ::API::Validations::Types::WorkhorseFile,
desc: 'The CSV file containing the reassignments',
documentation: { type: 'file' }
end
post ':id/placeholder_reassignments' do
require_gitlab_workhorse!
unless csv_upload_params[:file].original_filename.ends_with?('.csv')
unprocessable_entity!(s_('UserMapping|You must upload a CSV file with a .csv file extension.'))
end
uploader = UploadService.new(
user_group,
csv_upload_params[:file],
::Import::PlaceholderReassignmentsUploader
).execute
result = Import::SourceUsers::BulkReassignFromCsvService.new(
current_user,
user_group,
uploader.upload
).async_execute
if result.success?
{ message: s_('UserMapping|The file is being processed and you will receive an email when completed.') }
else
unprocessable_entity!(result.message)
end
end
end
end
end
......@@ -3,14 +3,51 @@
require 'spec_helper'
RSpec.describe API::GroupPlaceholderReassignments, feature_category: :importers do
include WorkhorseHelpers
let_it_be(:group_owner) { create(:user) }
let(:current_user) { group_owner }
let_it_be(:group) { create(:group, :public, owners: group_owner) }
let_it_be(:source_user) { create(:import_source_user, namespace: group) }
shared_examples 'it has authentication and authorization requirements' do
context 'when no token supplied' do
let(:current_user) { nil }
it 'returns 401' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when a non-group-owner token is supplied' do
let(:current_user) { build(:user) }
it 'returns 403' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when importer_user_mapping_reassignment_csv flag is disabled' do
before do
stub_feature_flags(importer_user_mapping_reassignment_csv: false)
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'GET /groups/:id/placeholder_reassignments' do
let(:url) { "/groups/#{group.id}/placeholder_reassignments" }
subject(:request_csv) { get api(url, group_owner) }
subject(:request_csv) { get api(url, current_user) }
it 'returns the CSV data' do
request_csv
......@@ -36,36 +73,87 @@
end
end
context 'when no token supplied' do
subject(:request_csv) { get api(url) }
it_behaves_like 'it has authentication and authorization requirements'
end
it 'returns 401' do
request_csv
describe 'POST /groups/:id/placeholder_reassignments/authorize' do
include_context 'workhorse headers'
expect(response).to have_gitlab_http_status(:unauthorized)
end
let(:url) { "/groups/#{group.id}/placeholder_reassignments/authorize" }
subject(:make_request) { post api(url, current_user), headers: workhorse_headers }
it 'verifies file size limit' do
expect(::Import::PlaceholderReassignmentsUploader)
.to receive(:workhorse_authorize)
.with(a_hash_including(maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes))
.and_call_original
make_request
end
context 'when a non-group-owner token is supplied' do
subject(:request_csv) { get api(url, build(:user)) }
it 'returns 200' do
make_request
it 'returns 403' do
request_csv
expect(response).to have_gitlab_http_status(:ok)
end
expect(response).to have_gitlab_http_status(:forbidden)
it_behaves_like 'it has authentication and authorization requirements'
end
describe 'POST /groups/:id/placeholder_reassignments' do
include_context 'workhorse headers'
let(:url) { "/groups/#{group.id}/placeholder_reassignments" }
let(:file) { fixture_file_upload('spec/fixtures/import/user_mapping/user_mapping_upload.csv') }
subject(:make_request) { upload_reassignment_sheet(url, file, workhorse_headers, 'file.size': file.size) }
it 'returns 201' do
make_request
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message'])
.to eq(s_('UserMapping|The file is being processed and you will receive an email when completed.'))
end
it_behaves_like 'it has authentication and authorization requirements'
context 'when the wrong filetype is uploaded' do
let(:file) { fixture_file_upload('spec/fixtures/dk.png') }
it 'rejects the request' do
make_request
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq(s_('UserMapping|You must upload a CSV file with a .csv file extension.'))
end
end
context 'when importer_user_mapping_reassignment_csv flag is disabled' do
context 'when the reassignment service responds with an error' do
before do
stub_feature_flags(importer_user_mapping_reassignment_csv: false)
allow_next_instance_of(Import::SourceUsers::BulkReassignFromCsvService) do |service|
allow(service).to receive(:async_execute).and_return(ServiceResponse.error(message: 'my error message'))
end
end
it 'returns 404' do
request_csv
it 'passes the error along to the user' do
make_request
expect(response).to have_gitlab_http_status(:not_found)
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('my error message')
end
end
def upload_reassignment_sheet(url, file, headers = {}, params = {})
workhorse_finalize(
api(url, current_user),
method: :post,
file_key: :file,
params: params.merge(file: file),
headers: headers,
send_rewritten_field: true
)
end
end
end
......@@ -71,8 +71,8 @@ internal/upload/destination/objectstore/uploader.go:5:2: G501: Blocklisted impor
internal/upload/destination/objectstore/uploader.go:95:12: G401: Use of weak cryptographic primitive (gosec)
internal/upload/exif/exif.go:103:10: G204: Subprocess launched with variable (gosec)
internal/upstream/routes.go:171:74: `(*upstream).wsRoute` - `matchers` always receives `nil` (unparam)
internal/upstream/routes.go:231: Function 'configureRoutes' is too long (340 > 60) (funlen)
internal/upstream/routes.go:487: internal/upstream/routes.go:487: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox)
internal/upstream/routes.go:231: Function 'configureRoutes' is too long (342 > 60) (funlen)
internal/upstream/routes.go:489: internal/upstream/routes.go:489: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox)
internal/upstream/upstream.go:116: internal/upstream/upstream.go:116: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: move to LabKit https://gitlab.com/..." (godox)
internal/zipartifacts/metadata.go:118:54: G115: integer overflow conversion int -> uint32 (gosec)
internal/zipartifacts/open_archive.go:78:28: response body must be closed (bodyclose)
......@@ -391,6 +391,8 @@ func configureRoutes(u *upstream) {
newRoute(apiPattern+`v4/projects/import`, "api_projects_import", railsBackend), mimeMultipartUploader),
u.route("POST",
newRoute(apiPattern+`v4/projects/import-relation`, "api_projects_import_relation", railsBackend), mimeMultipartUploader),
u.route("POST",
newRoute(apiGroupPattern+`/placeholder_reassignments`, "api_group_placeholder_assignment", railsBackend), mimeMultipartUploader),
u.route("POST",
newRoute(groupPattern+`-/group_members/bulk_reassignment_file`, "group_placeholder_assignment", railsBackend), mimeMultipartUploader),
// Project Import via UI upload acceleration
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册