summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2016-04-18 08:13:16 -0400
committerKamil Trzcinski <ayufan@ayufan.eu>2016-04-29 16:45:00 +0200
commitf41a3e24d20b26b53c5321571ef89f441c32aa4d (patch)
tree2122f78aa2bda74e3a1287306eaba41798465079
parentbfc6a0e3718c1b4d5e3d2adcc1ef16cf5274df5c (diff)
downloadgitlab-ce-f41a3e24d20b26b53c5321571ef89f441c32aa4d.tar.gz
Added authentication service for docker registry
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock1
-rw-r--r--app/models/ability.rb8
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/project.rb5
-rw-r--r--config/initializers/1_settings.rb39
-rw-r--r--db/migrate/20160407120251_add_images_enabled_for_project.rb5
-rw-r--r--db/schema.rb1
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/auth.rb166
10 files changed, 226 insertions, 2 deletions
diff --git a/Gemfile b/Gemfile
index 7882e467f8d..512c6babd7e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -35,6 +35,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'rack-oauth2', '~> 1.2.1'
+gem 'jwt'
# Spam and anti-bot protection
gem 'recaptcha', require: 'recaptcha/rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 1dcda0daff6..2b578429b3c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -957,6 +957,7 @@ DEPENDENCIES
jquery-scrollto-rails (~> 1.4.3)
jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0)
+ jwt
kaminari (~> 0.16.3)
letter_opener_web (~> 1.3.0)
licensee (~> 8.0.0)
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 6103a2947e2..ba27b9a9b14 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -203,6 +203,7 @@ class Ability
:admin_label,
:read_commit_status,
:read_build,
+ :read_image,
]
end
@@ -216,7 +217,9 @@ class Ability
:update_build,
:create_merge_request,
:create_wiki,
- :push_code
+ :push_code,
+ :create_image,
+ :update_image,
]
end
@@ -242,7 +245,8 @@ class Ability
:admin_wiki,
:admin_project,
:admin_commit_status,
- :admin_build
+ :admin_build,
+ :admin_image
]
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 553cd447971..c2ddee527e5 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -426,6 +426,7 @@ module Ci
variables << { key: :CI_BUILD_NAME, value: name, 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_DOCKER_REGISTRY, value: project.registry_repository_url, public: true } if project.registry_repository_url
variables
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 5c6c36e6b31..76265a59ea7 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -64,6 +64,7 @@ class Project < ActiveRecord::Base
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :wall_enabled, false
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 }
# set last_activity_at to the same as created_at
@@ -369,6 +370,10 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(path_with_namespace, self)
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')
repository.commit(id)
end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 8db2c05fe45..01ee8a0d525 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -27,6 +27,30 @@ class Settings < Settingslogic
].join('')
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
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
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['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['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil?
Settings.gitlab['restricted_signup_domains'] ||= []
@@ -243,6 +268,20 @@ Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path']
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
#
Settings['lfs'] ||= Settingslogic.new({})
diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb
new file mode 100644
index 00000000000..6a221a7fb03
--- /dev/null
+++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb
@@ -0,0 +1,5 @@
+class AddImagesEnabledForProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :images_enabled, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 42457d92353..bf46028d23f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -760,6 +760,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do
t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
+ t.boolean "images_enabled"
end
add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
diff --git a/lib/api/api.rb b/lib/api/api.rb
index cc1004f8005..6ddfe11d98e 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -58,5 +58,6 @@ module API
mount Variables
mount Runners
mount Licenses
+ mount Auth
end
end
diff --git a/lib/api/auth.rb b/lib/api/auth.rb
new file mode 100644
index 00000000000..b992e497307
--- /dev/null
+++ b/lib/api/auth.rb
@@ -0,0 +1,166 @@
+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