Commit 42ada09c authored by Stan Hu's avatar Stan Hu

Merge branch 'fj-28429-generate-lfs-token-authorization' into 'master'

Generate LFS token authorization for user LFS requests

Closes #32553 and #28429

See merge request gitlab-org/gitlab!17332
parents d1bfec9b b2bfd36b
...@@ -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