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
def check_password_expiration
return if session[:impersonator_id] || !current_user&.allow_password_authentication?
password_expires_at = current_user&.password_expires_at
if password_expires_at && password_expires_at < Time.now
if current_user&.password_expired?
return redirect_to new_profile_password_path
end
end
......
......@@ -2,6 +2,7 @@
class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest
include Gitlab::Utils::StrongMemoize
LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream'
......@@ -81,7 +82,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
download: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}",
header: {
Authorization: request.headers['Authorization']
Authorization: authorization_header
}.compact
}
}
......@@ -92,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
upload: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
header: {
Authorization: request.headers['Authorization'],
Authorization: authorization_header,
# git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
# ensures that Workhorse can intercept the request.
'Content-Type': LFS_TRANSFER_CONTENT_TYPE
......@@ -122,6 +123,18 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
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
Projects::LfsApiController.prepend_if_ee('EE::Projects::LfsApiController')
......@@ -1519,6 +1519,10 @@ class User < ApplicationRecord
todos.find_by(target: target, state: :pending)
end
def password_expired?
!!(password_expires_at && password_expires_at < Time.now)
end
# @deprecated
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
authentication_abilities =
if token_handler.user?
full_authentication_abilities
read_write_project_authentication_abilities
elsif token_handler.deploy_key_pushable?(project)
read_write_authentication_abilities
else
......@@ -272,10 +272,21 @@ module Gitlab
]
end
def read_only_authentication_abilities
def read_only_project_authentication_abilities
[
: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
]
end
......
......@@ -34,8 +34,11 @@ module Gitlab
HMACToken.new(actor).token(DEFAULT_EXPIRE_TIME)
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)
HMACToken.new(actor).token_valid?(token_to_check)
HMACToken.new(actor).token_valid?(token_to_check) && valid_user?
end
def deploy_key_pushable?(project)
......@@ -46,6 +49,12 @@ module Gitlab
user? ? :lfs_token : :lfs_deploy_token
end
def valid_user?
return true unless user?
!actor.blocked? && (!actor.allow_password_authentication? || !actor.password_expired?)
end
def authentication_payload(repository_http_path)
{
username: actor_name,
......@@ -55,6 +64,10 @@ module Gitlab
}
end
def basic_encoding
ActionController::HttpAuthentication::Basic.encode_credentials(actor_name, token)
end
private # rubocop:disable Lint/UselessAccessModifier
class HMACToken
......
This diff is collapsed.
......@@ -115,6 +115,46 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
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
......
......@@ -3616,4 +3616,34 @@ describe User do
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
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