Commit 812806e6 authored by Bryce Johnson's avatar Bryce Johnson

Merge branch 'merge-request-push-compare-ui' of...

Merge branch 'merge-request-push-compare-ui' of gitlab.com:gitlab-org/gitlab-ce into merge-request-push-compare-ui
parents c1d6cfc3 292cc9e9
...@@ -5,7 +5,9 @@ v 8.12.0 (unreleased) ...@@ -5,7 +5,9 @@ v 8.12.0 (unreleased)
- Only check :can_resolve permission if the note is resolvable - Only check :can_resolve permission if the note is resolvable
- Bump fog-aws to v0.11.0 to support ap-south-1 region - Bump fog-aws to v0.11.0 to support ap-south-1 region
- Add ability to fork to a specific namespace using API. (ritave) - Add ability to fork to a specific namespace using API. (ritave)
- Allow to set request_access_enabled for groups and projects
- Cleanup misalignments in Issue list view !6206 - Cleanup misalignments in Issue list view !6206
- Only create a protected branch upon a push to a new branch if a rule for that branch doesn't exist
- Prune events older than 12 months. (ritave) - Prune events older than 12 months. (ritave)
- Prepend blank line to `Closes` message on merge request linked to issue (lukehowell) - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
- Fix issues/merge-request templates dropdown for forked projects - Fix issues/merge-request templates dropdown for forked projects
...@@ -15,6 +17,7 @@ v 8.12.0 (unreleased) ...@@ -15,6 +17,7 @@ v 8.12.0 (unreleased)
- Make push events have equal vertical spacing. - Make push events have equal vertical spacing.
- Add two-factor recovery endpoint to internal API !5510 - Add two-factor recovery endpoint to internal API !5510
- Pass the "Remember me" value to the U2F authentication form - Pass the "Remember me" value to the U2F authentication form
- Only update projects.last_activity_at once per hour when creating a new event
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
- Move pushes_since_gc from the database to Redis - Move pushes_since_gc from the database to Redis
- Add font color contrast to external label in admin area (ClemMakesApps) - Add font color contrast to external label in admin area (ClemMakesApps)
...@@ -22,9 +25,12 @@ v 8.12.0 (unreleased) ...@@ -22,9 +25,12 @@ v 8.12.0 (unreleased)
- Instructions for enabling Git packfile bitmaps !6104 - Instructions for enabling Git packfile bitmaps !6104
- Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint - Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint
- Fix pagination on user snippets page - Fix pagination on user snippets page
- Run CI builds with the permissions of users !5735
- Fix sorting of issues in API - Fix sorting of issues in API
- Sort project variables by key. !6275 (Diego Souza) - Sort project variables by key. !6275 (Diego Souza)
- Ensure specs on sorting of issues in API are deterministic on MySQL - Ensure specs on sorting of issues in API are deterministic on MySQL
- Added ability to use predefined CI variables for environment name
- Added ability to specify URL in environment configuration in gitlab-ci.yml
- Escape search term before passing it to Regexp.new !6241 (winniehell) - Escape search term before passing it to Regexp.new !6241 (winniehell)
- Fix pinned sidebar behavior in smaller viewports !6169 - Fix pinned sidebar behavior in smaller viewports !6169
- Fix file permissions change when updating a file on the Gitlab UI !5979 - Fix file permissions change when updating a file on the Gitlab UI !5979
...@@ -139,6 +145,7 @@ v 8.12.0 (unreleased) ...@@ -139,6 +145,7 @@ v 8.12.0 (unreleased)
- Use default clone protocol on "check out, review, and merge locally" help page URL - Use default clone protocol on "check out, review, and merge locally" help page URL
- API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska) - API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
- Allow bulk update merge requests from merge requests index page - Allow bulk update merge requests from merge requests index page
- Ensure validation messages are shown within the milestone form
- Add notification_settings API calls !5632 (mahcsig) - Add notification_settings API calls !5632 (mahcsig)
- Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska) - Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska)
- Fix URLs with anchors in wiki !6300 (houqp) - Fix URLs with anchors in wiki !6300 (houqp)
...@@ -148,7 +155,9 @@ v 8.12.0 (unreleased) ...@@ -148,7 +155,9 @@ v 8.12.0 (unreleased)
- Add specs to removing project (Katarzyna Kobierska Ula Budziszewska) - Add specs to removing project (Katarzyna Kobierska Ula Budziszewska)
- Clean environment variables when running git hooks - Clean environment variables when running git hooks
- Add UX improvements for merge request version diffs - Add UX improvements for merge request version diffs
- Fix Import/Export issues importing protected branches and some specific models
- Fix non-master branch readme display in tree view - Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
v 8.11.6 v 8.11.6
- Fix unnecessary horizontal scroll area in pipeline visualizations. !6005 - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
## Canonical source ## Canonical source
The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible. The cannonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
## Open source software to collaborate on code ## Open source software to collaborate on code
......
...@@ -11,7 +11,10 @@ class JwtController < ApplicationController ...@@ -11,7 +11,10 @@ class JwtController < ApplicationController
service = SERVICES[params[:service]] service = SERVICES[params[:service]]
return head :not_found unless service return head :not_found unless service
result = service.new(@project, @user, auth_params).execute @authentication_result ||= Gitlab::Auth::Result.new
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
execute(authentication_abilities: @authentication_result.authentication_abilities)
render json: result, status: result[:http_status] render json: result, status: result[:http_status]
end end
...@@ -20,30 +23,23 @@ class JwtController < ApplicationController ...@@ -20,30 +23,23 @@ class JwtController < ApplicationController
def authenticate_project_or_user def authenticate_project_or_user
authenticate_with_http_basic do |login, password| authenticate_with_http_basic do |login, password|
# if it's possible we first try to authenticate project with login and password @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
@project = authenticate_project(login, password)
return if @project
@user = authenticate_user(login, password)
return if @user
render_403 render_403 unless @authentication_result.success? &&
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end end
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
end end
def auth_params def render_missing_personal_token
params.permit(:service, :scope, :account, :client_id) render plain: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
status: 401
end end
def authenticate_project(login, password) def auth_params
if login == 'gitlab-ci-token' params.permit(:service, :scope, :account, :client_id)
Project.with_builds_enabled.find_by(runners_token: password)
end
end
def authenticate_user(login, password)
user = Gitlab::Auth.find_with_user_password(login, password)
Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
user
end end
end end
...@@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: @build.to_json(methods: :trace_html) render json: {
id: @build.id,
status: @build.status,
trace_html: @build.trace_html
}
end end
end end
end end
......
...@@ -4,7 +4,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -4,7 +4,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper include KerberosSpnegoHelper
attr_reader :user attr_reader :authentication_result
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
alias_method :user, :actor
# Git clients will not know what authenticity token to send along # Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
...@@ -15,32 +19,25 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -15,32 +19,25 @@ class Projects::GitHttpClientController < Projects::ApplicationController
private private
def authenticate_user def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
if project && project.public? && download_request? if project && project.public? && download_request?
return # Allow access return # Allow access
end end
if allow_basic_auth? && basic_auth_provided? if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request) login, password = user_name_and_password(request)
auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
if auth_result.type == :ci && download_request?
@ci = true
elsif auth_result.type == :oauth && !download_request?
# Not allowed
elsif auth_result.type == :missing_personal_token
render_missing_personal_token
return # Render above denied access, nothing left to do
else
@user = auth_result.user
end
if ci? || user if handle_basic_authentication(login, password)
return # Allow access return # Allow access
end end
elsif allow_kerberos_spnego_auth? && spnego_provided? elsif allow_kerberos_spnego_auth? && spnego_provided?
@user = find_kerberos_user user = find_kerberos_user
if user if user
@authentication_result = Gitlab::Auth::Result.new(
user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
send_final_spnego_response send_final_spnego_response
return # Allow access return # Allow access
end end
...@@ -48,6 +45,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -48,6 +45,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401 render plain: "HTTP Basic: Access denied\n", status: 401
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
end end
def basic_auth_provided? def basic_auth_provided?
...@@ -114,8 +113,39 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -114,8 +113,39 @@ class Projects::GitHttpClientController < Projects::ApplicationController
render plain: 'Not Found', status: :not_found render plain: 'Not Found', status: :not_found
end end
def handle_basic_authentication(login, password)
@authentication_result = Gitlab::Auth.find_for_git_client(
login, password, project: project, ip: request.ip)
return false unless @authentication_result.success?
if download_request?
authentication_has_download_access?
else
authentication_has_upload_access?
end
end
def ci? def ci?
@ci.present? authentication_result.ci? &&
authentication_project &&
authentication_project == project
end
def authentication_has_download_access?
has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
end
def authentication_has_upload_access?
has_authentication_ability?(:push_code)
end
def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability)
end
def authentication_project
authentication_result.project
end end
def verify_workhorse_api! def verify_workhorse_api!
......
...@@ -86,7 +86,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -86,7 +86,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end end
def access def access
@access ||= Gitlab::GitAccess.new(user, project, 'http') @access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
end end
def access_check def access_check
......
...@@ -428,6 +428,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -428,6 +428,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def validates_merge_request def validates_merge_request
# If source project was removed and merge request for some reason
# wasn't close (Ex. mr from fork to origin)
return invalid_mr if !@merge_request.source_project && @merge_request.open?
# Show git not found page # Show git not found page
# if there is no saved commits between source & target branch # if there is no saved commits between source & target branch
if @merge_request.commits.blank? if @merge_request.commits.blank?
......
...@@ -25,13 +25,21 @@ module LfsHelper ...@@ -25,13 +25,21 @@ module LfsHelper
def lfs_download_access? def lfs_download_access?
return false unless project.lfs_enabled? return false unless project.lfs_enabled?
project.public? || ci? || (user && user.can?(:download_code, project)) project.public? || ci? || user_can_download_code? || build_can_download_code?
end
def user_can_download_code?
has_authentication_ability?(:download_code) && can?(user, :download_code, project)
end
def build_can_download_code?
has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
end end
def lfs_upload_access? def lfs_upload_access?
return false unless project.lfs_enabled? return false unless project.lfs_enabled?
user && user.can?(:push_code, project) has_authentication_ability?(:push_code) && can?(user, :push_code, project)
end end
def render_lfs_forbidden def render_lfs_forbidden
......
module Ci module Ci
class Build < CommitStatus class Build < CommitStatus
include TokenAuthenticatable
belongs_to :runner, class_name: 'Ci::Runner' belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
belongs_to :erased_by, class_name: 'User' belongs_to :erased_by, class_name: 'User'
...@@ -23,7 +25,10 @@ module Ci ...@@ -23,7 +25,10 @@ module Ci
acts_as_taggable acts_as_taggable
add_authentication_token_field :token
before_save :update_artifacts_size, if: :artifacts_file_changed? before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
before_destroy { project } before_destroy { project }
after_create :execute_hooks after_create :execute_hooks
...@@ -38,6 +43,7 @@ module Ci ...@@ -38,6 +43,7 @@ module Ci
new_build.status = 'pending' new_build.status = 'pending'
new_build.runner_id = nil new_build.runner_id = nil
new_build.trigger_request_id = nil new_build.trigger_request_id = nil
new_build.token = nil
new_build.save new_build.save
end end
...@@ -79,11 +85,14 @@ module Ci ...@@ -79,11 +85,14 @@ module Ci
after_transition any => [:success] do |build| after_transition any => [:success] do |build|
if build.environment.present? if build.environment.present?
service = CreateDeploymentService.new(build.project, build.user, service = CreateDeploymentService.new(
build.project, build.user,
environment: build.environment, environment: build.environment,
sha: build.sha, sha: build.sha,
ref: build.ref, ref: build.ref,
tag: build.tag) tag: build.tag,
options: build.options[:environment],
variables: build.variables)
service.execute(build) service.execute(build)
end end
end end
...@@ -173,7 +182,7 @@ module Ci ...@@ -173,7 +182,7 @@ module Ci
end end
def repo_url def repo_url
auth = "gitlab-ci-token:#{token}@" auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix| project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
prefix + auth prefix + auth
end end
...@@ -235,12 +244,7 @@ module Ci ...@@ -235,12 +244,7 @@ module Ci
end end
def trace def trace
trace = raw_trace hide_secrets(raw_trace)
if project && trace.present? && project.runners_token.present?
trace.gsub(project.runners_token, 'xxxxxx')
else
trace
end
end end
def trace_length def trace_length
...@@ -253,6 +257,7 @@ module Ci ...@@ -253,6 +257,7 @@ module Ci
def trace=(trace) def trace=(trace)
recreate_trace_dir recreate_trace_dir
trace = hide_secrets(trace)
File.write(path_to_trace, trace) File.write(path_to_trace, trace)
end end
...@@ -266,6 +271,8 @@ module Ci ...@@ -266,6 +271,8 @@ module Ci
def append_trace(trace_part, offset) def append_trace(trace_part, offset)
recreate_trace_dir recreate_trace_dir
trace_part = hide_secrets(trace_part)
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace) File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
File.open(path_to_trace, 'ab') do |f| File.open(path_to_trace, 'ab') do |f|
f.write(trace_part) f.write(trace_part)
...@@ -341,12 +348,8 @@ module Ci ...@@ -341,12 +348,8 @@ module Ci
) )
end end
def token
project.runners_token
end
def valid_token?(token) def valid_token?(token)
project.valid_runners_token?(token) self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end end
def has_tags? def has_tags?
...@@ -488,5 +491,11 @@ module Ci ...@@ -488,5 +491,11 @@ module Ci
pipeline.config_processor.build_attributes(name) pipeline.config_processor.build_attributes(name)
end end
def hide_secrets(trace)
trace = Ci::MaskSecret.mask(trace, project.runners_token) if project
trace = Ci::MaskSecret.mask(trace, token)
trace
end
end end
end end
...@@ -2,6 +2,7 @@ module Ci ...@@ -2,6 +2,7 @@ module Ci
class Pipeline < ActiveRecord::Base class Pipeline < ActiveRecord::Base
extend Ci::Model extend Ci::Model
include HasStatus include HasStatus
include Importable
self.table_name = 'ci_commits' self.table_name = 'ci_commits'
...@@ -12,12 +13,12 @@ module Ci ...@@ -12,12 +13,12 @@ module Ci
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
validates_presence_of :sha validates_presence_of :sha, unless: :importing?
validates_presence_of :ref validates_presence_of :ref, unless: :importing?
validates_presence_of :status validates_presence_of :status, unless: :importing?
validate :valid_commit_sha validate :valid_commit_sha, unless: :importing?
after_save :keep_around_commits after_save :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses delegate :stages, to: :statuses
......
...@@ -20,7 +20,7 @@ module HasStatus ...@@ -20,7 +20,7 @@ module HasStatus
skipped = scope.skipped.select('count(*)').to_sql skipped = scope.skipped.select('count(*)').to_sql
deduce_status = "(CASE deduce_status = "(CASE
WHEN (#{builds})=(#{created}) THEN NULL WHEN (#{builds})=(#{created}) THEN 'created'
WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending' WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
......
...@@ -4,6 +4,7 @@ class Environment < ActiveRecord::Base ...@@ -4,6 +4,7 @@ class Environment < ActiveRecord::Base
has_many :deployments has_many :deployments
before_validation :nullify_external_url before_validation :nullify_external_url
before_save :set_environment_type
validates :name, validates :name,
presence: true, presence: true,
...@@ -26,6 +27,17 @@ class Environment < ActiveRecord::Base ...@@ -26,6 +27,17 @@ class Environment < ActiveRecord::Base
self.external_url = nil if self.external_url.blank? self.external_url = nil if self.external_url.blank?
end end
def set_environment_type
names = name.split('/')
self.environment_type =
if names.many?
names.first
else
nil
end
end
def includes_commit?(commit) def includes_commit?(commit)
return false unless last_deployment return false unless last_deployment
......
...@@ -13,6 +13,8 @@ class Event < ActiveRecord::Base ...@@ -13,6 +13,8 @@ class Event < ActiveRecord::Base
LEFT = 9 # User left project LEFT = 9 # User left project
DESTROYED = 10 DESTROYED = 10
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true
...@@ -324,8 +326,17 @@ class Event < ActiveRecord::Base ...@@ -324,8 +326,17 @@ class Event < ActiveRecord::Base
end end
def reset_project_activity def reset_project_activity
if project && Gitlab::ExclusiveLease.new("project:update_last_activity_at:#{project.id}", timeout: 60).try_obtain return unless project
project.update_column(:last_activity_at, self.created_at)
end # Don't even bother obtaining a lock if the last update happened less than
# 60 minutes ago.
return if project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
return unless Gitlab::ExclusiveLease.
new("project:update_last_activity_at:#{project.id}",
timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i).
try_obtain
project.update_column(:last_activity_at, created_at)
end end
end end
...@@ -1137,12 +1137,6 @@ class Project < ActiveRecord::Base ...@@ -1137,12 +1137,6 @@ class Project < ActiveRecord::Base
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end end
# TODO (ayufan): For now we use runners_token (backward compatibility)
# In 8.4 every build will have its own individual token valid for time of build
def valid_build_token?(token)
self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
def build_coverage_enabled? def build_coverage_enabled?
build_coverage_regex.present? build_coverage_regex.present?
end end
......
...@@ -64,6 +64,12 @@ class ProjectPolicy < BasePolicy ...@@ -64,6 +64,12 @@ class ProjectPolicy < BasePolicy
can! :read_deployment can! :read_deployment
end end
# Permissions given when an user is team member of a project
def team_member_reporter_access!
can! :build_download_code
can! :build_read_container_image
end
def developer_access! def developer_access!
can! :admin_merge_request can! :admin_merge_request
can! :update_merge_request can! :update_merge_request
...@@ -109,6 +115,8 @@ class ProjectPolicy < BasePolicy ...@@ -109,6 +115,8 @@ class ProjectPolicy < BasePolicy
can! :read_commit_status can! :read_commit_status
can! :read_pipeline can! :read_pipeline
can! :read_container_image can! :read_container_image
can! :build_download_code
can! :build_read_container_image
end end
def owner_access! def owner_access!
...@@ -132,6 +140,7 @@ class ProjectPolicy < BasePolicy ...@@ -132,6 +140,7 @@ class ProjectPolicy < BasePolicy
guest_access! if access >= Gitlab::Access::GUEST guest_access! if access >= Gitlab::Access::GUEST
reporter_access! if access >= Gitlab::Access::REPORTER reporter_access! if access >= Gitlab::Access::REPORTER
team_member_reporter_access! if access >= Gitlab::Access::REPORTER
developer_access! if access >= Gitlab::Access::DEVELOPER developer_access! if access >= Gitlab::Access::DEVELOPER
master_access! if access >= Gitlab::Access::MASTER master_access! if access >= Gitlab::Access::MASTER
end end
......
...@@ -4,7 +4,9 @@ module Auth ...@@ -4,7 +4,9 @@ module Auth
AUDIENCE = 'container_registry' AUDIENCE = 'container_registry'
def execute def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities || []
return error('not found', 404) unless registry.enabled return error('not found', 404) unless registry.enabled
unless current_user || project unless current_user || project
...@@ -74,9 +76,9 @@ module Auth ...@@ -74,9 +76,9 @@ module Auth
case requested_action case requested_action
when 'pull' when 'pull'
requested_project == project || can?(current_user, :read_container_image, requested_project) requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project)
when 'push' when 'push'
requested_project == project || can?(current_user, :create_container_image, requested_project) build_can_push?(requested_project) || user_can_push?(requested_project)
else else
false false
end end
...@@ -85,5 +87,29 @@ module Auth ...@@ -85,5 +87,29 @@ module Auth
def registry def registry
Gitlab.config.registry Gitlab.config.registry
end end
def build_can_pull?(requested_project)
# Build can:
# 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member
@authentication_abilities.include?(:build_read_container_image) &&
(requested_project == project || can?(current_user, :build_read_container_image, requested_project))
end
def user_can_pull?(requested_project)
@authentication_abilities.include?(:read_container_image) &&
can?(current_user, :read_container_image, requested_project)
end
def build_can_push?(requested_project)
# Build can push only to the project from which it originates
@authentication_abilities.include?(:build_create_container_image) &&
requested_project == project
end
def user_can_push?(requested_project)
@authentication_abilities.include?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
end
end end
end end
...@@ -2,9 +2,7 @@ require_relative 'base_service' ...@@ -2,9 +2,7 @@ require_relative 'base_service'
class CreateDeploymentService < BaseService class CreateDeploymentService < BaseService
def execute(deployable = nil) def execute(deployable = nil)
environment = project.environments.find_or_create_by( environment = find_or_create_environment
name: params[:environment]
)
project.deployments.create( project.deployments.create(
environment: environment, environment: environment,
...@@ -15,4 +13,38 @@ class CreateDeploymentService < BaseService ...@@ -15,4 +13,38 @@ class CreateDeploymentService < BaseService
deployable: deployable deployable: deployable
) )
end end
private
def find_or_create_environment
project.environments.find_or_create_by(name: expanded_name) do |environment|
environment.external_url = expanded_url
end
end
def expanded_name
ExpandVariables.expand(name, variables)
end
def expanded_url
return unless url
@expanded_url ||= ExpandVariables.expand(url, variables)
end
def name
params[:environment]
end
def url
options[:url]
end
def options
params[:options] || {}
end
def variables
params[:variables] || []
end
end end
...@@ -87,7 +87,7 @@ class GitPushService < BaseService ...@@ -87,7 +87,7 @@ class GitPushService < BaseService
project.change_head(branch_name) project.change_head(branch_name)
# Set protection on the default branch if configured # Set protection on the default branch if configured
if current_application_settings.default_branch_protection != PROTECTION_NONE if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch)
params = { params = {
name: @project.default_branch, name: @project.default_branch,
......
...@@ -3,7 +3,7 @@ module Milestones ...@@ -3,7 +3,7 @@ module Milestones
def execute def execute
milestone = project.milestones.new(params) milestone = project.milestones.new(params)
if milestone.save! if milestone.save
event_service.open_milestone(milestone, current_user) event_service.open_milestone(milestone, current_user)
end end
......
class AddTokenToBuild < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :ci_builds, :token, :string
end
end
class AddIndexForBuildToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def change
add_concurrent_index :ci_builds, :token, unique: true
end
end
class AddEnvironmentTypeToEnvironments < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :environments, :environment_type, :string
end
end
...@@ -181,6 +181,7 @@ ActiveRecord::Schema.define(version: 20160913212128) do ...@@ -181,6 +181,7 @@ ActiveRecord::Schema.define(version: 20160913212128) do
t.string "when" t.string "when"
t.text "yaml_variables" t.text "yaml_variables"
t.datetime "queued_at" t.datetime "queued_at"
t.string "token"
end end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
...@@ -192,6 +193,7 @@ ActiveRecord::Schema.define(version: 20160913212128) do ...@@ -192,6 +193,7 @@ ActiveRecord::Schema.define(version: 20160913212128) do
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
create_table "ci_commits", force: :cascade do |t| create_table "ci_commits", force: :cascade do |t|
t.integer "project_id" t.integer "project_id"
...@@ -394,6 +396,7 @@ ActiveRecord::Schema.define(version: 20160913212128) do ...@@ -394,6 +396,7 @@ ActiveRecord::Schema.define(version: 20160913212128) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "external_url" t.string "external_url"
t.string "environment_type"
end end
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
......
...@@ -84,7 +84,8 @@ Parameters: ...@@ -84,7 +84,8 @@ Parameters:
"forks_count": 0, "forks_count": 0,
"open_issues_count": 3, "open_issues_count": 3,
"public_builds": true, "public_builds": true,
"shared_with_groups": [] "shared_with_groups": [],
"request_access_enabled": false
} }
] ]
``` ```
...@@ -118,6 +119,7 @@ Example response: ...@@ -118,6 +119,7 @@ Example response:
"visibility_level": 20, "visibility_level": 20,
"avatar_url": null, "avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter", "web_url": "https://gitlab.example.com/groups/twitter",
"request_access_enabled": false,
"projects": [ "projects": [
{ {
"id": 7, "id": 7,
...@@ -163,7 +165,8 @@ Example response: ...@@ -163,7 +165,8 @@ Example response:
"forks_count": 0, "forks_count": 0,
"open_issues_count": 3, "open_issues_count": 3,
"public_builds": true, "public_builds": true,
"shared_with_groups": [] "shared_with_groups": [],
"request_access_enabled": false
}, },
{ {
"id": 6, "id": 6,
...@@ -209,7 +212,8 @@ Example response: ...@@ -209,7 +212,8 @@ Example response:
"forks_count": 0, "forks_count": 0,
"open_issues_count": 8, "open_issues_count": 8,
"public_builds": true, "public_builds": true,
"shared_with_groups": [] "shared_with_groups": [],
"request_access_enabled": false
} }
], ],
"shared_projects": [ "shared_projects": [
...@@ -289,6 +293,7 @@ Parameters: ...@@ -289,6 +293,7 @@ Parameters:
- `description` (optional) - The group's description - `description` (optional) - The group's description
- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public. - `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group - `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group
- `request_access_enabled` (optional) - Allow users to request member access.
## Transfer project to group ## Transfer project to group
...@@ -319,6 +324,7 @@ PUT /groups/:id ...@@ -319,6 +324,7 @@ PUT /groups/:id
| `description` | string | no | The description of the group | | `description` | string | no | The description of the group |
| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. | | `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group | | `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group |
| `request_access_enabled` | boolean | no | Allow users to request member access. |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental" curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
...@@ -336,6 +342,7 @@ Example response: ...@@ -336,6 +342,7 @@ Example response:
"visibility_level": 10, "visibility_level": 10,
"avatar_url": null, "avatar_url": null,
"web_url": "http://gitlab.example.com/groups/h5bp", "web_url": "http://gitlab.example.com/groups/h5bp",
"request_access_enabled": false,
"projects": [ "projects": [
{ {
"id": 9, "id": 9,
...@@ -380,7 +387,8 @@ Example response: ...@@ -380,7 +387,8 @@ Example response:
"forks_count": 0, "forks_count": 0,
"open_issues_count": 3, "open_issues_count": 3,
"public_builds": true, "public_builds": true,
"shared_with_groups": [] "shared_with_groups": [],
"request_access_enabled": false
} }
] ]
} }
......
...@@ -85,7 +85,8 @@ Parameters: ...@@ -85,7 +85,8 @@ Parameters:
"runners_token": "b8547b1dc37721d05889db52fa2f02", "runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true, "public_builds": true,
"shared_with_groups": [], "shared_with_groups": [],
"only_allow_merge_if_build_succeeds": false "only_allow_merge_if_build_succeeds": false,
"request_access_enabled": false
}, },
{ {
"id": 6, "id": 6,
...@@ -146,7 +147,8 @@ Parameters: ...@@ -146,7 +147,8 @@ Parameters:
"runners_token": "b8547b1dc37721d05889db52fa2f02", "runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true, "public_builds": true,
"shared_with_groups": [], "shared_with_groups": [],
"only_allow_merge_if_build_succeeds": false "only_allow_merge_if_build_succeeds": false,
"request_access_enabled": false
} }
] ]
``` ```
...@@ -283,7 +285,8 @@ Parameters: ...@@ -283,7 +285,8 @@ Parameters:
"group_access_level": 10 "group_access_level": 10
} }
], ],
"only_allow_merge_if_build_succeeds": false "only_allow_merge_if_build_succeeds": false,
"request_access_enabled": false
} }
``` ```
...@@ -453,6 +456,7 @@ Parameters: ...@@ -453,6 +456,7 @@ Parameters:
- `public_builds` (optional) - `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional) - `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional) - `lfs_enabled` (optional)
- `request_access_enabled` (optional) - Allow users to request member access.
### Create project for user ### Create project for user
...@@ -480,6 +484,7 @@ Parameters: ...@@ -480,6 +484,7 @@ Parameters:
- `public_builds` (optional) - `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional) - `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional) - `lfs_enabled` (optional)
- `request_access_enabled` (optional) - Allow users to request member access.
### Edit project ### Edit project
...@@ -508,6 +513,7 @@ Parameters: ...@@ -508,6 +513,7 @@ Parameters:
- `public_builds` (optional) - `public_builds` (optional)
- `only_allow_merge_if_build_succeeds` (optional) - `only_allow_merge_if_build_succeeds` (optional)
- `lfs_enabled` (optional) - `lfs_enabled` (optional)
- `request_access_enabled` (optional) - Allow users to request member access.
On success, method returns 200 with the updated project. If parameters are On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned. invalid, 400 is returned.
...@@ -588,7 +594,8 @@ Example response: ...@@ -588,7 +594,8 @@ Example response:
"star_count": 1, "star_count": 1,
"public_builds": true, "public_builds": true,
"shared_with_groups": [], "shared_with_groups": [],
"only_allow_merge_if_build_succeeds": false "only_allow_merge_if_build_succeeds": false,
"request_access_enabled": false
} }
``` ```
...@@ -655,7 +662,8 @@ Example response: ...@@ -655,7 +662,8 @@ Example response:
"star_count": 0, "star_count": 0,
"public_builds": true, "public_builds": true,
"shared_with_groups": [], "shared_with_groups": [],
"only_allow_merge_if_build_succeeds": false "only_allow_merge_if_build_succeeds": false,
"request_access_enabled": false
} }
``` ```
...@@ -742,7 +750,8 @@ Example response: ...@@ -742,7 +750,8 @@ Example response:
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true, "public_builds": true,
"shared_with_groups": [], "shared_with_groups": [],
"only_allow_merge_if_build_succeeds": false "only_allow_merge_if_build_succeeds": false,
"request_access_enabled": false
} }
``` ```
...@@ -829,7 +838,8 @@ Example response: ...@@ -829,7 +838,8 @@ Example response:
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b", "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true, "public_builds": true,
"shared_with_groups": [], "shared_with_groups": [],
"only_allow_merge_if_build_succeeds": false "only_allow_merge_if_build_succeeds": false,
"request_access_enabled": false
} }
``` ```
......
...@@ -67,7 +67,7 @@ PUT /application/settings ...@@ -67,7 +67,7 @@ PUT /application/settings
| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.| | `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. | | `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` | | `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` |
| `domain_blacklist` | array of strings | yes (if `domain_whitelist_enabled` is `true` | People trying to sign-up with emails from this domain will not be allowed to do so. | | `domain_blacklist` | array of strings | yes (if `domain_blacklist_enabled` is `true`) | People trying to sign-up with emails from this domain will not be allowed to do so. |
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider | | `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
| `after_sign_out_path` | string | no | Where to redirect users after logout | | `after_sign_out_path` | string | no | Where to redirect users after logout |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
......
...@@ -16,4 +16,4 @@ Apart from those, here is an collection of tutorials and guides on setting up yo ...@@ -16,4 +16,4 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples) - [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
[gitlab-ci-templates][https://gitlab.com/gitlab-org/gitlab-ci-yml] [gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
...@@ -90,8 +90,7 @@ builds, including deploy builds. This can be an array or a multi-line string. ...@@ -90,8 +90,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
### after_script ### after_script
>**Note:** > Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
`after_script` is used to define the command that will be run after for all `after_script` is used to define the command that will be run after for all
builds. This has to be an array or a multi-line string. builds. This has to be an array or a multi-line string.
...@@ -135,8 +134,7 @@ Alias for [stages](#stages). ...@@ -135,8 +134,7 @@ Alias for [stages](#stages).
### variables ### variables
>**Note:** > Introduced in GitLab Runner v0.5.0.
Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
build environment. The variables are stored in the Git repository and are meant build environment. The variables are stored in the Git repository and are meant
...@@ -158,8 +156,7 @@ Variables can be also defined on [job level](#job-variables). ...@@ -158,8 +156,7 @@ Variables can be also defined on [job level](#job-variables).
### cache ### cache
>**Note:** > Introduced in GitLab Runner v0.7.0.
Introduced in GitLab Runner v0.7.0.
`cache` is used to specify a list of files and directories which should be `cache` is used to specify a list of files and directories which should be
cached between builds. cached between builds.
...@@ -220,8 +217,7 @@ will be always present. For implementation details, please check GitLab Runner. ...@@ -220,8 +217,7 @@ will be always present. For implementation details, please check GitLab Runner.
#### cache:key #### cache:key
>**Note:** > Introduced in GitLab Runner v1.0.0.
Introduced in GitLab Runner v1.0.0.
The `key` directive allows you to define the affinity of caching The `key` directive allows you to define the affinity of caching
between jobs, allowing to have a single cache for all jobs, between jobs, allowing to have a single cache for all jobs,
...@@ -531,8 +527,7 @@ The above script will: ...@@ -531,8 +527,7 @@ The above script will:
#### Manual actions #### Manual actions
>**Note:** > Introduced in GitLab 8.10.
Introduced in GitLab 8.10.
Manual actions are a special type of job that are not executed automatically; Manual actions are a special type of job that are not executed automatically;
they need to be explicitly started by a user. Manual actions can be started they need to be explicitly started by a user. Manual actions can be started
...@@ -543,17 +538,16 @@ An example usage of manual actions is deployment to production. ...@@ -543,17 +538,16 @@ An example usage of manual actions is deployment to production.
### environment ### environment
>**Note:** > Introduced in GitLab 8.9.
Introduced in GitLab 8.9.
`environment` is used to define that a job deploys to a specific environment. `environment` is used to define that a job deploys to a specific [environment].
This allows easy tracking of all deployments to your environments straight from This allows easy tracking of all deployments to your environments straight from
GitLab. GitLab.
If `environment` is specified and no environment under that name exists, a new If `environment` is specified and no environment under that name exists, a new
one will be created automatically. one will be created automatically.
The `environment` name must contain only letters, digits, '-' and '_'. Common The `environment` name must contain only letters, digits, '-', '_', '/', '$', '{', '}' and spaces. Common
names are `qa`, `staging`, and `production`, but you can use whatever name works names are `qa`, `staging`, and `production`, but you can use whatever name works
with your workflow. with your workflow.
...@@ -571,6 +565,35 @@ deploy to production: ...@@ -571,6 +565,35 @@ deploy to production:
The `deploy to production` job will be marked as doing deployment to The `deploy to production` job will be marked as doing deployment to
`production` environment. `production` environment.
#### dynamic environments
> [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
`environment` can also represent a configuration hash with `name` and `url`.
These parameters can use any of the defined CI [variables](#variables)
(including predefined, secure variables and `.gitlab-ci.yml` variables).
The common use case is to create dynamic environments for branches and use them
as review apps.
---
**Example configurations**
```
deploy as review app:
stage: deploy
script: ...
environment:
name: review-apps/$CI_BUILD_REF_NAME
url: https://$CI_BUILD_REF_NAME.review.example.com/
```
The `deploy as review app` job will be marked as deployment to dynamically
create the `review-apps/branch-name` environment.
This environment should be accessible under `https://branch-name.review.example.com/`.
### artifacts ### artifacts
>**Notes:** >**Notes:**
...@@ -638,8 +661,7 @@ be available for download in the GitLab UI. ...@@ -638,8 +661,7 @@ be available for download in the GitLab UI.
#### artifacts:name #### artifacts:name
>**Note:** > Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
The `name` directive allows you to define the name of the created artifacts The `name` directive allows you to define the name of the created artifacts
archive. That way, you can have a unique name for every archive which could be archive. That way, you can have a unique name for every archive which could be
...@@ -702,8 +724,7 @@ job: ...@@ -702,8 +724,7 @@ job:
#### artifacts:when #### artifacts:when
>**Note:** > Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
`artifacts:when` is used to upload artifacts on build failure or despite the `artifacts:when` is used to upload artifacts on build failure or despite the
failure. failure.
...@@ -728,8 +749,7 @@ job: ...@@ -728,8 +749,7 @@ job:
#### artifacts:expire_in #### artifacts:expire_in
>**Note:** > Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
`artifacts:expire_in` is used to delete uploaded artifacts after the specified `artifacts:expire_in` is used to delete uploaded artifacts after the specified
time. By default, artifacts are stored on GitLab forever. `expire_in` allows you time. By default, artifacts are stored on GitLab forever. `expire_in` allows you
...@@ -764,8 +784,7 @@ job: ...@@ -764,8 +784,7 @@ job:
### dependencies ### dependencies
>**Note:** > Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
This feature should be used in conjunction with [`artifacts`](#artifacts) and This feature should be used in conjunction with [`artifacts`](#artifacts) and
allows you to define the artifacts to pass between different builds. allows you to define the artifacts to pass between different builds.
...@@ -839,9 +858,8 @@ job: ...@@ -839,9 +858,8 @@ job:
## Git Strategy ## Git Strategy
>**Note:** > Introduced in GitLab 8.9 as an experimental feature. May change in future
Introduced in GitLab 8.9 as an experimental feature. May change in future releases or be removed completely.
releases or be removed completely.
You can set the `GIT_STRATEGY` used for getting recent application code. `clone` You can set the `GIT_STRATEGY` used for getting recent application code. `clone`
is slower, but makes sure you have a clean directory before every build. `fetch` is slower, but makes sure you have a clean directory before every build. `fetch`
...@@ -863,8 +881,7 @@ variables: ...@@ -863,8 +881,7 @@ variables:
## Shallow cloning ## Shallow cloning
>**Note:** > Introduced in GitLab 8.9 as an experimental feature. May change in future
Introduced in GitLab 8.9 as an experimental feature. May change in future
releases or be removed completely. releases or be removed completely.
You can specify the depth of fetching and cloning using `GIT_DEPTH`. This allows You can specify the depth of fetching and cloning using `GIT_DEPTH`. This allows
...@@ -894,8 +911,7 @@ variables: ...@@ -894,8 +911,7 @@ variables:
## Hidden keys ## Hidden keys
>**Note:** > Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
Keys that start with a dot (`.`) will be not processed by GitLab CI. You can Keys that start with a dot (`.`) will be not processed by GitLab CI. You can
use this feature to ignore jobs, or use the use this feature to ignore jobs, or use the
...@@ -923,8 +939,7 @@ Read more about the various [YAML features](https://learnxinyminutes.com/docs/ya ...@@ -923,8 +939,7 @@ Read more about the various [YAML features](https://learnxinyminutes.com/docs/ya
### Anchors ### Anchors
>**Note:** > Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
YAML also has a handy feature called 'anchors', which let you easily duplicate YAML also has a handy feature called 'anchors', which let you easily duplicate
content across your document. Anchors can be used to duplicate/inherit content across your document. Anchors can be used to duplicate/inherit
...@@ -1067,3 +1082,5 @@ Visit the [examples README][examples] to see a list of examples using GitLab ...@@ -1067,3 +1082,5 @@ Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages. CI with various languages.
[examples]: ../examples/README.md [examples]: ../examples/README.md
[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
[environment]: ../environments.md
...@@ -78,9 +78,9 @@ delete them. ...@@ -78,9 +78,9 @@ delete them.
> **Note:** > **Note:**
This feature requires GitLab 8.8 and GitLab Runner 1.2. This feature requires GitLab 8.8 and GitLab Runner 1.2.
Make sure that your GitLab Runner is configured to allow building docker images. Make sure that your GitLab Runner is configured to allow building Docker images by
You have to check the [Using Docker Build documentation](../ci/docker/using_docker_build.md). following the [Using Docker Build](../ci/docker/using_docker_build.md)
Then see the CI documentation on [Using the GitLab Container Registry](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry). and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
## Limitations ## Limitations
......
...@@ -79,6 +79,9 @@ gitlab_rails['backup_upload_connection'] = { ...@@ -79,6 +79,9 @@ gitlab_rails['backup_upload_connection'] = {
'region' => 'eu-west-1', 'region' => 'eu-west-1',
'aws_access_key_id' => 'AKIAKIAKI', 'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123' 'aws_secret_access_key' => 'secret123'
# If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
# ie. 'aws_access_key_id' => '',
# 'use_iam_profile' => 'true'
} }
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket' gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
``` ```
...@@ -95,6 +98,9 @@ For installations from source: ...@@ -95,6 +98,9 @@ For installations from source:
region: eu-west-1 region: eu-west-1
aws_access_key_id: AKIAKIAKI aws_access_key_id: AKIAKIAKI
aws_secret_access_key: 'secret123' aws_secret_access_key: 'secret123'
# If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
# ie. aws_access_key_id: ''
# use_iam_profile: 'true'
# The remote 'directory' to store your backups. For S3, this would be the bucket name. # The remote 'directory' to store your backups. For S3, this would be the bucket name.
remote_directory: 'my.s3.bucket' remote_directory: 'my.s3.bucket'
# Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
......
...@@ -101,4 +101,36 @@ inside GitLab that make that possible. ...@@ -101,4 +101,36 @@ inside GitLab that make that possible.
![Build artifacts browser](img/build_artifacts_browser.png) ![Build artifacts browser](img/build_artifacts_browser.png)
## Downloading the latest build artifacts
It is possible to download the latest artifacts of a build via a well known URL
so you can use it for scripting purposes.
The structure of the URL is the following:
```
https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
```
For example, to download the latest artifacts of the job named `rspec 6 20` of
the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
namespace, the URL would be:
```
https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20
```
The latest builds are also exposed in the UI in various places. Specifically,
look for the download button in:
- the main project's page
- the branches page
- the tags page
If the latest build has failed to upload the artifacts, you can see that
information in the UI.
![Latest artifacts button](img/build_latest_artifacts_browser.png)
[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository" [gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
>**Notes:** >**Notes:**
> >
> - [Introduced][ce-3050] in GitLab 8.9. > - [Introduced][ce-3050] in GitLab 8.9.
> - Importing will not be possible if the import instance version is lower > - Importing will not be possible if the import instance version differs from
> than that of the exporter. > that of the exporter.
> - For existing installations, the project import option has to be enabled in > - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'. > application settings (`/admin/application_settings`) under 'Import sources'.
> You will have to be an administrator to enable and use the import functionality. > You will have to be an administrator to enable and use the import functionality.
...@@ -17,6 +17,20 @@ ...@@ -17,6 +17,20 @@
Existing projects running on any GitLab instance or GitLab.com can be exported Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance. with all their related data and be moved into a new GitLab instance.
## Version history
| GitLab version | Import/Export version |
| -------- | -------- |
| 8.12.0 to current | 0.1.4 |
| 8.10.3 | 0.1.3 |
| 8.10.0 | 0.1.2 |
| 8.9.5 | 0.1.1 |
| 8.9.0 | 0.1.0 |
> The table reflects what GitLab version we updated the Import/Export version at.
> For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
> and the exports between them will be compatible.
## Exported contents ## Exported contents
The following items will be exported: The following items will be exported:
......
# Import your project from GitHub to GitLab # Import your project from GitHub to GitLab
Import your projects from GitHub to GitLab with minimal effort.
## Overview
>**Note:** >**Note:**
In order to enable the GitHub import setting, you may also want to If you are an administrator you can enable the [GitHub integration][gh-import]
enable the [GitHub integration][gh-import] in your GitLab instance. This in your GitLab instance sitewide. This configuration is optional, users will be
configuration is optional, you will be able import your GitHub repositories able import their GitHub repositories with a [personal access token][gh-token].
with a Personal Access Token.
At its current state, GitHub importer can import: - At its current state, GitHub importer can import:
- the repository description (GitLab 7.7+)
- the Git repository data (GitLab 7.7+)
- the issues (GitLab 7.7+)
- the pull requests (GitLab 8.4+)
- the wiki pages (GitLab 8.4+)
- the milestones (GitLab 8.7+)
- the labels (GitLab 8.7+)
- the release note descriptions (GitLab 8.12+)
- References to pull requests and issues are preserved (GitLab 8.7+)
- Repository public access is retained. If a repository is private in GitHub
it will be created as private in GitLab as well.
- the repository description (introduced in GitLab 7.7) ## How it works
- the git repository data (introduced in GitLab 7.7)
- the issues (introduced in GitLab 7.7)
- the pull requests (introduced in GitLab 8.4)
- the wiki pages (introduced in GitLab 8.4)
- the milestones (introduced in GitLab 8.7)
- the labels (introduced in GitLab 8.7)
- the release note descriptions (introduced in GitLab 8.12)
With GitLab 8.7+, references to pull requests and issues are preserved. When issues/pull requests are being imported, the GitHub importer tries to find
the GitHub author/assignee in GitLab's database using the GitHub ID. For this
to work, the GitHub author/assignee should have signed in beforehand in GitLab
and [**associated their GitHub account**][social sign-in]. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original GitHub author is kept.
The importer page is visible when you [create a new project][new-project]. The importer will create any new namespaces (groups) if they don't exist or in
Click on the **GitHub** link and, if you are logged in via the GitHub the case the namespace is taken, the repository will be imported under the user's
integration, you will be redirected to GitHub for permission to access your namespace that started the import process.
projects. After accepting, you'll be automatically redirected to the importer.
If you are not using the GitHub integration, you can still perform a one-off ## Importing your GitHub repositories
authorization with GitHub to access your projects.
Alternatively, you can also enter a GitHub Personal Access Token. Once you enter The importer page is visible when you create a new project.
your token, you'll be taken to the importer.
![New project page on GitLab](img/import_projects_from_github_new_project_page.png) ![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
--- Click on the **GitHub** link and the import authorization process will start.
There are two ways to authorize access to your GitHub repositories:
While at the GitHub importer page, you can see the import statuses of your 1. [Using the GitHub integration][gh-integration] (if it's enabled by your
GitHub projects. Those that are being imported will show a _started_ status, GitLab administrator). This is the preferred way as it's possible to
those already imported will be green, whereas those that are not yet imported preserve the GitHub authors/assignees. Read more in the [How it works](#how-it-works)
have an **Import** button on the right side of the table. If you want, you can section.
import all your GitHub projects in one go by hitting **Import all projects** 1. [Using a personal access token][gh-token] provided by GitHub.
in the upper left corner.
![GitHub importer page](img/import_projects_from_github_importer.png) ![Select authentication method](img/import_projects_from_github_select_auth_method.png)
### Authorize access to your repositories using the GitHub integration
--- If the [GitHub integration][gh-import] is enabled by your GitLab administrator,
you can use it instead of the personal access token.
1. First you may want to connect your GitHub account to GitLab in order for
the username mapping to be correct. Follow the [social sign-in] documentation
on how to do so.
1. Once you connect GitHub, click the **List your GitHub repositories** button
and you will be redirected to GitHub for permission to access your projects.
1. After accepting, you'll be automatically redirected to the importer.
You can now go on and [select which repositories to import](#select-which-repositories-to-import).
### Authorize access to your repositories using a personal access token
>**Note:**
For a proper author/assignee mapping for issues and pull requests, the
[GitHub integration][gh-integration] should be used instead of the
[personal access token][gh-token]. If the GitHub integration is enabled by your
GitLab administrator, it should be the preferred method to import your repositories.
Read more in the [How it works](#how-it-works) section.
The importer will create any new namespaces if they don't exist or in the If you are not using the GitHub integration, you can still perform a one-off
case the namespace is taken, the project will be imported on the user's authorization with GitHub to grant GitLab access your repositories:
namespace.
1. Go to <https://github.com/settings/tokens/new>.
1. Enter a token description.
1. Check the `repo` scope.
1. Click **Generate token**.
1. Copy the token hash.
1. Go back to GitLab and provide the token to the GitHub importer.
1. Hit the **List your GitHub repositories** button and wait while GitLab reads
your repositories' information. Once done, you'll be taken to the importer
page to select the repositories to import.
### Select which repositories to import
After you've authorized access to your GitHub repositories, you will be
redirected to the GitHub importer page.
From there, you can see the import statuses of your GitHub repositories.
- Those that are being imported will show a _started_ status,
- those already successfully imported will be green with a _done_ status,
- whereas those that are not yet imported will have an **Import** button on the
right side of the table.
If you want, you can import all your GitHub projects in one go by hitting
**Import all projects** in the upper left corner.
![GitHub importer page](img/import_projects_from_github_importer.png)
[gh-import]: ../../integration/github.md "GitHub integration" [gh-import]: ../../integration/github.md "GitHub integration"
[ee-gh]: http://docs.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab" [new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
[social sign-in]: ../../profile/account/social_sign_in.md
...@@ -100,6 +100,7 @@ module API ...@@ -100,6 +100,7 @@ module API
SharedGroup.represent(project.project_group_links.all, options) SharedGroup.represent(project.project_group_links.all, options)
end end
expose :only_allow_merge_if_build_succeeds expose :only_allow_merge_if_build_succeeds
expose :request_access_enabled
end end
class Member < UserBasic class Member < UserBasic
...@@ -125,6 +126,7 @@ module API ...@@ -125,6 +126,7 @@ module API
expose :lfs_enabled?, as: :lfs_enabled expose :lfs_enabled?, as: :lfs_enabled
expose :avatar_url expose :avatar_url
expose :web_url expose :web_url
expose :request_access_enabled
end end
class GroupDetail < Group class GroupDetail < Group
......
...@@ -28,13 +28,14 @@ module API ...@@ -28,13 +28,14 @@ module API
# description (optional) - The description of the group # description (optional) - The description of the group
# visibility_level (optional) - The visibility level of the group # visibility_level (optional) - The visibility level of the group
# lfs_enabled (optional) - Enable/disable LFS for the projects in this group # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
# request_access_enabled (optional) - Allow users to request member access
# Example Request: # Example Request:
# POST /groups # POST /groups
post do post do
authorize! :create_group authorize! :create_group
required_attributes! [:name, :path] required_attributes! [:name, :path]
attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled] attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
@group = Group.new(attrs) @group = Group.new(attrs)
if @group.save if @group.save
...@@ -53,13 +54,14 @@ module API ...@@ -53,13 +54,14 @@ module API
# description (optional) - The description of the group # description (optional) - The description of the group
# visibility_level (optional) - The visibility level of the group # visibility_level (optional) - The visibility level of the group
# lfs_enabled (optional) - Enable/disable LFS for the projects in this group # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
# request_access_enabled (optional) - Allow users to request member access
# Example Request: # Example Request:
# PUT /groups/:id # PUT /groups/:id
put ':id' do put ':id' do
group = find_group(params[:id]) group = find_group(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled] attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
if ::Groups::UpdateService.new(group, current_user, attrs).execute if ::Groups::UpdateService.new(group, current_user, attrs).execute
present group, with: Entities::GroupDetail present group, with: Entities::GroupDetail
......
...@@ -35,6 +35,14 @@ module API ...@@ -35,6 +35,14 @@ module API
Project.find_with_namespace(project_path) Project.find_with_namespace(project_path)
end end
end end
def ssh_authentication_abilities
[
:read_project,
:download_code,
:push_code
]
end
end end
post "/allowed" do post "/allowed" do
...@@ -51,9 +59,9 @@ module API ...@@ -51,9 +59,9 @@ module API
access = access =
if wiki? if wiki?
Gitlab::GitAccessWiki.new(actor, project, protocol) Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
else else
Gitlab::GitAccess.new(actor, project, protocol) Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
end end
access_status = access.check(params[:action], params[:changes]) access_status = access.check(params[:action], params[:changes])
......
...@@ -106,27 +106,29 @@ module API ...@@ -106,27 +106,29 @@ module API
# import_url (optional) # import_url (optional)
# public_builds (optional) # public_builds (optional)
# lfs_enabled (optional) # lfs_enabled (optional)
# request_access_enabled (optional) - Allow users to request member access
# Example Request # Example Request
# POST /projects # POST /projects
post do post do
required_attributes! [:name] required_attributes! [:name]
attrs = attributes_for_keys [:name, attrs = attributes_for_keys [:builds_enabled,
:path, :container_registry_enabled,
:description, :description,
:import_url,
:issues_enabled, :issues_enabled,
:lfs_enabled,
:merge_requests_enabled, :merge_requests_enabled,
:builds_enabled, :name,
:wiki_enabled,
:snippets_enabled,
:container_registry_enabled,
:shared_runners_enabled,
:namespace_id, :namespace_id,
:only_allow_merge_if_build_succeeds,
:path,
:public, :public,
:visibility_level,
:import_url,
:public_builds, :public_builds,
:only_allow_merge_if_build_succeeds, :request_access_enabled,
:lfs_enabled] :shared_runners_enabled,
:snippets_enabled,
:visibility_level,
:wiki_enabled]
attrs = map_public_to_visibility_level(attrs) attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute @project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved? if @project.saved?
...@@ -159,26 +161,28 @@ module API ...@@ -159,26 +161,28 @@ module API
# import_url (optional) # import_url (optional)
# public_builds (optional) # public_builds (optional)
# lfs_enabled (optional) # lfs_enabled (optional)
# request_access_enabled (optional) - Allow users to request member access
# Example Request # Example Request
# POST /projects/user/:user_id # POST /projects/user/:user_id
post "user/:user_id" do post "user/:user_id" do
authenticated_as_admin! authenticated_as_admin!
user = User.find(params[:user_id]) user = User.find(params[:user_id])
attrs = attributes_for_keys [:name, attrs = attributes_for_keys [:builds_enabled,
:description,
:default_branch, :default_branch,
:description,
:import_url,
:issues_enabled, :issues_enabled,
:lfs_enabled,
:merge_requests_enabled, :merge_requests_enabled,
:builds_enabled, :name,
:wiki_enabled, :only_allow_merge_if_build_succeeds,
:snippets_enabled,
:shared_runners_enabled,
:public, :public,
:visibility_level,
:import_url,
:public_builds, :public_builds,
:only_allow_merge_if_build_succeeds, :request_access_enabled,
:lfs_enabled] :shared_runners_enabled,
:snippets_enabled,
:visibility_level,
:wiki_enabled]
attrs = map_public_to_visibility_level(attrs) attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute @project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved? if @project.saved?
...@@ -242,22 +246,23 @@ module API ...@@ -242,22 +246,23 @@ module API
# Example Request # Example Request
# PUT /projects/:id # PUT /projects/:id
put ':id' do put ':id' do
attrs = attributes_for_keys [:name, attrs = attributes_for_keys [:builds_enabled,
:path, :container_registry_enabled,
:description,
:default_branch, :default_branch,
:description,
:issues_enabled, :issues_enabled,
:lfs_enabled,
:merge_requests_enabled, :merge_requests_enabled,
:builds_enabled, :name,
:wiki_enabled, :only_allow_merge_if_build_succeeds,
:snippets_enabled, :path,
:container_registry_enabled,
:shared_runners_enabled,
:public, :public,
:visibility_level,
:public_builds, :public_builds,
:only_allow_merge_if_build_succeeds, :request_access_enabled,
:lfs_enabled] :shared_runners_enabled,
:snippets_enabled,
:visibility_level,
:wiki_enabled]
attrs = map_public_to_visibility_level(attrs) attrs = map_public_to_visibility_level(attrs)
authorize_admin_project authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present? authorize! :rename_project, user_project if attrs[:name].present?
......
...@@ -15,6 +15,15 @@ module Ci ...@@ -15,6 +15,15 @@ module Ci
expose :filename, :size expose :filename, :size
end end
class BuildOptions < Grape::Entity
expose :image
expose :services
expose :artifacts
expose :cache
expose :dependencies
expose :after_script
end
class Build < Grape::Entity class Build < Grape::Entity
expose :id, :ref, :tag, :sha, :status expose :id, :ref, :tag, :sha, :status
expose :name, :token, :stage expose :name, :token, :stage
......
...@@ -14,12 +14,20 @@ module Ci ...@@ -14,12 +14,20 @@ module Ci
end end
def authenticate_build_token!(build) def authenticate_build_token!(build)
token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s forbidden! unless build_token_valid?(build)
forbidden! unless token && build.valid_token?(token)
end end
def runner_registration_token_valid? def runner_registration_token_valid?
params[:token] == current_application_settings.runners_registration_token ActiveSupport::SecurityUtils.variable_size_secure_compare(
params[:token],
current_application_settings.runners_registration_token)
end
def build_token_valid?(build)
token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
# We require to also check `runners_token` to maintain compatibility with old version of runners
token && (build.valid_token?(token) || build.project.valid_runners_token?(token))
end end
def update_runner_last_contact(save: true) def update_runner_last_contact(save: true)
......
...@@ -60,7 +60,7 @@ module Ci ...@@ -60,7 +60,7 @@ module Ci
name: job[:name].to_s, name: job[:name].to_s,
allow_failure: job[:allow_failure] || false, allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success', when: job[:when] || 'on_success',
environment: job[:environment], environment: job[:environment_name],
yaml_variables: yaml_variables(name), yaml_variables: yaml_variables(name),
options: { options: {
image: job[:image], image: job[:image],
...@@ -69,6 +69,7 @@ module Ci ...@@ -69,6 +69,7 @@ module Ci
cache: job[:cache], cache: job[:cache],
dependencies: job[:dependencies], dependencies: job[:dependencies],
after_script: job[:after_script], after_script: job[:after_script],
environment: job[:environment],
}.compact }.compact
} }
end end
......
module Ci::MaskSecret
class << self
def mask(value, token)
return value unless value.present? && token.present?
value.gsub(token, 'x' * token.length)
end
end
end
module ExpandVariables
class << self
def expand(value, variables)
# Convert hash array to variables
if variables.is_a?(Array)
variables = variables.reduce({}) do |hash, variable|
hash[variable[:key]] = variable[:value]
hash
end
end
value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do
variables[$1 || $2]
end
end
end
end
module Gitlab module Gitlab
module Auth module Auth
Result = Struct.new(:user, :type) class MissingPersonalTokenError < StandardError; end
class << self class << self
def find_for_git_client(login, password, project:, ip:) def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil? raise "Must provide an IP for rate limiting" if ip.nil?
result = Result.new result =
service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(login, password) ||
Gitlab::Auth::Result.new
if valid_ci_request?(login, password, project) rate_limit!(ip, success: result.success?, login: login)
result.type = :ci
else
result = populate_result(login, password)
end
success = result.user.present? || [:ci, :missing_personal_token].include?(result.type)
rate_limit!(ip, success: success, login: login)
result result
end end
...@@ -57,44 +57,31 @@ module Gitlab ...@@ -57,44 +57,31 @@ module Gitlab
private private
def valid_ci_request?(login, password, project) def service_request_check(login, password, project)
matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login) matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
return false unless project && matched_login.present? return unless project && matched_login.present?
underscored_service = matched_login['service'].underscore underscored_service = matched_login['service'].underscore
if underscored_service == 'gitlab_ci' if Service.available_services_names.include?(underscored_service)
project && project.valid_build_token?(password)
elsif Service.available_services_names.include?(underscored_service)
# We treat underscored_service as a trusted input because it is included # We treat underscored_service as a trusted input because it is included
# in the Service.available_services_names whitelist. # in the Service.available_services_names whitelist.
service = project.public_send("#{underscored_service}_service") service = project.public_send("#{underscored_service}_service")
service && service.activated? && service.valid_token?(password) if service && service.activated? && service.valid_token?(password)
Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)
end end
end end
def populate_result(login, password)
result =
user_with_password_for_git(login, password) ||
oauth_access_token_check(login, password) ||
personal_access_token_check(login, password)
if result
result.type = nil unless result.user
if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap
result.type = :missing_personal_token
end
end
result || Result.new
end end
def user_with_password_for_git(login, password) def user_with_password_for_git(login, password)
user = find_with_user_password(login, password) user = find_with_user_password(login, password)
Result.new(user, :gitlab_or_ldap) if user return unless user
raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
end end
def oauth_access_token_check(login, password) def oauth_access_token_check(login, password)
...@@ -102,7 +89,7 @@ module Gitlab ...@@ -102,7 +89,7 @@ module Gitlab
token = Doorkeeper::AccessToken.by_token(password) token = Doorkeeper::AccessToken.by_token(password)
if token && token.accessible? if token && token.accessible?
user = User.find_by(id: token.resource_owner_id) user = User.find_by(id: token.resource_owner_id)
Result.new(user, :oauth) Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
end end
end end
end end
...@@ -111,9 +98,52 @@ module Gitlab ...@@ -111,9 +98,52 @@ module Gitlab
if login && password if login && password
user = User.find_by_personal_access_token(password) user = User.find_by_personal_access_token(password)
validation = User.by_login(login) validation = User.by_login(login)
Result.new(user, :personal_token) if user == validation Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
end
end
def build_access_token_check(login, password)
return unless login == 'gitlab-ci-token'
return unless password
build = ::Ci::Build.running.find_by_token(password)
return unless build
return unless build.project.builds_enabled?
if build.user
# If user is assigned to build, use restricted credentials of user
Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
else
# Otherwise use generic CI credentials (backward compatibility)
Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities)
end end
end end
public
def build_authentication_abilities
[
:read_project,
:build_download_code,
:build_read_container_image,
:build_create_container_image
]
end
def read_authentication_abilities
[
:read_project,
:download_code,
:read_container_image
]
end
def full_authentication_abilities
read_authentication_abilities + [
:push_code,
:create_container_image
]
end
end end
end end
end end
module Gitlab
module Auth
Result = Struct.new(:actor, :project, :type, :authentication_abilities) do
def ci?
type == :ci
end
def success?
actor.present? || type == :ci
end
end
end
end
module Gitlab
module Ci
class Config
module Node
##
# Entry that represents an environment.
#
class Environment < Entry
include Validatable
ALLOWED_KEYS = %i[name url]
validations do
validate do
unless hash? || string?
errors.add(:config, 'should be a hash or a string')
end
end
validates :name, presence: true
validates :name,
type: {
with: String,
message: Gitlab::Regex.environment_name_regex_message }
validates :name,
format: {
with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
with_options if: :hash? do
validates :config, allowed_keys: ALLOWED_KEYS
validates :url,
length: { maximum: 255 },
addressable_url: true,
allow_nil: true
end
end
def hash?
@config.is_a?(Hash)
end
def string?
@config.is_a?(String)
end
def name
value[:name]
end
def url
value[:url]
end
def value
case @config
when String then { name: @config }
when Hash then @config
else {}
end
end
end
end
end
end
end
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
type stage when artifacts cache dependencies before_script type stage when artifacts cache dependencies before_script
after_script variables environment] after_script variables environment]
attributes :tags, :allow_failure, :when, :environment, :dependencies attributes :tags, :allow_failure, :when, :dependencies
validations do validations do
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
...@@ -29,58 +29,53 @@ module Gitlab ...@@ -29,58 +29,53 @@ module Gitlab
inclusion: { in: %w[on_success on_failure always manual], inclusion: { in: %w[on_success on_failure always manual],
message: 'should be on_success, on_failure, ' \ message: 'should be on_success, on_failure, ' \
'always or manual' } 'always or manual' }
validates :environment,
type: {
with: String,
message: Gitlab::Regex.environment_name_regex_message }
validates :environment,
format: {
with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
validates :dependencies, array_of_strings: true validates :dependencies, array_of_strings: true
end end
end end
node :before_script, Script, node :before_script, Node::Script,
description: 'Global before script overridden in this job.' description: 'Global before script overridden in this job.'
node :script, Commands, node :script, Node::Commands,
description: 'Commands that will be executed in this job.' description: 'Commands that will be executed in this job.'
node :stage, Stage, node :stage, Node::Stage,
description: 'Pipeline stage this job will be executed into.' description: 'Pipeline stage this job will be executed into.'
node :type, Stage, node :type, Node::Stage,
description: 'Deprecated: stage this job will be executed into.' description: 'Deprecated: stage this job will be executed into.'
node :after_script, Script, node :after_script, Node::Script,
description: 'Commands that will be executed when finishing job.' description: 'Commands that will be executed when finishing job.'
node :cache, Cache, node :cache, Node::Cache,
description: 'Cache definition for this job.' description: 'Cache definition for this job.'
node :image, Image, node :image, Node::Image,
description: 'Image that will be used to execute this job.' description: 'Image that will be used to execute this job.'
node :services, Services, node :services, Node::Services,
description: 'Services that will be used to execute this job.' description: 'Services that will be used to execute this job.'
node :only, Trigger, node :only, Node::Trigger,
description: 'Refs policy this job will be executed for.' description: 'Refs policy this job will be executed for.'
node :except, Trigger, node :except, Node::Trigger,
description: 'Refs policy this job will be executed for.' description: 'Refs policy this job will be executed for.'
node :variables, Variables, node :variables, Node::Variables,
description: 'Environment variables available for this job.' description: 'Environment variables available for this job.'
node :artifacts, Artifacts, node :artifacts, Node::Artifacts,
description: 'Artifacts configuration for this job.' description: 'Artifacts configuration for this job.'
node :environment, Node::Environment,
description: 'Environment configuration for this job.'
helpers :before_script, :script, :stage, :type, :after_script, helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables, :cache, :image, :services, :only, :except, :variables,
:artifacts, :commands :artifacts, :commands, :environment
def compose!(deps = nil) def compose!(deps = nil)
super do super do
...@@ -133,6 +128,8 @@ module Gitlab ...@@ -133,6 +128,8 @@ module Gitlab
only: only, only: only,
except: except, except: except,
variables: variables_defined? ? variables : nil, variables: variables_defined? ? variables : nil,
environment: environment_defined? ? environment : nil,
environment_name: environment_defined? ? environment[:name] : nil,
artifacts: artifacts, artifacts: artifacts,
after_script: after_script } after_script: after_script }
end end
......
...@@ -5,12 +5,13 @@ module Gitlab ...@@ -5,12 +5,13 @@ module Gitlab
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive } DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack } PUSH_COMMANDS = %w{ git-receive-pack }
attr_reader :actor, :project, :protocol, :user_access attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
def initialize(actor, project, protocol) def initialize(actor, project, protocol, authentication_abilities:)
@actor = actor @actor = actor
@project = project @project = project
@protocol = protocol @protocol = protocol
@authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project) @user_access = UserAccess.new(user, project: project)
end end
...@@ -60,14 +61,26 @@ module Gitlab ...@@ -60,14 +61,26 @@ module Gitlab
end end
def user_download_access_check def user_download_access_check
unless user_access.can_do_action?(:download_code) unless user_can_download_code? || build_can_download_code?
return build_status_object(false, "You are not allowed to download code from this project.") return build_status_object(false, "You are not allowed to download code from this project.")
end end
build_status_object(true) build_status_object(true)
end end
def user_can_download_code?
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
end
def build_can_download_code?
authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
end
def user_push_access_check(changes) def user_push_access_check(changes)
unless authentication_abilities.include?(:push_code)
return build_status_object(false, "You are not allowed to upload code for this project.")
end
if changes.blank? if changes.blank?
return build_status_object(true) return build_status_object(true)
end end
......
...@@ -2,7 +2,8 @@ module Gitlab ...@@ -2,7 +2,8 @@ module Gitlab
module ImportExport module ImportExport
extend self extend self
VERSION = '0.1.3' # For every version update, the version history in import_export.md has to be kept up to date.
VERSION = '0.1.4'
FILENAME_LIMIT = 50 FILENAME_LIMIT = 50
def export_path(relative_path:) def export_path(relative_path:)
......
...@@ -35,7 +35,9 @@ project_tree: ...@@ -35,7 +35,9 @@ project_tree:
- :deploy_keys - :deploy_keys
- :services - :services
- :hooks - :hooks
- :protected_branches - protected_branches:
- :merge_access_levels
- :push_access_levels
- :labels - :labels
- milestones: - milestones:
- :events - :events
......
...@@ -7,7 +7,9 @@ module Gitlab ...@@ -7,7 +7,9 @@ module Gitlab
variables: 'Ci::Variable', variables: 'Ci::Variable',
triggers: 'Ci::Trigger', triggers: 'Ci::Trigger',
builds: 'Ci::Build', builds: 'Ci::Build',
hooks: 'ProjectHook' }.freeze hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
...@@ -17,6 +19,8 @@ module Gitlab ...@@ -17,6 +19,8 @@ module Gitlab
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze
FINDER_ATTRIBUTES = %w[title project_id].freeze
def self.create(*args) def self.create(*args)
new(*args).create new(*args).create
end end
...@@ -149,7 +153,7 @@ module Gitlab ...@@ -149,7 +153,7 @@ module Gitlab
end end
def parsed_relation_hash def parsed_relation_hash
@relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) } @parsed_relation_hash ||= @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
end end
def set_st_diffs def set_st_diffs
...@@ -161,14 +165,30 @@ module Gitlab ...@@ -161,14 +165,30 @@ module Gitlab
# Otherwise always create the record, skipping the extra SELECT clause. # Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin @existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name) if EXISTING_OBJECT_CHECK.include?(@relation_name)
existing_object = relation_class.find_or_initialize_by(parsed_relation_hash.slice('title', 'project_id')) events = parsed_relation_hash.delete('events')
existing_object.assign_attributes(parsed_relation_hash)
unless events.blank?
existing_object.assign_attributes(events: events)
end
existing_object existing_object
else else
relation_class.new(parsed_relation_hash) relation_class.new(parsed_relation_hash)
end end
end end
end end
def existing_object
@existing_object ||=
begin
finder_hash = parsed_relation_hash.slice(*FINDER_ATTRIBUTES)
existing_object = relation_class.find_or_create_by(finder_hash)
# Done in two steps, as MySQL behaves differently than PostgreSQL using
# the +find_or_create_by+ method and does not return the ID the second time.
existing_object.update(parsed_relation_hash)
existing_object
end
end
end end
end end
end end
...@@ -24,8 +24,8 @@ module Gitlab ...@@ -24,8 +24,8 @@ module Gitlab
end end
def verify_version!(version) def verify_version!(version)
if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version) if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
raise Gitlab::ImportExport::Error.new("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}") raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
else else
true true
end end
......
...@@ -96,11 +96,11 @@ module Gitlab ...@@ -96,11 +96,11 @@ module Gitlab
end end
def environment_name_regex def environment_name_regex
@environment_name_regex ||= /\A[a-zA-Z0-9_-]+\z/.freeze @environment_name_regex ||= /\A[a-zA-Z0-9_\\\/\${}. -]+\z/.freeze
end end
def environment_name_regex_message def environment_name_regex_message
"can contain only letters, digits, '-' and '_'." "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces"
end end
end end
end end
...@@ -94,15 +94,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -94,15 +94,8 @@ describe 'Issue Boards', feature: true, js: true do
end end
it 'shows issues in lists' do it 'shows issues in lists' do
page.within(find('.board:nth-child(2)')) do wait_for_board_cards(2, 2)
expect(page.find('.board-header')).to have_content('2') wait_for_board_cards(3, 2)
expect(page).to have_selector('.card', count: 2)
end
page.within(find('.board:nth-child(3)')) do
expect(page.find('.board-header')).to have_content('2')
expect(page).to have_selector('.card', count: 2)
end
end end
it 'shows confidential issues with icon' do it 'shows confidential issues with icon' do
...@@ -203,37 +196,33 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -203,37 +196,33 @@ describe 'Issue Boards', feature: true, js: true do
context 'backlog' do context 'backlog' do
it 'shows issues in backlog with no labels' do it 'shows issues in backlog with no labels' do
page.within(find('.board', match: :first)) do wait_for_board_cards(1, 6)
expect(page.find('.board-header')).to have_content('6')
expect(page).to have_selector('.card', count: 6)
end
end end
it 'moves issue from backlog into list' do it 'moves issue from backlog into list' do
drag_to(list_to_index: 1) drag_to(list_to_index: 1)
page.within(find('.board', match: :first)) do
expect(page.find('.board-header')).to have_content('5')
expect(page).to have_selector('.card', count: 5)
end
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 5)
page.within(find('.board:nth-child(2)')) do wait_for_board_cards(2, 3)
expect(page.find('.board-header')).to have_content('3')
expect(page).to have_selector('.card', count: 3)
end
end end
end end
context 'done' do context 'done' do
it 'shows list of done issues' do it 'shows list of done issues' do
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) wait_for_board_cards(4, 1)
wait_for_ajax
end end
it 'moves issue to done' do it 'moves issue to done' do
drag_to(list_from_index: 0, list_to_index: 3) drag_to(list_from_index: 0, list_to_index: 3)
wait_for_board_cards(1, 5)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
wait_for_board_cards(4, 2)
expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
expect(find('.board:nth-child(4)')).to have_content(issue9.title) expect(find('.board:nth-child(4)')).to have_content(issue9.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title) expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
...@@ -242,8 +231,12 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -242,8 +231,12 @@ describe 'Issue Boards', feature: true, js: true do
it 'removes all of the same issue to done' do it 'removes all of the same issue to done' do
drag_to(list_from_index: 1, list_to_index: 3) drag_to(list_from_index: 1, list_to_index: 3)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) wait_for_board_cards(1, 6)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1) wait_for_board_cards(2, 1)
wait_for_board_cards(3, 1)
wait_for_board_cards(4, 2)
expect(find('.board:nth-child(2)')).not_to have_content(issue6.title)
expect(find('.board:nth-child(4)')).to have_content(issue6.title) expect(find('.board:nth-child(4)')).to have_content(issue6.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title) expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
end end
...@@ -253,6 +246,11 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -253,6 +246,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'changes position of list' do it 'changes position of list' do
drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header') drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
wait_for_board_cards(1, 6)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(development.title) expect(find('.board:nth-child(2)')).to have_content(development.title)
expect(find('.board:nth-child(2)')).to have_content(planning.title) expect(find('.board:nth-child(2)')).to have_content(planning.title)
end end
...@@ -260,8 +258,11 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -260,8 +258,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves between lists' do it 'issue moves between lists' do
drag_to(list_from_index: 1, card_index: 1, list_to_index: 2) drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) wait_for_board_cards(1, 6)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 3) wait_for_board_cards(2, 1)
wait_for_board_cards(3, 3)
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(3)')).to have_content(issue6.title) expect(find('.board:nth-child(3)')).to have_content(issue6.title)
expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title) expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
end end
...@@ -269,8 +270,11 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -269,8 +270,11 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves between lists' do it 'issue moves between lists' do
drag_to(list_from_index: 2, list_to_index: 1) drag_to(list_from_index: 2, list_to_index: 1)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3) wait_for_board_cards(1, 6)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1) wait_for_board_cards(2, 3)
wait_for_board_cards(3, 1)
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(issue7.title) expect(find('.board:nth-child(2)')).to have_content(issue7.title)
expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title) expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
end end
...@@ -278,8 +282,12 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -278,8 +282,12 @@ describe 'Issue Boards', feature: true, js: true do
it 'issue moves from done' do it 'issue moves from done' do
drag_to(list_from_index: 3, list_to_index: 1) drag_to(list_from_index: 3, list_to_index: 1)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
expect(find('.board:nth-child(2)')).to have_content(issue8.title) expect(find('.board:nth-child(2)')).to have_content(issue8.title)
wait_for_board_cards(1, 6)
wait_for_board_cards(2, 3)
wait_for_board_cards(3, 2)
wait_for_board_cards(4, 0)
end end
context 'issue card' do context 'issue card' do
...@@ -342,10 +350,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -342,10 +350,7 @@ describe 'Issue Boards', feature: true, js: true do
end end
it 'moves issues from backlog into new list' do it 'moves issues from backlog into new list' do
page.within(find('.board', match: :first)) do wait_for_board_cards(1, 6)
expect(page.find('.board-header')).to have_content('6')
expect(page).to have_selector('.card', count: 6)
end
click_button 'Create new list' click_button 'Create new list'
wait_for_ajax wait_for_ajax
...@@ -356,10 +361,7 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -356,10 +361,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
page.within(find('.board', match: :first)) do wait_for_board_cards(1, 5)
expect(page.find('.board-header')).to have_content('5')
expect(page).to have_selector('.card', count: 5)
end
end end
end end
end end
...@@ -379,16 +381,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -379,16 +381,8 @@ describe 'Issue Boards', feature: true, js: true do
end end
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 1)
page.within(find('.board', match: :first)) do wait_for_empty_boards((2..4))
expect(page.find('.board-header')).to have_content('1')
expect(page).to have_selector('.card', count: 1)
end
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('0')
expect(page).to have_selector('.card', count: 0)
end
end end
it 'filters by assignee' do it 'filters by assignee' do
...@@ -406,15 +400,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -406,15 +400,8 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
page.within(find('.board', match: :first)) do wait_for_board_cards(1, 1)
expect(page.find('.board-header')).to have_content('1') wait_for_empty_boards((2..4))
expect(page).to have_selector('.card', count: 1)
end
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('0')
expect(page).to have_selector('.card', count: 0)
end
end end
it 'filters by milestone' do it 'filters by milestone' do
...@@ -431,16 +418,10 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -431,16 +418,10 @@ describe 'Issue Boards', feature: true, js: true do
end end
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 0)
page.within(find('.board', match: :first)) do wait_for_board_cards(2, 1)
expect(page.find('.board-header')).to have_content('0') wait_for_board_cards(3, 0)
expect(page).to have_selector('.card', count: 0) wait_for_board_cards(4, 0)
end
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('1')
expect(page).to have_selector('.card', count: 1)
end
end end
it 'filters by label' do it 'filters by label' do
...@@ -456,16 +437,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -456,16 +437,8 @@ describe 'Issue Boards', feature: true, js: true do
end end
wait_for_vue_resource wait_for_vue_resource
wait_for_board_cards(1, 1)
page.within(find('.board', match: :first)) do wait_for_empty_boards((2..4))
expect(page.find('.board-header')).to have_content('1')
expect(page).to have_selector('.card', count: 1)
end
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('0')
expect(page).to have_selector('.card', count: 0)
end
end end
it 'infinite scrolls list with label filter' do it 'infinite scrolls list with label filter' do
...@@ -519,15 +492,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -519,15 +492,8 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
page.within(find('.board', match: :first)) do wait_for_board_cards(1, 1)
expect(page.find('.board-header')).to have_content('1') wait_for_empty_boards((2..4))
expect(page).to have_selector('.card', count: 1)
end
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('0')
expect(page).to have_selector('.card', count: 0)
end
end end
it 'filters by no label' do it 'filters by no label' do
...@@ -544,15 +510,10 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -544,15 +510,10 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
page.within(find('.board', match: :first)) do wait_for_board_cards(1, 5)
expect(page.find('.board-header')).to have_content('5') wait_for_board_cards(2, 0)
expect(page).to have_selector('.card', count: 5) wait_for_board_cards(3, 0)
end wait_for_board_cards(4, 1)
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('0')
expect(page).to have_selector('.card', count: 0)
end
end end
it 'filters by clicking label button on issue' do it 'filters by clicking label button on issue' do
...@@ -565,15 +526,8 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -565,15 +526,8 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
page.within(find('.board', match: :first)) do wait_for_board_cards(1, 1)
expect(page.find('.board-header')).to have_content('1') wait_for_empty_boards((2..4))
expect(page).to have_selector('.card', count: 1)
end
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('0')
expect(page).to have_selector('.card', count: 0)
end
page.within('.labels-filter') do page.within('.labels-filter') do
expect(find('.dropdown-toggle-text')).to have_content(bug.title) expect(find('.dropdown-toggle-text')).to have_content(bug.title)
...@@ -648,4 +602,17 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -648,4 +602,17 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource wait_for_vue_resource
end end
def wait_for_board_cards(board_number, expected_cards)
page.within(find(".board:nth-child(#{board_number})")) do
expect(page.find('.board-header')).to have_content(expected_cards.to_s)
expect(page).to have_selector('.card', count: expected_cards)
end
end
def wait_for_empty_boards(board_numbers)
board_numbers.each do |board|
wait_for_board_cards(board, 0)
end
end
end end
...@@ -150,7 +150,7 @@ feature 'Environments', feature: true do ...@@ -150,7 +150,7 @@ feature 'Environments', feature: true do
context 'for invalid name' do context 'for invalid name' do
before do before do
fill_in('Name', with: 'name with spaces') fill_in('Name', with: 'name,with,commas')
click_on 'Save' click_on 'Save'
end end
......
...@@ -3,9 +3,8 @@ require 'rails_helper' ...@@ -3,9 +3,8 @@ require 'rails_helper'
feature 'Milestone', feature: true do feature 'Milestone', feature: true do
include WaitForAjax include WaitForAjax
let(:project) { create(:project, :public) } let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project, title: 8.7) }
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -13,7 +12,7 @@ feature 'Milestone', feature: true do ...@@ -13,7 +12,7 @@ feature 'Milestone', feature: true do
end end
feature 'Create a milestone' do feature 'Create a milestone' do
scenario 'shows an informative message for a new issue' do scenario 'shows an informative message for a new milestone' do
visit new_namespace_project_milestone_path(project.namespace, project) visit new_namespace_project_milestone_path(project.namespace, project)
page.within '.milestone-form' do page.within '.milestone-form' do
fill_in "milestone_title", with: '8.7' fill_in "milestone_title", with: '8.7'
...@@ -26,10 +25,26 @@ feature 'Milestone', feature: true do ...@@ -26,10 +25,26 @@ feature 'Milestone', feature: true do
feature 'Open a milestone with closed issues' do feature 'Open a milestone with closed issues' do
scenario 'shows an informative message' do scenario 'shows an informative message' do
milestone = create(:milestone, project: project, title: 8.7)
create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed") create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed")
visit namespace_project_milestone_path(project.namespace, project, milestone) visit namespace_project_milestone_path(project.namespace, project, milestone)
expect(find('.alert-success')).to have_content('All issues for this milestone are closed. You may close this milestone now.') expect(find('.alert-success')).to have_content('All issues for this milestone are closed. You may close this milestone now.')
end end
end end
feature 'Open a milestone with an existing title' do
scenario 'displays validation message' do
milestone = create(:milestone, project: project, title: 8.7)
visit new_namespace_project_milestone_path(project.namespace, project)
page.within '.milestone-form' do
fill_in "milestone_title", with: milestone.title
end
find('input[name="commit"]').click
expect(find('.alert-danger')).to have_content('Title has already been taken')
end
end
end end
...@@ -754,6 +754,20 @@ module Ci ...@@ -754,6 +754,20 @@ module Ci
it 'does return production' do it 'does return production' do
expect(builds.size).to eq(1) expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment) expect(builds.first[:environment]).to eq(environment)
expect(builds.first[:options]).to include(environment: { name: environment })
end
end
context 'when hash is specified' do
let(:environment) do
{ name: 'production',
url: 'http://production.gitlab.com' }
end
it 'does return production and URL' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment[:name])
expect(builds.first[:options]).to include(environment: environment)
end end
end end
...@@ -770,15 +784,16 @@ module Ci ...@@ -770,15 +784,16 @@ module Ci
let(:environment) { 1 } let(:environment) { 1 }
it 'raises error' do it 'raises error' do
expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}") expect { builds }.to raise_error(
'jobs:deploy_to_production:environment config should be a hash or a string')
end end
end end
context 'is not a valid string' do context 'is not a valid string' do
let(:environment) { 'production staging' } let(:environment) { 'production:staging' }
it 'raises error' do it 'raises error' do
expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}") expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
end end
end end
end end
......
require 'spec_helper'
describe Ci::MaskSecret, lib: true do
subject { described_class }
describe '#mask' do
it 'masks exact number of characters' do
expect(subject.mask('token', 'oke')).to eq('txxxn')
end
it 'masks multiple occurrences' do
expect(subject.mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
end
it 'does not mask if not found' do
expect(subject.mask('token', 'not')).to eq('token')
end
end
end
require 'spec_helper'
describe ExpandVariables do
describe '#expand' do
subject { described_class.expand(value, variables) }
tests = [
{ value: 'key',
result: 'key',
variables: []
},
{ value: 'key$variable',
result: 'key',
variables: []
},
{ value: 'key$variable',
result: 'keyvalue',
variables: [
{ key: 'variable', value: 'value' }
]
},
{ value: 'key${variable}',
result: 'keyvalue',
variables: [
{ key: 'variable', value: 'value' }
]
},
{ value: 'key$variable$variable2',
result: 'keyvalueresult',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
]
},
{ value: 'key${variable}${variable2}',
result: 'keyvalueresult',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
]
},
{ value: 'key$variable2$variable',
result: 'keyresultvalue',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' },
]
},
{ value: 'key${variable2}${variable}',
result: 'keyresultvalue',
variables: [
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
]
},
{ value: 'review/$CI_BUILD_REF_NAME',
result: 'review/feature/add-review-apps',
variables: [
{ key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' }
]
},
]
tests.each do |test|
context "#{test[:value]} resolves to #{test[:result]}" do
let(:value) { test[:value] }
let(:variables) { test[:variables] }
it { is_expected.to eq(test[:result]) }
end
end
end
end
...@@ -4,15 +4,53 @@ describe Gitlab::Auth, lib: true do ...@@ -4,15 +4,53 @@ describe Gitlab::Auth, lib: true do
let(:gl_auth) { described_class } let(:gl_auth) { described_class }
describe 'find_for_git_client' do describe 'find_for_git_client' do
it 'recognizes CI' do context 'build token' do
token = '123' subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
context 'for running build' do
let!(:build) { create(:ci_build, :running) }
let(:project) { build.project }
before do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'gitlab-ci-token')
end
it 'recognises user-less build' do
expect(subject).to eq(Gitlab::Auth::Result.new(nil, build.project, :ci, build_authentication_abilities))
end
it 'recognises user token' do
build.update(user: create(:user))
expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities))
end
end
(HasStatus::AVAILABLE_STATUSES - ['running']).each do |build_status|
context "for #{build_status} build" do
let!(:build) { create(:ci_build, status: build_status) }
let(:project) { build.project }
before do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'gitlab-ci-token')
end
it 'denies authentication' do
expect(subject).to eq(Gitlab::Auth::Result.new)
end
end
end
end
it 'recognizes other ci services' do
project = create(:empty_project) project = create(:empty_project)
project.update_attributes(runners_token: token) project.create_drone_ci_service(active: true)
project.drone_ci_service.update(token: 'token')
ip = 'ip' ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token') expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
expect(gl_auth.find_for_git_client('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci)) expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
end end
it 'recognizes master passwords' do it 'recognizes master passwords' do
...@@ -20,7 +58,7 @@ describe Gitlab::Auth, lib: true do ...@@ -20,7 +58,7 @@ describe Gitlab::Auth, lib: true do
ip = 'ip' ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap)) expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
end end
it 'recognizes OAuth tokens' do it 'recognizes OAuth tokens' do
...@@ -30,7 +68,7 @@ describe Gitlab::Auth, lib: true do ...@@ -30,7 +68,7 @@ describe Gitlab::Auth, lib: true do
ip = 'ip' ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth)) expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
end end
it 'returns double nil for invalid credentials' do it 'returns double nil for invalid credentials' do
...@@ -92,4 +130,30 @@ describe Gitlab::Auth, lib: true do ...@@ -92,4 +130,30 @@ describe Gitlab::Auth, lib: true do
end end
end end
end end
private
def build_authentication_abilities
[
:read_project,
:build_download_code,
:build_read_container_image,
:build_create_container_image
]
end
def read_authentication_abilities
[
:read_project,
:download_code,
:read_container_image
]
end
def full_authentication_abilities
read_authentication_abilities + [
:push_code,
:create_container_image
]
end
end end
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Environment do
let(:entry) { described_class.new(config) }
before { entry.compose! }
context 'when configuration is a string' do
let(:config) { 'production' }
describe '#string?' do
it 'is string configuration' do
expect(entry).to be_string
end
end
describe '#hash?' do
it 'is not hash configuration' do
expect(entry).not_to be_hash
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns valid hash' do
expect(entry.value).to eq(name: 'production')
end
end
describe '#name' do
it 'returns environment name' do
expect(entry.name).to eq 'production'
end
end
describe '#url' do
it 'returns environment url' do
expect(entry.url).to be_nil
end
end
end
context 'when configuration is a hash' do
let(:config) do
{ name: 'development', url: 'https://example.gitlab.com' }
end
describe '#string?' do
it 'is not string configuration' do
expect(entry).not_to be_string
end
end
describe '#hash?' do
it 'is hash configuration' do
expect(entry).to be_hash
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
describe '#value' do
it 'returns valid hash' do
expect(entry.value).to eq config
end
end
describe '#name' do
it 'returns environment name' do
expect(entry.name).to eq 'development'
end
end
describe '#url' do
it 'returns environment url' do
expect(entry.url).to eq 'https://example.gitlab.com'
end
end
end
context 'when variables are used for environment' do
let(:config) do
{ name: 'review/$CI_BUILD_REF_NAME',
url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' }
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when configuration is invalid' do
context 'when configuration is an array' do
let(:config) { ['env'] }
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
describe '#errors' do
it 'contains error about invalid type' do
expect(entry.errors)
.to include 'environment config should be a hash or a string'
end
end
end
context 'when environment name is not present' do
let(:config) { { url: 'https://example.gitlab.com' } }
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
describe '#errors?' do
it 'contains error about missing environment name' do
expect(entry.errors)
.to include "environment name can't be blank"
end
end
end
context 'when invalid URL is used' do
let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } }
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
describe '#errors?' do
it 'contains error about invalid URL' do
expect(entry.errors)
.to include "environment url must be a valid url"
end
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::GitAccess, lib: true do describe Gitlab::GitAccess, lib: true do
let(:access) { Gitlab::GitAccess.new(actor, project, 'web') } let(:access) { Gitlab::GitAccess.new(actor, project, 'web', authentication_abilities: authentication_abilities) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:actor) { user } let(:actor) { user }
let(:authentication_abilities) do
[
:read_project,
:download_code,
:push_code
]
end
describe '#check with single protocols allowed' do describe '#check with single protocols allowed' do
def disable_protocol(protocol) def disable_protocol(protocol)
...@@ -15,7 +22,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -15,7 +22,7 @@ describe Gitlab::GitAccess, lib: true do
context 'ssh disabled' do context 'ssh disabled' do
before do before do
disable_protocol('ssh') disable_protocol('ssh')
@acc = Gitlab::GitAccess.new(actor, project, 'ssh') @acc = Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities)
end end
it 'blocks ssh git push' do it 'blocks ssh git push' do
...@@ -30,7 +37,7 @@ describe Gitlab::GitAccess, lib: true do ...@@ -30,7 +37,7 @@ describe Gitlab::GitAccess, lib: true do
context 'http disabled' do context 'http disabled' do
before do before do
disable_protocol('http') disable_protocol('http')
@acc = Gitlab::GitAccess.new(actor, project, 'http') @acc = Gitlab::GitAccess.new(actor, project, 'http', authentication_abilities: authentication_abilities)
end end
it 'blocks http push' do it 'blocks http push' do
...@@ -111,6 +118,36 @@ describe Gitlab::GitAccess, lib: true do ...@@ -111,6 +118,36 @@ describe Gitlab::GitAccess, lib: true do
end end
end end
end end
describe 'build authentication_abilities permissions' do
let(:authentication_abilities) { build_authentication_abilities }
describe 'reporter user' do
before { project.team << [user, :reporter] }
context 'pull code' do
it { expect(subject).to be_allowed }
end
end
describe 'admin user' do
let(:user) { create(:admin) }
context 'when member of the project' do
before { project.team << [user, :reporter] }
context 'pull code' do
it { expect(subject).to be_allowed }
end
end
context 'when is not member of the project' do
context 'pull code' do
it { expect(subject).not_to be_allowed }
end
end
end
end
end end
describe 'push_access_check' do describe 'push_access_check' do
...@@ -283,15 +320,11 @@ describe Gitlab::GitAccess, lib: true do ...@@ -283,15 +320,11 @@ describe Gitlab::GitAccess, lib: true do
end end
end end
describe 'deploy key permissions' do shared_examples 'can not push code' do
let(:key) { create(:deploy_key) }
let(:actor) { key }
context 'push code' do
subject { access.check('git-receive-pack', '_any') } subject { access.check('git-receive-pack', '_any') }
context 'when project is authorized' do context 'when project is authorized' do
before { key.projects << project } before { authorize }
it { expect(subject).not_to be_allowed } it { expect(subject).not_to be_allowed }
end end
...@@ -316,5 +349,42 @@ describe Gitlab::GitAccess, lib: true do ...@@ -316,5 +349,42 @@ describe Gitlab::GitAccess, lib: true do
end end
end end
end end
describe 'build authentication abilities' do
let(:authentication_abilities) { build_authentication_abilities }
it_behaves_like 'can not push code' do
def authorize
project.team << [user, :reporter]
end
end
end
describe 'deploy key permissions' do
let(:key) { create(:deploy_key) }
let(:actor) { key }
it_behaves_like 'can not push code' do
def authorize
key.projects << project
end
end
end
private
def build_authentication_abilities
[
:read_project,
:build_download_code
]
end
def full_authentication_abilities
[
:read_project,
:download_code,
:push_code
]
end end
end end
require 'spec_helper' require 'spec_helper'
describe Gitlab::GitAccessWiki, lib: true do describe Gitlab::GitAccessWiki, lib: true do
let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web') } let(:access) { Gitlab::GitAccessWiki.new(user, project, 'web', authentication_abilities: authentication_abilities) }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:authentication_abilities) do
[
:read_project,
:download_code,
:push_code
]
end
describe 'push_allowed?' do describe 'push_allowed?' do
before do before do
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
"test_ee_field": "test", "test_ee_field": "test",
"milestone": { "milestone": {
"id": 1, "id": 1,
"title": "v0.0", "title": "test milestone",
"project_id": 8, "project_id": 8,
"description": "test milestone", "description": "test milestone",
"due_date": null, "due_date": null,
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
{ {
"id": 2, "id": 2,
"label_id": 2, "label_id": 2,
"target_id": 3, "target_id": 40,
"target_type": "Issue", "target_type": "Issue",
"created_at": "2016-07-22T08:57:02.840Z", "created_at": "2016-07-22T08:57:02.840Z",
"updated_at": "2016-07-22T08:57:02.840Z", "updated_at": "2016-07-22T08:57:02.840Z",
...@@ -281,6 +281,31 @@ ...@@ -281,6 +281,31 @@
"deleted_at": null, "deleted_at": null,
"due_date": null, "due_date": null,
"moved_to_id": null, "moved_to_id": null,
"milestone": {
"id": 1,
"title": "test milestone",
"project_id": 8,
"description": "test milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
"state": "active",
"iid": 1,
"events": [
{
"id": 487,
"target_type": "Milestone",
"target_id": 1,
"title": null,
"data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
"action": 1,
"author_id": 18
}
]
},
"notes": [ "notes": [
{ {
"id": 359, "id": 359,
...@@ -494,6 +519,27 @@ ...@@ -494,6 +519,27 @@
"deleted_at": null, "deleted_at": null,
"due_date": null, "due_date": null,
"moved_to_id": null, "moved_to_id": null,
"label_links": [
{
"id": 99,
"label_id": 2,
"target_id": 38,
"target_type": "Issue",
"created_at": "2016-07-22T08:57:02.840Z",
"updated_at": "2016-07-22T08:57:02.840Z",
"label": {
"id": 2,
"title": "test2",
"color": "#428bca",
"project_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"priority": null
}
}
],
"notes": [ "notes": [
{ {
"id": 367, "id": 367,
...@@ -6478,7 +6524,7 @@ ...@@ -6478,7 +6524,7 @@
{ {
"id": 37, "id": 37,
"project_id": 5, "project_id": 5,
"ref": "master", "ref": null,
"sha": "048721d90c449b244b7b4c53a9186b04330174ec", "sha": "048721d90c449b244b7b4c53a9186b04330174ec",
"before_sha": null, "before_sha": null,
"push_data": null, "push_data": null,
...@@ -7301,6 +7347,30 @@ ...@@ -7301,6 +7347,30 @@
], ],
"protected_branches": [ "protected_branches": [
{
"id": 1,
"project_id": 9,
"name": "master",
"created_at": "2016-08-30T07:32:52.426Z",
"updated_at": "2016-08-30T07:32:52.426Z",
"merge_access_levels": [
{
"id": 1,
"protected_branch_id": 1,
"access_level": 40,
"created_at": "2016-08-30T07:32:52.458Z",
"updated_at": "2016-08-30T07:32:52.458Z"
}
],
"push_access_levels": [
{
"id": 1,
"protected_branch_id": 1,
"access_level": 40,
"created_at": "2016-08-30T07:32:52.490Z",
"updated_at": "2016-08-30T07:32:52.490Z"
}
]
}
] ]
} }
\ No newline at end of file
...@@ -29,12 +29,30 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -29,12 +29,30 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED) expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
end end
it 'has the same label associated to two issues' do
restored_project_json
expect(Label.first.issues.count).to eq(2)
end
it 'has milestones associated to two separate issues' do
restored_project_json
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
end
it 'creates a valid pipeline note' do it 'creates a valid pipeline note' do
restored_project_json restored_project_json
expect(Ci::Pipeline.first.notes).not_to be_empty expect(Ci::Pipeline.first.notes).not_to be_empty
end end
it 'restores pipelines with missing ref' do
restored_project_json
expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
end
it 'restores the correct event with symbolised data' do it 'restores the correct event with symbolised data' do
restored_project_json restored_project_json
...@@ -49,6 +67,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -49,6 +67,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end end
it 'contains the merge access levels on a protected branch' do
restored_project_json
expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
end
it 'contains the push access levels on a protected branch' do
restored_project_json
expect(ProtectedBranch.first.push_access_levels).not_to be_empty
end
context 'event at forth level of the tree' do context 'event at forth level of the tree' do
let(:event) { Event.where(title: 'test levels').first } let(:event) { Event.where(title: 'test levels').first }
...@@ -77,12 +107,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -77,12 +107,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil expect(Label.first.label_links.first.target).not_to be_nil
end end
it 'has milestones associated to issues' do
restored_project_json
expect(Milestone.find_by_description('test milestone').issues).not_to be_empty
end
context 'Merge requests' do context 'Merge requests' do
before do before do
restored_project_json restored_project_json
......
...@@ -23,7 +23,7 @@ describe Gitlab::ImportExport::VersionChecker, services: true do ...@@ -23,7 +23,7 @@ describe Gitlab::ImportExport::VersionChecker, services: true do
it 'shows the correct error message' do it 'shows the correct error message' do
described_class.check!(shared: shared) described_class.check!(shared: shared)
expect(shared.errors.first).to eq("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}") expect(shared.errors.first).to eq("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
end end
end end
end end
......
...@@ -88,9 +88,7 @@ describe Ci::Build, models: true do ...@@ -88,9 +88,7 @@ describe Ci::Build, models: true do
end end
describe '#trace' do describe '#trace' do
subject { build.trace_html } it { expect(build.trace).to be_nil }
it { is_expected.to be_empty }
context 'when build.trace contains text' do context 'when build.trace contains text' do
let(:text) { 'example output' } let(:text) { 'example output' }
...@@ -98,16 +96,80 @@ describe Ci::Build, models: true do ...@@ -98,16 +96,80 @@ describe Ci::Build, models: true do
build.trace = text build.trace = text
end end
it { is_expected.to include(text) } it { expect(build.trace).to eq(text) }
it { expect(subject.length).to be >= text.length } end
context 'when build.trace hides runners token' do
let(:token) { 'my_secret_token' }
before do
build.update(trace: token)
build.project.update(runners_token: token)
end
it { expect(build.trace).not_to include(token) }
it { expect(build.raw_trace).to include(token) }
end
context 'when build.trace hides build token' do
let(:token) { 'my_secret_token' }
before do
build.update(trace: token)
build.update(token: token)
end
it { expect(build.trace).not_to include(token) }
it { expect(build.raw_trace).to include(token) }
end
end
describe '#raw_trace' do
subject { build.raw_trace }
context 'when build.trace hides runners token' do
let(:token) { 'my_secret_token' }
before do
build.project.update(runners_token: token)
build.update(trace: token)
end
it { is_expected.not_to include(token) }
end
context 'when build.trace hides build token' do
let(:token) { 'my_secret_token' }
before do
build.update(token: token)
build.update(trace: token)
end
it { is_expected.not_to include(token) }
end
end
context '#append_trace' do
subject { build.trace_html }
context 'when build.trace hides runners token' do
let(:token) { 'my_secret_token' }
before do
build.project.update(runners_token: token)
build.append_trace(token, 0)
end
it { is_expected.not_to include(token) }
end end
context 'when build.trace hides token' do context 'when build.trace hides build token' do
let(:token) { 'my_secret_token' } let(:token) { 'my_secret_token' }
before do before do
build.project.update_attributes(runners_token: token) build.update(token: token)
build.update_attributes(trace: token) build.append_trace(token, 0)
end end
it { is_expected.not_to include(token) } it { is_expected.not_to include(token) }
......
...@@ -8,7 +8,7 @@ describe Ci::Build, models: true do ...@@ -8,7 +8,7 @@ describe Ci::Build, models: true do
it 'obfuscates project runners token' do it 'obfuscates project runners token' do
allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}") allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
expect(build.trace).to eq("Test: xxxxxx") expect(build.trace).to eq("Test: xxxxxxxxxxxxxxxxxxxx")
end end
it 'empty project runners token' do it 'empty project runners token' do
......
...@@ -63,4 +63,20 @@ describe Environment, models: true do ...@@ -63,4 +63,20 @@ describe Environment, models: true do
end end
end end
end end
describe '#environment_type' do
subject { environment.environment_type }
it 'sets a environment type if name has multiple segments' do
environment.update!(name: 'production/worker.gitlab.com')
is_expected.to eq('production')
end
it 'nullifies a type if it\'s a simple name' do
environment.update!(name: 'production')
is_expected.to be_nil
end
end
end end
...@@ -16,18 +16,12 @@ describe Event, models: true do ...@@ -16,18 +16,12 @@ describe Event, models: true do
describe 'Callbacks' do describe 'Callbacks' do
describe 'after_create :reset_project_activity' do describe 'after_create :reset_project_activity' do
let(:project) { create(:project) } let(:project) { create(:empty_project) }
context "project's last activity was less than 5 minutes ago" do it 'calls the reset_project_activity method' do
it 'does not update project.last_activity_at if it has been touched less than 5 minutes ago' do expect_any_instance_of(Event).to receive(:reset_project_activity)
create_event(project, project.owner)
project.update_column(:last_activity_at, 5.minutes.ago)
project_last_activity_at = project.last_activity_at
create_event(project, project.owner) create_event(project, project.owner)
expect(project.last_activity_at).to eq(project_last_activity_at)
end
end end
end end
end end
...@@ -161,6 +155,35 @@ describe Event, models: true do ...@@ -161,6 +155,35 @@ describe Event, models: true do
end end
end end
describe '#reset_project_activity' do
let(:project) { create(:empty_project) }
context 'when a project was updated less than 1 hour ago' do
it 'does not update the project' do
project.update(last_activity_at: Time.now)
expect(project).not_to receive(:update_column).
with(:last_activity_at, a_kind_of(Time))
create_event(project, project.owner)
end
end
context 'when a project was updated more than 1 hour ago' do
it 'updates the project' do
project.update(last_activity_at: 1.year.ago)
expect_any_instance_of(Gitlab::ExclusiveLease).
to receive(:try_obtain).and_return(true)
expect(project).to receive(:update_column).
with(:last_activity_at, a_kind_of(Time))
create_event(project, project.owner)
end
end
end
def create_event(project, user, attrs = {}) def create_event(project, user, attrs = {})
data = { data = {
before: Gitlab::Git::BLANK_SHA, before: Gitlab::Git::BLANK_SHA,
......
...@@ -110,7 +110,7 @@ describe API::API, api: true do ...@@ -110,7 +110,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['status']).to be_nil expect(json_response['status']).to eq("created")
end end
end end
......
...@@ -120,10 +120,11 @@ describe API::API, api: true do ...@@ -120,10 +120,11 @@ describe API::API, api: true do
context 'when authenticated as the group owner' do context 'when authenticated as the group owner' do
it 'updates the group' do it 'updates the group' do
put api("/groups/#{group1.id}", user1), name: new_group_name put api("/groups/#{group1.id}", user1), name: new_group_name, request_access_enabled: true
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['name']).to eq(new_group_name) expect(json_response['name']).to eq(new_group_name)
expect(json_response['request_access_enabled']).to eq(true)
end end
it 'returns 404 for a non existing group' do it 'returns 404 for a non existing group' do
...@@ -238,8 +239,14 @@ describe API::API, api: true do ...@@ -238,8 +239,14 @@ describe API::API, api: true do
context "when authenticated as user with group permissions" do context "when authenticated as user with group permissions" do
it "creates group" do it "creates group" do
post api("/groups", user3), attributes_for(:group) group = attributes_for(:group, { request_access_enabled: false })
post api("/groups", user3), group
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response["name"]).to eq(group[:name])
expect(json_response["path"]).to eq(group[:path])
expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
end end
it "does not create group, duplicate" do it "does not create group, duplicate" do
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::API, api: true do describe API::API, api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace ) } let!(:project) { create(:empty_project, namespace: user.namespace ) }
let!(:closed_milestone) { create(:closed_milestone, project: project) } let!(:closed_milestone) { create(:closed_milestone, project: project) }
let!(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
...@@ -12,6 +12,7 @@ describe API::API, api: true do ...@@ -12,6 +12,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/milestones' do describe 'GET /projects/:id/milestones' do
it 'returns project milestones' do it 'returns project milestones' do
get api("/projects/#{project.id}/milestones", user) get api("/projects/#{project.id}/milestones", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title) expect(json_response.first['title']).to eq(milestone.title)
...@@ -19,6 +20,7 @@ describe API::API, api: true do ...@@ -19,6 +20,7 @@ describe API::API, api: true do
it 'returns a 401 error if user not authenticated' do it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones") get api("/projects/#{project.id}/milestones")
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
...@@ -44,6 +46,7 @@ describe API::API, api: true do ...@@ -44,6 +46,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/milestones/:milestone_id' do describe 'GET /projects/:id/milestones/:milestone_id' do
it 'returns a project milestone by id' do it 'returns a project milestone by id' do
get api("/projects/#{project.id}/milestones/#{milestone.id}", user) get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['title']).to eq(milestone.title) expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid) expect(json_response['iid']).to eq(milestone.iid)
...@@ -60,11 +63,13 @@ describe API::API, api: true do ...@@ -60,11 +63,13 @@ describe API::API, api: true do
it 'returns 401 error if user not authenticated' do it 'returns 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}") get api("/projects/#{project.id}/milestones/#{milestone.id}")
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
it 'returns a 404 error if milestone id not found' do it 'returns a 404 error if milestone id not found' do
get api("/projects/#{project.id}/milestones/1234", user) get api("/projects/#{project.id}/milestones/1234", user)
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end
...@@ -72,6 +77,7 @@ describe API::API, api: true do ...@@ -72,6 +77,7 @@ describe API::API, api: true do
describe 'POST /projects/:id/milestones' do describe 'POST /projects/:id/milestones' do
it 'creates a new project milestone' do it 'creates a new project milestone' do
post api("/projects/#{project.id}/milestones", user), title: 'new milestone' post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new milestone') expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
...@@ -80,6 +86,7 @@ describe API::API, api: true do ...@@ -80,6 +86,7 @@ describe API::API, api: true do
it 'creates a new project milestone with description and due date' do it 'creates a new project milestone with description and due date' do
post api("/projects/#{project.id}/milestones", user), post api("/projects/#{project.id}/milestones", user),
title: 'new milestone', description: 'release', due_date: '2013-03-02' title: 'new milestone', description: 'release', due_date: '2013-03-02'
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['description']).to eq('release') expect(json_response['description']).to eq('release')
expect(json_response['due_date']).to eq('2013-03-02') expect(json_response['due_date']).to eq('2013-03-02')
...@@ -87,6 +94,14 @@ describe API::API, api: true do ...@@ -87,6 +94,14 @@ describe API::API, api: true do
it 'returns a 400 error if title is missing' do it 'returns a 400 error if title is missing' do
post api("/projects/#{project.id}/milestones", user) post api("/projects/#{project.id}/milestones", user)
expect(response).to have_http_status(400)
end
it 'returns a 400 error if params are invalid (duplicate title)' do
post api("/projects/#{project.id}/milestones", user),
title: milestone.title, description: 'release', due_date: '2013-03-02'
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
end end
...@@ -95,6 +110,7 @@ describe API::API, api: true do ...@@ -95,6 +110,7 @@ describe API::API, api: true do
it 'updates a project milestone' do it 'updates a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user), put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
title: 'updated title' title: 'updated title'
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title') expect(json_response['title']).to eq('updated title')
end end
...@@ -102,6 +118,7 @@ describe API::API, api: true do ...@@ -102,6 +118,7 @@ describe API::API, api: true do
it 'returns a 404 error if milestone id not found' do it 'returns a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user), put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title' title: 'updated title'
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
end end
...@@ -131,6 +148,7 @@ describe API::API, api: true do ...@@ -131,6 +148,7 @@ describe API::API, api: true do
end end
it 'returns project issues for a particular milestone' do it 'returns project issues for a particular milestone' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title) expect(json_response.first['milestone']['title']).to eq(milestone.title)
...@@ -138,11 +156,12 @@ describe API::API, api: true do ...@@ -138,11 +156,12 @@ describe API::API, api: true do
it 'returns a 401 error if user not authenticated' do it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
describe 'confidential issues' do describe 'confidential issues' do
let(:public_project) { create(:project, :public) } let(:public_project) { create(:empty_project, :public) }
let(:milestone) { create(:milestone, project: public_project) } let(:milestone) { create(:milestone, project: public_project) }
let(:issue) { create(:issue, project: public_project) } let(:issue) { create(:issue, project: public_project) }
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
......
...@@ -225,7 +225,8 @@ describe API::API, api: true do ...@@ -225,7 +225,8 @@ describe API::API, api: true do
issues_enabled: false, issues_enabled: false,
merge_requests_enabled: false, merge_requests_enabled: false,
wiki_enabled: false, wiki_enabled: false,
only_allow_merge_if_build_succeeds: false only_allow_merge_if_build_succeeds: false,
request_access_enabled: true
}) })
post api('/projects', user), project post api('/projects', user), project
...@@ -352,7 +353,8 @@ describe API::API, api: true do ...@@ -352,7 +353,8 @@ describe API::API, api: true do
description: FFaker::Lorem.sentence, description: FFaker::Lorem.sentence,
issues_enabled: false, issues_enabled: false,
merge_requests_enabled: false, merge_requests_enabled: false,
wiki_enabled: false wiki_enabled: false,
request_access_enabled: true
}) })
post api("/projects/user/#{user.id}", admin), project post api("/projects/user/#{user.id}", admin), project
...@@ -887,6 +889,15 @@ describe API::API, api: true do ...@@ -887,6 +889,15 @@ describe API::API, api: true do
expect(json_response['message']['name']).to eq(['has already been taken']) expect(json_response['message']['name']).to eq(['has already been taken'])
end end
it 'updates request_access_enabled' do
project_param = { request_access_enabled: false }
put api("/projects/#{project.id}", user), project_param
expect(response).to have_http_status(200)
expect(json_response['request_access_enabled']).to eq(false)
end
it 'updates path & name to existing path & name in different namespace' do it 'updates path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name } project_param = { path: project4.path, name: project4.name }
put api("/projects/#{project3.id}", user), project_param put api("/projects/#{project3.id}", user), project_param
...@@ -948,7 +959,8 @@ describe API::API, api: true do ...@@ -948,7 +959,8 @@ describe API::API, api: true do
wiki_enabled: true, wiki_enabled: true,
snippets_enabled: true, snippets_enabled: true,
merge_requests_enabled: true, merge_requests_enabled: true,
description: 'new description' } description: 'new description',
request_access_enabled: true }
put api("/projects/#{project.id}", user3), project_param put api("/projects/#{project.id}", user3), project_param
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
......
...@@ -254,7 +254,8 @@ describe Ci::API::API do ...@@ -254,7 +254,8 @@ describe Ci::API::API do
let(:get_url) { ci_api("/builds/#{build.id}/artifacts") } let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } } let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) } let(:token) { build.token }
let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) }
before { build.run! } before { build.run! }
...@@ -262,6 +263,7 @@ describe Ci::API::API do ...@@ -262,6 +263,7 @@ describe Ci::API::API do
context "should authorize posting artifact to running build" do context "should authorize posting artifact to running build" do
it "using token as parameter" do it "using token as parameter" do
post authorize_url, { token: build.token }, headers post authorize_url, { token: build.token }, headers
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil expect(json_response["TempPath"]).not_to be_nil
...@@ -269,6 +271,15 @@ describe Ci::API::API do ...@@ -269,6 +271,15 @@ describe Ci::API::API do
it "using token as header" do it "using token as header" do
post authorize_url, {}, headers_with_token post authorize_url, {}, headers_with_token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
end
it "using runners token" do
post authorize_url, { token: build.project.runners_token }, headers
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil expect(json_response["TempPath"]).not_to be_nil
...@@ -276,7 +287,9 @@ describe Ci::API::API do ...@@ -276,7 +287,9 @@ describe Ci::API::API do
it "reject requests that did not go through gitlab-workhorse" do it "reject requests that did not go through gitlab-workhorse" do
headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
post authorize_url, { token: build.token }, headers post authorize_url, { token: build.token }, headers
expect(response).to have_http_status(500) expect(response).to have_http_status(500)
end end
end end
...@@ -284,13 +297,17 @@ describe Ci::API::API do ...@@ -284,13 +297,17 @@ describe Ci::API::API do
context "should fail to post too large artifact" do context "should fail to post too large artifact" do
it "using token as parameter" do it "using token as parameter" do
stub_application_setting(max_artifacts_size: 0) stub_application_setting(max_artifacts_size: 0)
post authorize_url, { token: build.token, filesize: 100 }, headers post authorize_url, { token: build.token, filesize: 100 }, headers
expect(response).to have_http_status(413) expect(response).to have_http_status(413)
end end
it "using token as header" do it "using token as header" do
stub_application_setting(max_artifacts_size: 0) stub_application_setting(max_artifacts_size: 0)
post authorize_url, { filesize: 100 }, headers_with_token post authorize_url, { filesize: 100 }, headers_with_token
expect(response).to have_http_status(413) expect(response).to have_http_status(413)
end end
end end
...@@ -358,6 +375,16 @@ describe Ci::API::API do ...@@ -358,6 +375,16 @@ describe Ci::API::API do
it_behaves_like 'successful artifacts upload' it_behaves_like 'successful artifacts upload'
end end
context 'when using runners token' do
let(:token) { build.project.runners_token }
before do
upload_artifacts(file_upload, headers_with_token)
end
it_behaves_like 'successful artifacts upload'
end
end end
context 'posts artifacts file and metadata file' do context 'posts artifacts file and metadata file' do
...@@ -497,10 +524,12 @@ describe Ci::API::API do ...@@ -497,10 +524,12 @@ describe Ci::API::API do
before do before do
delete delete_url, token: build.token delete delete_url, token: build.token
build.reload
end end
shared_examples 'having removable artifacts' do
it 'removes build artifacts' do it 'removes build artifacts' do
build.reload
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(build.artifacts_file.exists?).to be_falsy expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy expect(build.artifacts_metadata.exists?).to be_falsy
...@@ -508,8 +537,27 @@ describe Ci::API::API do ...@@ -508,8 +537,27 @@ describe Ci::API::API do
end end
end end
context 'when using build token' do
before do
delete delete_url, token: build.token
end
it_behaves_like 'having removable artifacts'
end
context 'when using runnners token' do
before do
delete delete_url, token: build.project.runners_token
end
it_behaves_like 'having removable artifacts'
end
end
describe 'GET /builds/:id/artifacts' do describe 'GET /builds/:id/artifacts' do
before { get get_url, token: build.token } before do
get get_url, token: token
end
context 'build has artifacts' do context 'build has artifacts' do
let(:build) { create(:ci_build, :artifacts) } let(:build) { create(:ci_build, :artifacts) }
...@@ -518,13 +566,29 @@ describe Ci::API::API do ...@@ -518,13 +566,29 @@ describe Ci::API::API do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end end
it 'downloads artifact' do shared_examples 'having downloadable artifacts' do
it 'download artifacts' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.headers).to include download_headers expect(response.headers).to include download_headers
end end
end end
context 'when using build token' do
let(:token) { build.token }
it_behaves_like 'having downloadable artifacts'
end
context 'when using runnners token' do
let(:token) { build.project.runners_token }
it_behaves_like 'having downloadable artifacts'
end
end
context 'build does not has artifacts' do context 'build does not has artifacts' do
let(:token) { build.token }
it 'responds with not found' do it 'responds with not found' do
expect(response).to have_http_status(404) expect(response).to have_http_status(404)
end end
......
...@@ -300,26 +300,80 @@ describe 'Git HTTP requests', lib: true do ...@@ -300,26 +300,80 @@ describe 'Git HTTP requests', lib: true do
end end
context "when a gitlab ci token is provided" do context "when a gitlab ci token is provided" do
let(:token) { 123 } let(:build) { create(:ci_build, :running) }
let(:project) { FactoryGirl.create :empty_project } let(:project) { build.project }
let(:other_project) { create(:empty_project) }
before do before do
project.update_attributes(runners_token: token)
project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED) project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
end end
context 'when build created by system is authenticated' do
it "downloads get status 200" do it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end end
it "uploads get status 401 (no project existence information leak)" do it "uploads get status 401 (no project existence information leak)" do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
it "downloads from other project get status 404" do
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(404)
end
end
context 'and build created by' do
before do
build.update(user: user)
project.team << [user, :reporter]
end
shared_examples 'can download code only from own projects' do
it 'downloads get status 200' do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it 'uploads get status 403' do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(401)
end
end
context 'administrator' do
let(:user) { create(:admin) }
it_behaves_like 'can download code only from own projects'
it 'downloads from other project get status 403' do
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(403)
end
end
context 'regular user' do
let(:user) { create(:user) }
it_behaves_like 'can download code only from own projects'
it 'downloads from other project get status 404' do
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(404)
end
end
end
end end
end end
end end
......
...@@ -22,11 +22,13 @@ describe JwtController do ...@@ -22,11 +22,13 @@ describe JwtController do
context 'when using authorized request' do context 'when using authorized request' do
context 'using CI token' do context 'using CI token' do
let(:project) { create(:empty_project, runners_token: 'token') } let(:build) { create(:ci_build, :running) }
let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } } let(:project) { build.project }
let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
context 'project with enabled CI' do context 'project with enabled CI' do
subject! { get '/jwt/auth', parameters, headers } subject! { get '/jwt/auth', parameters, headers }
it { expect(service_class).to have_received(:new).with(project, nil, parameters) } it { expect(service_class).to have_received(:new).with(project, nil, parameters) }
end end
...@@ -43,13 +45,31 @@ describe JwtController do ...@@ -43,13 +45,31 @@ describe JwtController do
context 'using User login' do context 'using User login' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:headers) { { authorization: credentials('user', 'password') } } let(:headers) { { authorization: credentials(user.username, user.password) } }
before { expect(Gitlab::Auth).to receive(:find_with_user_password).with('user', 'password').and_return(user) }
subject! { get '/jwt/auth', parameters, headers } subject! { get '/jwt/auth', parameters, headers }
it { expect(service_class).to have_received(:new).with(nil, user, parameters) } it { expect(service_class).to have_received(:new).with(nil, user, parameters) }
context 'when user has 2FA enabled' do
let(:user) { create(:user, :two_factor) }
context 'without personal token' do
it 'rejects the authorization attempt' do
expect(response).to have_http_status(401)
expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
end
end
context 'with personal token' do
let(:access_token) { create(:personal_access_token, user: user) }
let(:headers) { { authorization: credentials(user.username, access_token.token) } }
it 'rejects the authorization attempt' do
expect(response).to have_http_status(200)
end
end
end
end end
context 'using invalid login' do context 'using invalid login' do
......
...@@ -14,6 +14,7 @@ describe 'Git LFS API and storage' do ...@@ -14,6 +14,7 @@ describe 'Git LFS API and storage' do
end end
let(:authorization) { } let(:authorization) { }
let(:sendfile) { } let(:sendfile) { }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:sample_oid) { lfs_object.oid } let(:sample_oid) { lfs_object.oid }
let(:sample_size) { lfs_object.size } let(:sample_size) { lfs_object.size }
...@@ -244,15 +245,64 @@ describe 'Git LFS API and storage' do ...@@ -244,15 +245,64 @@ describe 'Git LFS API and storage' do
end end
end end
context 'when CI is authorized' do context 'when build is authorized as' do
let(:authorization) { authorize_ci_project } let(:authorization) { authorize_ci_project }
shared_examples 'can download LFS only from own projects' do
context 'for own project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:update_permissions) do let(:update_permissions) do
project.team << [user, :reporter]
project.lfs_objects << lfs_object project.lfs_objects << lfs_object
end end
it_behaves_like 'responds with a file' it_behaves_like 'responds with a file'
end end
context 'for other project' do
let(:other_project) { create(:empty_project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:update_permissions) do
project.lfs_objects << lfs_object
end
it 'rejects downloading code' do
expect(response).to have_http_status(other_project_status)
end
end
end
context 'administrator' do
let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
# We render 403, because administrator does have normally access
let(:other_project_status) { 403 }
end
end
context 'regular user' do
let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it_behaves_like 'can download LFS only from own projects' do
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
end
end
end end
context 'without required headers' do context 'without required headers' do
...@@ -431,12 +481,64 @@ describe 'Git LFS API and storage' do ...@@ -431,12 +481,64 @@ describe 'Git LFS API and storage' do
end end
end end
context 'when CI is authorized' do context 'when build is authorized as' do
let(:authorization) { authorize_ci_project } let(:authorization) { authorize_ci_project }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
shared_examples 'can download LFS only from own projects' do
context 'for own project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:update_user_permissions) do
project.team << [user, :reporter]
end
it_behaves_like 'an authorized requests' it_behaves_like 'an authorized requests'
end end
context 'for other project' do
let(:other_project) { create(:empty_project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it 'rejects downloading code' do
expect(response).to have_http_status(other_project_status)
end
end
end
context 'administrator' do
let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
# We render 403, because administrator does have normally access
let(:other_project_status) { 403 }
end
end
context 'regular user' do
let(:user) { create(:user) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it_behaves_like 'can download LFS only from own projects' do
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
end
end
context 'when user is not authenticated' do context 'when user is not authenticated' do
describe 'is accessing public project' do describe 'is accessing public project' do
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
...@@ -583,14 +685,40 @@ describe 'Git LFS API and storage' do ...@@ -583,14 +685,40 @@ describe 'Git LFS API and storage' do
end end
end end
context 'when CI is authorized' do context 'when build is authorized' do
let(:authorization) { authorize_ci_project } let(:authorization) { authorize_ci_project }
context 'build has an user' do
let(:user) { create(:user) }
context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
context 'tries to push to other project' do
let(:other_project) { create(:empty_project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 401' do it 'responds with 401' do
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
end end
end end
end
context 'when user is not authenticated' do context 'when user is not authenticated' do
context 'when user has push access' do context 'when user has push access' do
...@@ -609,14 +737,6 @@ describe 'Git LFS API and storage' do ...@@ -609,14 +737,6 @@ describe 'Git LFS API and storage' do
end end
end end
end end
context 'when CI is authorized' do
let(:authorization) { authorize_ci_project }
it 'responds with status 403' do
expect(response).to have_http_status(401)
end
end
end end
describe 'unsupported' do describe 'unsupported' do
...@@ -779,10 +899,51 @@ describe 'Git LFS API and storage' do ...@@ -779,10 +899,51 @@ describe 'Git LFS API and storage' do
end end
end end
context 'when CI is authenticated' do context 'when build is authorized' do
let(:authorization) { authorize_ci_project } let(:authorization) { authorize_ci_project }
it_behaves_like 'unauthorized' context 'build has an user' do
let(:user) { create(:user) }
context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
before do
project.team << [user, :developer]
put_authorize
end
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
context 'tries to push to other project' do
let(:other_project) { create(:empty_project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
before do
put_authorize
end
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
before do
put_authorize
end
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
end end
context 'for unauthenticated' do context 'for unauthenticated' do
...@@ -839,10 +1000,42 @@ describe 'Git LFS API and storage' do ...@@ -839,10 +1000,42 @@ describe 'Git LFS API and storage' do
end end
end end
context 'when CI is authenticated' do context 'when build is authorized' do
let(:authorization) { authorize_ci_project } let(:authorization) { authorize_ci_project }
it_behaves_like 'unauthorized' before do
put_authorize
end
context 'build has an user' do
let(:user) { create(:user) }
context 'tries to push to own project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
context 'tries to push to other project' do
let(:other_project) { create(:empty_project) }
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'responds with 401' do
expect(response).to have_http_status(401)
end
end
end end
context 'for unauthenticated' do context 'for unauthenticated' do
...@@ -897,7 +1090,7 @@ describe 'Git LFS API and storage' do ...@@ -897,7 +1090,7 @@ describe 'Git LFS API and storage' do
end end
def authorize_ci_project def authorize_ci_project
ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', project.runners_token) ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', build.token)
end end
def authorize_user def authorize_user
......
...@@ -6,8 +6,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -6,8 +6,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
let(:current_params) { {} } let(:current_params) { {} }
let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
let(:payload) { JWT.decode(subject[:token], rsa_key).first } let(:payload) { JWT.decode(subject[:token], rsa_key).first }
let(:authentication_abilities) do
[
:read_container_image,
:create_container_image
]
end
subject { described_class.new(current_project, current_user, current_params).execute } subject { described_class.new(current_project, current_user, current_params).execute(authentication_abilities: authentication_abilities) }
before do before do
allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil) allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
...@@ -189,13 +195,22 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -189,13 +195,22 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end end
end end
context 'project authorization' do context 'build authorized as user' do
let(:current_project) { create(:empty_project) } let(:current_project) { create(:empty_project) }
let(:current_user) { create(:user) }
let(:authentication_abilities) do
[
:build_read_container_image,
:build_create_container_image
]
end
context 'allow to use scope-less authentication' do before do
it_behaves_like 'a valid token' current_project.team << [current_user, :developer]
end end
it_behaves_like 'a valid token'
context 'allow to pull and push images' do context 'allow to pull and push images' do
let(:current_params) do let(:current_params) do
{ scope: "repository:#{current_project.path_with_namespace}:pull,push" } { scope: "repository:#{current_project.path_with_namespace}:pull,push" }
...@@ -214,13 +229,45 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -214,13 +229,45 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'allow for public' do context 'allow for public' do
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
it_behaves_like 'a pullable'
end
shared_examples 'pullable for being team member' do
context 'when you are not member' do
it_behaves_like 'an inaccessible'
end
context 'when you are member' do
before do
project.team << [current_user, :developer]
end
it_behaves_like 'a pullable' it_behaves_like 'a pullable'
end end
end
context 'disallow for private' do context 'for private' do
let(:project) { create(:empty_project, :private) } let(:project) { create(:empty_project, :private) }
it_behaves_like 'pullable for being team member'
context 'when you are admin' do
let(:current_user) { create(:admin) }
context 'when you are not member' do
it_behaves_like 'an inaccessible' it_behaves_like 'an inaccessible'
end end
context 'when you are member' do
before do
project.team << [current_user, :developer]
end
it_behaves_like 'a pullable'
end
end
end
end end
context 'when pushing' do context 'when pushing' do
...@@ -230,6 +277,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -230,6 +277,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'disallow for all' do context 'disallow for all' do
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
before do
project.team << [current_user, :developer]
end
it_behaves_like 'an inaccessible' it_behaves_like 'an inaccessible'
end end
end end
......
...@@ -41,7 +41,7 @@ describe CreateDeploymentService, services: true do ...@@ -41,7 +41,7 @@ describe CreateDeploymentService, services: true do
context 'for environment with invalid name' do context 'for environment with invalid name' do
let(:params) do let(:params) do
{ environment: 'name with spaces', { environment: 'name,with,commas',
ref: 'master', ref: 'master',
tag: false, tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142', sha: '97de212e80737a608d939f648d959671fb0a0142',
...@@ -56,6 +56,34 @@ describe CreateDeploymentService, services: true do ...@@ -56,6 +56,34 @@ describe CreateDeploymentService, services: true do
expect(subject).not_to be_persisted expect(subject).not_to be_persisted
end end
end end
context 'when variables are used' do
let(:params) do
{ environment: 'review-apps/$CI_BUILD_REF_NAME',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
options: {
name: 'review-apps/$CI_BUILD_REF_NAME',
url: 'http://$CI_BUILD_REF_NAME.review-apps.gitlab.com'
},
variables: [
{ key: 'CI_BUILD_REF_NAME', value: 'feature-review-apps' }
]
}
end
it 'does create a new environment' do
expect { subject }.to change { Environment.count }.by(1)
expect(subject.environment.name).to eq('review-apps/feature-review-apps')
expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com')
end
it 'does create a new deployment' do
expect(subject).to be_persisted
end
end
end end
describe 'processing of builds' do describe 'processing of builds' do
...@@ -95,6 +123,12 @@ describe CreateDeploymentService, services: true do ...@@ -95,6 +123,12 @@ describe CreateDeploymentService, services: true do
expect(Deployment.last.deployable).to eq(deployable) expect(Deployment.last.deployable).to eq(deployable)
end end
it 'create environment has URL set' do
subject
expect(Deployment.last.environment.external_url).not_to be_nil
end
end end
context 'without environment specified' do context 'without environment specified' do
...@@ -107,7 +141,10 @@ describe CreateDeploymentService, services: true do ...@@ -107,7 +141,10 @@ describe CreateDeploymentService, services: true do
context 'when environment is specified' do context 'when environment is specified' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production') } let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) }
let(:options) do
{ environment: { name: 'production', url: 'http://gitlab.com' } }
end
context 'when build succeeds' do context 'when build succeeds' do
it_behaves_like 'does create environment and deployment' do it_behaves_like 'does create environment and deployment' do
......
...@@ -253,6 +253,21 @@ describe GitPushService, services: true do ...@@ -253,6 +253,21 @@ describe GitPushService, services: true do
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end end
it "when pushing a branch for the first time with an existing branch permission configured" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
create(:protected_branch, :no_one_can_push, :developers_can_merge, project: project, name: 'master')
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
expect_any_instance_of(ProtectedBranches::CreateService).not_to receive(:execute)
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
expect(project.protected_branches).not_to be_empty
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::NO_ACCESS])
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do
stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
......
...@@ -4,6 +4,10 @@ describe Projects::HousekeepingService do ...@@ -4,6 +4,10 @@ describe Projects::HousekeepingService do
subject { Projects::HousekeepingService.new(project) } subject { Projects::HousekeepingService.new(project) }
let(:project) { create :project } let(:project) { create :project }
before do
project.reset_pushes_since_gc
end
after do after do
project.reset_pushes_since_gc project.reset_pushes_since_gc
end end
......
require 'spec_helper'
describe ProtectedBranches::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { project.owner }
let(:params) do
{
name: 'master',
merge_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ],
push_access_levels_attributes: [ { access_level: Gitlab::Access::MASTER } ]
}
end
describe '#execute' do
subject(:service) { described_class.new(project, user, params) }
it 'creates a new protected branch' do
expect { service.execute }.to change(ProtectedBranch, :count).by(1)
expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
end
end
...@@ -9,11 +9,13 @@ describe 'projects/pipelines/show' do ...@@ -9,11 +9,13 @@ describe 'projects/pipelines/show' do
before do before do
controller.prepend_view_path('app/views/projects') controller.prepend_view_path('app/views/projects')
create_build('build', 0, 'build') create_build('build', 0, 'build', :success)
create_build('test', 1, 'rspec 0:2') create_build('test', 1, 'rspec 0:2', :pending)
create_build('test', 1, 'rspec 1:2') create_build('test', 1, 'rspec 1:2', :running)
create_build('test', 1, 'audit') create_build('test', 1, 'spinach 0:2', :created)
create_build('deploy', 2, 'production') create_build('test', 1, 'spinach 1:2', :created)
create_build('test', 1, 'audit', :created)
create_build('deploy', 2, 'production', :created)
create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3) create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
...@@ -37,6 +39,7 @@ describe 'projects/pipelines/show' do ...@@ -37,6 +39,7 @@ describe 'projects/pipelines/show' do
# builds # builds
expect(rendered).to have_text('rspec') expect(rendered).to have_text('rspec')
expect(rendered).to have_text('spinach')
expect(rendered).to have_text('rspec 0:2') expect(rendered).to have_text('rspec 0:2')
expect(rendered).to have_text('production') expect(rendered).to have_text('production')
expect(rendered).to have_text('jenkins') expect(rendered).to have_text('jenkins')
...@@ -44,7 +47,7 @@ describe 'projects/pipelines/show' do ...@@ -44,7 +47,7 @@ describe 'projects/pipelines/show' do
private private
def create_build(stage, stage_idx, name) def create_build(stage, stage_idx, name, status)
create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name) create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
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