summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/jwt_controller.rb114
-rw-r--r--app/services/jwt/docker_authentication_service.rb65
-rw-r--r--lib/jwt/rsa_token.rb36
-rw-r--r--lib/jwt/token.rb48
4 files changed, 162 insertions, 101 deletions
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 7e70c70c89c..2a92627cb1b 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -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?
- end
-
- case params[:service]
- when 'docker'
- docker_token_auth(params[:scope], params[:offline_token])
- else
- return render_404
+ head :forbidden if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request)
end
- end
- private
-
- def render_400
- head :invalid_request
- end
+ service = SERVICES[params[:service]]
+ head :not_found unless service
- def render_404
- head :not_found
- end
+ result = service.new(@ci_project, @user, auth_params).execute
+ return head result[:http_status] if result[:http_status]
- def render_403
- head :forbidden
+ render json: result
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
+ private
- 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
diff --git a/app/services/jwt/docker_authentication_service.rb b/app/services/jwt/docker_authentication_service.rb
new file mode 100644
index 00000000000..ce28085e5d6
--- /dev/null
+++ b/app/services/jwt/docker_authentication_service.rb
@@ -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
diff --git a/lib/jwt/rsa_token.rb b/lib/jwt/rsa_token.rb
new file mode 100644
index 00000000000..cc265e3b31a
--- /dev/null
+++ b/lib/jwt/rsa_token.rb
@@ -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
diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb
new file mode 100644
index 00000000000..38cbc8004e7
--- /dev/null
+++ b/lib/jwt/token.rb
@@ -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 \ No newline at end of file