From ad55390243fe7e1e101b2d3c225dce651175e917 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 11 Dec 2025 14:59:21 -0600 Subject: [PATCH 1/8] Add sign_in_path JSON endpoint to session controller This new path will allow the sign-in Vue app to determine the appropriate sign in path for a given login value (username or email address). For FOSS, always nil, which means stay here. For SaaS the sign-in may redirect to another cell or in the future to an organization-specific sign-in page. --- app/controllers/sessions_controller.rb | 16 +++++ config/routes/user.rb | 1 + ee/app/controllers/ee/sessions_controller.rb | 13 ++++ ee/lib/gitlab/saas.rb | 1 + ee/spec/requests/ee/sessions_spec.rb | 68 ++++++++++++++++++++ spec/requests/sessions_spec.rb | 65 +++++++++++++++++++ 6 files changed, 164 insertions(+) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 1fd3999f1c1e44..e6f0ede445bbd5 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -112,8 +112,24 @@ def destroy end end + def sign_in_path + respond_to do |format| + format.json do + render json: { sign_in_path: determine_sign_in_path } + end + format.html do + render_404 + end + end + end + private + # Overridden in EE + def determine_sign_in_path + nil + end + override :after_pending_invitations_hook def after_pending_invitations_hook member = resource.members.last diff --git a/config/routes/user.rb b/config/routes/user.rb index 57eda7cb80ef77..65f7f994ee6f66 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -59,6 +59,7 @@ def override_omniauth(provider, controller, path_prefix = '/users/auth') get '/users/skip_verification_confirmation', to: 'sessions#skip_verification_confirmation' post '/users/fallback_to_email_otp', to: 'sessions#fallback_to_email_otp' post '/users/passkeys/sign_in', to: 'sessions#new_passkey', as: :users_passkeys_sign_in + get '/users/sign_in_path', to: 'sessions#sign_in_path', as: :users_sign_in_path # Redirect on GitHub authorization request errors. E.g. it could happen when user: # 1. cancel authorization the GitLab OAuth app via GitHub to import GitHub repos diff --git a/ee/app/controllers/ee/sessions_controller.rb b/ee/app/controllers/ee/sessions_controller.rb index 6ef9115e495b0a..b3be2fa47aac3d 100644 --- a/ee/app/controllers/ee/sessions_controller.rb +++ b/ee/app/controllers/ee/sessions_controller.rb @@ -30,6 +30,19 @@ def new private + override :determine_sign_in_path + def determine_sign_in_path + return super unless ::Gitlab::Saas.feature_available?(:redirect_sign_in_when_login_not_found) + + login = user_params[:login] + return if login.blank? + + user = ::User.find_by_login(login) + return if user.present? + + new_user_session_path(login: login) + end + def gitlab_geo_logout return unless ::Gitlab::Geo.secondary? diff --git a/ee/lib/gitlab/saas.rb b/ee/lib/gitlab/saas.rb index 7f0d6c01159669..d0c86cf69503e1 100644 --- a/ee/lib/gitlab/saas.rb +++ b/ee/lib/gitlab/saas.rb @@ -35,6 +35,7 @@ module Saas secret_detection_service ci_component_usages_in_projects disable_ropc_for_all_applications + redirect_sign_in_when_login_not_found disable_ropc_for_new_applications deduplicate_ci_tags targeted_messages diff --git a/ee/spec/requests/ee/sessions_spec.rb b/ee/spec/requests/ee/sessions_spec.rb index e6fc2eeaa29696..f29b7c679c5c66 100644 --- a/ee/spec/requests/ee/sessions_spec.rb +++ b/ee/spec/requests/ee/sessions_spec.rb @@ -93,4 +93,72 @@ end end end + + describe 'GET /users/sign_in_path', :saas do + context 'when redirect_sign_in_when_login_not_found feature is enabled' do + before do + stub_saas_features(redirect_sign_in_when_login_not_found: true) + end + + context 'when requesting JSON format' do + context 'when login parameter is not provided' do + it 'returns nil' do + get users_sign_in_path_path, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + + context 'when login parameter is blank' do + it 'returns nil' do + get users_sign_in_path_path, params: { login: '' }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + + context 'when user is found by username' do + it 'returns nil' do + get users_sign_in_path_path, params: { login: user.username }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + + context 'when user is found by email' do + it 'returns nil' do + get users_sign_in_path_path, params: { login: user.email }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + + context 'when user is not found' do + it 'returns sign in path with login parameter' do + get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => '/users/sign_in?login=nonexistent' }) + end + end + end + end + + context 'when redirect_sign_in_when_login_not_found feature is disabled' do + before do + stub_saas_features(redirect_sign_in_when_login_not_found: false) + end + + it 'returns nil even when user is not found' do + get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + end end diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb index 2bb44ec457dc76..bae5771b28abd9 100644 --- a/spec/requests/sessions_spec.rb +++ b/spec/requests/sessions_spec.rb @@ -60,4 +60,69 @@ def authenticate_2fa(otp_attempt:) post(user_session_path(params: { user: user_params.merge(otp_attempt: otp_attempt) })) end end + + describe 'GET /users/sign_in_path' do + context 'when requesting JSON format' do + context 'when login parameter is not provided' do + it 'returns nil' do + get users_sign_in_path_path, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + + context 'when login parameter is blank' do + it 'returns nil' do + get users_sign_in_path_path, params: { login: '' }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + + context 'when user is found by username' do + it 'returns nil' do + get users_sign_in_path_path, params: { login: user.username }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + + context 'when user is found by email' do + it 'returns nil' do + get users_sign_in_path_path, params: { login: user.email }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + + context 'when user is not found' do + it 'returns nil in FOSS' do + get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end + end + + context 'when requesting HTML format' do + it 'returns 404' do + get users_sign_in_path_path, as: :html + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when format is not specified' do + it 'returns 404 (defaults to HTML)' do + get users_sign_in_path_path + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end -- GitLab From 6d0af18ce7ef0039920e9d2da9e68170b74f9eac Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Fri, 12 Dec 2025 12:05:31 -0600 Subject: [PATCH 2/8] Add feature flag and fixes --- app/controllers/sessions_controller.rb | 2 + .../feature_flags/beta/two_step_sign_in.yml | 10 +++++ ee/app/controllers/ee/sessions_controller.rb | 6 ++- .../redirect_sign_in_when_login_not_found.yml | 5 +++ spec/controllers/sessions_controller_spec.rb | 37 +++++++++++++++++++ 5 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 config/feature_flags/beta/two_step_sign_in.yml create mode 100644 ee/config/saas_features/redirect_sign_in_when_login_not_found.yml diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index e6f0ede445bbd5..96ae1afd22113b 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -113,6 +113,8 @@ def destroy end def sign_in_path + return render_404 unless Feature.enabled?(:two_step_sign_in, :instance) + respond_to do |format| format.json do render json: { sign_in_path: determine_sign_in_path } diff --git a/config/feature_flags/beta/two_step_sign_in.yml b/config/feature_flags/beta/two_step_sign_in.yml new file mode 100644 index 00000000000000..b7a868bfe172b8 --- /dev/null +++ b/config/feature_flags/beta/two_step_sign_in.yml @@ -0,0 +1,10 @@ +--- +name: two_step_sign_in +description: GitLab.com 2-step sign-in +feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/19425 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/216203 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/583981 +milestone: '18.8' +group: group::organizations +type: beta +default_enabled: false diff --git a/ee/app/controllers/ee/sessions_controller.rb b/ee/app/controllers/ee/sessions_controller.rb index b3be2fa47aac3d..2239bee9cb679f 100644 --- a/ee/app/controllers/ee/sessions_controller.rb +++ b/ee/app/controllers/ee/sessions_controller.rb @@ -30,11 +30,15 @@ def new private + def safe_login_param + params.permit(:login)[:login] + end + override :determine_sign_in_path def determine_sign_in_path return super unless ::Gitlab::Saas.feature_available?(:redirect_sign_in_when_login_not_found) - login = user_params[:login] + login = safe_login_param return if login.blank? user = ::User.find_by_login(login) diff --git a/ee/config/saas_features/redirect_sign_in_when_login_not_found.yml b/ee/config/saas_features/redirect_sign_in_when_login_not_found.yml new file mode 100644 index 00000000000000..0211acad06aa61 --- /dev/null +++ b/ee/config/saas_features/redirect_sign_in_when_login_not_found.yml @@ -0,0 +1,5 @@ +--- +name: redirect_sign_in_when_login_not_found +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/216203 +milestone: '18.8' +group: group::organizations diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 1ee87c2309f32e..ec4d1798630ad6 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -952,6 +952,43 @@ def authenticate_2fa_with_passkeys end end + describe '#sign_in_path' do + context 'when format is json' do + context 'when two_step_sign_in feature flag is enabled' do + before do + stub_feature_flags(two_step_sign_in: true) + end + + it 'returns sign_in_path in json response' do + get :sign_in_path, format: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to have_key('sign_in_path') + end + end + + context 'when two_step_sign_in feature flag is disabled' do + before do + stub_feature_flags(two_step_sign_in: false) + end + + it 'returns 404' do + get :sign_in_path, format: :json + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when format is html' do + it 'returns 404' do + get :sign_in_path, format: :html + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe '#destroy' do before do sign_in(user) -- GitLab From 45eaeea54d566934c0712118025675ac0aae4590 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Mon, 15 Dec 2025 09:22:07 -0600 Subject: [PATCH 3/8] Add feature flag and further testing --- ee/app/controllers/ee/sessions_controller.rb | 2 +- ee/spec/requests/ee/sessions_spec.rb | 77 ++++++++------------ spec/controllers/sessions_controller_spec.rb | 37 ---------- spec/requests/sessions_spec.rb | 55 +++----------- 4 files changed, 43 insertions(+), 128 deletions(-) diff --git a/ee/app/controllers/ee/sessions_controller.rb b/ee/app/controllers/ee/sessions_controller.rb index 2239bee9cb679f..e9c7966ce56cda 100644 --- a/ee/app/controllers/ee/sessions_controller.rb +++ b/ee/app/controllers/ee/sessions_controller.rb @@ -31,7 +31,7 @@ def new private def safe_login_param - params.permit(:login)[:login] + params.permit(:login)[:login].to_s end override :determine_sign_in_path diff --git a/ee/spec/requests/ee/sessions_spec.rb b/ee/spec/requests/ee/sessions_spec.rb index f29b7c679c5c66..ccd828ac7b6f42 100644 --- a/ee/spec/requests/ee/sessions_spec.rb +++ b/ee/spec/requests/ee/sessions_spec.rb @@ -95,55 +95,43 @@ end describe 'GET /users/sign_in_path', :saas do - context 'when redirect_sign_in_when_login_not_found feature is enabled' do - before do - stub_saas_features(redirect_sign_in_when_login_not_found: true) - end + shared_examples 'returns nil sign_in_path' do |login_value| + it 'returns nil' do + params = login_value.nil? ? {} : { login: login_value } + get users_sign_in_path_path, params: params, as: :json - context 'when requesting JSON format' do - context 'when login parameter is not provided' do - it 'returns nil' do - get users_sign_in_path_path, as: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end - end - - context 'when login parameter is blank' do - it 'returns nil' do - get users_sign_in_path_path, params: { login: '' }, as: :json + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end - end + context 'when requesting JSON format' do + context 'when login parameter is not provided' do + it_behaves_like 'returns nil sign_in_path', nil + end - context 'when user is found by username' do - it 'returns nil' do - get users_sign_in_path_path, params: { login: user.username }, as: :json + context 'when login parameter is blank' do + it_behaves_like 'returns nil sign_in_path', '' + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end - end + context 'when user is found by username' do + it_behaves_like 'returns nil sign_in_path', -> { user.username } + end - context 'when user is found by email' do - it 'returns nil' do - get users_sign_in_path_path, params: { login: user.email }, as: :json + context 'when login is not a string' do + it_behaves_like 'returns nil sign_in_path', -> { [user.username, 'attacker@example.com'] } + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end - end + context 'when user is found by email' do + it_behaves_like 'returns nil sign_in_path', -> { user.email } + end - context 'when user is not found' do - it 'returns sign in path with login parameter' do - get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json + context 'when user is not found' do + it 'returns sign in path with login parameter' do + get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => '/users/sign_in?login=nonexistent' }) - end + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => '/users/sign_in?login=nonexistent' }) end end end @@ -153,12 +141,7 @@ stub_saas_features(redirect_sign_in_when_login_not_found: false) end - it 'returns nil even when user is not found' do - get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end + it_behaves_like 'returns nil sign_in_path', -> { 'nonexistent' } end end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index ec4d1798630ad6..1ee87c2309f32e 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -952,43 +952,6 @@ def authenticate_2fa_with_passkeys end end - describe '#sign_in_path' do - context 'when format is json' do - context 'when two_step_sign_in feature flag is enabled' do - before do - stub_feature_flags(two_step_sign_in: true) - end - - it 'returns sign_in_path in json response' do - get :sign_in_path, format: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to have_key('sign_in_path') - end - end - - context 'when two_step_sign_in feature flag is disabled' do - before do - stub_feature_flags(two_step_sign_in: false) - end - - it 'returns 404' do - get :sign_in_path, format: :json - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'when format is html' do - it 'returns 404' do - get :sign_in_path, format: :html - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - describe '#destroy' do before do sign_in(user) diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb index bae5771b28abd9..4c6e93fed983b1 100644 --- a/spec/requests/sessions_spec.rb +++ b/spec/requests/sessions_spec.rb @@ -63,49 +63,26 @@ def authenticate_2fa(otp_attempt:) describe 'GET /users/sign_in_path' do context 'when requesting JSON format' do - context 'when login parameter is not provided' do - it 'returns nil' do - get users_sign_in_path_path, as: :json + it 'renders 404 when the feature flag is disabled' do + stub_feature_flags(two_step_sign_in: false) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end - end - - context 'when login parameter is blank' do - it 'returns nil' do - get users_sign_in_path_path, params: { login: '' }, as: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end - end + get users_sign_in_path_path, as: :json - context 'when user is found by username' do - it 'returns nil' do - get users_sign_in_path_path, params: { login: user.username }, as: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end + expect(response).to have_gitlab_http_status(:not_found) end - context 'when user is found by email' do - it 'returns nil' do - get users_sign_in_path_path, params: { login: user.email }, as: :json + it 'returns nil when login parameter is not provided' do + get users_sign_in_path_path, as: :json - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) end - context 'when user is not found' do - it 'returns nil in FOSS' do - get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json + it 'returns nil when login parameter is blank' do + get users_sign_in_path_path, params: { login: '' }, as: :json - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) - end + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) end end @@ -116,13 +93,5 @@ def authenticate_2fa(otp_attempt:) expect(response).to have_gitlab_http_status(:not_found) end end - - context 'when format is not specified' do - it 'returns 404 (defaults to HTML)' do - get users_sign_in_path_path - - expect(response).to have_gitlab_http_status(:not_found) - end - end end end -- GitLab From 1d58fd85860b30e697ba8fcd0a1a63d40a9fc2be Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Mon, 15 Dec 2025 09:34:18 -0600 Subject: [PATCH 4/8] Change to request type feature flag --- app/controllers/sessions_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 96ae1afd22113b..b79af562f46f8d 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -113,7 +113,7 @@ def destroy end def sign_in_path - return render_404 unless Feature.enabled?(:two_step_sign_in, :instance) + return render_404 unless Feature.enabled?(:two_step_sign_in, Feature.current_request) respond_to do |format| format.json do -- GitLab From afa1128a32eb107ffb2947fb55638b889e6a4f75 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Tue, 16 Dec 2025 08:59:54 -0600 Subject: [PATCH 5/8] Input validation specs --- ee/spec/requests/ee/sessions_spec.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/ee/spec/requests/ee/sessions_spec.rb b/ee/spec/requests/ee/sessions_spec.rb index ccd828ac7b6f42..5d8bb177dd4a3c 100644 --- a/ee/spec/requests/ee/sessions_spec.rb +++ b/ee/spec/requests/ee/sessions_spec.rb @@ -94,7 +94,7 @@ end end - describe 'GET /users/sign_in_path', :saas do + describe 'GET /users/sign_in_path', :saas_redirect_sign_in_when_login_not_found do shared_examples 'returns nil sign_in_path' do |login_value| it 'returns nil' do params = login_value.nil? ? {} : { login: login_value } @@ -119,7 +119,16 @@ end context 'when login is not a string' do - it_behaves_like 'returns nil sign_in_path', -> { [user.username, 'attacker@example.com'] } + it 'ensures User.find_by_login does not receive an array' do + array_param = [user.username, 'attacker@example.com'] + + expect(User).not_to receive(:find_by_login) + + get users_sign_in_path_path, params: { login: array_param }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end end context 'when user is found by email' do @@ -134,6 +143,10 @@ expect(json_response).to eq({ 'sign_in_path' => '/users/sign_in?login=nonexistent' }) end end + + context 'when saas feature is not available' do + it_behaves_like 'returns nil sign_in_path', -> { 'nonexistant' } + end end context 'when redirect_sign_in_when_login_not_found feature is disabled' do -- GitLab From e69f8ec52948afcc2acf03d0dd69e5a3c57dba29 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Tue, 16 Dec 2025 09:14:58 -0600 Subject: [PATCH 6/8] Login param length validation --- ee/app/controllers/ee/sessions_controller.rb | 3 ++- ee/spec/requests/ee/sessions_spec.rb | 25 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/ee/app/controllers/ee/sessions_controller.rb b/ee/app/controllers/ee/sessions_controller.rb index e9c7966ce56cda..e2b763d0f25aeb 100644 --- a/ee/app/controllers/ee/sessions_controller.rb +++ b/ee/app/controllers/ee/sessions_controller.rb @@ -31,7 +31,8 @@ def new private def safe_login_param - params.permit(:login)[:login].to_s + login = params.permit(:login)[:login].to_s + login.truncate(User::MAX_USERNAME_LENGTH, omission: '') end override :determine_sign_in_path diff --git a/ee/spec/requests/ee/sessions_spec.rb b/ee/spec/requests/ee/sessions_spec.rb index 5d8bb177dd4a3c..0419e028f7f3cf 100644 --- a/ee/spec/requests/ee/sessions_spec.rb +++ b/ee/spec/requests/ee/sessions_spec.rb @@ -131,6 +131,31 @@ end end + context 'when login parameter exceeds maximum length' do + it 'truncates login to MAX_USERNAME_LENGTH characters' do + long_login = 'a' * 300 + truncated_login = 'a' * User::MAX_USERNAME_LENGTH + + expect(User).to receive(:find_by_login).with(truncated_login).and_call_original + + get users_sign_in_path_path, params: { login: long_login }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => "/users/sign_in?login=#{truncated_login}" }) + end + + it 'handles login at exactly MAX_USERNAME_LENGTH characters' do + exact_length_login = 'b' * User::MAX_USERNAME_LENGTH + + expect(User).to receive(:find_by_login).with(exact_length_login).and_call_original + + get users_sign_in_path_path, params: { login: exact_length_login }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => "/users/sign_in?login=#{exact_length_login}" }) + end + end + context 'when user is found by email' do it_behaves_like 'returns nil sign_in_path', -> { user.email } end -- GitLab From 253245df94ae1357cf1dbe49a496f815fd04f7e0 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Wed, 17 Dec 2025 11:03:41 -0600 Subject: [PATCH 7/8] Fix max char --- ee/app/controllers/ee/sessions_controller.rb | 2 +- ee/spec/requests/ee/sessions_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ee/app/controllers/ee/sessions_controller.rb b/ee/app/controllers/ee/sessions_controller.rb index e2b763d0f25aeb..2267f1bbb0e767 100644 --- a/ee/app/controllers/ee/sessions_controller.rb +++ b/ee/app/controllers/ee/sessions_controller.rb @@ -32,7 +32,7 @@ def new def safe_login_param login = params.permit(:login)[:login].to_s - login.truncate(User::MAX_USERNAME_LENGTH, omission: '') + login.truncate(::User::MAX_USERNAME_LENGTH, omission: '') end override :determine_sign_in_path diff --git a/ee/spec/requests/ee/sessions_spec.rb b/ee/spec/requests/ee/sessions_spec.rb index 0419e028f7f3cf..b498d47ef83dd6 100644 --- a/ee/spec/requests/ee/sessions_spec.rb +++ b/ee/spec/requests/ee/sessions_spec.rb @@ -134,7 +134,7 @@ context 'when login parameter exceeds maximum length' do it 'truncates login to MAX_USERNAME_LENGTH characters' do long_login = 'a' * 300 - truncated_login = 'a' * User::MAX_USERNAME_LENGTH + truncated_login = 'a' * ::User::MAX_USERNAME_LENGTH expect(User).to receive(:find_by_login).with(truncated_login).and_call_original @@ -145,7 +145,7 @@ end it 'handles login at exactly MAX_USERNAME_LENGTH characters' do - exact_length_login = 'b' * User::MAX_USERNAME_LENGTH + exact_length_login = 'b' * ::User::MAX_USERNAME_LENGTH expect(User).to receive(:find_by_login).with(exact_length_login).and_call_original -- GitLab From cf22709aea82828fc4e7ef03e6019c62bef8dd34 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Thu, 18 Dec 2025 10:20:08 -0600 Subject: [PATCH 8/8] Update specs --- ee/spec/requests/ee/sessions_spec.rb | 106 +++++++++++++++------------ spec/requests/sessions_spec.rb | 32 ++++---- 2 files changed, 76 insertions(+), 62 deletions(-) diff --git a/ee/spec/requests/ee/sessions_spec.rb b/ee/spec/requests/ee/sessions_spec.rb index b498d47ef83dd6..2c235d48e544a2 100644 --- a/ee/spec/requests/ee/sessions_spec.rb +++ b/ee/spec/requests/ee/sessions_spec.rb @@ -105,80 +105,90 @@ end end - context 'when requesting JSON format' do - context 'when login parameter is not provided' do - it_behaves_like 'returns nil sign_in_path', nil - end - - context 'when login parameter is blank' do - it_behaves_like 'returns nil sign_in_path', '' - end + context 'when redirect_sign_in_when_login_not_found saas feature is available' do + context 'when requesting JSON format' do + it 'renders 404 when the feature flag is disabled' do + stub_feature_flags(two_step_sign_in: false) - context 'when user is found by username' do - it_behaves_like 'returns nil sign_in_path', -> { user.username } - end + get users_sign_in_path_path, params: { login: 'nonexistant' }, as: :json - context 'when login is not a string' do - it 'ensures User.find_by_login does not receive an array' do - array_param = [user.username, 'attacker@example.com'] + expect(response).to have_gitlab_http_status(:not_found) + end - expect(User).not_to receive(:find_by_login) + context 'when login parameter is not provided' do + it_behaves_like 'returns nil sign_in_path', nil + end - get users_sign_in_path_path, params: { login: array_param }, as: :json + context 'when login parameter is blank' do + it_behaves_like 'returns nil sign_in_path', '' + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) + context 'when user is found by username' do + it_behaves_like 'returns nil sign_in_path', -> { user.username } end - end - context 'when login parameter exceeds maximum length' do - it 'truncates login to MAX_USERNAME_LENGTH characters' do - long_login = 'a' * 300 - truncated_login = 'a' * ::User::MAX_USERNAME_LENGTH + context 'when login is not a string' do + it 'ensures User.find_by_login does not receive an array' do + array_param = [user.username, 'attacker@example.com'] - expect(User).to receive(:find_by_login).with(truncated_login).and_call_original + expect(User).not_to receive(:find_by_login) - get users_sign_in_path_path, params: { login: long_login }, as: :json + get users_sign_in_path_path, params: { login: array_param }, as: :json - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => "/users/sign_in?login=#{truncated_login}" }) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) + end end - it 'handles login at exactly MAX_USERNAME_LENGTH characters' do - exact_length_login = 'b' * ::User::MAX_USERNAME_LENGTH + context 'when login parameter exceeds maximum length' do + it 'truncates login to MAX_USERNAME_LENGTH characters' do + long_login = 'a' * 300 + truncated_login = 'a' * ::User::MAX_USERNAME_LENGTH + + expect(User).to receive(:find_by_login).with(truncated_login).and_call_original + + get users_sign_in_path_path, params: { login: long_login }, as: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => "/users/sign_in?login=#{truncated_login}" }) + end + + it 'handles login at exactly MAX_USERNAME_LENGTH characters' do + exact_length_login = 'b' * ::User::MAX_USERNAME_LENGTH - expect(User).to receive(:find_by_login).with(exact_length_login).and_call_original + expect(User).to receive(:find_by_login).with(exact_length_login).and_call_original - get users_sign_in_path_path, params: { login: exact_length_login }, as: :json + get users_sign_in_path_path, params: { login: exact_length_login }, as: :json - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => "/users/sign_in?login=#{exact_length_login}" }) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => "/users/sign_in?login=#{exact_length_login}" }) + end end - end - context 'when user is found by email' do - it_behaves_like 'returns nil sign_in_path', -> { user.email } - end + context 'when user is found by email' do + it_behaves_like 'returns nil sign_in_path', -> { user.email } + end - context 'when user is not found' do - it 'returns sign in path with login parameter' do - get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json + context 'when user is not found' do + it 'returns sign in path with login parameter' do + get users_sign_in_path_path, params: { login: 'nonexistent' }, as: :json - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => '/users/sign_in?login=nonexistent' }) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => '/users/sign_in?login=nonexistent' }) + end end end - context 'when saas feature is not available' do - it_behaves_like 'returns nil sign_in_path', -> { 'nonexistant' } + context 'when requesting HTML format' do + it 'returns 404' do + get users_sign_in_path_path, as: :html + + expect(response).to have_gitlab_http_status(:not_found) + end end end context 'when redirect_sign_in_when_login_not_found feature is disabled' do - before do - stub_saas_features(redirect_sign_in_when_login_not_found: false) - end - it_behaves_like 'returns nil sign_in_path', -> { 'nonexistent' } end end diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb index 4c6e93fed983b1..790c44f3a51e4d 100644 --- a/spec/requests/sessions_spec.rb +++ b/spec/requests/sessions_spec.rb @@ -62,27 +62,31 @@ def authenticate_2fa(otp_attempt:) end describe 'GET /users/sign_in_path' do - context 'when requesting JSON format' do - it 'renders 404 when the feature flag is disabled' do - stub_feature_flags(two_step_sign_in: false) - - get users_sign_in_path_path, as: :json + shared_examples 'returns nil sign_in_path' do |login_value| + it 'returns nil' do + params = login_value.nil? ? {} : { login: login_value } + get users_sign_in_path_path, params: params, as: :json - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'sign_in_path' => nil }) end + end - it 'returns nil when login parameter is not provided' do - get users_sign_in_path_path, as: :json + context 'when requesting JSON format' do + context 'when login parameter is not provided' do + it_behaves_like 'returns nil sign_in_path', nil + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) + context 'when login parameter is blank' do + it_behaves_like 'returns nil sign_in_path', '' end - it 'returns nil when login parameter is blank' do - get users_sign_in_path_path, params: { login: '' }, as: :json + context 'when user is found by username' do + it_behaves_like 'returns nil sign_in_path', -> { user.username } + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'sign_in_path' => nil }) + context 'when user is not found found' do + it_behaves_like 'returns nil sign_in_path', -> { 'nonexistent' } end end -- GitLab