Commit 71d61c67 authored by Sebastián Arcila Valenzuela's avatar Sebastián Arcila Valenzuela Committed by Imre Farkas

Add /internal/two_factor_config endpoint

This endpoint is used to check if we need to ask for 2FA from the
GitLab PAM part of https://gitlab.com/groups/gitlab-org/-/epics/2889#note_397341768
parent ce4d9e77
---
name: two_factor_for_cli
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39703
rollout_issue_url:
type: development
group: group::access
default_enabled: false
...@@ -99,6 +99,14 @@ module API ...@@ -99,6 +99,14 @@ module API
@project = @container = access_checker.container @project = @container = access_checker.container
end end
end end
def validate_actor_key(actor, key_id)
return 'Could not find a user without a key' unless key_id
return 'Could not find the given key' unless actor.key
'Could not find a user for the given key' unless actor.user
end
end end
namespace 'internal' do namespace 'internal' do
...@@ -163,28 +171,23 @@ module API ...@@ -163,28 +171,23 @@ module API
redis: redis_ping redis: redis_ping
} }
end end
post '/two_factor_recovery_codes' do post '/two_factor_recovery_codes' do
status 200 status 200
actor.update_last_used_at! actor.update_last_used_at!
user = actor.user user = actor.user
if params[:key_id] error_message = validate_actor_key(actor, params[:key_id])
unless actor.key
break { success: false, message: 'Could not find the given key' }
end
if actor.key.is_a?(DeployKey)
break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
end
unless user if params[:user_id] && user.nil?
break { success: false, message: 'Could not find a user for the given key' }
end
elsif params[:user_id] && user.nil?
break { success: false, message: 'Could not find the given user' } break { success: false, message: 'Could not find the given user' }
elsif error_message
break { success: false, message: error_message }
end end
break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' } if actor.key.is_a?(DeployKey)
unless user.two_factor_enabled? unless user.two_factor_enabled?
break { success: false, message: 'Two-factor authentication is not enabled for this user' } break { success: false, message: 'Two-factor authentication is not enabled for this user' }
end end
...@@ -204,20 +207,14 @@ module API ...@@ -204,20 +207,14 @@ module API
actor.update_last_used_at! actor.update_last_used_at!
user = actor.user user = actor.user
if params[:key_id] error_message = validate_actor_key(actor, params[:key_id])
unless actor.key
break { success: false, message: 'Could not find the given key' }
end
if actor.key.is_a?(DeployKey) break { success: false, message: 'Deploy keys cannot be used to create personal access tokens' } if actor.key.is_a?(DeployKey)
break { success: false, message: 'Deploy keys cannot be used to create personal access tokens' }
end
unless user if params[:user_id] && user.nil?
break { success: false, message: 'Could not find a user for the given key' }
end
elsif params[:user_id] && user.nil?
break { success: false, message: 'Could not find the given user' } break { success: false, message: 'Could not find the given user' }
elsif error_message
break { success: false, message: error_message }
end end
if params[:name].blank? if params[:name].blank?
...@@ -269,6 +266,28 @@ module API ...@@ -269,6 +266,28 @@ module API
present response, with: Entities::InternalPostReceive::Response present response, with: Entities::InternalPostReceive::Response
end end
post '/two_factor_config' do
status 200
break { success: false } unless Feature.enabled?(:two_factor_for_cli)
actor.update_last_used_at!
user = actor.user
error_message = validate_actor_key(actor, params[:key_id])
if error_message
{ success: false, message: error_message }
elsif actor.key.is_a?(DeployKey)
{ success: true, two_factor_required: false }
else
{
success: true,
two_factor_required: user.two_factor_enabled?
}
end
end
end end
end end
end end
......
...@@ -50,43 +50,63 @@ RSpec.describe API::Internal::Base do ...@@ -50,43 +50,63 @@ RSpec.describe API::Internal::Base do
end end
end end
describe 'GET /internal/two_factor_recovery_codes' do shared_examples 'actor key validations' do
it 'returns an error message when the key does not exist' do context 'key id is not provided' do
post api('/internal/two_factor_recovery_codes'), let(:key_id) { nil }
params: {
secret_token: secret_token,
key_id: non_existing_record_id
}
expect(json_response['success']).to be_falsey it 'returns an error message' do
expect(json_response['message']).to eq('Could not find the given key') subject
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq('Could not find a user without a key')
end
end end
it 'returns an error message when the key is a deploy key' do context 'key does not exist' do
deploy_key = create(:deploy_key) let(:key_id) { non_existing_record_id }
post api('/internal/two_factor_recovery_codes'), it 'returns an error message' do
params: { subject
secret_token: secret_token,
key_id: deploy_key.id
}
expect(json_response['success']).to be_falsey expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq('Deploy keys cannot be used to retrieve recovery codes') expect(json_response['message']).to eq('Could not find the given key')
end
end end
it 'returns an error message when the user does not exist' do context 'key without user' do
key_without_user = create(:key, user: nil) let(:key_id) { create(:key, user: nil).id }
it 'returns an error message' do
subject
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq('Could not find a user for the given key')
end
end
end
describe 'GET /internal/two_factor_recovery_codes' do
let(:key_id) { key.id }
subject do
post api('/internal/two_factor_recovery_codes'), post api('/internal/two_factor_recovery_codes'),
params: { params: {
secret_token: secret_token, secret_token: secret_token,
key_id: key_without_user.id key_id: key_id
} }
end
expect(json_response['success']).to be_falsey it_behaves_like 'actor key validations'
expect(json_response['message']).to eq('Could not find a user for the given key')
expect(json_response['recovery_codes']).to be_nil context 'key is a deploy key' do
let(:key_id) { create(:deploy_key).id }
it 'returns an error message' do
subject
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq('Deploy keys cannot be used to retrieve recovery codes')
end
end end
context 'when two-factor is enabled' do context 'when two-factor is enabled' do
...@@ -95,11 +115,7 @@ RSpec.describe API::Internal::Base do ...@@ -95,11 +115,7 @@ RSpec.describe API::Internal::Base do
allow_any_instance_of(User) allow_any_instance_of(User)
.to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861)) .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861))
post api('/internal/two_factor_recovery_codes'), subject
params: {
secret_token: secret_token,
key_id: key.id
}
expect(json_response['success']).to be_truthy expect(json_response['success']).to be_truthy
expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861)) expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861))
...@@ -110,11 +126,7 @@ RSpec.describe API::Internal::Base do ...@@ -110,11 +126,7 @@ RSpec.describe API::Internal::Base do
it 'returns an error message' do it 'returns an error message' do
allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false) allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false)
post api('/internal/two_factor_recovery_codes'), subject
params: {
secret_token: secret_token,
key_id: key.id
}
expect(json_response['success']).to be_falsey expect(json_response['success']).to be_falsey
expect(json_response['recovery_codes']).to be_nil expect(json_response['recovery_codes']).to be_nil
...@@ -123,42 +135,27 @@ RSpec.describe API::Internal::Base do ...@@ -123,42 +135,27 @@ RSpec.describe API::Internal::Base do
end end
describe 'POST /internal/personal_access_token' do describe 'POST /internal/personal_access_token' do
it 'returns an error message when the key does not exist' do let(:key_id) { key.id }
post api('/internal/personal_access_token'),
params: {
secret_token: secret_token,
key_id: non_existing_record_id
}
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq('Could not find the given key')
end
it 'returns an error message when the key is a deploy key' do
deploy_key = create(:deploy_key)
subject do
post api('/internal/personal_access_token'), post api('/internal/personal_access_token'),
params: { params: {
secret_token: secret_token, secret_token: secret_token,
key_id: deploy_key.id key_id: key_id
} }
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq('Deploy keys cannot be used to create personal access tokens')
end end
it 'returns an error message when the user does not exist' do it_behaves_like 'actor key validations'
key_without_user = create(:key, user: nil)
post api('/internal/personal_access_token'), context 'key is a deploy key' do
params: { let(:key_id) { create(:deploy_key).id }
secret_token: secret_token,
key_id: key_without_user.id
}
expect(json_response['success']).to be_falsey it 'returns an error message' do
expect(json_response['message']).to eq('Could not find a user for the given key') subject
expect(json_response['token']).to be_nil
expect(json_response['success']).to be_falsey
expect(json_response['message']).to eq('Deploy keys cannot be used to create personal access tokens')
end
end end
it 'returns an error message when given an non existent user' do it 'returns an error message when given an non existent user' do
...@@ -1209,6 +1206,73 @@ RSpec.describe API::Internal::Base do ...@@ -1209,6 +1206,73 @@ RSpec.describe API::Internal::Base do
end end
end end
describe 'POST /internal/two_factor_config' do
let(:key_id) { key.id }
before do
stub_feature_flags(two_factor_for_cli: true)
end
subject do
post api('/internal/two_factor_config'),
params: {
secret_token: secret_token,
key_id: key_id
}
end
it_behaves_like 'actor key validations'
context 'when the key is a deploy key' do
let(:key) { create(:deploy_key) }
it 'does not required two factor' do
subject
expect(json_response['success']).to be_truthy
expect(json_response['two_factor_required']).to be_falsey
end
end
context 'when two-factor is enabled' do
it 'returns user two factor config' do
allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
subject
expect(json_response['success']).to be_truthy
expect(json_response['two_factor_required']).to be_truthy
end
end
context 'when two-factor is not enabled' do
it 'returns an error message' do
allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false)
subject
expect(json_response['success']).to be_truthy
expect(json_response['two_factor_required']).to be_falsey
end
end
context 'two_factor_for_cli feature is disabled' do
before do
stub_feature_flags(two_factor_for_cli: false)
end
context 'when two-factor is enabled for the user' do
it 'returns user two factor config' do
allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
subject
expect(json_response['success']).to be_falsey
end
end
end
end
def lfs_auth_project(project) def lfs_auth_project(project)
post( post(
api("/internal/lfs_authenticate"), api("/internal/lfs_authenticate"),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment