diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2016-04-18 15:13:16 +0300 |
---|---|---|
committer | Kamil Trzcinski <ayufan@ayufan.eu> | 2016-04-29 17:45:00 +0300 |
commit | f41a3e24d20b26b53c5321571ef89f441c32aa4d (patch) | |
tree | 2122f78aa2bda74e3a1287306eaba41798465079 | |
parent | bfc6a0e3718c1b4d5e3d2adcc1ef16cf5274df5c (diff) |
Added authentication service for docker registry
-rw-r--r-- | Gemfile | 1 | ||||
-rw-r--r-- | Gemfile.lock | 1 | ||||
-rw-r--r-- | app/models/ability.rb | 8 | ||||
-rw-r--r-- | app/models/ci/build.rb | 1 | ||||
-rw-r--r-- | app/models/project.rb | 5 | ||||
-rw-r--r-- | config/initializers/1_settings.rb | 39 | ||||
-rw-r--r-- | db/migrate/20160407120251_add_images_enabled_for_project.rb | 5 | ||||
-rw-r--r-- | db/schema.rb | 1 | ||||
-rw-r--r-- | lib/api/api.rb | 1 | ||||
-rw-r--r-- | lib/api/auth.rb | 166 |
10 files changed, 226 insertions, 2 deletions
@@ -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 |