Commit b2bfd36b authored by Francisco Javier López's avatar Francisco Javier López Committed by Stan Hu

Generate LFS token authorization for user LFS requests

When the user performs an LFS batch request to execute
a download or upload, we generate an LFS token in order
to avoid authenticating them in each latter request.
parent d1bfec9b
...@@ -288,9 +288,7 @@ class ApplicationController < ActionController::Base ...@@ -288,9 +288,7 @@ class ApplicationController < ActionController::Base
def check_password_expiration def check_password_expiration
return if session[:impersonator_id] || !current_user&.allow_password_authentication? return if session[:impersonator_id] || !current_user&.allow_password_authentication?
password_expires_at = current_user&.password_expires_at if current_user&.password_expired?
if password_expires_at && password_expires_at < Time.now
return redirect_to new_profile_password_path return redirect_to new_profile_password_path
end end
end end
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Projects::LfsApiController < Projects::GitHttpClientController class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest include LfsRequest
include Gitlab::Utils::StrongMemoize
LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream' LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream'
...@@ -81,7 +82,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -81,7 +82,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
download: { download: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}", href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}",
header: { header: {
Authorization: request.headers['Authorization'] Authorization: authorization_header
}.compact }.compact
} }
} }
...@@ -92,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -92,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
upload: { upload: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
header: { header: {
Authorization: request.headers['Authorization'], Authorization: authorization_header,
# git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This # git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
# ensures that Workhorse can intercept the request. # ensures that Workhorse can intercept the request.
'Content-Type': LFS_TRANSFER_CONTENT_TYPE 'Content-Type': LFS_TRANSFER_CONTENT_TYPE
...@@ -122,6 +123,18 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -122,6 +123,18 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def lfs_read_only_message def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.') _('You cannot write to this read-only GitLab instance.')
end end
def authorization_header
strong_memoize(:authorization_header) do
lfs_auth_header || request.headers['Authorization']
end
end
def lfs_auth_header
return unless user.is_a?(User)
Gitlab::LfsToken.new(user).basic_encoding
end
end end
Projects::LfsApiController.prepend_if_ee('EE::Projects::LfsApiController') Projects::LfsApiController.prepend_if_ee('EE::Projects::LfsApiController')
...@@ -1519,6 +1519,10 @@ class User < ApplicationRecord ...@@ -1519,6 +1519,10 @@ class User < ApplicationRecord
todos.find_by(target: target, state: :pending) todos.find_by(target: target, state: :pending)
end end
def password_expired?
!!(password_expires_at && password_expires_at < Time.now)
end
# @deprecated # @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
......
---
title: Generate LFS token authorization for user LFS requests
merge_request: 17332
author:
type: fixed
...@@ -231,7 +231,7 @@ module Gitlab ...@@ -231,7 +231,7 @@ module Gitlab
authentication_abilities = authentication_abilities =
if token_handler.user? if token_handler.user?
full_authentication_abilities read_write_project_authentication_abilities
elsif token_handler.deploy_key_pushable?(project) elsif token_handler.deploy_key_pushable?(project)
read_write_authentication_abilities read_write_authentication_abilities
else else
...@@ -272,10 +272,21 @@ module Gitlab ...@@ -272,10 +272,21 @@ module Gitlab
] ]
end end
def read_only_authentication_abilities def read_only_project_authentication_abilities
[ [
:read_project, :read_project,
:download_code, :download_code
]
end
def read_write_project_authentication_abilities
read_only_project_authentication_abilities + [
:push_code
]
end
def read_only_authentication_abilities
read_only_project_authentication_abilities + [
:read_container_image :read_container_image
] ]
end end
......
...@@ -34,8 +34,11 @@ module Gitlab ...@@ -34,8 +34,11 @@ module Gitlab
HMACToken.new(actor).token(DEFAULT_EXPIRE_TIME) HMACToken.new(actor).token(DEFAULT_EXPIRE_TIME)
end end
# When the token is an lfs one and the actor
# is blocked or the password has been changed,
# the token is no longer valid
def token_valid?(token_to_check) def token_valid?(token_to_check)
HMACToken.new(actor).token_valid?(token_to_check) HMACToken.new(actor).token_valid?(token_to_check) && valid_user?
end end
def deploy_key_pushable?(project) def deploy_key_pushable?(project)
...@@ -46,6 +49,12 @@ module Gitlab ...@@ -46,6 +49,12 @@ module Gitlab
user? ? :lfs_token : :lfs_deploy_token user? ? :lfs_token : :lfs_deploy_token
end end
def valid_user?
return true unless user?
!actor.blocked? && (!actor.allow_password_authentication? || !actor.password_expired?)
end
def authentication_payload(repository_http_path) def authentication_payload(repository_http_path)
{ {
username: actor_name, username: actor_name,
...@@ -55,6 +64,10 @@ module Gitlab ...@@ -55,6 +64,10 @@ module Gitlab
} }
end end
def basic_encoding
ActionController::HttpAuthentication::Basic.encode_credentials(actor_name, token)
end
private # rubocop:disable Lint/UselessAccessModifier private # rubocop:disable Lint/UselessAccessModifier
class HMACToken class HMACToken
......
This diff is collapsed.
...@@ -115,6 +115,46 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do ...@@ -115,6 +115,46 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
end end
end end
context 'when the actor is a regular user' do
context 'when the user is blocked' do
let(:actor) { create(:user, :blocked) }
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
end
end
context 'when the user password is expired' do
let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
end
end
end
context 'when the actor is an ldap user' do
before do
allow(actor).to receive(:ldap_user?).and_return(true)
end
context 'when the user is blocked' do
let(:actor) { create(:user, :blocked) }
it 'returns false' do
expect(lfs_token.token_valid?(lfs_token.token)).to be_falsey
end
end
context 'when the user password is expired' do
let(:actor) { create(:user, password_expires_at: 1.minute.ago) }
it 'returns true' do
expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
end
end
end
end end
end end
......
...@@ -3616,4 +3616,34 @@ describe User do ...@@ -3616,4 +3616,34 @@ describe User do
end end
end end
end end
describe '#password_expired?' do
let(:user) { build(:user, password_expires_at: password_expires_at) }
subject { user.password_expired? }
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
it 'returns false' do
is_expected.to be_falsey
end
end
context 'when password_expires_at is in the past' do
let(:password_expires_at) { 1.minute.ago }
it 'returns true' do
is_expected.to be_truthy
end
end
context 'when password_expires_at is in the future' do
let(:password_expires_at) { 1.minute.from_now }
it 'returns false' do
is_expected.to be_falsey
end
end
end
end end
This diff is collapsed.
# frozen_string_literal: true
require_relative 'workhorse_helpers'
module LfsHttpHelpers
include WorkhorseHelpers
def authorize_ci_project
ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
end
def authorize_user
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
def authorize_deploy_key
Gitlab::LfsToken.new(key).basic_encoding
end
def authorize_user_key
Gitlab::LfsToken.new(user).basic_encoding
end
def authorize_deploy_token
ActionController::HttpAuthentication::Basic.encode_credentials(deploy_token.username, deploy_token.token)
end
def post_lfs_json(url, body = nil, headers = nil)
params = body.try(:to_json)
headers = (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)
post(url, params: params, headers: headers)
end
def batch_url(project)
"#{project.http_url_to_repo}/info/lfs/objects/batch"
end
def objects_url(project, oid = nil, size = nil)
File.join(["#{project.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s))
end
def authorize_url(project, oid, size)
File.join(objects_url(project, oid, size), 'authorize')
end
def download_body(objects)
request_body('download', objects)
end
def upload_body(objects)
request_body('upload', objects)
end
def request_body(operation, objects)
objects = [objects] unless objects.is_a?(Array)
{
'operation' => operation,
'objects' => objects
}
end
end
# frozen_string_literal: true
shared_examples 'LFS http 200 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 200 }
end
end
shared_examples 'LFS http 401 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 401 }
end
end
shared_examples 'LFS http 403 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 403 }
let(:message) { 'Access forbidden. Check your access level.' }
end
end
shared_examples 'LFS http 501 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 501 }
let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' }
end
end
shared_examples 'LFS http 404 response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 404 }
end
end
shared_examples 'LFS http expected response code and message' do
let(:response_code) { }
let(:message) { }
it 'responds with the expected response code and message' do
expect(response).to have_gitlab_http_status(response_code)
expect(json_response['message']).to eq(message) if message
end
end
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