Added JWT controller
This commit is contained in:
parent
0a280158ef
commit
105017c308
1
Gemfile
1
Gemfile
|
@ -225,6 +225,7 @@ gem 'request_store', '~> 1.3.0'
|
|||
gem 'select2-rails', '~> 3.5.9'
|
||||
gem 'virtus', '~> 1.0.1'
|
||||
gem 'net-ssh', '~> 3.0.1'
|
||||
gem 'base32', '~> 0.3.0'
|
||||
|
||||
# Sentry integration
|
||||
gem 'sentry-raven', '~> 0.15'
|
||||
|
|
|
@ -74,6 +74,7 @@ GEM
|
|||
ice_nine (~> 0.11.0)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
babosa (1.0.2)
|
||||
base32 (0.3.2)
|
||||
bcrypt (3.1.10)
|
||||
benchmark-ips (2.3.0)
|
||||
better_errors (1.0.1)
|
||||
|
@ -897,6 +898,7 @@ DEPENDENCIES
|
|||
attr_encrypted (~> 1.3.4)
|
||||
awesome_print (~> 1.2.0)
|
||||
babosa (~> 1.0.2)
|
||||
base32 (~> 0.3.0)
|
||||
benchmark-ips
|
||||
better_errors (~> 1.0.1)
|
||||
binding_of_caller (~> 0.7.2)
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
class JwtController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
||||
def auth
|
||||
@authenticated = authenticate_with_http_basic do |login, password|
|
||||
@ci_project = ci_project(login, password)
|
||||
@user = authenticate_user(login, password) unless @ci_project
|
||||
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
|
||||
end
|
||||
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) }
|
||||
end
|
||||
|
||||
def ci_project(login, password)
|
||||
matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
|
||||
|
||||
if matched_login.present?
|
||||
underscored_service = matched_login['s'].underscore
|
||||
|
||||
if underscored_service == 'gitlab_ci'
|
||||
Project.find_by(builds_enabled: true, runners_token: password)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_user(login, password)
|
||||
user = Gitlab::Auth.new.find(login, password)
|
||||
|
||||
# If the user authenticated successfully, we reset the auth failure count
|
||||
# from Rack::Attack for that IP. A client may attempt to authenticate
|
||||
# with a username and blank password first, and only after it receives
|
||||
# a 401 error does it present a password. Resetting the count prevents
|
||||
# false positives from occurring.
|
||||
#
|
||||
# Otherwise, we let Rack::Attack know there was a failed authentication
|
||||
# attempt from this IP. This information is stored in the Rails cache
|
||||
# (Redis) and will be used by the Rack::Attack middleware to decide
|
||||
# whether to block requests from this IP.
|
||||
config = Gitlab.config.rack_attack.git_basic_auth
|
||||
|
||||
if config.enabled
|
||||
if user
|
||||
# A successful login will reset the auth failure count from this IP
|
||||
Rack::Attack::Allow2Ban.reset(request.ip, config)
|
||||
else
|
||||
banned = Rack::Attack::Allow2Ban.filter(request.ip, config) do
|
||||
# Unless the IP is whitelisted, return true so that Allow2Ban
|
||||
# increments the counter (stored in Rails.cache) for the IP
|
||||
if config.ip_whitelist.include?(request.ip)
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
if banned
|
||||
Rails.logger.info "IP #{request.ip} failed to login " \
|
||||
"as #{login} but has been temporarily banned from Git auth"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
|
@ -63,6 +63,9 @@ Rails.application.routes.draw do
|
|||
get 'search' => 'search#show'
|
||||
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
|
||||
|
||||
# JSON Web Token
|
||||
get 'jwt/auth' => 'jwt#auth'
|
||||
|
||||
# API
|
||||
API::API.logger Rails.logger
|
||||
mount API::API => '/api'
|
||||
|
|
Loading…
Reference in New Issue