diff --git a/CHANGELOG b/CHANGELOG index 3adbb0d8927..98638b8eb28 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 7c21bd181dc..a5b4031c30f 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -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' diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 71ac367830d..05a2ea67aa2 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -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] diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index db1704af75e..91f0270818a 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -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 diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 8537c252b58..afaf4b7cefb 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -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 : ""