Merge branch '2fa-check-git-http' into 'master'
2FA checks for Git over HTTP ## What does this MR do? This MR allows the use of `PersonalAccessTokens` to access Git over HTTP and makes that the only allowed method if the user has 2FA enabled. If a user with 2FA enabled tries to access Git over HTTP using his username and password the request will be denied and the user will be presented with the following message: ``` remote: HTTP Basic: Access denied remote: You have 2FA enabled, please use a personal access token for Git over HTTP. remote: You can generate one at http://localhost:3000/profile/personal_access_tokens fatal: Authentication failed for 'http://localhost:3000/documentcloud/underscore.git/' ``` ## What are the relevant issue numbers? Fixes #13568 See merge request !5764
This commit is contained in:
commit
a59c33e4a3
5 changed files with 92 additions and 6 deletions
|
@ -68,6 +68,7 @@ v 8.11.0 (unreleased)
|
|||
- Upgrade Grape from 0.13.0 to 0.15.0. !4601
|
||||
- Trigram indexes for the "ci_runners" table have been removed to speed up UPDATE queries
|
||||
- Fix devise deprecation warnings.
|
||||
- Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764
|
||||
- Update version_sorter and use new interface for faster tag sorting
|
||||
- Optimize checking if a user has read access to a list of issues !5370
|
||||
- Store all DB secrets in secrets.yml, under descriptive names !5274
|
||||
|
|
|
@ -27,6 +27,9 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
@ci = true
|
||||
elsif auth_result.type == :oauth && !download_request?
|
||||
# Not allowed
|
||||
elsif auth_result.type == :missing_personal_token
|
||||
render_missing_personal_token
|
||||
return # Render above denied access, nothing left to do
|
||||
else
|
||||
@user = auth_result.user
|
||||
end
|
||||
|
@ -91,6 +94,13 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
[nil, nil]
|
||||
end
|
||||
|
||||
def render_missing_personal_token
|
||||
render plain: "HTTP Basic: Access denied\n" \
|
||||
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
|
||||
"You can generate one at #{profile_personal_access_tokens_url}",
|
||||
status: 401
|
||||
end
|
||||
|
||||
def repository
|
||||
_, suffix = project_id_with_suffix
|
||||
if suffix == '.wiki.git'
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
= page_title
|
||||
%p
|
||||
You can generate a personal access token for each application you use that needs access to the GitLab API.
|
||||
%p
|
||||
You can also use personal access tokens to authenticate against Git over HTTP.
|
||||
They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.
|
||||
|
||||
.col-lg-9
|
||||
|
||||
- if flash[:personal_access_token]
|
||||
|
|
|
@ -10,13 +10,12 @@ module Gitlab
|
|||
|
||||
if valid_ci_request?(login, password, project)
|
||||
result.type = :ci
|
||||
elsif result.user = find_with_user_password(login, password)
|
||||
result.type = :gitlab_or_ldap
|
||||
elsif result.user = oauth_access_token_check(login, password)
|
||||
result.type = :oauth
|
||||
else
|
||||
result = populate_result(login, password)
|
||||
end
|
||||
|
||||
rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login)
|
||||
success = result.user.present? || [:ci, :missing_personal_token].include?(result.type)
|
||||
rate_limit!(ip, success: success, login: login)
|
||||
result
|
||||
end
|
||||
|
||||
|
@ -76,10 +75,43 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def populate_result(login, password)
|
||||
result =
|
||||
user_with_password_for_git(login, password) ||
|
||||
oauth_access_token_check(login, password) ||
|
||||
personal_access_token_check(login, password)
|
||||
|
||||
if result
|
||||
result.type = nil unless result.user
|
||||
|
||||
if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap
|
||||
result.type = :missing_personal_token
|
||||
end
|
||||
end
|
||||
|
||||
result || Result.new
|
||||
end
|
||||
|
||||
def user_with_password_for_git(login, password)
|
||||
user = find_with_user_password(login, password)
|
||||
Result.new(user, :gitlab_or_ldap) if user
|
||||
end
|
||||
|
||||
def oauth_access_token_check(login, password)
|
||||
if login == "oauth2" && password.present?
|
||||
token = Doorkeeper::AccessToken.by_token(password)
|
||||
token && token.accessible? && User.find_by(id: token.resource_owner_id)
|
||||
if token && token.accessible?
|
||||
user = User.find_by(id: token.resource_owner_id)
|
||||
Result.new(user, :oauth)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def personal_access_token_check(login, password)
|
||||
if login && password
|
||||
user = User.find_by_personal_access_token(password)
|
||||
validation = User.by_login(login)
|
||||
Result.new(user, :personal_token) if user == validation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -198,6 +198,45 @@ describe 'Git HTTP requests', lib: true do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user has 2FA enabled' do
|
||||
let(:user) { create(:user, :two_factor) }
|
||||
let(:access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
end
|
||||
|
||||
context 'when username and password are provided' do
|
||||
it 'rejects the clone attempt' do
|
||||
download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_http_status(401)
|
||||
expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects the push attempt' do
|
||||
upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_http_status(401)
|
||||
expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when username and personal access token are provided' do
|
||||
it 'allows clones' do
|
||||
download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows pushes' do
|
||||
upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when blank password attempts follow a valid login" do
|
||||
def attempt_login(include_password)
|
||||
password = include_password ? user.password : ""
|
||||
|
|
Loading…
Reference in a new issue