From b1f59d7efa14eb450a2fdc5d9b53d8034c639f87 Mon Sep 17 00:00:00 2001 From: Sam Word Date: Tue, 29 Apr 2025 16:39:20 -0400 Subject: [PATCH 01/10] Allow reassignment to inactive users if application setting is enabled --- .../import/source_users/reassign_service.rb | 28 +++++--- locale/gitlab.pot | 8 ++- .../source_users/reassign_service_spec.rb | 71 +++++++++++++++---- 3 files changed, 83 insertions(+), 24 deletions(-) diff --git a/app/services/import/source_users/reassign_service.rb b/app/services/import/source_users/reassign_service.rb index 951cf94ffe5984..6ae84233258afc 100644 --- a/app/services/import/source_users/reassign_service.rb +++ b/app/services/import/source_users/reassign_service.rb @@ -64,7 +64,7 @@ def run_validations return error_invalid_permissions unless current_user.can?(:admin_import_source_user, import_source_user) return error_namespace_type if root_namespace.user_namespace? - error_invalid_assignee unless valid_assignee?(assignee_user) + error_invalid_assignee unless valid_assignee? end def error_invalid_assignee @@ -76,25 +76,33 @@ def error_invalid_assignee end def invalid_assignee_message - if allow_mapping_to_admins? + if allow_mapping_to_inactive_users? && allow_mapping_to_admins? s_('UserMapping|You can assign users with regular, auditor, or administrator access only.') + elsif allow_mapping_to_admins? + s_('UserMapping|You can assign only active users with regular, auditor, or administrator access.') + elsif allow_mapping_to_inactive_users? + s_('UserMapping|You can assign users with regular or auditor access only.') else - s_('UserMapping|You can assign only active users with regular or auditor access. ' \ - 'To assign users with administrator access, ask your GitLab administrator to ' \ - 'enable the "Allow contribution mapping to administrators" setting.') + s_('UserMapping|You can assign only active users with regular or auditor access.') end end - def valid_assignee?(user) - user.present? && - user.human? && - user.active? && + def valid_assignee? + assignee_user.present? && + assignee_user.human? && + (allow_mapping_to_inactive_users? ? true : assignee_user.active?) && # rubocop:disable Cop/UserAdmin -- This should not be affected by admin mode. # We just want to know whether the user CAN have admin privileges or not. - (allow_mapping_to_admins? ? true : !user.admin?) + (allow_mapping_to_admins? ? true : !assignee_user.admin?) # rubocop:enable Cop/UserAdmin end + def allow_mapping_to_inactive_users? + return false unless Feature.enabled?(:importer_user_mapping_allow_bypass_of_confirmation, current_user) + + current_user.can?(:admin_all_resources) && ::Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation? + end + def allow_mapping_to_admins? ::Gitlab::CurrentSettings.allow_contribution_mapping_to_admins? end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5ecdbc5f9b6975..cfc3e207d564e0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -65604,12 +65604,18 @@ msgstr "" msgid "UserMapping|User ID" msgstr "" -msgid "UserMapping|You can assign only active users with regular or auditor access. To assign users with administrator access, ask your GitLab administrator to enable the \"Allow contribution mapping to administrators\" setting." +msgid "UserMapping|You can assign only active users with regular or auditor access." +msgstr "" + +msgid "UserMapping|You can assign only active users with regular, auditor, or administrator access." msgstr "" msgid "UserMapping|You can assign only users with linked SAML and SCIM identities. Ensure the user has signed into GitLab through your SAML SSO provider and has an active SCIM identity for this group." msgstr "" +msgid "UserMapping|You can assign users with regular or auditor access only." +msgstr "" + msgid "UserMapping|You can assign users with regular, auditor, or administrator access only." msgstr "" diff --git a/spec/services/import/source_users/reassign_service_spec.rb b/spec/services/import/source_users/reassign_service_spec.rb index b00be0b13a22a2..b7b303e32e3312 100644 --- a/spec/services/import/source_users/reassign_service_spec.rb +++ b/spec/services/import/source_users/reassign_service_spec.rb @@ -56,6 +56,7 @@ context 'when import source user does not have an reassignable status' do before do + allow(current_user).to receive(:can?).and_call_original allow(current_user).to receive(:can?).with(:admin_import_source_user, import_source_user).and_return(true) allow(import_source_user).to receive(:reassignable_status?).and_return(false) end @@ -68,36 +69,28 @@ let(:assignee_user) { nil } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access. ' \ - 'To assign users with administrator access, ask your GitLab administrator to ' \ - 'enable the "Allow contribution mapping to administrators" setting.') + error: s_('UserMapping|You can assign only active users with regular or auditor access.') end context 'when assignee user is not a human' do let(:assignee_user) { create(:user, :bot) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access. ' \ - 'To assign users with administrator access, ask your GitLab administrator to ' \ - 'enable the "Allow contribution mapping to administrators" setting.') + error: s_('UserMapping|You can assign only active users with regular or auditor access.') end context 'when assignee user is not active' do let(:assignee_user) { create(:user, :deactivated) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access. ' \ - 'To assign users with administrator access, ask your GitLab administrator to ' \ - 'enable the "Allow contribution mapping to administrators" setting.') + error: s_('UserMapping|You can assign only active users with regular or auditor access.') end context 'when assignee user is an admin' do let(:assignee_user) { create(:user, :admin) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access. ' \ - 'To assign users with administrator access, ask your GitLab administrator to ' \ - 'enable the "Allow contribution mapping to administrators" setting.') + error: s_('UserMapping|You can assign only active users with regular or auditor access.') end context 'when allow_contribution_mapping_to_admins setting is enabled' do @@ -109,7 +102,7 @@ let(:assignee_user) { create(:user, :deactivated) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign users with regular, auditor, or administrator access only.') + error: s_('UserMapping|You can assign only active users with regular, auditor, or administrator access.') end context 'and the assignee user is an admin' do @@ -134,6 +127,58 @@ end end + context 'when allow_bypass_placeholder_confirmation application setting is enabled' do + before do + stub_application_setting(allow_bypass_placeholder_confirmation: true) + end + + context 'and the current user is an admin' do + before do + allow(current_user).to receive(:can?).and_call_original + allow(current_user).to receive(:can?).with(:admin_all_resources).and_return(true) + end + + context 'and importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do + let(:assignee_user) { create(:user, :blocked) } + + before do + stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) + end + + it_behaves_like 'an error response', 'invalid assignee', + error: s_('UserMapping|You can assign only active users with regular or auditor access.') + end + + context 'and the assignee user is blocked' do + let(:assignee_user) { create(:user, :blocked) } + + it 'assigns the user' do + expect(Notify).to receive_message_chain(:import_source_user_reassign, :deliver_later) + expect { result } + .to trigger_internal_events('propose_placeholder_user_reassignment') + .with( + namespace: import_source_user.namespace, + user: current_user, + additional_properties: { + label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), + property: Gitlab::GlobalAnonymousId.user_id(assignee_user), + import_type: import_source_user.import_type + } + ) + + expect(result).to be_success + end + end + end + + context 'and the current user is not an admin' do + let(:assignee_user) { create(:user, :blocked) } + + it_behaves_like 'an error response', 'invalid assignee', + error: s_('UserMapping|You can assign only active users with regular or auditor access.') + end + end + context 'when an error occurs' do before do allow(import_source_user).to receive(:reassign).and_return(false) -- GitLab From d88181e24219d0a05dcb346fb13f101bc9c26ce5 Mon Sep 17 00:00:00 2001 From: Sam Word Date: Thu, 1 May 2025 16:54:46 -0400 Subject: [PATCH 02/10] Also show inactive users in reassignment dropdown when setting enabled --- app/assets/javascripts/members/index.js | 2 + .../components/placeholder_actions.vue | 14 +++++- app/helpers/groups/group_members_helper.rb | 9 +++- .../components/placeholder_actions_spec.js | 39 +++++++++++++++- .../groups/group_members_helper_spec.rb | 45 ++++++++++++++++++- 5 files changed, 104 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js index d707aa543f1b0a..5d6805fa8889a3 100644 --- a/app/assets/javascripts/members/index.js +++ b/app/assets/javascripts/members/index.js @@ -39,6 +39,7 @@ export const initMembersApp = (el, context, options) => { namespaceUserLimit, availableRoles, reassignmentCsvPath, + allowInactivePlaceholderReassignment, ...vuexStoreAttributes } = parseDataAttributes(el); @@ -84,6 +85,7 @@ export const initMembersApp = (el, context, options) => { availableRoles, context, reassignmentCsvPath, + allowInactivePlaceholderReassignment, group: { id: isGroup ? sourceId : null, name: groupName, diff --git a/app/assets/javascripts/members/placeholders/components/placeholder_actions.vue b/app/assets/javascripts/members/placeholders/components/placeholder_actions.vue index 268ac0590c5943..810868fa52465e 100644 --- a/app/assets/javascripts/members/placeholders/components/placeholder_actions.vue +++ b/app/assets/javascripts/members/placeholders/components/placeholder_actions.vue @@ -33,6 +33,11 @@ export default { GlButton, GlCollapsibleListbox, }, + inject: { + allowInactivePlaceholderReassignment: { + default: false, + }, + }, props: { sourceUser: { type: Object, @@ -74,12 +79,17 @@ export default { computed: { queryVariables() { - return { + const query = { first: USERS_PER_PAGE, - active: true, humans: true, search: this.search, }; + + if (!this.allowInactivePlaceholderReassignment) { + query.active = true; + } + + return query; }, hasNextPage() { diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index fe44a1b3a3033b..580e01c0e2fa5a 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -30,7 +30,8 @@ def group_members_app_data( can_approve_access_requests: true, # true for CE, overridden in EE placeholder: placeholder_users, available_roles: available_group_roles(group), - reassignment_csv_path: group_bulk_reassignment_file_path(group) + reassignment_csv_path: group_bulk_reassignment_file_path(group), + allow_inactive_placeholder_reassignment: allow_inactive_placeholder_reassignment? } end # rubocop:enable Metrics/ParameterLists @@ -92,6 +93,12 @@ def available_group_roles(group) { title: name, value: "static-#{access_level}" } end end + + def allow_inactive_placeholder_reassignment? + return false unless Feature.enabled?(:importer_user_mapping_allow_bypass_of_confirmation, current_user) + + Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation? && current_user.can?(:admin_all_resources) + end end Groups::GroupMembersHelper.prepend_mod_with('Groups::GroupMembersHelper') diff --git a/spec/frontend/members/placeholders/components/placeholder_actions_spec.js b/spec/frontend/members/placeholders/components/placeholder_actions_spec.js index 0e35acb78c7a8d..add8c6cb95d546 100644 --- a/spec/frontend/members/placeholders/components/placeholder_actions_spec.js +++ b/spec/frontend/members/placeholders/components/placeholder_actions_spec.js @@ -37,6 +37,9 @@ describe('PlaceholderActions', () => { const defaultProps = { sourceUser: mockSourceUsers[0], }; + const defaultInjectedAttributes = { + allowInactivePlaceholderReassignment: false, + }; const usersQueryHandler = jest.fn().mockResolvedValue(mockUsersQueryResponse); const sourceUsersQueryHandler = jest.fn().mockResolvedValue(mockSourceUsersQueryResponse()); const reassignMutationHandler = jest.fn().mockResolvedValue(mockReassignMutationResponse); @@ -53,7 +56,11 @@ describe('PlaceholderActions', () => { show: jest.fn(), }; - const createComponent = ({ seachUsersQueryHandler = usersQueryHandler, props = {} } = {}) => { + const createComponent = ({ + seachUsersQueryHandler = usersQueryHandler, + props = {}, + provide = defaultInjectedAttributes, + } = {}) => { mockApollo = createMockApollo([ [searchUsersQuery, seachUsersQueryHandler], [importSourceUsersQuery, sourceUsersQueryHandler], @@ -79,6 +86,9 @@ describe('PlaceholderActions', () => { ...defaultProps, ...props, }, + provide: { + ...provide, + }, mocks: { $toast }, }); }; @@ -434,4 +444,31 @@ describe('PlaceholderActions', () => { expect(wrapper.findByText(reassignmentError).exists()).toBe(true); }); }); + + describe('when placeholders are not allowed to be reassigned to inactive users', () => { + it('includes active: true in searchUsersQuery variables', () => { + createComponent(); + + expect(usersQueryHandler).toHaveBeenCalledWith({ + active: true, + first: 20, + humans: true, + search: '', + after: '', + }); + }); + }); + + describe('when placeholders are allowed to be reassigned to inactive users', () => { + it('does not include active in searchUsersQuery variables', () => { + createComponent({ provide: { allowInactivePlaceholderReassignment: true } }); + + expect(usersQueryHandler).toHaveBeenCalledWith({ + first: 20, + humans: true, + search: '', + after: '', + }); + }); + }); }); diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb index 413b5cbe442709..4a99e8c1ad7046 100644 --- a/spec/helpers/groups/group_members_helper_spec.rb +++ b/spec/helpers/groups/group_members_helper_spec.rb @@ -65,7 +65,8 @@ group_name: shared_group.name, group_path: shared_group.full_path, can_approve_access_requests: true, - available_roles: available_roles + available_roles: available_roles, + allow_inactive_placeholder_reassignment: false } expect(subject).to include(expected) @@ -183,6 +184,48 @@ ) end end + + context 'when allow_bypass_placeholder_confirmation application setting is disabled' do + before do + allow(current_user).to receive(:can?).and_call_original + allow(current_user).to receive(:can?).with(:admin_all_resources).and_return(true) + end + + it 'returns false for allow_inactive_placeholder_reassignment for admin users' do + expect(subject[:allow_inactive_placeholder_reassignment]).to be(false) + end + end + + context 'when allow_bypass_placeholder_confirmation application setting is enabled' do + before do + stub_application_setting(allow_bypass_placeholder_confirmation: true) + end + + it 'returns false for allow_inactive_placeholder_reassignment for non-admin users' do + expect(subject[:allow_inactive_placeholder_reassignment]).to be(false) + end + + context 'and the the current user is an admin' do + before do + allow(current_user).to receive(:can?).and_call_original + allow(current_user).to receive(:can?).with(:admin_all_resources).and_return(true) + end + + it 'returns true for allow_inactive_placeholder_reassignment' do + expect(subject[:allow_inactive_placeholder_reassignment]).to be(true) + end + + context 'and the importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do + before do + stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) + end + + it 'returns false for allow_inactive_placeholder_reassignment' do + expect(subject[:allow_inactive_placeholder_reassignment]).to be(false) + end + end + end + end end describe '#group_member_header_subtext' do -- GitLab From c30ab51885405c9bd56d5dfb33a2b194dac5edca Mon Sep 17 00:00:00 2001 From: Sam Word Date: Thu, 1 May 2025 17:01:31 -0400 Subject: [PATCH 03/10] Added reassign_to_user state tracking to reassignment events --- app/services/import/source_users/base_service.rb | 3 ++- .../events/accept_placeholder_user_reassignment.yml | 2 ++ .../events/cancel_placeholder_user_reassignment.yml | 2 ++ config/events/fail_placeholder_user_reassignment.yml | 2 ++ .../events/propose_placeholder_user_reassignment.yml | 2 ++ .../events/reject_placeholder_user_reassignment.yml | 4 +++- .../source_users/accept_reassignment_service_spec.rb | 6 ++++-- .../source_users/cancel_reassignment_service_spec.rb | 3 ++- .../import/source_users/reassign_service_spec.rb | 12 ++++++++---- .../source_users/reject_reassignment_service_spec.rb | 3 ++- 10 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/services/import/source_users/base_service.rb b/app/services/import/source_users/base_service.rb index 4dbde352bd666d..bb844d49cb9083 100644 --- a/app/services/import/source_users/base_service.rb +++ b/app/services/import/source_users/base_service.rb @@ -36,7 +36,8 @@ def track_reassignment_event(event_name, reassign_to_user: import_source_user.re additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(reassign_to_user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: reassign_to_user&.state } ) end diff --git a/config/events/accept_placeholder_user_reassignment.yml b/config/events/accept_placeholder_user_reassignment.yml index 2377654922c362..027429380db604 100644 --- a/config/events/accept_placeholder_user_reassignment.yml +++ b/config/events/accept_placeholder_user_reassignment.yml @@ -12,6 +12,8 @@ additional_properties: description: id of the user that is replacing the placeholder import_type: description: the import source e.g. gitlab_migration, github, bitbucket + reassign_to_user_state: + description: the state of the user that is replacing the placeholder e.g. active, blocked product_group: import_and_integrate product_categories: - importers diff --git a/config/events/cancel_placeholder_user_reassignment.yml b/config/events/cancel_placeholder_user_reassignment.yml index 8e31a044289fa0..37e1c3d703e7e4 100644 --- a/config/events/cancel_placeholder_user_reassignment.yml +++ b/config/events/cancel_placeholder_user_reassignment.yml @@ -12,6 +12,8 @@ additional_properties: description: id of the user that was to replace the placeholder import_type: description: the import source e.g. gitlab_migration, github, bitbucket + reassign_to_user_state: + description: the state of the user that was to replace the placeholder e.g. active, blocked product_group: import_and_integrate product_categories: - importers diff --git a/config/events/fail_placeholder_user_reassignment.yml b/config/events/fail_placeholder_user_reassignment.yml index 75da6a52680526..9f407c47eb4332 100644 --- a/config/events/fail_placeholder_user_reassignment.yml +++ b/config/events/fail_placeholder_user_reassignment.yml @@ -12,6 +12,8 @@ additional_properties: description: id of the user that did not replace the placeholder import_type: description: the import source e.g. gitlab_migration, github, bitbucket + reassign_to_user_state: + description: the state of the user that did not replace the placeholder e.g. active, blocked product_group: import_and_integrate product_categories: - importers diff --git a/config/events/propose_placeholder_user_reassignment.yml b/config/events/propose_placeholder_user_reassignment.yml index 81d14803607f4e..f4c8f92d24c52b 100644 --- a/config/events/propose_placeholder_user_reassignment.yml +++ b/config/events/propose_placeholder_user_reassignment.yml @@ -12,6 +12,8 @@ additional_properties: description: id of the user that is replacing the placeholder import_type: description: the import source e.g. gitlab_migration, github, bitbucket + reassign_to_user_state: + description: the state of the user that is replacing the placeholder e.g. active, blocked product_group: import_and_integrate product_categories: - importers diff --git a/config/events/reject_placeholder_user_reassignment.yml b/config/events/reject_placeholder_user_reassignment.yml index 164da7dc5aab2d..27cadc9d844266 100644 --- a/config/events/reject_placeholder_user_reassignment.yml +++ b/config/events/reject_placeholder_user_reassignment.yml @@ -11,7 +11,9 @@ additional_properties: property: description: id of the user that was to replace the placeholder import_type: - description: the import source e.g. gitlab_migration, github, bitbucket, gitea + description: the import source e.g. gitlab_migration, github, bitbucket, gitea + reassign_to_user_state: + description: the state of the user that was to replace the placeholder e.g. active, blocked product_group: import_and_integrate product_categories: - importers diff --git a/spec/services/import/source_users/accept_reassignment_service_spec.rb b/spec/services/import/source_users/accept_reassignment_service_spec.rb index 764f5aa433e3db..9bfab09c2294e5 100644 --- a/spec/services/import/source_users/accept_reassignment_service_spec.rb +++ b/spec/services/import/source_users/accept_reassignment_service_spec.rb @@ -36,7 +36,8 @@ additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(import_source_user.reassign_to_user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: import_source_user.reassign_to_user.state } ) end @@ -94,7 +95,8 @@ additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(import_source_user.reassign_to_user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: import_source_user.reassign_to_user.state } ) diff --git a/spec/services/import/source_users/cancel_reassignment_service_spec.rb b/spec/services/import/source_users/cancel_reassignment_service_spec.rb index 7a3dd82ef50849..a337c0e1a64935 100644 --- a/spec/services/import/source_users/cancel_reassignment_service_spec.rb +++ b/spec/services/import/source_users/cancel_reassignment_service_spec.rb @@ -25,7 +25,8 @@ additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: user.state } ) diff --git a/spec/services/import/source_users/reassign_service_spec.rb b/spec/services/import/source_users/reassign_service_spec.rb index b7b303e32e3312..eeec8e17c707a4 100644 --- a/spec/services/import/source_users/reassign_service_spec.rb +++ b/spec/services/import/source_users/reassign_service_spec.rb @@ -26,7 +26,8 @@ additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(assignee_user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: assignee_user.state } ) @@ -118,7 +119,8 @@ additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(assignee_user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: assignee_user.state } ) @@ -162,7 +164,8 @@ additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(assignee_user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: assignee_user.state } ) @@ -197,7 +200,8 @@ additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(assignee_user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: assignee_user.state } ) end diff --git a/spec/services/import/source_users/reject_reassignment_service_spec.rb b/spec/services/import/source_users/reject_reassignment_service_spec.rb index 7828c6dab310a4..7de65fd5597329 100644 --- a/spec/services/import/source_users/reject_reassignment_service_spec.rb +++ b/spec/services/import/source_users/reject_reassignment_service_spec.rb @@ -30,7 +30,8 @@ additional_properties: { label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), property: Gitlab::GlobalAnonymousId.user_id(import_source_user.reassign_to_user), - import_type: import_source_user.import_type + import_type: import_source_user.import_type, + reassign_to_user_state: import_source_user.reassign_to_user.state } ) expect(result).to be_success -- GitLab From 5ebc750578412c20ce9d922e0ffd8b6d4f169b12 Mon Sep 17 00:00:00 2001 From: Sam Word Date: Fri, 2 May 2025 14:41:19 -0400 Subject: [PATCH 04/10] Refactors from frontend feedback --- app/assets/javascripts/members/index.js | 3 ++- .../components/placeholder_actions.vue | 5 +---- app/helpers/groups/group_members_helper.rb | 2 +- .../import/source_users/reassign_service.rb | 4 ++-- locale/gitlab.pot | 4 ++-- .../components/placeholder_actions_spec.js | 7 ++----- spec/helpers/groups/group_members_helper_spec.rb | 10 +++++----- .../import/source_users/reassign_service_spec.rb | 14 +++++++------- 8 files changed, 22 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js index 5d6805fa8889a3..cb19c51ee6b7ac 100644 --- a/app/assets/javascripts/members/index.js +++ b/app/assets/javascripts/members/index.js @@ -4,6 +4,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; import { parseDataAttributes } from '~/members/utils'; +import { parseBoolean } from '~/lib/utils/common_utils'; import { TABS } from 'ee_else_ce/members/tabs_metadata'; import MembersTabs from './components/members_tabs.vue'; import membersStore from './store'; @@ -85,7 +86,7 @@ export const initMembersApp = (el, context, options) => { availableRoles, context, reassignmentCsvPath, - allowInactivePlaceholderReassignment, + allowInactivePlaceholderReassignment: parseBoolean(allowInactivePlaceholderReassignment), group: { id: isGroup ? sourceId : null, name: groupName, diff --git a/app/assets/javascripts/members/placeholders/components/placeholder_actions.vue b/app/assets/javascripts/members/placeholders/components/placeholder_actions.vue index 810868fa52465e..53670d30f73465 100644 --- a/app/assets/javascripts/members/placeholders/components/placeholder_actions.vue +++ b/app/assets/javascripts/members/placeholders/components/placeholder_actions.vue @@ -81,14 +81,11 @@ export default { queryVariables() { const query = { first: USERS_PER_PAGE, + ...(this.allowInactivePlaceholderReassignment ? {} : { active: true }), humans: true, search: this.search, }; - if (!this.allowInactivePlaceholderReassignment) { - query.active = true; - } - return query; }, diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index 580e01c0e2fa5a..22960dff49290c 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -31,7 +31,7 @@ def group_members_app_data( placeholder: placeholder_users, available_roles: available_group_roles(group), reassignment_csv_path: group_bulk_reassignment_file_path(group), - allow_inactive_placeholder_reassignment: allow_inactive_placeholder_reassignment? + allow_inactive_placeholder_reassignment: allow_inactive_placeholder_reassignment?.to_s } end # rubocop:enable Metrics/ParameterLists diff --git a/app/services/import/source_users/reassign_service.rb b/app/services/import/source_users/reassign_service.rb index 6ae84233258afc..6686009415e199 100644 --- a/app/services/import/source_users/reassign_service.rb +++ b/app/services/import/source_users/reassign_service.rb @@ -79,11 +79,11 @@ def invalid_assignee_message if allow_mapping_to_inactive_users? && allow_mapping_to_admins? s_('UserMapping|You can assign users with regular, auditor, or administrator access only.') elsif allow_mapping_to_admins? - s_('UserMapping|You can assign only active users with regular, auditor, or administrator access.') + s_('UserMapping|You can assign active users with regular, auditor, or administrator access only.') elsif allow_mapping_to_inactive_users? s_('UserMapping|You can assign users with regular or auditor access only.') else - s_('UserMapping|You can assign only active users with regular or auditor access.') + s_('UserMapping|You can assign active users with regular or auditor access only.') end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index cfc3e207d564e0..bc9041a8f4fd4a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -65604,10 +65604,10 @@ msgstr "" msgid "UserMapping|User ID" msgstr "" -msgid "UserMapping|You can assign only active users with regular or auditor access." +msgid "UserMapping|You can assign active users with regular or auditor access only." msgstr "" -msgid "UserMapping|You can assign only active users with regular, auditor, or administrator access." +msgid "UserMapping|You can assign active users with regular, auditor, or administrator access only." msgstr "" msgid "UserMapping|You can assign only users with linked SAML and SCIM identities. Ensure the user has signed into GitLab through your SAML SSO provider and has an active SCIM identity for this group." diff --git a/spec/frontend/members/placeholders/components/placeholder_actions_spec.js b/spec/frontend/members/placeholders/components/placeholder_actions_spec.js index add8c6cb95d546..b97abad27ce3e8 100644 --- a/spec/frontend/members/placeholders/components/placeholder_actions_spec.js +++ b/spec/frontend/members/placeholders/components/placeholder_actions_spec.js @@ -37,9 +37,6 @@ describe('PlaceholderActions', () => { const defaultProps = { sourceUser: mockSourceUsers[0], }; - const defaultInjectedAttributes = { - allowInactivePlaceholderReassignment: false, - }; const usersQueryHandler = jest.fn().mockResolvedValue(mockUsersQueryResponse); const sourceUsersQueryHandler = jest.fn().mockResolvedValue(mockSourceUsersQueryResponse()); const reassignMutationHandler = jest.fn().mockResolvedValue(mockReassignMutationResponse); @@ -59,7 +56,7 @@ describe('PlaceholderActions', () => { const createComponent = ({ seachUsersQueryHandler = usersQueryHandler, props = {}, - provide = defaultInjectedAttributes, + provide = {}, } = {}) => { mockApollo = createMockApollo([ [searchUsersQuery, seachUsersQueryHandler], @@ -447,7 +444,7 @@ describe('PlaceholderActions', () => { describe('when placeholders are not allowed to be reassigned to inactive users', () => { it('includes active: true in searchUsersQuery variables', () => { - createComponent(); + createComponent({ provide: { allowInactivePlaceholderReassignment: false } }); expect(usersQueryHandler).toHaveBeenCalledWith({ active: true, diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb index 4a99e8c1ad7046..69c85f691ad854 100644 --- a/spec/helpers/groups/group_members_helper_spec.rb +++ b/spec/helpers/groups/group_members_helper_spec.rb @@ -66,7 +66,7 @@ group_path: shared_group.full_path, can_approve_access_requests: true, available_roles: available_roles, - allow_inactive_placeholder_reassignment: false + allow_inactive_placeholder_reassignment: 'false' } expect(subject).to include(expected) @@ -192,7 +192,7 @@ end it 'returns false for allow_inactive_placeholder_reassignment for admin users' do - expect(subject[:allow_inactive_placeholder_reassignment]).to be(false) + expect(subject[:allow_inactive_placeholder_reassignment]).to eq('false') end end @@ -202,7 +202,7 @@ end it 'returns false for allow_inactive_placeholder_reassignment for non-admin users' do - expect(subject[:allow_inactive_placeholder_reassignment]).to be(false) + expect(subject[:allow_inactive_placeholder_reassignment]).to eq('false') end context 'and the the current user is an admin' do @@ -212,7 +212,7 @@ end it 'returns true for allow_inactive_placeholder_reassignment' do - expect(subject[:allow_inactive_placeholder_reassignment]).to be(true) + expect(subject[:allow_inactive_placeholder_reassignment]).to eq('true') end context 'and the importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do @@ -221,7 +221,7 @@ end it 'returns false for allow_inactive_placeholder_reassignment' do - expect(subject[:allow_inactive_placeholder_reassignment]).to be(false) + expect(subject[:allow_inactive_placeholder_reassignment]).to eq('false') end end end diff --git a/spec/services/import/source_users/reassign_service_spec.rb b/spec/services/import/source_users/reassign_service_spec.rb index eeec8e17c707a4..ac573543bdfdae 100644 --- a/spec/services/import/source_users/reassign_service_spec.rb +++ b/spec/services/import/source_users/reassign_service_spec.rb @@ -70,28 +70,28 @@ let(:assignee_user) { nil } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access.') + error: s_('UserMapping|You can assign active users with regular or auditor access only.') end context 'when assignee user is not a human' do let(:assignee_user) { create(:user, :bot) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access.') + error: s_('UserMapping|You can assign active users with regular or auditor access only.') end context 'when assignee user is not active' do let(:assignee_user) { create(:user, :deactivated) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access.') + error: s_('UserMapping|You can assign active users with regular or auditor access only.') end context 'when assignee user is an admin' do let(:assignee_user) { create(:user, :admin) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access.') + error: s_('UserMapping|You can assign active users with regular or auditor access only.') end context 'when allow_contribution_mapping_to_admins setting is enabled' do @@ -103,7 +103,7 @@ let(:assignee_user) { create(:user, :deactivated) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular, auditor, or administrator access.') + error: s_('UserMapping|You can assign active users with regular, auditor, or administrator access only.') end context 'and the assignee user is an admin' do @@ -148,7 +148,7 @@ end it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access.') + error: s_('UserMapping|You can assign active users with regular or auditor access only.') end context 'and the assignee user is blocked' do @@ -178,7 +178,7 @@ let(:assignee_user) { create(:user, :blocked) } it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign only active users with regular or auditor access.') + error: s_('UserMapping|You can assign active users with regular or auditor access only.') end end -- GitLab From d43d73931b240f99cc3bf2bcc2663574b3525c35 Mon Sep 17 00:00:00 2001 From: Sam Word Date: Mon, 5 May 2025 14:10:17 -0400 Subject: [PATCH 05/10] Updated EE error message to be consistent with non-EE message --- .../services/ee/import/source_users/reassign_service.rb | 8 +------- .../ee/import/source_users/reassign_service_spec.rb | 8 ++------ locale/gitlab.pot | 2 +- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/ee/app/services/ee/import/source_users/reassign_service.rb b/ee/app/services/ee/import/source_users/reassign_service.rb index 83f9cbae0a5d6b..bf005672630052 100644 --- a/ee/app/services/ee/import/source_users/reassign_service.rb +++ b/ee/app/services/ee/import/source_users/reassign_service.rb @@ -23,17 +23,11 @@ def valid_assignee_if_sso_enforcement_is_applicable? def error_invalid_assignee_due_to_sso_enforcement ServiceResponse.error( - message: invalid_assignee_due_to_sso_enforcement_message, + message: s_('UserMapping|You can assign users with linked SAML and SCIM identities only.'), reason: :invalid_assignee, payload: import_source_user ) end - - def invalid_assignee_due_to_sso_enforcement_message - s_('UserMapping|You can assign only users with linked SAML and SCIM identities. ' \ - 'Ensure the user has signed into GitLab through your SAML SSO provider and has an ' \ - 'active SCIM identity for this group.') - end end end end diff --git a/ee/spec/services/ee/import/source_users/reassign_service_spec.rb b/ee/spec/services/ee/import/source_users/reassign_service_spec.rb index 7f6cc099b7c5fd..20ef6b5b7a336a 100644 --- a/ee/spec/services/ee/import/source_users/reassign_service_spec.rb +++ b/ee/spec/services/ee/import/source_users/reassign_service_spec.rb @@ -51,9 +51,7 @@ context 'when assignee user account does not meet SSO enforcement requirements' do it_behaves_like 'an error response', 'invalid_assignee', - error: 'You can assign only users with linked SAML and SCIM identities. ' \ - 'Ensure the user has signed into GitLab through your SAML SSO provider and has an ' \ - 'active SCIM identity for this group.' + error: 'You can assign users with linked SAML and SCIM identities only.' context 'when assignee user account with linked SAML, but SCIM identity is inactive' do let_it_be(:group_saml_identity) do @@ -65,9 +63,7 @@ end it_behaves_like 'an error response', 'invalid_assignee', - error: 'You can assign only users with linked SAML and SCIM identities. ' \ - 'Ensure the user has signed into GitLab through your SAML SSO provider and has an ' \ - 'active SCIM identity for this group.' + error: 'You can assign users with linked SAML and SCIM identities only.' end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bc9041a8f4fd4a..78c30c3d9ee7bd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -65610,7 +65610,7 @@ msgstr "" msgid "UserMapping|You can assign active users with regular, auditor, or administrator access only." msgstr "" -msgid "UserMapping|You can assign only users with linked SAML and SCIM identities. Ensure the user has signed into GitLab through your SAML SSO provider and has an active SCIM identity for this group." +msgid "UserMapping|You can assign users with linked SAML and SCIM identities only." msgstr "" msgid "UserMapping|You can assign users with regular or auditor access only." -- GitLab From bff8b059fb967129328a80c535bf4b00acf3c718 Mon Sep 17 00:00:00 2001 From: Sam Word Date: Mon, 5 May 2025 14:11:11 -0400 Subject: [PATCH 06/10] Moved bypass checks to ApplicationSetting, added impersonation check --- app/helpers/groups/group_members_helper.rb | 4 +- app/models/application_setting.rb | 13 ++ .../import/source_users/reassign_service.rb | 4 +- .../groups/group_members_helper_spec.rb | 36 ++---- spec/models/application_setting_spec.rb | 114 ++++++++++++++++++ .../source_users/reassign_service_spec.rb | 65 +++------- 6 files changed, 158 insertions(+), 78 deletions(-) diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index 22960dff49290c..a1ab68d1c61012 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -95,9 +95,7 @@ def available_group_roles(group) end def allow_inactive_placeholder_reassignment? - return false unless Feature.enabled?(:importer_user_mapping_allow_bypass_of_confirmation, current_user) - - Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation? && current_user.can?(:admin_all_resources) + Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation_enabled?(current_user) end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index a1929cb313de9d..4a72492ffee49c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1181,6 +1181,19 @@ def session_expire_from_init_enabled? session_expire_from_init? && Feature.enabled?(:session_expire_from_init, :instance) end + def allow_bypass_placeholder_confirmation_enabled?(reassigning_user = nil, check_admin_mode: true) + return false unless reassigning_user + + # rubocop:disable Cop/UserAdmin -- Make sure reassigning user can have admin privileges if calling context doesn't have access to user session + reassigning_user_is_admin = check_admin_mode ? reassigning_user.can_admin_all_resources? : reassigning_user.admin? + # rubocop:enable Cop/UserAdmin + + allow_bypass_placeholder_confirmation && + reassigning_user_is_admin && + Gitlab.config.gitlab.impersonation_enabled && + Feature.enabled?(:importer_user_mapping_allow_bypass_of_confirmation, reassigning_user) + end + private def parsed_grafana_url diff --git a/app/services/import/source_users/reassign_service.rb b/app/services/import/source_users/reassign_service.rb index 6686009415e199..1e7c5f37d2ac7f 100644 --- a/app/services/import/source_users/reassign_service.rb +++ b/app/services/import/source_users/reassign_service.rb @@ -98,9 +98,7 @@ def valid_assignee? end def allow_mapping_to_inactive_users? - return false unless Feature.enabled?(:importer_user_mapping_allow_bypass_of_confirmation, current_user) - - current_user.can?(:admin_all_resources) && ::Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation? + ::Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation_enabled?(current_user) end def allow_mapping_to_admins? diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb index 69c85f691ad854..bc0f3590e79018 100644 --- a/spec/helpers/groups/group_members_helper_spec.rb +++ b/spec/helpers/groups/group_members_helper_spec.rb @@ -187,43 +187,25 @@ context 'when allow_bypass_placeholder_confirmation application setting is disabled' do before do - allow(current_user).to receive(:can?).and_call_original - allow(current_user).to receive(:can?).with(:admin_all_resources).and_return(true) + allow(Gitlab::CurrentSettings).to receive( + :allow_bypass_placeholder_confirmation_enabled? + ).with(current_user).and_return(false) end - it 'returns false for allow_inactive_placeholder_reassignment for admin users' do + it 'returns false' do expect(subject[:allow_inactive_placeholder_reassignment]).to eq('false') end end context 'when allow_bypass_placeholder_confirmation application setting is enabled' do before do - stub_application_setting(allow_bypass_placeholder_confirmation: true) + allow(Gitlab::CurrentSettings).to receive( + :allow_bypass_placeholder_confirmation_enabled? + ).with(current_user).and_return(true) end - it 'returns false for allow_inactive_placeholder_reassignment for non-admin users' do - expect(subject[:allow_inactive_placeholder_reassignment]).to eq('false') - end - - context 'and the the current user is an admin' do - before do - allow(current_user).to receive(:can?).and_call_original - allow(current_user).to receive(:can?).with(:admin_all_resources).and_return(true) - end - - it 'returns true for allow_inactive_placeholder_reassignment' do - expect(subject[:allow_inactive_placeholder_reassignment]).to eq('true') - end - - context 'and the importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do - before do - stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) - end - - it 'returns false for allow_inactive_placeholder_reassignment' do - expect(subject[:allow_inactive_placeholder_reassignment]).to eq('false') - end - end + it 'returns true' do + expect(subject[:allow_inactive_placeholder_reassignment]).to eq('true') end end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 2dba70b1ef66e0..40c8495b27a708 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2204,4 +2204,118 @@ def expect_invalid describe '#ci_delete_pipelines_in_seconds_limit_human_readable_long' do it { expect(setting.ci_delete_pipelines_in_seconds_limit_human_readable_long).to eq('1 year') } end + + describe 'allow_bypass_placeholder_confirmation_enabled' do + before do + stub_config_setting(impersonation_enabled: false) + end + + context 'when allow_bypass_placeholder_confirmation setting is enabled' do + before do + setting.update!(allow_bypass_placeholder_confirmation: true) + end + + context 'and no user is given' do + it 'returns false' do + expect(setting.allow_bypass_placeholder_confirmation_enabled?).to be(false) + end + end + + context 'and the reassigning user is not an admin' do + let_it_be(:reassigning_user) { create(:user) } + + it 'returns false' do + expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) + end + + context 'and admin mode check is skipped' do + it 'returns false' do + expect( + setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user, check_admin_mode: false) + ).to be(false) + end + end + end + + context 'and the reassigning user is an admin' do + let_it_be(:reassigning_user) { create(:user, :admin) } + + context 'and admin mode is enabled', :enable_admin_mode do + it 'returns false' do + expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) + end + + context 'and impersonation is enabled' do + before do + stub_config_setting(impersonation_enabled: true) + end + + it 'returns true' do + expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(true) + end + + context 'and the importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do + before do + stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) + end + + it 'returns false' do + expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) + end + end + end + end + + context 'and admin mode is disabled' do + it 'returns false' do + expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) + end + end + + context 'and admin mode check is skipped' do + it 'returns false' do + expect( + setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user, check_admin_mode: false) + ).to be(false) + end + + context 'and impersonation is enabled' do + before do + stub_config_setting(impersonation_enabled: true) + end + + it 'returns true' do + expect( + setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user, check_admin_mode: false) + ).to be(true) + end + + context 'and the importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do + before do + stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) + end + + it 'returns false' do + expect( + setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user, check_admin_mode: false) + ).to be(false) + end + end + end + end + end + end + + context 'when allow_bypass_placeholder_confirmation application setting is disabled', :enable_admin_mode do + let_it_be(:reassigning_user) { create(:user, :admin) } + + before do + stub_config_setting(impersonation_enabled: true) + end + + it 'returns false' do + expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) + end + end + end end diff --git a/spec/services/import/source_users/reassign_service_spec.rb b/spec/services/import/source_users/reassign_service_spec.rb index ac573543bdfdae..c4692a6ba32523 100644 --- a/spec/services/import/source_users/reassign_service_spec.rb +++ b/spec/services/import/source_users/reassign_service_spec.rb @@ -130,55 +130,30 @@ end context 'when allow_bypass_placeholder_confirmation application setting is enabled' do - before do - stub_application_setting(allow_bypass_placeholder_confirmation: true) - end - - context 'and the current user is an admin' do - before do - allow(current_user).to receive(:can?).and_call_original - allow(current_user).to receive(:can?).with(:admin_all_resources).and_return(true) - end - - context 'and importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do - let(:assignee_user) { create(:user, :blocked) } - - before do - stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) - end - - it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign active users with regular or auditor access only.') - end + let(:assignee_user) { create(:user, :blocked) } - context 'and the assignee user is blocked' do - let(:assignee_user) { create(:user, :blocked) } - - it 'assigns the user' do - expect(Notify).to receive_message_chain(:import_source_user_reassign, :deliver_later) - expect { result } - .to trigger_internal_events('propose_placeholder_user_reassignment') - .with( - namespace: import_source_user.namespace, - user: current_user, - additional_properties: { - label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), - property: Gitlab::GlobalAnonymousId.user_id(assignee_user), - import_type: import_source_user.import_type, - reassign_to_user_state: assignee_user.state - } - ) - - expect(result).to be_success - end - end + before do + allow(Gitlab::CurrentSettings).to receive( + :allow_bypass_placeholder_confirmation_enabled? + ).with(current_user).and_return(true) end - context 'and the current user is not an admin' do - let(:assignee_user) { create(:user, :blocked) } + it 'allows reassignment to an inactive user' do + expect(Notify).to receive_message_chain(:import_source_user_reassign, :deliver_later) + expect { result } + .to trigger_internal_events('propose_placeholder_user_reassignment') + .with( + namespace: import_source_user.namespace, + user: current_user, + additional_properties: { + label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), + property: Gitlab::GlobalAnonymousId.user_id(assignee_user), + import_type: import_source_user.import_type, + reassign_to_user_state: assignee_user.state + } + ) - it_behaves_like 'an error response', 'invalid assignee', - error: s_('UserMapping|You can assign active users with regular or auditor access only.') + expect(result).to be_success end end -- GitLab From a9333d1dc7f122bb617a5dd28c61071fa8559895 Mon Sep 17 00:00:00 2001 From: Sam Word Date: Mon, 5 May 2025 14:22:45 -0400 Subject: [PATCH 07/10] Added spec coverage for undercover cop --- .../source_users/reassign_service_spec.rb | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/spec/services/import/source_users/reassign_service_spec.rb b/spec/services/import/source_users/reassign_service_spec.rb index c4692a6ba32523..bebada624edbd1 100644 --- a/spec/services/import/source_users/reassign_service_spec.rb +++ b/spec/services/import/source_users/reassign_service_spec.rb @@ -15,8 +15,9 @@ import_source_user.namespace.add_owner(user) end - context 'when reassignment is successful' do - it 'returns success' do + shared_examples 'a success response' do + it 'returns success', :aggregate_failures do + expect(Import::ReassignPlaceholderUserRecordsWorker).not_to receive(:perform_async) expect(Notify).to receive_message_chain(:import_source_user_reassign, :deliver_later) expect { result } .to trigger_internal_events('propose_placeholder_user_reassignment') @@ -39,6 +40,10 @@ end end + context 'when the reassigned by and reassigning user are valid' do + it_behaves_like 'a success response' + end + shared_examples 'an error response' do |desc, error:| it "returns #{desc} error" do expect(Notify).not_to receive(:import_source_user_reassign) @@ -109,51 +114,43 @@ context 'and the assignee user is an admin' do let(:assignee_user) { create(:user, :admin) } - it 'assigns the user' do - expect(Notify).to receive_message_chain(:import_source_user_reassign, :deliver_later) - expect { result } - .to trigger_internal_events('propose_placeholder_user_reassignment') - .with( - namespace: import_source_user.namespace, - user: current_user, - additional_properties: { - label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), - property: Gitlab::GlobalAnonymousId.user_id(assignee_user), - import_type: import_source_user.import_type, - reassign_to_user_state: assignee_user.state - } - ) + it_behaves_like 'a success response' + end - expect(result).to be_success + context 'and allow_bypass_placeholder_confirmation is enabled' do + before do + allow(Gitlab::CurrentSettings).to receive( + :allow_bypass_placeholder_confirmation_enabled? + ).with(current_user).and_return(true) + end + + context 'and assignee user is not a human' do + let(:assignee_user) { create(:user, :bot) } + + it_behaves_like 'an error response', 'invalid assignee', + error: s_('UserMapping|You can assign users with regular, auditor, or administrator access only.') end end end - context 'when allow_bypass_placeholder_confirmation application setting is enabled' do - let(:assignee_user) { create(:user, :blocked) } - + context 'when allow_bypass_placeholder_confirmation is enabled' do before do allow(Gitlab::CurrentSettings).to receive( :allow_bypass_placeholder_confirmation_enabled? ).with(current_user).and_return(true) end - it 'allows reassignment to an inactive user' do - expect(Notify).to receive_message_chain(:import_source_user_reassign, :deliver_later) - expect { result } - .to trigger_internal_events('propose_placeholder_user_reassignment') - .with( - namespace: import_source_user.namespace, - user: current_user, - additional_properties: { - label: Gitlab::GlobalAnonymousId.user_id(import_source_user.placeholder_user), - property: Gitlab::GlobalAnonymousId.user_id(assignee_user), - import_type: import_source_user.import_type, - reassign_to_user_state: assignee_user.state - } - ) + context 'and the assignee user is an admin' do + let(:assignee_user) { create(:user, :admin) } - expect(result).to be_success + it_behaves_like 'an error response', 'invalid assignee', + error: s_('UserMapping|You can assign users with regular or auditor access only.') + end + + context 'and the assignee user is blocked' do + let(:assignee_user) { create(:user, :blocked) } + + it_behaves_like 'a success response' end end -- GitLab From d51c37e355487b6f50bf5b824473750623c4863b Mon Sep 17 00:00:00 2001 From: Sam Word Date: Tue, 6 May 2025 12:15:17 -0400 Subject: [PATCH 08/10] Moved bypass check to new module, added more readable tests --- app/helpers/groups/group_members_helper.rb | 2 +- app/models/application_setting.rb | 13 -- .../import/source_users/reassign_service.rb | 9 +- .../bypass_confirmation_authorizer.rb | 28 +++++ .../groups/group_members_helper_spec.rb | 12 +- .../bypass_confirmation_authorizer_spec.rb | 62 ++++++++++ spec/models/application_setting_spec.rb | 114 ------------------ .../source_users/reassign_service_spec.rb | 12 +- 8 files changed, 111 insertions(+), 141 deletions(-) create mode 100644 lib/import/user_mapping/bypass_confirmation_authorizer.rb create mode 100644 spec/lib/import/user_mapping/bypass_confirmation_authorizer_spec.rb diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index a1ab68d1c61012..e20b7a984f65fa 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -95,7 +95,7 @@ def available_group_roles(group) end def allow_inactive_placeholder_reassignment? - Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation_enabled?(current_user) + Import::UserMapping::BypassConfirmationAuthorizer.new(current_user).allow_mapping_to_inactive_users? end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 4a72492ffee49c..a1929cb313de9d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1181,19 +1181,6 @@ def session_expire_from_init_enabled? session_expire_from_init? && Feature.enabled?(:session_expire_from_init, :instance) end - def allow_bypass_placeholder_confirmation_enabled?(reassigning_user = nil, check_admin_mode: true) - return false unless reassigning_user - - # rubocop:disable Cop/UserAdmin -- Make sure reassigning user can have admin privileges if calling context doesn't have access to user session - reassigning_user_is_admin = check_admin_mode ? reassigning_user.can_admin_all_resources? : reassigning_user.admin? - # rubocop:enable Cop/UserAdmin - - allow_bypass_placeholder_confirmation && - reassigning_user_is_admin && - Gitlab.config.gitlab.impersonation_enabled && - Feature.enabled?(:importer_user_mapping_allow_bypass_of_confirmation, reassigning_user) - end - private def parsed_grafana_url diff --git a/app/services/import/source_users/reassign_service.rb b/app/services/import/source_users/reassign_service.rb index 1e7c5f37d2ac7f..c616867d4c5fe9 100644 --- a/app/services/import/source_users/reassign_service.rb +++ b/app/services/import/source_users/reassign_service.rb @@ -3,6 +3,8 @@ module Import module SourceUsers class ReassignService < BaseService + include Gitlab::Utils::StrongMemoize + def initialize(import_source_user, assignee_user, current_user:) @import_source_user = import_source_user @current_user = current_user @@ -98,12 +100,17 @@ def valid_assignee? end def allow_mapping_to_inactive_users? - ::Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation_enabled?(current_user) + bypass_confirmation_authorizer.allow_mapping_to_inactive_users? end def allow_mapping_to_admins? ::Gitlab::CurrentSettings.allow_contribution_mapping_to_admins? end + + def bypass_confirmation_authorizer + Import::UserMapping::BypassConfirmationAuthorizer.new(current_user) + end + strong_memoize_attr :bypass_confirmation_authorizer end end end diff --git a/lib/import/user_mapping/bypass_confirmation_authorizer.rb b/lib/import/user_mapping/bypass_confirmation_authorizer.rb new file mode 100644 index 00000000000000..bc10a94fe55368 --- /dev/null +++ b/lib/import/user_mapping/bypass_confirmation_authorizer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Import + module UserMapping + class BypassConfirmationAuthorizer + def initialize(reassigning_user) + @reassigning_user = reassigning_user + end + + def allow_mapping_to_inactive_users? + allow_admin_bypass_placeholder_confirmation? + end + + # private + + attr_reader :reassigning_user + + def allow_admin_bypass_placeholder_confirmation? + return false unless reassigning_user + return false unless Feature.enabled?(:importer_user_mapping_allow_bypass_of_confirmation, reassigning_user) + + ::Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation && + reassigning_user.can_admin_all_resources? && + Gitlab.config.gitlab.impersonation_enabled + end + end + end +end diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb index bc0f3590e79018..c95d70806c049d 100644 --- a/spec/helpers/groups/group_members_helper_spec.rb +++ b/spec/helpers/groups/group_members_helper_spec.rb @@ -187,9 +187,9 @@ context 'when allow_bypass_placeholder_confirmation application setting is disabled' do before do - allow(Gitlab::CurrentSettings).to receive( - :allow_bypass_placeholder_confirmation_enabled? - ).with(current_user).and_return(false) + expect_next_instance_of(Import::UserMapping::BypassConfirmationAuthorizer, current_user) do |authorizer| + allow(authorizer).to receive(:allow_mapping_to_inactive_users?).and_return(false) + end end it 'returns false' do @@ -199,9 +199,9 @@ context 'when allow_bypass_placeholder_confirmation application setting is enabled' do before do - allow(Gitlab::CurrentSettings).to receive( - :allow_bypass_placeholder_confirmation_enabled? - ).with(current_user).and_return(true) + expect_next_instance_of(Import::UserMapping::BypassConfirmationAuthorizer, current_user) do |authorizer| + allow(authorizer).to receive(:allow_mapping_to_inactive_users?).and_return(true) + end end it 'returns true' do diff --git a/spec/lib/import/user_mapping/bypass_confirmation_authorizer_spec.rb b/spec/lib/import/user_mapping/bypass_confirmation_authorizer_spec.rb new file mode 100644 index 00000000000000..b548906b630f28 --- /dev/null +++ b/spec/lib/import/user_mapping/bypass_confirmation_authorizer_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::UserMapping::BypassConfirmationAuthorizer, feature_category: :importers do + subject(:authorizer) { described_class.new(reassigning_user) } + + describe '#allow_mapping_to_inactive_users?' do + let_it_be(:reassigning_user) { create(:user, :admin) } + + subject(:allow_mapping_to_inactive_users) { authorizer.allow_mapping_to_inactive_users? } + + before do + stub_application_setting(allow_bypass_placeholder_confirmation: true) + stub_config_setting(impersonation_enabled: true) + end + + it 'returns true for admins with bypass application setting and impersonation enabled', :enable_admin_mode do + expect(allow_mapping_to_inactive_users).to be(true) + end + + context 'when the importer_user_mapping_allow_bypass_of_confirmation flag is disabled', :enable_admin_mode do + before do + stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) + end + + it { is_expected.to be(false) } + end + + context 'when the allow_bypass_placeholder_confirmation application setting is disabled', :enable_admin_mode do + before do + stub_application_setting(allow_bypass_placeholder_confirmation: false) + end + + it { is_expected.to be(false) } + end + + context 'when admin mode is disabled for the admin user' do + it { is_expected.to be(false) } + end + + context 'when the reassigning user is not an admin', :enable_admin_mode do + let!(:reassigning_user) { create(:user) } + + it { is_expected.to be(false) } + end + + context 'when the reassigning user is nil', :enable_admin_mode do + let!(:reassigning_user) { nil } + + it { is_expected.to be(false) } + end + + context 'when user impersonanation is disabled', :enable_admin_mode do + before do + stub_config_setting(impersonation_enabled: false) + end + + it { is_expected.to be(false) } + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 40c8495b27a708..2dba70b1ef66e0 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2204,118 +2204,4 @@ def expect_invalid describe '#ci_delete_pipelines_in_seconds_limit_human_readable_long' do it { expect(setting.ci_delete_pipelines_in_seconds_limit_human_readable_long).to eq('1 year') } end - - describe 'allow_bypass_placeholder_confirmation_enabled' do - before do - stub_config_setting(impersonation_enabled: false) - end - - context 'when allow_bypass_placeholder_confirmation setting is enabled' do - before do - setting.update!(allow_bypass_placeholder_confirmation: true) - end - - context 'and no user is given' do - it 'returns false' do - expect(setting.allow_bypass_placeholder_confirmation_enabled?).to be(false) - end - end - - context 'and the reassigning user is not an admin' do - let_it_be(:reassigning_user) { create(:user) } - - it 'returns false' do - expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) - end - - context 'and admin mode check is skipped' do - it 'returns false' do - expect( - setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user, check_admin_mode: false) - ).to be(false) - end - end - end - - context 'and the reassigning user is an admin' do - let_it_be(:reassigning_user) { create(:user, :admin) } - - context 'and admin mode is enabled', :enable_admin_mode do - it 'returns false' do - expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) - end - - context 'and impersonation is enabled' do - before do - stub_config_setting(impersonation_enabled: true) - end - - it 'returns true' do - expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(true) - end - - context 'and the importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do - before do - stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) - end - - it 'returns false' do - expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) - end - end - end - end - - context 'and admin mode is disabled' do - it 'returns false' do - expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) - end - end - - context 'and admin mode check is skipped' do - it 'returns false' do - expect( - setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user, check_admin_mode: false) - ).to be(false) - end - - context 'and impersonation is enabled' do - before do - stub_config_setting(impersonation_enabled: true) - end - - it 'returns true' do - expect( - setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user, check_admin_mode: false) - ).to be(true) - end - - context 'and the importer_user_mapping_allow_bypass_of_confirmation feature flag is disabled' do - before do - stub_feature_flags(importer_user_mapping_allow_bypass_of_confirmation: false) - end - - it 'returns false' do - expect( - setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user, check_admin_mode: false) - ).to be(false) - end - end - end - end - end - end - - context 'when allow_bypass_placeholder_confirmation application setting is disabled', :enable_admin_mode do - let_it_be(:reassigning_user) { create(:user, :admin) } - - before do - stub_config_setting(impersonation_enabled: true) - end - - it 'returns false' do - expect(setting.allow_bypass_placeholder_confirmation_enabled?(reassigning_user)).to be(false) - end - end - end end diff --git a/spec/services/import/source_users/reassign_service_spec.rb b/spec/services/import/source_users/reassign_service_spec.rb index bebada624edbd1..34180e4c6e339a 100644 --- a/spec/services/import/source_users/reassign_service_spec.rb +++ b/spec/services/import/source_users/reassign_service_spec.rb @@ -119,9 +119,9 @@ context 'and allow_bypass_placeholder_confirmation is enabled' do before do - allow(Gitlab::CurrentSettings).to receive( - :allow_bypass_placeholder_confirmation_enabled? - ).with(current_user).and_return(true) + expect_next_instance_of(Import::UserMapping::BypassConfirmationAuthorizer, current_user) do |authorizer| + allow(authorizer).to receive(:allow_mapping_to_inactive_users?).and_return(true) + end end context 'and assignee user is not a human' do @@ -135,9 +135,9 @@ context 'when allow_bypass_placeholder_confirmation is enabled' do before do - allow(Gitlab::CurrentSettings).to receive( - :allow_bypass_placeholder_confirmation_enabled? - ).with(current_user).and_return(true) + expect_next_instance_of(Import::UserMapping::BypassConfirmationAuthorizer, current_user) do |authorizer| + allow(authorizer).to receive(:allow_mapping_to_inactive_users?).and_return(true) + end end context 'and the assignee user is an admin' do -- GitLab From 3a23ec01c11e2778ff1e9386dbc8a3ede816d15a Mon Sep 17 00:00:00 2001 From: Sam Word Date: Tue, 6 May 2025 12:32:22 -0400 Subject: [PATCH 09/10] Small refactors from feedback, added more reassign spec contexts --- .../import/source_users/reassign_service.rb | 4 ++-- .../bypass_confirmation_authorizer.rb | 2 +- .../bypass_confirmation_authorizer_spec.rb | 2 +- .../source_users/reassign_service_spec.rb | 19 +++++++++++++++---- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/services/import/source_users/reassign_service.rb b/app/services/import/source_users/reassign_service.rb index c616867d4c5fe9..6c09dfbf07174a 100644 --- a/app/services/import/source_users/reassign_service.rb +++ b/app/services/import/source_users/reassign_service.rb @@ -92,10 +92,10 @@ def invalid_assignee_message def valid_assignee? assignee_user.present? && assignee_user.human? && - (allow_mapping_to_inactive_users? ? true : assignee_user.active?) && + (allow_mapping_to_inactive_users? || assignee_user.active?) && # rubocop:disable Cop/UserAdmin -- This should not be affected by admin mode. # We just want to know whether the user CAN have admin privileges or not. - (allow_mapping_to_admins? ? true : !assignee_user.admin?) + (allow_mapping_to_admins? || !assignee_user.admin?) # rubocop:enable Cop/UserAdmin end diff --git a/lib/import/user_mapping/bypass_confirmation_authorizer.rb b/lib/import/user_mapping/bypass_confirmation_authorizer.rb index bc10a94fe55368..ecfcf83f1d4b27 100644 --- a/lib/import/user_mapping/bypass_confirmation_authorizer.rb +++ b/lib/import/user_mapping/bypass_confirmation_authorizer.rb @@ -11,7 +11,7 @@ def allow_mapping_to_inactive_users? allow_admin_bypass_placeholder_confirmation? end - # private + private attr_reader :reassigning_user diff --git a/spec/lib/import/user_mapping/bypass_confirmation_authorizer_spec.rb b/spec/lib/import/user_mapping/bypass_confirmation_authorizer_spec.rb index b548906b630f28..f5a62ad7a9eb83 100644 --- a/spec/lib/import/user_mapping/bypass_confirmation_authorizer_spec.rb +++ b/spec/lib/import/user_mapping/bypass_confirmation_authorizer_spec.rb @@ -51,7 +51,7 @@ it { is_expected.to be(false) } end - context 'when user impersonanation is disabled', :enable_admin_mode do + context 'when user impersonation is disabled', :enable_admin_mode do before do stub_config_setting(impersonation_enabled: false) end diff --git a/spec/services/import/source_users/reassign_service_spec.rb b/spec/services/import/source_users/reassign_service_spec.rb index 34180e4c6e339a..e03452ebc03d94 100644 --- a/spec/services/import/source_users/reassign_service_spec.rb +++ b/spec/services/import/source_users/reassign_service_spec.rb @@ -62,7 +62,6 @@ context 'when import source user does not have an reassignable status' do before do - allow(current_user).to receive(:can?).and_call_original allow(current_user).to receive(:can?).with(:admin_import_source_user, import_source_user).and_return(true) allow(import_source_user).to receive(:reassignable_status?).and_return(false) end @@ -104,7 +103,7 @@ stub_application_setting(allow_contribution_mapping_to_admins: true) end - context 'and the assignee user is invalid' do + context 'and the assignee user is inactive' do let(:assignee_user) { create(:user, :deactivated) } it_behaves_like 'an error response', 'invalid assignee', @@ -117,7 +116,7 @@ it_behaves_like 'a success response' end - context 'and allow_bypass_placeholder_confirmation is enabled' do + context 'and mapping to inactive users is allowed' do before do expect_next_instance_of(Import::UserMapping::BypassConfirmationAuthorizer, current_user) do |authorizer| allow(authorizer).to receive(:allow_mapping_to_inactive_users?).and_return(true) @@ -133,7 +132,7 @@ end end - context 'when allow_bypass_placeholder_confirmation is enabled' do + context 'when mapping to inactive users is allowed' do before do expect_next_instance_of(Import::UserMapping::BypassConfirmationAuthorizer, current_user) do |authorizer| allow(authorizer).to receive(:allow_mapping_to_inactive_users?).and_return(true) @@ -152,6 +151,18 @@ it_behaves_like 'a success response' end + + context 'and the assignee user is banned' do + let(:assignee_user) { create(:user, :banned) } + + it_behaves_like 'a success response' + end + + context 'and the assignee user is deactivated' do + let(:assignee_user) { create(:user, :deactivated) } + + it_behaves_like 'a success response' + end end context 'when an error occurs' do -- GitLab From 815e587a4b525616880f74f45d86afd503fc6884 Mon Sep 17 00:00:00 2001 From: Sam Word Date: Wed, 7 May 2025 00:08:42 -0400 Subject: [PATCH 10/10] Undo SSO enforcement message change --- .../services/ee/import/source_users/reassign_service.rb | 8 +++++++- .../ee/import/source_users/reassign_service_spec.rb | 8 ++++++-- locale/gitlab.pot | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/ee/app/services/ee/import/source_users/reassign_service.rb b/ee/app/services/ee/import/source_users/reassign_service.rb index bf005672630052..83f9cbae0a5d6b 100644 --- a/ee/app/services/ee/import/source_users/reassign_service.rb +++ b/ee/app/services/ee/import/source_users/reassign_service.rb @@ -23,11 +23,17 @@ def valid_assignee_if_sso_enforcement_is_applicable? def error_invalid_assignee_due_to_sso_enforcement ServiceResponse.error( - message: s_('UserMapping|You can assign users with linked SAML and SCIM identities only.'), + message: invalid_assignee_due_to_sso_enforcement_message, reason: :invalid_assignee, payload: import_source_user ) end + + def invalid_assignee_due_to_sso_enforcement_message + s_('UserMapping|You can assign only users with linked SAML and SCIM identities. ' \ + 'Ensure the user has signed into GitLab through your SAML SSO provider and has an ' \ + 'active SCIM identity for this group.') + end end end end diff --git a/ee/spec/services/ee/import/source_users/reassign_service_spec.rb b/ee/spec/services/ee/import/source_users/reassign_service_spec.rb index 20ef6b5b7a336a..7f6cc099b7c5fd 100644 --- a/ee/spec/services/ee/import/source_users/reassign_service_spec.rb +++ b/ee/spec/services/ee/import/source_users/reassign_service_spec.rb @@ -51,7 +51,9 @@ context 'when assignee user account does not meet SSO enforcement requirements' do it_behaves_like 'an error response', 'invalid_assignee', - error: 'You can assign users with linked SAML and SCIM identities only.' + error: 'You can assign only users with linked SAML and SCIM identities. ' \ + 'Ensure the user has signed into GitLab through your SAML SSO provider and has an ' \ + 'active SCIM identity for this group.' context 'when assignee user account with linked SAML, but SCIM identity is inactive' do let_it_be(:group_saml_identity) do @@ -63,7 +65,9 @@ end it_behaves_like 'an error response', 'invalid_assignee', - error: 'You can assign users with linked SAML and SCIM identities only.' + error: 'You can assign only users with linked SAML and SCIM identities. ' \ + 'Ensure the user has signed into GitLab through your SAML SSO provider and has an ' \ + 'active SCIM identity for this group.' end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 78c30c3d9ee7bd..bc9041a8f4fd4a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -65610,7 +65610,7 @@ msgstr "" msgid "UserMapping|You can assign active users with regular, auditor, or administrator access only." msgstr "" -msgid "UserMapping|You can assign users with linked SAML and SCIM identities only." +msgid "UserMapping|You can assign only users with linked SAML and SCIM identities. Ensure the user has signed into GitLab through your SAML SSO provider and has an active SCIM identity for this group." msgstr "" msgid "UserMapping|You can assign users with regular or auditor access only." -- GitLab