Commit f41a3e24 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Added authentication service for docker registry

parent bfc6a0e3
...@@ -35,6 +35,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0' ...@@ -35,6 +35,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'rack-oauth2', '~> 1.2.1' gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt'
# Spam and anti-bot protection # Spam and anti-bot protection
gem 'recaptcha', require: 'recaptcha/rails' gem 'recaptcha', require: 'recaptcha/rails'
......
...@@ -957,6 +957,7 @@ DEPENDENCIES ...@@ -957,6 +957,7 @@ DEPENDENCIES
jquery-scrollto-rails (~> 1.4.3) jquery-scrollto-rails (~> 1.4.3)
jquery-turbolinks (~> 2.1.0) jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0) jquery-ui-rails (~> 5.0.0)
jwt
kaminari (~> 0.16.3) kaminari (~> 0.16.3)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
licensee (~> 8.0.0) licensee (~> 8.0.0)
......
...@@ -203,6 +203,7 @@ class Ability ...@@ -203,6 +203,7 @@ class Ability
:admin_label, :admin_label,
:read_commit_status, :read_commit_status,
:read_build, :read_build,
:read_image,
] ]
end end
...@@ -216,7 +217,9 @@ class Ability ...@@ -216,7 +217,9 @@ class Ability
:update_build, :update_build,
:create_merge_request, :create_merge_request,
:create_wiki, :create_wiki,
:push_code :push_code,
:create_image,
:update_image,
] ]
end end
...@@ -242,7 +245,8 @@ class Ability ...@@ -242,7 +245,8 @@ class Ability
:admin_wiki, :admin_wiki,
:admin_project, :admin_project,
:admin_commit_status, :admin_commit_status,
:admin_build :admin_build,
:admin_image
] ]
end end
......
...@@ -426,6 +426,7 @@ module Ci ...@@ -426,6 +426,7 @@ module Ci
variables << { key: :CI_BUILD_NAME, value: name, public: true } variables << { key: :CI_BUILD_NAME, value: name, public: true }
variables << { key: :CI_BUILD_STAGE, value: stage, public: true } variables << { key: :CI_BUILD_STAGE, value: stage, public: true }
variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request
variables << { key: :CI_DOCKER_REGISTRY, value: project.registry_repository_url, public: true } if project.registry_repository_url
variables variables
end end
end end
......
...@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base ...@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base
default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :wall_enabled, false default_value_for :wall_enabled, false
default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :images_enabled, gitlab_config_features.images
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
...@@ -369,6 +370,10 @@ class Project < ActiveRecord::Base ...@@ -369,6 +370,10 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(path_with_namespace, self) @repository ||= Repository.new(path_with_namespace, self)
end end
def registry_repository_url
"#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" if images_enabled? && Gitlab.config.registry.enabled
end
def commit(id = 'HEAD') def commit(id = 'HEAD')
repository.commit(id) repository.commit(id)
end end
......
...@@ -27,6 +27,30 @@ class Settings < Settingslogic ...@@ -27,6 +27,30 @@ class Settings < Settingslogic
].join('') ].join('')
end end
def build_registry_api_url
if registry.port.to_i == (registry.https ? 443 : 80)
custom_port = nil
else
custom_port = ":#{registry.port}"
end
[ registry.protocol,
"://",
registry.internal_host,
custom_port
].join('')
end
def build_registry_host_with_port
if registry.port.to_i == (registry.https ? 443 : 80)
custom_port = nil
else
custom_port = ":#{registry.port}"
end
[ registry.host,
custom_port
].join('')
end
def build_gitlab_shell_ssh_path_prefix def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
...@@ -211,6 +235,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g ...@@ -211,6 +235,7 @@ Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.g
Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil?
Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil?
Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
Settings.gitlab.default_projects_features['images'] = true if Settings.gitlab.default_projects_features['images'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil?
Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['restricted_signup_domains'] ||= []
...@@ -242,6 +267,20 @@ Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? ...@@ -242,6 +267,20 @@ Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil?
Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root)
Settings.artifacts['max_size'] ||= 100 # in megabytes Settings.artifacts['max_size'] ||= 100 # in megabytes
#
# Registry
#
Settings['registry'] ||= Settingslogic.new({})
Settings.registry['registry'] = false if Settings.registry['enabled'].nil?
Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], "registry"), Rails.root)
Settings.registry['host'] ||= "example.com"
Settings.registry['internal_host']||= "localhost"
Settings.registry['https'] = false if Settings.registry['https'].nil?
Settings.registry['port'] ||= Settings.registry.https ? 443 : 80
Settings.registry['protocol'] ||= Settings.registry.https ? "https" : "http"
Settings.registry['api_url'] ||= Settings.send(:build_registry_api_url)
Settings.registry['host_port'] ||= Settings.send(:build_registry_host_with_port)
# #
# Git LFS # Git LFS
# #
......
class AddImagesEnabledForProject < ActiveRecord::Migration
def change
add_column :projects, :images_enabled, :boolean
end
end
...@@ -760,6 +760,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do ...@@ -760,6 +760,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do
t.integer "pushes_since_gc", default: 0 t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed" t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at" t.datetime "last_repository_check_at"
t.boolean "images_enabled"
end end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
......
...@@ -58,5 +58,6 @@ module API ...@@ -58,5 +58,6 @@ module API
mount Variables mount Variables
mount Runners mount Runners
mount Licenses mount Licenses
mount Auth
end end
end end
module API
# Projects builds API
class Auth < Grape::API
namespace 'auth' do
get 'token' do
required_attributes! [:scope, :service]
keys = attributes_for_keys [:scope, :service]
case keys[:service]
when 'docker'
docker_token_auth(keys[:scope])
else
not_found!
end
end
end
helpers do
def docker_token_auth(scope)
@type, @path, actions = scope.split(':', 3)
bad_request!("invalid type: #{type}") unless type == 'repository'
@actions = actions.split(',')
bad_request!('missing actions') if @actions.empty?
@project = Project.find_with_namespace(path)
not_found!('Project') unless @project
auth!
authorize_actions!(@actions)
{ token: encode(docker_payload) }
end
def auth!
auth = BasicRequest.new(request.env)
return unless auth.provided?
return bad_request unless auth.basic?
# Authentication with username and password
login, password = auth.credentials
if ci_request?(login, password)
@ci = true
return
end
@user = authenticate_user(login, password)
if @user
request.env['REMOTE_USER'] = @auth.username
end
end
def ci_request?(login, password)
matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
if @project && matched_login.present?
underscored_service = matched_login['s'].underscore
if underscored_service == 'gitlab_ci'
return @project.valid_build_token?(password)
end
end
false
end
def authenticate_user(login, password)
user = Gitlab::Auth.new.find(login, password)
unless user
user = oauth_access_token_check(login, password)
end
# 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 docker_payload
{
access: [
type: @type,
name: @path,
actions: @actions
],
exp: Time.now.to_i + 3600
}
end
def private_key
@private_key ||= OpenSSL::PKey::RSA.new File.read 'config/registry.key'
end
def encode(payload)
JWT.encode(payload, private_key, 'RS256')
end
def authorize_actions!(actions)
actions.each do |action|
forbidden! unless can_access?(action)
end
end
def can_access?(action)
case action
when 'pull'
@ci || can?(@user, :download_code, @project)
when 'push'
@ci || can?(@user, :push_code, @project)
else
false
end
end
class BasicRequest < Rack::Auth::AbstractRequest
def basic?
"basic" == scheme
end
def credentials
@credentials ||= params.unpack("m*").first.split(/:/, 2)
end
def username
credentials.first
end
end
end
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