diff options
33 files changed, 908 insertions, 106 deletions
diff --git a/CHANGELOG b/CHANGELOG index 02b94e5a0b7..087a339f3f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,7 @@ v 8.8.0 (unreleased) - Update SVG sanitizer to conform to SVG 1.1 - Speed up push emails with multiple recipients by only generating the email once - Updated search UI + - Added authentication service for Container Registry - Display informative message when new milestone is created - Sanitize milestones and labels titles - Support multi-line tag messages. !3833 (Calin Seciu) @@ -53,6 +54,7 @@ v 8.8.0 (unreleased) - Hide left sidebar on phone screens to give more space for content - Redesign navigation for profile and group pages - Add counter metrics for rails cache + - Import pull requests from GitHub where the source or target branches were removed v 8.7.5 - Fix relative links in wiki pages. !4050 @@ -36,6 +36,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' @@ -224,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' diff --git a/Gemfile.lock b/Gemfile.lock index 4b51bf58bba..b55764504c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,6 +70,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) @@ -893,6 +894,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) @@ -954,6 +956,7 @@ DEPENDENCIES jquery-rails (~> 4.1.0) 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/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb new file mode 100644 index 00000000000..f5aa5397ff1 --- /dev/null +++ b/app/controllers/jwt_controller.rb @@ -0,0 +1,87 @@ +class JwtController < ApplicationController + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + before_action :authenticate_project_or_user + + SERVICES = { + Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService, + } + + def auth + service = SERVICES[params[:service]] + return head :not_found unless service + + result = service.new(@project, @user, auth_params).execute + + render json: result, status: result[:http_status] + end + + private + + def authenticate_project_or_user + authenticate_with_http_basic do |login, password| + # if it's possible we first try to authenticate project with login and password + @project = authenticate_project(login, password) + return if @project + + @user = authenticate_user(login, password) + return if @user + + render_403 + end + end + + def auth_params + params.permit(:service, :scope, :offline_token, :account, :client_id) + end + + def authenticate_project(login, password) + if login == 'gitlab_ci_token' + Project.find_by(builds_enabled: true, runners_token: password) + end + end + + def authenticate_user(login, password) + # TODO: this is a copy and paste from grack_auth, + # it should be refactored in the future + + 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" + return + end + end + end + + user + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3768efe142a..f4ec60ad2c7 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -235,7 +235,8 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, + :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, + :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :public_builds, diff --git a/app/models/ability.rb b/app/models/ability.rb index 6103a2947e2..f70268d3138 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -61,6 +61,7 @@ class Ability :read_merge_request, :read_note, :read_commit_status, + :read_container_image, :download_code ] @@ -203,6 +204,7 @@ class Ability :admin_label, :read_commit_status, :read_build, + :read_container_image, ] end @@ -216,7 +218,9 @@ class Ability :update_build, :create_merge_request, :create_wiki, - :push_code + :push_code, + :create_container_image, + :update_container_image, ] end @@ -242,7 +246,8 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build + :admin_build, + :admin_container_image, ] end @@ -287,6 +292,10 @@ class Ability rules += named_abilities('build') end + unless project.container_registry_enabled + rules += named_abilities('container_image') + end + rules end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5c5e6007aa0..45ddcf6812a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -26,6 +26,10 @@ class MergeRequest < ActiveRecord::Base # when creating new merge request attr_accessor :can_be_created, :compare_commits, :compare + # Temporary fields to store target_sha, and base_sha to + # compare when importing pull requests from GitHub + attr_accessor :base_target_sha, :head_source_sha + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -490,10 +494,14 @@ class MergeRequest < ActiveRecord::Base end def target_sha - @target_sha ||= target_project.repository.commit(target_branch).try(:sha) + return @base_target_sha if defined?(@base_target_sha) + + target_project.repository.commit(target_branch).try(:sha) end def source_sha + return @head_source_sha if defined?(@head_source_sha) + last_commit.try(:sha) || source_tip.try(:sha) end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index eb42c07b9b9..6ad8fc3f034 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -6,7 +6,7 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request - delegate :target_branch, :source_branch, to: :merge_request, prefix: nil + delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil state_machine :state, initial: :empty do state :collected @@ -38,8 +38,8 @@ class MergeRequestDiff < ActiveRecord::Base @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha, + self.base, + self.head, ) compare.diffs(options) end @@ -144,7 +144,7 @@ class MergeRequestDiff < ActiveRecord::Base self.st_diffs = new_diffs - self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch) + self.base_commit_sha = self.repository.merge_base(self.head, self.base) self.save end @@ -160,10 +160,24 @@ class MergeRequestDiff < ActiveRecord::Base end def source_sha + return head_source_sha if head_source_sha.present? + source_commit = merge_request.source_project.commit(source_branch) source_commit.try(:sha) end + def target_sha + merge_request.target_sha + end + + def base + self.target_sha || self.target_branch + end + + def head + self.source_sha + end + def compare @compare ||= begin @@ -172,8 +186,8 @@ class MergeRequestDiff < ActiveRecord::Base Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha + self.base, + self.head ) end end diff --git a/app/models/project.rb b/app/models/project.rb index 418b85e028a..6e85841db44 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -22,6 +22,7 @@ class Project < ActiveRecord::Base default_value_for :builds_enabled, gitlab_config_features.builds default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at @@ -327,6 +328,12 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end + def container_registry_url + if container_registry_enabled? && Gitlab.config.registry.enabled + "#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}" + end + end + def commit(id = 'HEAD') repository.commit(id) end diff --git a/app/models/repository.rb b/app/models/repository.rb index 0eff74320f3..3716ea6ad6c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -195,6 +195,10 @@ class Repository cache.fetch(:branch_names) { branches.map(&:name) } end + def branch_exists?(branch_name) + branch_names.include?(branch_name) + end + def tag_names cache.fetch(:tag_names) { raw_repository.tag_names } end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb new file mode 100644 index 00000000000..b636f55d031 --- /dev/null +++ b/app/services/auth/container_registry_authentication_service.rb @@ -0,0 +1,70 @@ +module Auth + class ContainerRegistryAuthenticationService < BaseService + AUDIENCE = 'container_registry' + + def execute + return error('not found', 404) unless registry.enabled + + if params[:offline_token] + return error('forbidden', 403) unless current_user + else + return error('forbidden', 403) unless scope + end + + { token: authorized_token(scope).encoded } + end + + private + + def authorized_token(*accesses) + token = JSONWebToken::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token[:access] = accesses.compact + token + end + + def scope + return unless params[:scope] + + @scope ||= process_scope(params[:scope]) + end + + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + return unless type == 'repository' + + process_repository_access(type, name, actions) + end + + def process_repository_access(type, name, actions) + requested_project = Project.find_with_namespace(name) + return unless requested_project + + actions = actions.select do |action| + can_access?(requested_project, action) + end + + { type: type, name: name, actions: actions } if actions.present? + end + + def can_access?(requested_project, requested_action) + return false unless requested_project.container_registry_enabled? + + case requested_action + when 'pull' + requested_project == project || can?(current_user, :read_container_image, requested_project) + when 'push' + requested_project == project || can?(current_user, :create_container_image, requested_project) + else + false + end + end + + def registry + Gitlab.config.registry + end + end +end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 76a4f41193c..f6a53fddf17 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -84,6 +84,16 @@ %br %span.descr Share code pastes with others out of git repository + - if Gitlab.config.registry.enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry + %br + %span.descr Enable Container Registry for this repository + = render 'builds_settings', f: f %fieldset.features diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e682bcb976d..d935121d88b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -98,6 +98,7 @@ production: &base wiki: true snippets: false builds: true + container_registry: true ## Webhook settings # Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10) @@ -175,6 +176,14 @@ production: &base repository_archive_cache_worker: cron: "0 * * * *" + registry: + # enabled: true + # host: registry.example.com + # port: 5000 + # api_url: http://localhost:5000/ + # key: config/registry.key + # issuer: omnibus-certificate + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index a69b933d811..d1fcb053bee 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -206,12 +206,13 @@ Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 Settings.gitlab['session_expire_delay'] ||= 10080 -Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? -Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].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['builds'] = true if Settings.gitlab.default_projects_features['builds'].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['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? +Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].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['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? +Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].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'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] @@ -243,6 +244,16 @@ Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] Settings.artifacts['max_size'] ||= 100 # in megabytes # +# Registry +# +Settings['registry'] ||= Settingslogic.new({}) +Settings.registry['enabled'] ||= false +Settings.registry['host'] ||= "example.com" +Settings.registry['api_url'] ||= "http://localhost:5000/" +Settings.registry['key'] ||= nil +Settings.registry['issuer'] ||= nil + +# # Git LFS # Settings['lfs'] ||= Settingslogic.new({}) diff --git a/config/routes.rb b/config/routes.rb index 9e776a1f541..e1b72556098 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,6 +64,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' 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..47f0ca8e8de --- /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, :container_registry_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index e1117a0d858..af4f4c609e7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -762,6 +762,7 @@ ActiveRecord::Schema.define(version: 20160509201028) do t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" + t.boolean "container_registry_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/doc/api/projects.md b/doc/api/projects.md index de1faadebf5..f5f195b97df 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -424,6 +424,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -447,6 +448,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -472,6 +474,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `public_builds` (optional) diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 6219693b8a8..6be5ea0b486 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -27,6 +27,7 @@ documentation](../workflow/add-user/add-user.md). | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | | See a commit status | | ✓ | ✓ | ✓ | ✓ | +| See a container registry | | ✓ | ✓ | ✓ | ✓ | | Manage merge requests | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | @@ -37,6 +38,7 @@ documentation](../workflow/add-user/add-user.md). | Write a wiki | | | ✓ | ✓ | ✓ | | Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ | +| Update a container registry | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 93a5798e21e..dbd03ea74fa 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -66,7 +66,8 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled + expose :created_at, :last_activity_at expose :shared_runners_enabled expose :creator_id expose :namespace diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 9b595772675..5a22d14988f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -94,6 +94,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 @@ -112,6 +113,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :container_registry_enabled, :shared_runners_enabled, :namespace_id, :public, @@ -143,6 +145,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) @@ -206,6 +209,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project @@ -222,6 +226,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :container_registry_enabled, :shared_runners_enabled, :public, :visibility_level, diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb new file mode 100644 index 00000000000..a15fc84b418 --- /dev/null +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -0,0 +1,29 @@ +module Gitlab + module GithubImport + class BranchFormatter < BaseFormatter + delegate :repo, :sha, :ref, to: :raw_data + + def exists? + project.repository.branch_exists?(ref) + end + + def name + @name ||= exists? ? ref : "#{ref}-#{short_id}" + end + + def valid? + repo.present? + end + + def valid? + repo.present? + end + + private + + def short_id + sha.to_s[0..7] + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 0f9e3ee14ee..408d9b79632 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,12 +3,15 @@ module Gitlab class Importer include Gitlab::ShellAdapter - attr_reader :project, :client + attr_reader :client, :project, :repo, :repo_url def initialize(project) - @project = project - if import_data_credentials - @client = Client.new(import_data_credentials[:user]) + @project = project + @repo = project.import_source + @repo_url = project.import_url + + if credentials + @client = Client.new(credentials[:user]) @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" @@ -22,12 +25,12 @@ module Gitlab private - def import_data_credentials - @import_data_credentials ||= project.import_data.credentials if project.import_data + def credentials + @credentials ||= project.import_data.credentials if project.import_data end def import_labels - client.labels(project.import_source).each do |raw_data| + client.labels(repo).each do |raw_data| Label.create!(LabelFormatter.new(project, raw_data).attributes) end @@ -37,7 +40,7 @@ module Gitlab end def import_milestones - client.list_milestones(project.import_source, state: :all).each do |raw_data| + client.list_milestones(repo, state: :all).each do |raw_data| Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes) end @@ -47,9 +50,7 @@ module Gitlab end def import_issues - client.list_issues(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| + client.list_issues(repo, state: :all, sort: :created, direction: :asc).each do |raw_data| gh_issue = IssueFormatter.new(project, raw_data) if gh_issue.valid? @@ -68,29 +69,50 @@ module Gitlab end def import_pull_requests - client.pull_requests(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| - pull_request = PullRequestFormatter.new(project, raw_data) - - if pull_request.valid? - merge_request = MergeRequest.new(pull_request.attributes) - - if merge_request.save - apply_labels(pull_request.number, merge_request) - import_comments(pull_request.number, merge_request) - import_comments_on_diff(pull_request.number, merge_request) - end + pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc) + .map { |raw| PullRequestFormatter.new(project, raw) } + .select(&:valid?) + + source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] } + target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] } + branches_removed = source_branches_removed | target_branches_removed + + create_refs(branches_removed) + + pull_requests.each do |pull_request| + merge_request = MergeRequest.new(pull_request.attributes) + + if merge_request.save + apply_labels(pull_request.number, merge_request) + import_comments(pull_request.number, merge_request) + import_comments_on_diff(pull_request.number, merge_request) end end + delete_refs(branches_removed) + true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message end + def create_refs(branches) + branches.each do |name, sha| + client.create_ref(repo, "refs/heads/#{name}", sha) + end + + project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') + end + + def delete_refs(branches) + branches.each do |name, _| + client.delete_ref(repo, "heads/#{name}") + project.repository.rm_branch(project.creator, name) + end + end + def apply_labels(number, issuable) - issue = client.issue(project.import_source, number) + issue = client.issue(repo, number) if issue.labels.count > 0 label_ids = issue.labels.map do |raw| @@ -102,12 +124,12 @@ module Gitlab end def import_comments(issue_number, noteable) - comments = client.issue_comments(project.import_source, issue_number) + comments = client.issue_comments(repo, issue_number) create_comments(comments, noteable) end def import_comments_on_diff(pull_request_number, merge_request) - comments = client.pull_request_comments(project.import_source, pull_request_number) + comments = client.pull_request_comments(repo, pull_request_number) create_comments(comments, merge_request) end diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index d21b942ad4b..574737b31c1 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,15 +1,20 @@ module Gitlab module GithubImport class PullRequestFormatter < BaseFormatter + delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true + delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true + def attributes { iid: number, title: raw_data.title, description: description, - source_project: source_project, - source_branch: source_branch.name, - target_project: target_project, - target_branch: target_branch.name, + source_project: source_branch_project, + source_branch: source_branch_name, + head_source_sha: source_branch_sha, + target_project: target_branch_project, + target_branch: target_branch_name, + base_target_sha: target_branch_sha, state: state, milestone: milestone, author_id: author_id, @@ -24,7 +29,15 @@ module Gitlab end def valid? - !cross_project? && source_branch.present? && target_branch.present? + source_branch.valid? && target_branch.valid? && !cross_project? + end + + def source_branch + @source_branch ||= BranchFormatter.new(project, raw_data.head) + end + + def target_branch + @target_branch ||= BranchFormatter.new(project, raw_data.base) end private @@ -52,7 +65,7 @@ module Gitlab end def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + source_branch_repo.id != target_branch_repo.id end def description @@ -65,30 +78,6 @@ module Gitlab end end - def source_project - project - end - - def source_repo - raw_data.head.repo - end - - def source_branch - source_project.repository.find_branch(raw_data.head.ref) - end - - def target_project - project - end - - def target_repo - raw_data.base.repo - end - - def target_branch - target_project.repository.find_branch(raw_data.base.ref) - end - def state @state ||= case true when raw_data.state == 'closed' && raw_data.merged_at.present? diff --git a/lib/json_web_token/rsa_token.rb b/lib/json_web_token/rsa_token.rb new file mode 100644 index 00000000000..d6d6af7089c --- /dev/null +++ b/lib/json_web_token/rsa_token.rb @@ -0,0 +1,42 @@ +module JSONWebToken + 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 public_key + key.public_key + end + + def kid + # calculate sha256 from DER encoded ASN1 + kid = Digest::SHA256.digest(public_key.to_der) + + # we encode only 30 bytes with base32 + kid = Base32.encode(kid[0..29]) + + # insert colon every 4 characters + kid.scan(/.{4}/).join(':') + end + end +end diff --git a/lib/json_web_token/token.rb b/lib/json_web_token/token.rb new file mode 100644 index 00000000000..5b67715b0b2 --- /dev/null +++ b/lib/json_web_token/token.rb @@ -0,0 +1,46 @@ +module JSONWebToken + class Token + attr_accessor :issuer, :subject, :audience, :id + attr_accessor :issued_at, :not_before, :expire_time + + def initialize + @id = SecureRandom.uuid + @issued_at = Time.now + # we give a few seconds for time shift + @not_before = issued_at - 5.seconds + # default 60 seconds should be more than enough for this authentication token + @expire_time = issued_at + 1.minute + @custom_payload = {} + end + + def [](key) + @custom_payload[key] + end + + def []=(key, value) + @custom_payload[key] = value + end + + def encoded + raise NotImplementedError + end + + def payload + @custom_payload.merge(default_payload) + 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 diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb new file mode 100644 index 00000000000..3cb634ba010 --- /dev/null +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::BranchFormatter, lib: true do + let(:project) { create(:project) } + let(:repo) { double } + let(:raw) do + { + ref: 'feature', + repo: repo, + sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + } + end + + describe '#exists?' do + it 'returns true when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.exists?).to eq true + end + + it 'returns false when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.exists?).to eq false + end + end + + describe '#name' do + it 'returns raw ref when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.name).to eq 'feature' + end + + it 'returns formatted ref when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.name).to eq 'removed-branch-2e5d3239' + end + end + + describe '#repo' do + it 'returns raw repo' do + branch = described_class.new(project, double(raw)) + + expect(branch.repo).to eq repo + end + end + + describe '#sha' do + it 'returns raw sha' do + branch = described_class.new(project, double(raw)) + + expect(branch.sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end + end + + describe '#valid?' do + it 'returns true when repository exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.valid?).to eq true + end + + it 'returns false when repository does not exist' do + branch = described_class.new(project, double(raw.merge(repo: nil))) + + expect(branch.valid?).to eq false + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index e59c0ca110e..120f59e6e71 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -4,9 +4,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { double(ref: 'feature', repo: source_repo) } + let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo) } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7') } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -41,8 +41,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'opened', milestone: nil, author_id: project.creator_id, @@ -66,8 +68,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'closed', milestone: nil, author_id: project.creator_id, @@ -91,8 +95,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'merged', milestone: nil, author_id: project.creator_id, @@ -137,11 +143,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:milestone) { double(number: 45) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } - it 'returns nil when milestone does not exists' do + it 'returns nil when milestone does not exist' do expect(pull_request.attributes.fetch(:milestone)).to be_nil end - it 'returns milestone when is exists' do + it 'returns milestone when it exists' do milestone = create(:milestone, project: project, iid: 45) expect(pull_request.attributes.fetch(:milestone)).to eq milestone @@ -158,36 +164,16 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#valid?' do - let(:invalid_branch) { double(ref: 'invalid-branch').as_null_object } - - context 'when source, and target repositories are the same' do - context 'and source and target branches exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: target_branch)) } - - it 'returns true' do - expect(pull_request.valid?).to eq true - end - end - - context 'and source branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: invalid_branch, base: target_branch)) } - - it 'returns false' do - expect(pull_request.valid?).to eq false - end - end - - context 'and target branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: invalid_branch)) } + context 'when source, and target repos are not a fork' do + let(:raw_data) { double(base_data) } - it 'returns false' do - expect(pull_request.valid?).to eq false - end + it 'returns true' do + expect(pull_request.valid?).to eq true end end context 'when source repo is a fork' do - let(:source_repo) { double(id: 2, fork: true) } + let(:source_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do @@ -196,7 +182,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when target repo is a fork' do - let(:target_repo) { double(id: 2, fork: true) } + let(:target_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb new file mode 100644 index 00000000000..0c3d3ea7019 --- /dev/null +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -0,0 +1,43 @@ +describe JSONWebToken::RSAToken do + let(:rsa_key) do + OpenSSL::PKey::RSA.new <<-eos.strip_heredoc + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAMA5sXIBE0HwgIB40iNidN4PGWzOyLQK0bsdOBNgpEXkDlZBvnak + OUgAPF+rME4PB0Yl415DabUI40T5UNmlwxcCAwEAAQJAZtY2pSwIFm3JAXIh0cZZ + iXcAfiJ+YzuqinUOS+eW2sBCAEzjcARlU/o6sFQgtsOi4FOMczAd1Yx8UDMXMmrw + 2QIhAPBgVhJiTF09pdmeFWutCvTJDlFFAQNbrbo2X2x/9WF9AiEAzLgqMKeStSRu + H9N16TuDrUoO8R+DPqriCwkKrSHaWyMCIFzMhE4inuKcSywBaLmiG4m3GQzs++Al + A6PRG/PSTpQtAiBxtBg6zdf+JC3GH3zt/dA0/10tL4OF2wORfYQghRzyYQIhAL2l + 0ZQW+yLIZAGrdBFWYEAa52GZosncmzBNlsoTgwE4 + -----END RSA PRIVATE KEY----- + eos + end + let(:rsa_token) { described_class.new(nil) } + let(:rsa_encoded) { rsa_token.encoded } + + before { allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) } + + context 'token' do + context 'for valid key to be validated' do + before { rsa_token['key'] = 'value' } + + subject { JWT.decode(rsa_encoded, rsa_key) } + + it { expect{subject}.to_not raise_error } + it { expect(subject.first).to include('key' => 'value') } + it do + expect(subject.second).to eq( + "typ" => "JWT", + "alg" => "RS256", + "kid" => "OGXY:4TR7:FAVO:WEM2:XXEW:E4FP:TKL7:7ACK:TZAF:D54P:SUIA:P3B2") + end + end + + context 'for invalid key to raise an exception' do + let(:new_key) { OpenSSL::PKey::RSA.generate(512) } + subject { JWT.decode(rsa_encoded, new_key) } + + it { expect{subject}.to raise_error(JWT::DecodeError) } + end + end +end diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb new file mode 100644 index 00000000000..3d955e4d774 --- /dev/null +++ b/spec/lib/json_web_token/token_spec.rb @@ -0,0 +1,18 @@ +describe JSONWebToken::Token do + let(:token) { described_class.new } + + context 'custom parameters' do + let(:value) { 'value' } + before { token[:key] = value } + + it { expect(token[:key]).to eq(value) } + it { expect(token.payload).to include(key: value) } + end + + context 'embeds default payload' do + subject { token.payload } + let(:default) { token.send(:default_payload) } + + it { is_expected.to include(default) } + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c8578749b21..9eef08c6d00 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -64,7 +64,13 @@ describe MergeRequest, models: true do describe '#target_sha' do context 'when the target branch does not exist anymore' do - subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + let(:project) { create(:project) } + + subject { create(:merge_request, source_project: project, target_project: project) } + + before do + project.repository.raw_repository.delete_branch(subject.target_branch) + end it 'returns nil' do expect(subject.target_sha).to be_nil @@ -289,7 +295,12 @@ describe MergeRequest, models: true do let(:fork_project) { create(:project, forked_from_project: project) } context 'when the target branch does not exist anymore' do - subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + subject { create(:merge_request, source_project: project, target_project: project) } + + before do + project.repository.raw_repository.delete_branch(subject.target_branch) + subject.reload + end it 'does not crash' do expect{ subject.diverged_commits_count }.not_to raise_error diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb new file mode 100644 index 00000000000..7bb71365a48 --- /dev/null +++ b/spec/requests/jwt_controller_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe JwtController do + let(:service) { double(execute: {}) } + let(:service_class) { double(new: service) } + let(:service_name) { 'test' } + let(:parameters) { { service: service_name } } + + before { stub_const('JwtController::SERVICES', service_name => service_class) } + + context 'existing service' do + subject! { get '/jwt/auth', parameters } + + it { expect(response.status).to eq(200) } + + context 'returning custom http code' do + let(:service) { double(execute: { http_status: 505 }) } + + it { expect(response.status).to eq(505) } + end + end + + context 'when using authorized request' do + context 'using CI token' do + let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } + let(:headers) { { authorization: credentials('gitlab_ci_token', project.runners_token) } } + + subject! { get '/jwt/auth', parameters, headers } + + context 'project with enabled CI' do + let(:builds_enabled) { true } + + it { expect(service_class).to have_received(:new).with(project, nil, parameters) } + end + + context 'project with disabled CI' do + let(:builds_enabled) { false } + + it { expect(response.status).to eq(403) } + end + end + + context 'using User login' do + let(:user) { create(:user) } + let(:headers) { { authorization: credentials('user', 'password') } } + + before { expect_any_instance_of(Gitlab::Auth).to receive(:find).with('user', 'password').and_return(user) } + + subject! { get '/jwt/auth', parameters, headers } + + it { expect(service_class).to have_received(:new).with(nil, user, parameters) } + end + + context 'using invalid login' do + let(:headers) { { authorization: credentials('invalid', 'password') } } + + subject! { get '/jwt/auth', parameters, headers } + + it { expect(response.status).to eq(403) } + end + end + + context 'unknown service' do + subject! { get '/jwt/auth', service: 'unknown' } + + it { expect(response.status).to eq(404) } + end + + def credentials(login, password) + ActionController::HttpAuthentication::Basic.encode_credentials(login, password) + end +end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb new file mode 100644 index 00000000000..3ea252ed44f --- /dev/null +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -0,0 +1,216 @@ +require 'spec_helper' + +describe Auth::ContainerRegistryAuthenticationService, services: true do + let(:current_project) { nil } + let(:current_user) { nil } + let(:current_params) { {} } + let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } + let(:registry_settings) do + { + enabled: true, + issuer: 'rspec', + key: nil + } + end + let(:payload) { JWT.decode(subject[:token], rsa_key).first } + + subject { described_class.new(current_project, current_user, current_params).execute } + + before do + allow(Gitlab.config.registry).to receive_messages(registry_settings) + allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) + end + + shared_examples 'an authenticated' do + it { is_expected.to include(:token) } + it { expect(payload).to include('access') } + end + + shared_examples 'a accessible' do + let(:access) do + [{ + 'type' => 'repository', + 'name' => project.path_with_namespace, + 'actions' => actions, + }] + end + + it_behaves_like 'an authenticated' + it { expect(payload).to include('access' => access) } + end + + shared_examples 'a pullable' do + it_behaves_like 'a accessible' do + let(:actions) { ['pull'] } + end + end + + shared_examples 'a pushable' do + it_behaves_like 'a accessible' do + let(:actions) { ['push'] } + end + end + + shared_examples 'a pullable and pushable' do + it_behaves_like 'a accessible' do + let(:actions) { ['pull', 'push'] } + end + end + + shared_examples 'a forbidden' do + it { is_expected.to include(http_status: 403) } + it { is_expected.to_not include(:token) } + end + + context 'user authorization' do + let(:project) { create(:project) } + let(:current_user) { create(:user) } + + context 'allow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'an authenticated' + end + + context 'allow developer to push images' do + before { project.team << [current_user, :developer] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'a pushable' + end + + context 'allow reporter to pull images' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a pullable' + end + + context 'return a least of privileges' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push,pull" } + end + + it_behaves_like 'a pullable' + end + + context 'disallow guest to pull or push images' do + before { project.team << [current_user, :guest] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'a forbidden' + end + end + + context 'project authorization' do + let(:current_project) { create(:empty_project) } + + context 'disallow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'a forbidden' + end + + context 'allow to pull and push images' do + let(:current_params) do + { scope: "repository:#{current_project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'a pullable and pushable' do + let(:project) { current_project } + end + end + + context 'for other projects' do + context 'when pulling' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + context 'allow for public' do + let(:project) { create(:empty_project, :public) } + it_behaves_like 'a pullable' + end + + context 'disallow for private' do + let(:project) { create(:empty_project, :private) } + it_behaves_like 'a forbidden' + end + end + + context 'when pushing' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + context 'disallow for all' do + let(:project) { create(:empty_project, :public) } + it_behaves_like 'a forbidden' + end + end + end + end + + context 'unauthorized' do + context 'disallow to use offline_token' do + let(:current_params) do + { offline_token: true } + end + + it_behaves_like 'a forbidden' + end + + context 'for invalid scope' do + let(:current_params) do + { scope: 'invalid:aa:bb' } + end + + it_behaves_like 'a forbidden' + end + + context 'for private project' do + let(:project) { create(:empty_project, :private) } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a forbidden' + end + + context 'for public project' do + let(:project) { create(:empty_project, :public) } + + context 'when pulling and pushing' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'a pullable' + end + + context 'when pushing' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'a forbidden' + end + end + end +end |