Split docker authentication service

This commit is contained in:
Kamil Trzcinski 2016-05-02 14:32:16 +02:00
parent 105017c308
commit 011a905a82
4 changed files with 163 additions and 102 deletions

View File

@ -2,6 +2,10 @@ class JwtController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
SERVICES = {
'docker' => Jwt::DockerAuthenticationService,
}
def auth
@authenticated = authenticate_with_http_basic do |login, password|
@ci_project = ci_project(login, password)
@ -9,46 +13,22 @@ class JwtController < ApplicationController
end
unless @authenticated
return render_403 if has_basic_credentials?
head :forbidden if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request)
end
case params[:service]
when 'docker'
docker_token_auth(params[:scope], params[:offline_token])
else
return render_404
end
service = SERVICES[params[:service]]
head :not_found unless service
result = service.new(@ci_project, @user, auth_params).execute
return head result[:http_status] if result[:http_status]
render json: result
end
private
def render_400
head :invalid_request
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) }
def auth_params
params.permit(:service, :scope, :offline_token, :account, :client_id)
end
def ci_project(login, password)
@ -102,72 +82,4 @@ class JwtController < ApplicationController
user
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

View File

@ -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

36
lib/jwt/rsa_token.rb Normal file
View File

@ -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

48
lib/jwt/token.rb Normal file
View File

@ -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