Split docker authentication service
This commit is contained in:
parent
105017c308
commit
011a905a82
|
@ -2,6 +2,10 @@ class JwtController < ApplicationController
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
|
||||||
|
SERVICES = {
|
||||||
|
'docker' => Jwt::DockerAuthenticationService,
|
||||||
|
}
|
||||||
|
|
||||||
def auth
|
def auth
|
||||||
@authenticated = authenticate_with_http_basic do |login, password|
|
@authenticated = authenticate_with_http_basic do |login, password|
|
||||||
@ci_project = ci_project(login, password)
|
@ci_project = ci_project(login, password)
|
||||||
|
@ -9,46 +13,22 @@ class JwtController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
unless @authenticated
|
unless @authenticated
|
||||||
return render_403 if has_basic_credentials?
|
head :forbidden if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request)
|
||||||
end
|
end
|
||||||
|
|
||||||
case params[:service]
|
service = SERVICES[params[:service]]
|
||||||
when 'docker'
|
head :not_found unless service
|
||||||
docker_token_auth(params[:scope], params[:offline_token])
|
|
||||||
else
|
result = service.new(@ci_project, @user, auth_params).execute
|
||||||
return render_404
|
return head result[:http_status] if result[:http_status]
|
||||||
end
|
|
||||||
|
render json: result
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def render_400
|
def auth_params
|
||||||
head :invalid_request
|
params.permit(:service, :scope, :offline_token, :account, :client_id)
|
||||||
end
|
|
||||||
|
|
||||||
def render_404
|
|
||||||
head :not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_403
|
|
||||||
head :forbidden
|
|
||||||
end
|
|
||||||
|
|
||||||
def docker_token_auth(scope, offline_token)
|
|
||||||
payload = {
|
|
||||||
aud: params[:service],
|
|
||||||
sub: @user.try(:username)
|
|
||||||
}
|
|
||||||
|
|
||||||
if offline_token
|
|
||||||
return render_403 unless @user
|
|
||||||
elsif scope
|
|
||||||
access = process_access(scope)
|
|
||||||
return render_404 unless access
|
|
||||||
payload[:access] = [access]
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: { token: encode(payload) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def ci_project(login, password)
|
def ci_project(login, password)
|
||||||
|
@ -102,72 +82,4 @@ class JwtController < ApplicationController
|
||||||
|
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_access(scope)
|
|
||||||
type, name, actions = scope.split(':', 3)
|
|
||||||
actions = actions.split(',')
|
|
||||||
|
|
||||||
case type
|
|
||||||
when 'repository'
|
|
||||||
process_repository_access(type, name, actions)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_repository_access(type, name, actions)
|
|
||||||
project = Project.find_with_namespace(name)
|
|
||||||
return unless project
|
|
||||||
|
|
||||||
actions = actions.select do |action|
|
|
||||||
can_access?(project, action)
|
|
||||||
end
|
|
||||||
|
|
||||||
{ type: 'repository', name: name, actions: actions } if actions
|
|
||||||
end
|
|
||||||
|
|
||||||
def default_payload
|
|
||||||
{
|
|
||||||
aud: 'docker',
|
|
||||||
sub: @user.try(:username),
|
|
||||||
aud: params[:service],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def private_key
|
|
||||||
@private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key
|
|
||||||
end
|
|
||||||
|
|
||||||
def encode(payload)
|
|
||||||
issued_at = Time.now
|
|
||||||
payload = payload.merge(
|
|
||||||
iss: Gitlab.config.registry.issuer,
|
|
||||||
iat: issued_at.to_i,
|
|
||||||
nbf: issued_at.to_i - 5.seconds.to_i,
|
|
||||||
exp: issued_at.to_i + 60.minutes.to_i,
|
|
||||||
jti: SecureRandom.uuid,
|
|
||||||
)
|
|
||||||
headers = {
|
|
||||||
kid: kid(private_key)
|
|
||||||
}
|
|
||||||
JWT.encode(payload, private_key, 'RS256', headers)
|
|
||||||
end
|
|
||||||
|
|
||||||
def can_access?(project, action)
|
|
||||||
case action
|
|
||||||
when 'pull'
|
|
||||||
project == @ci_project || can?(@user, :download_code, project)
|
|
||||||
when 'push'
|
|
||||||
project == @ci_project || can?(@user, :push_code, project)
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def kid(private_key)
|
|
||||||
sha256 = Digest::SHA256.new
|
|
||||||
sha256.update(private_key.public_key.to_der)
|
|
||||||
payload = StringIO.new(sha256.digest).read(30)
|
|
||||||
Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem|
|
|
||||||
mem << slice.join
|
|
||||||
end.join(':')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
module Jwt
|
||||||
|
class DockerAuthenticationService < BaseService
|
||||||
|
def execute
|
||||||
|
if params[:offline_token]
|
||||||
|
return error('forbidden', 403) unless current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
{ token: token.encoded }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def token
|
||||||
|
token = ::Jwt::RSAToken.new(registry.key)
|
||||||
|
token.issuer = registry.issuer
|
||||||
|
token.audience = params[:service]
|
||||||
|
token.subject = current_user.try(:username)
|
||||||
|
token[:access] = access
|
||||||
|
token
|
||||||
|
end
|
||||||
|
|
||||||
|
def access
|
||||||
|
return unless params[:scope]
|
||||||
|
|
||||||
|
scope = process_scope(params[:scope])
|
||||||
|
[scope].compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_scope(scope)
|
||||||
|
type, name, actions = scope.split(':', 3)
|
||||||
|
actions = actions.split(',')
|
||||||
|
|
||||||
|
case type
|
||||||
|
when 'repository'
|
||||||
|
process_repository_access(type, name, actions)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_repository_access(type, name, actions)
|
||||||
|
current_project = Project.find_with_namespace(name)
|
||||||
|
return unless current_project
|
||||||
|
|
||||||
|
actions = actions.select do |action|
|
||||||
|
can_access?(current_project, action)
|
||||||
|
end
|
||||||
|
|
||||||
|
{ type: type, name: name, actions: actions } if actions
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_access?(current_project, action)
|
||||||
|
case action
|
||||||
|
when 'pull'
|
||||||
|
current_project == project || can?(current_user, :download_code, current_project)
|
||||||
|
when 'push'
|
||||||
|
current_project == project || can?(current_user, :push_code, current_project)
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def registry
|
||||||
|
Gitlab.config.registry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
module Jwt
|
||||||
|
class RSAToken < Token
|
||||||
|
attr_reader :key_file
|
||||||
|
|
||||||
|
def initialize(key_file)
|
||||||
|
super()
|
||||||
|
@key_file = key_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def encoded
|
||||||
|
headers = {
|
||||||
|
kid: kid
|
||||||
|
}
|
||||||
|
JWT.encode(payload, key, 'RS256', headers)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def key_data
|
||||||
|
@key_data ||= File.read(key_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
def key
|
||||||
|
@key ||= OpenSSL::PKey::RSA.new(key_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
def kid
|
||||||
|
sha256 = Digest::SHA256.new
|
||||||
|
sha256.update(key.public_key.to_der)
|
||||||
|
payload = StringIO.new(sha256.digest).read(30)
|
||||||
|
Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem|
|
||||||
|
mem << slice.join
|
||||||
|
end.join(':')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,48 @@
|
||||||
|
module Jwt
|
||||||
|
class Token
|
||||||
|
attr_accessor :issuer, :subject, :audience, :id
|
||||||
|
attr_accessor :issued_at, :not_before, :expire_time
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@payload = {}
|
||||||
|
@id = SecureRandom.uuid
|
||||||
|
@issued_at = Time.now
|
||||||
|
@not_before = issued_at - 5.seconds
|
||||||
|
@expire_time = issued_at + 1.minute
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](key)
|
||||||
|
@payload[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
def []=(key, value)
|
||||||
|
@payload[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
def encoded
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload
|
||||||
|
@payload.merge(default_payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json
|
||||||
|
payload.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def default_payload
|
||||||
|
{
|
||||||
|
jti: id,
|
||||||
|
aud: audience,
|
||||||
|
sub: subject,
|
||||||
|
iss: issuer,
|
||||||
|
iat: issued_at.to_i,
|
||||||
|
nbf: not_before.to_i,
|
||||||
|
exp: expire_time.to_i
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue