Merge branch 'per-build-token-without-lfs' into 'master'
Make CI to use the permission of the user who is trigger the build This is continuation of https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5735, but with removed all LFS code that is added by: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6043. This also incorporates most of LFS code added in !6043 to simplify further merge. See merge request !6409
This commit is contained in:
commit
fe084819b4
|
@ -22,6 +22,7 @@ v 8.12.0 (unreleased)
|
|||
- Instructions for enabling Git packfile bitmaps !6104
|
||||
- Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint
|
||||
- Fix pagination on user snippets page
|
||||
- Run CI builds with the permissions of users !5735
|
||||
- Fix sorting of issues in API
|
||||
- Sort project variables by key. !6275 (Diego Souza)
|
||||
- Ensure specs on sorting of issues in API are deterministic on MySQL
|
||||
|
|
|
@ -11,7 +11,10 @@ class JwtController < ApplicationController
|
|||
service = SERVICES[params[: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]
|
||||
end
|
||||
|
@ -20,30 +23,23 @@ class JwtController < ApplicationController
|
|||
|
||||
def authenticate_project_or_user
|
||||
authenticate_with_http_basic do |login, password|
|
||||
# if it's possible we first try to authenticate project with login and password
|
||||
@project = authenticate_project(login, password)
|
||||
return if @project
|
||||
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
|
||||
|
||||
@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
|
||||
rescue Gitlab::Auth::MissingPersonalTokenError
|
||||
render_missing_personal_token
|
||||
end
|
||||
|
||||
def render_missing_personal_token
|
||||
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
|
||||
|
||||
def auth_params
|
||||
params.permit(:service, :scope, :account, :client_id)
|
||||
end
|
||||
|
||||
def authenticate_project(login, password)
|
||||
if login == 'gitlab-ci-token'
|
||||
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
|
||||
|
|
|
@ -35,7 +35,11 @@ class Projects::BuildsController < Projects::ApplicationController
|
|||
respond_to do |format|
|
||||
format.html
|
||||
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
|
||||
|
|
|
@ -4,7 +4,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
include ActionController::HttpAuthentication::Basic
|
||||
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
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
@ -15,32 +19,25 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
private
|
||||
|
||||
def authenticate_user
|
||||
@authentication_result = Gitlab::Auth::Result.new
|
||||
|
||||
if project && project.public? && download_request?
|
||||
return # Allow access
|
||||
end
|
||||
|
||||
if allow_basic_auth? && basic_auth_provided?
|
||||
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
|
||||
end
|
||||
elsif allow_kerberos_spnego_auth? && spnego_provided?
|
||||
@user = find_kerberos_user
|
||||
user = find_kerberos_user
|
||||
|
||||
if user
|
||||
@authentication_result = Gitlab::Auth::Result.new(
|
||||
user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
|
||||
|
||||
send_final_spnego_response
|
||||
return # Allow access
|
||||
end
|
||||
|
@ -48,6 +45,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
|
||||
send_challenges
|
||||
render plain: "HTTP Basic: Access denied\n", status: 401
|
||||
rescue Gitlab::Auth::MissingPersonalTokenError
|
||||
render_missing_personal_token
|
||||
end
|
||||
|
||||
def basic_auth_provided?
|
||||
|
@ -114,8 +113,39 @@ class Projects::GitHttpClientController < Projects::ApplicationController
|
|||
render plain: 'Not Found', status: :not_found
|
||||
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?
|
||||
@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
|
||||
|
||||
def verify_workhorse_api!
|
||||
|
|
|
@ -86,7 +86,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
|
|||
end
|
||||
|
||||
def access
|
||||
@access ||= Gitlab::GitAccess.new(user, project, 'http')
|
||||
@access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities)
|
||||
end
|
||||
|
||||
def access_check
|
||||
|
|
|
@ -25,13 +25,21 @@ module LfsHelper
|
|||
def lfs_download_access?
|
||||
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
|
||||
|
||||
def lfs_upload_access?
|
||||
return false unless project.lfs_enabled?
|
||||
|
||||
user && user.can?(:push_code, project)
|
||||
has_authentication_ability?(:push_code) && can?(user, :push_code, project)
|
||||
end
|
||||
|
||||
def render_lfs_forbidden
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
module Ci
|
||||
class Build < CommitStatus
|
||||
include TokenAuthenticatable
|
||||
|
||||
belongs_to :runner, class_name: 'Ci::Runner'
|
||||
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
|
||||
belongs_to :erased_by, class_name: 'User'
|
||||
|
@ -23,7 +25,10 @@ module Ci
|
|||
|
||||
acts_as_taggable
|
||||
|
||||
add_authentication_token_field :token
|
||||
|
||||
before_save :update_artifacts_size, if: :artifacts_file_changed?
|
||||
before_save :ensure_token
|
||||
before_destroy { project }
|
||||
|
||||
after_create :execute_hooks
|
||||
|
@ -38,6 +43,7 @@ module Ci
|
|||
new_build.status = 'pending'
|
||||
new_build.runner_id = nil
|
||||
new_build.trigger_request_id = nil
|
||||
new_build.token = nil
|
||||
new_build.save
|
||||
end
|
||||
|
||||
|
@ -176,7 +182,7 @@ module Ci
|
|||
end
|
||||
|
||||
def repo_url
|
||||
auth = "gitlab-ci-token:#{token}@"
|
||||
auth = "gitlab-ci-token:#{ensure_token!}@"
|
||||
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
|
||||
prefix + auth
|
||||
end
|
||||
|
@ -238,12 +244,7 @@ module Ci
|
|||
end
|
||||
|
||||
def trace
|
||||
trace = raw_trace
|
||||
if project && trace.present? && project.runners_token.present?
|
||||
trace.gsub(project.runners_token, 'xxxxxx')
|
||||
else
|
||||
trace
|
||||
end
|
||||
hide_secrets(raw_trace)
|
||||
end
|
||||
|
||||
def trace_length
|
||||
|
@ -256,6 +257,7 @@ module Ci
|
|||
|
||||
def trace=(trace)
|
||||
recreate_trace_dir
|
||||
trace = hide_secrets(trace)
|
||||
File.write(path_to_trace, trace)
|
||||
end
|
||||
|
||||
|
@ -269,6 +271,8 @@ module Ci
|
|||
def append_trace(trace_part, offset)
|
||||
recreate_trace_dir
|
||||
|
||||
trace_part = hide_secrets(trace_part)
|
||||
|
||||
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
|
||||
File.open(path_to_trace, 'ab') do |f|
|
||||
f.write(trace_part)
|
||||
|
@ -344,12 +348,8 @@ module Ci
|
|||
)
|
||||
end
|
||||
|
||||
def token
|
||||
project.runners_token
|
||||
end
|
||||
|
||||
def valid_token?(token)
|
||||
project.valid_runners_token?(token)
|
||||
self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
|
||||
end
|
||||
|
||||
def has_tags?
|
||||
|
@ -491,5 +491,11 @@ module Ci
|
|||
|
||||
pipeline.config_processor.build_attributes(name)
|
||||
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
|
||||
|
|
|
@ -1137,12 +1137,6 @@ class Project < ActiveRecord::Base
|
|||
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
|
||||
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?
|
||||
build_coverage_regex.present?
|
||||
end
|
||||
|
|
|
@ -64,6 +64,12 @@ class ProjectPolicy < BasePolicy
|
|||
can! :read_deployment
|
||||
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!
|
||||
can! :admin_merge_request
|
||||
can! :update_merge_request
|
||||
|
@ -109,6 +115,8 @@ class ProjectPolicy < BasePolicy
|
|||
can! :read_commit_status
|
||||
can! :read_pipeline
|
||||
can! :read_container_image
|
||||
can! :build_download_code
|
||||
can! :build_read_container_image
|
||||
end
|
||||
|
||||
def owner_access!
|
||||
|
@ -130,10 +138,11 @@ class ProjectPolicy < BasePolicy
|
|||
def team_access!(user)
|
||||
access = project.team.max_member_access(user.id)
|
||||
|
||||
guest_access! if access >= Gitlab::Access::GUEST
|
||||
reporter_access! if access >= Gitlab::Access::REPORTER
|
||||
developer_access! if access >= Gitlab::Access::DEVELOPER
|
||||
master_access! if access >= Gitlab::Access::MASTER
|
||||
guest_access! if access >= Gitlab::Access::GUEST
|
||||
reporter_access! if access >= Gitlab::Access::REPORTER
|
||||
team_member_reporter_access! if access >= Gitlab::Access::REPORTER
|
||||
developer_access! if access >= Gitlab::Access::DEVELOPER
|
||||
master_access! if access >= Gitlab::Access::MASTER
|
||||
end
|
||||
|
||||
def archived_access!
|
||||
|
|
|
@ -4,7 +4,9 @@ module Auth
|
|||
|
||||
AUDIENCE = 'container_registry'
|
||||
|
||||
def execute
|
||||
def execute(authentication_abilities:)
|
||||
@authentication_abilities = authentication_abilities || []
|
||||
|
||||
return error('not found', 404) unless registry.enabled
|
||||
|
||||
unless current_user || project
|
||||
|
@ -74,9 +76,9 @@ module Auth
|
|||
|
||||
case requested_action
|
||||
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'
|
||||
requested_project == project || can?(current_user, :create_container_image, requested_project)
|
||||
build_can_push?(requested_project) || user_can_push?(requested_project)
|
||||
else
|
||||
false
|
||||
end
|
||||
|
@ -85,5 +87,29 @@ module Auth
|
|||
def registry
|
||||
Gitlab.config.registry
|
||||
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
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
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
|
|
@ -0,0 +1,12 @@
|
|||
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
|
|
@ -181,6 +181,7 @@ ActiveRecord::Schema.define(version: 20160913212128) do
|
|||
t.string "when"
|
||||
t.text "yaml_variables"
|
||||
t.datetime "queued_at"
|
||||
t.string "token"
|
||||
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
|
||||
|
@ -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", ["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", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
|
||||
|
||||
create_table "ci_commits", force: :cascade do |t|
|
||||
t.integer "project_id"
|
||||
|
|
|
@ -35,6 +35,14 @@ module API
|
|||
Project.find_with_namespace(project_path)
|
||||
end
|
||||
end
|
||||
|
||||
def ssh_authentication_abilities
|
||||
[
|
||||
:read_project,
|
||||
:download_code,
|
||||
:push_code
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
post "/allowed" do
|
||||
|
@ -51,9 +59,9 @@ module API
|
|||
|
||||
access =
|
||||
if wiki?
|
||||
Gitlab::GitAccessWiki.new(actor, project, protocol)
|
||||
Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
|
||||
else
|
||||
Gitlab::GitAccess.new(actor, project, protocol)
|
||||
Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
|
||||
end
|
||||
|
||||
access_status = access.check(params[:action], params[:changes])
|
||||
|
|
|
@ -14,12 +14,20 @@ module Ci
|
|||
end
|
||||
|
||||
def authenticate_build_token!(build)
|
||||
token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
|
||||
forbidden! unless token && build.valid_token?(token)
|
||||
forbidden! unless build_token_valid?(build)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def update_runner_last_contact(save: true)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
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
|
|
@ -1,21 +1,21 @@
|
|||
module Gitlab
|
||||
module Auth
|
||||
Result = Struct.new(:user, :type)
|
||||
class MissingPersonalTokenError < StandardError; end
|
||||
|
||||
class << self
|
||||
def find_for_git_client(login, password, project:, ip:)
|
||||
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)
|
||||
result.type = :ci
|
||||
else
|
||||
result = populate_result(login, password)
|
||||
end
|
||||
rate_limit!(ip, success: result.success?, login: login)
|
||||
|
||||
success = result.user.present? || [:ci, :missing_personal_token].include?(result.type)
|
||||
rate_limit!(ip, success: success, login: login)
|
||||
result
|
||||
end
|
||||
|
||||
|
@ -57,44 +57,31 @@ module Gitlab
|
|||
|
||||
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)
|
||||
|
||||
return false unless project && matched_login.present?
|
||||
return unless project && matched_login.present?
|
||||
|
||||
underscored_service = matched_login['service'].underscore
|
||||
|
||||
if underscored_service == 'gitlab_ci'
|
||||
project && project.valid_build_token?(password)
|
||||
elsif Service.available_services_names.include?(underscored_service)
|
||||
if Service.available_services_names.include?(underscored_service)
|
||||
# We treat underscored_service as a trusted input because it is included
|
||||
# in the Service.available_services_names whitelist.
|
||||
service = project.public_send("#{underscored_service}_service")
|
||||
|
||||
service && service.activated? && service.valid_token?(password)
|
||||
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
|
||||
if service && service.activated? && service.valid_token?(password)
|
||||
Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)
|
||||
end
|
||||
end
|
||||
|
||||
result || Result.new
|
||||
end
|
||||
|
||||
def user_with_password_for_git(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
|
||||
|
||||
def oauth_access_token_check(login, password)
|
||||
|
@ -102,7 +89,7 @@ module Gitlab
|
|||
token = Doorkeeper::AccessToken.by_token(password)
|
||||
if token && token.accessible?
|
||||
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
|
||||
|
@ -111,9 +98,52 @@ module Gitlab
|
|||
if login && password
|
||||
user = User.find_by_personal_access_token(password)
|
||||
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
|
||||
|
||||
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
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
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
|
|
@ -5,12 +5,13 @@ module Gitlab
|
|||
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
|
||||
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
|
||||
@project = project
|
||||
@protocol = protocol
|
||||
@authentication_abilities = authentication_abilities
|
||||
@user_access = UserAccess.new(user, project: project)
|
||||
end
|
||||
|
||||
|
@ -60,14 +61,26 @@ module Gitlab
|
|||
end
|
||||
|
||||
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.")
|
||||
end
|
||||
|
||||
build_status_object(true)
|
||||
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)
|
||||
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?
|
||||
return build_status_object(true)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
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
|
|
@ -4,15 +4,53 @@ describe Gitlab::Auth, lib: true do
|
|||
let(:gl_auth) { described_class }
|
||||
|
||||
describe 'find_for_git_client' do
|
||||
it 'recognizes CI' do
|
||||
token = '123'
|
||||
context 'build token' do
|
||||
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.update_attributes(runners_token: token)
|
||||
project.create_drone_ci_service(active: true)
|
||||
project.drone_ci_service.update(token: 'token')
|
||||
|
||||
ip = 'ip'
|
||||
|
||||
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-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).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
|
||||
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
|
||||
|
||||
it 'recognizes master passwords' do
|
||||
|
@ -20,7 +58,7 @@ describe Gitlab::Auth, lib: true do
|
|||
ip = 'ip'
|
||||
|
||||
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
|
||||
|
||||
it 'recognizes OAuth tokens' do
|
||||
|
@ -30,7 +68,7 @@ describe Gitlab::Auth, lib: true do
|
|||
ip = 'ip'
|
||||
|
||||
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
|
||||
|
||||
it 'returns double nil for invalid credentials' do
|
||||
|
@ -92,4 +130,30 @@ describe Gitlab::Auth, lib: true do
|
|||
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
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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(:user) { create(:user) }
|
||||
let(:actor) { user }
|
||||
let(:authentication_abilities) do
|
||||
[
|
||||
:read_project,
|
||||
:download_code,
|
||||
:push_code
|
||||
]
|
||||
end
|
||||
|
||||
describe '#check with single protocols allowed' do
|
||||
def disable_protocol(protocol)
|
||||
|
@ -15,7 +22,7 @@ describe Gitlab::GitAccess, lib: true do
|
|||
context 'ssh disabled' do
|
||||
before do
|
||||
disable_protocol('ssh')
|
||||
@acc = Gitlab::GitAccess.new(actor, project, 'ssh')
|
||||
@acc = Gitlab::GitAccess.new(actor, project, 'ssh', authentication_abilities: authentication_abilities)
|
||||
end
|
||||
|
||||
it 'blocks ssh git push' do
|
||||
|
@ -30,7 +37,7 @@ describe Gitlab::GitAccess, lib: true do
|
|||
context 'http disabled' do
|
||||
before do
|
||||
disable_protocol('http')
|
||||
@acc = Gitlab::GitAccess.new(actor, project, 'http')
|
||||
@acc = Gitlab::GitAccess.new(actor, project, 'http', authentication_abilities: authentication_abilities)
|
||||
end
|
||||
|
||||
it 'blocks http push' do
|
||||
|
@ -111,6 +118,36 @@ describe Gitlab::GitAccess, lib: true do
|
|||
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
|
||||
|
||||
describe 'push_access_check' do
|
||||
|
@ -283,38 +320,71 @@ describe Gitlab::GitAccess, lib: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'deploy key permissions' do
|
||||
let(:key) { create(:deploy_key) }
|
||||
let(:actor) { key }
|
||||
shared_examples 'can not push code' do
|
||||
subject { access.check('git-receive-pack', '_any') }
|
||||
|
||||
context 'push code' do
|
||||
subject { access.check('git-receive-pack', '_any') }
|
||||
context 'when project is authorized' do
|
||||
before { authorize }
|
||||
|
||||
context 'when project is authorized' do
|
||||
before { key.projects << project }
|
||||
it { expect(subject).not_to be_allowed }
|
||||
end
|
||||
|
||||
context 'when unauthorized' do
|
||||
context 'to public project' do
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
it { expect(subject).not_to be_allowed }
|
||||
end
|
||||
|
||||
context 'when unauthorized' do
|
||||
context 'to public project' do
|
||||
let(:project) { create(:project, :public) }
|
||||
context 'to internal project' do
|
||||
let(:project) { create(:project, :internal) }
|
||||
|
||||
it { expect(subject).not_to be_allowed }
|
||||
end
|
||||
it { expect(subject).not_to be_allowed }
|
||||
end
|
||||
|
||||
context 'to internal project' do
|
||||
let(:project) { create(:project, :internal) }
|
||||
context 'to private project' do
|
||||
let(:project) { create(:project, :internal) }
|
||||
|
||||
it { expect(subject).not_to be_allowed }
|
||||
end
|
||||
|
||||
context 'to private project' do
|
||||
let(:project) { create(:project, :internal) }
|
||||
|
||||
it { expect(subject).not_to be_allowed }
|
||||
end
|
||||
it { expect(subject).not_to be_allowed }
|
||||
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
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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(:user) { create(:user) }
|
||||
let(:authentication_abilities) do
|
||||
[
|
||||
:read_project,
|
||||
:download_code,
|
||||
:push_code
|
||||
]
|
||||
end
|
||||
|
||||
describe 'push_allowed?' do
|
||||
before do
|
||||
|
|
|
@ -88,9 +88,7 @@ describe Ci::Build, models: true do
|
|||
end
|
||||
|
||||
describe '#trace' do
|
||||
subject { build.trace_html }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
it { expect(build.trace).to be_nil }
|
||||
|
||||
context 'when build.trace contains text' do
|
||||
let(:text) { 'example output' }
|
||||
|
@ -98,16 +96,80 @@ describe Ci::Build, models: true do
|
|||
build.trace = text
|
||||
end
|
||||
|
||||
it { is_expected.to include(text) }
|
||||
it { expect(subject.length).to be >= text.length }
|
||||
it { expect(build.trace).to eq(text) }
|
||||
end
|
||||
|
||||
context 'when build.trace hides token' do
|
||||
context 'when build.trace hides runners token' do
|
||||
let(:token) { 'my_secret_token' }
|
||||
|
||||
before do
|
||||
build.project.update_attributes(runners_token: token)
|
||||
build.update_attributes(trace: token)
|
||||
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
|
||||
|
||||
context 'when build.trace hides build token' do
|
||||
let(:token) { 'my_secret_token' }
|
||||
|
||||
before do
|
||||
build.update(token: token)
|
||||
build.append_trace(token, 0)
|
||||
end
|
||||
|
||||
it { is_expected.not_to include(token) }
|
||||
|
|
|
@ -8,7 +8,7 @@ describe Ci::Build, models: true do
|
|||
it 'obfuscates project runners token' do
|
||||
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
|
||||
|
||||
it 'empty project runners token' do
|
||||
|
|
|
@ -254,7 +254,8 @@ describe Ci::API::API do
|
|||
let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
|
||||
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_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! }
|
||||
|
||||
|
@ -262,6 +263,7 @@ describe Ci::API::API do
|
|||
context "should authorize posting artifact to running build" do
|
||||
it "using token as parameter" do
|
||||
post authorize_url, { token: build.token }, headers
|
||||
|
||||
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
|
||||
|
@ -269,6 +271,15 @@ describe Ci::API::API do
|
|||
|
||||
it "using token as header" do
|
||||
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.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
||||
expect(json_response["TempPath"]).not_to be_nil
|
||||
|
@ -276,7 +287,9 @@ describe Ci::API::API do
|
|||
|
||||
it "reject requests that did not go through gitlab-workhorse" do
|
||||
headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
|
||||
|
||||
post authorize_url, { token: build.token }, headers
|
||||
|
||||
expect(response).to have_http_status(500)
|
||||
end
|
||||
end
|
||||
|
@ -284,13 +297,17 @@ describe Ci::API::API do
|
|||
context "should fail to post too large artifact" do
|
||||
it "using token as parameter" do
|
||||
stub_application_setting(max_artifacts_size: 0)
|
||||
|
||||
post authorize_url, { token: build.token, filesize: 100 }, headers
|
||||
|
||||
expect(response).to have_http_status(413)
|
||||
end
|
||||
|
||||
it "using token as header" do
|
||||
stub_application_setting(max_artifacts_size: 0)
|
||||
|
||||
post authorize_url, { filesize: 100 }, headers_with_token
|
||||
|
||||
expect(response).to have_http_status(413)
|
||||
end
|
||||
end
|
||||
|
@ -358,6 +375,16 @@ describe Ci::API::API do
|
|||
|
||||
it_behaves_like 'successful artifacts upload'
|
||||
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
|
||||
|
||||
context 'posts artifacts file and metadata file' do
|
||||
|
@ -497,19 +524,40 @@ describe Ci::API::API do
|
|||
|
||||
before do
|
||||
delete delete_url, token: build.token
|
||||
build.reload
|
||||
end
|
||||
|
||||
it 'removes build artifacts' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(build.artifacts_file.exists?).to be_falsy
|
||||
expect(build.artifacts_metadata.exists?).to be_falsy
|
||||
expect(build.artifacts_size).to be_nil
|
||||
shared_examples 'having removable artifacts' do
|
||||
it 'removes build artifacts' do
|
||||
build.reload
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(build.artifacts_file.exists?).to be_falsy
|
||||
expect(build.artifacts_metadata.exists?).to be_falsy
|
||||
expect(build.artifacts_size).to be_nil
|
||||
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
|
||||
before { get get_url, token: build.token }
|
||||
before do
|
||||
get get_url, token: token
|
||||
end
|
||||
|
||||
context 'build has artifacts' do
|
||||
let(:build) { create(:ci_build, :artifacts) }
|
||||
|
@ -518,13 +566,29 @@ describe Ci::API::API do
|
|||
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
|
||||
end
|
||||
|
||||
it 'downloads artifact' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.headers).to include download_headers
|
||||
shared_examples 'having downloadable artifacts' do
|
||||
it 'download artifacts' do
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.headers).to include download_headers
|
||||
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
|
||||
let(:token) { build.token }
|
||||
|
||||
it 'responds with not found' do
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
|
|
@ -300,25 +300,79 @@ describe 'Git HTTP requests', lib: true do
|
|||
end
|
||||
|
||||
context "when a gitlab ci token is provided" do
|
||||
let(:token) { 123 }
|
||||
let(:project) { FactoryGirl.create :empty_project }
|
||||
let(:build) { create(:ci_build, :running) }
|
||||
let(:project) { build.project }
|
||||
let(:other_project) { create(:empty_project) }
|
||||
|
||||
before do
|
||||
project.update_attributes(runners_token: token)
|
||||
project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
|
||||
end
|
||||
|
||||
it "downloads get status 200" do
|
||||
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
|
||||
context 'when build created by system is authenticated' 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)
|
||||
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 401 (no project existence information leak)" do
|
||||
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
|
||||
|
||||
expect(response).to have_http_status(401)
|
||||
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
|
||||
|
||||
it "uploads get status 401 (no project existence information leak)" do
|
||||
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
|
||||
context 'and build created by' do
|
||||
before do
|
||||
build.update(user: user)
|
||||
project.team << [user, :reporter]
|
||||
end
|
||||
|
||||
expect(response).to have_http_status(401)
|
||||
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
|
||||
|
|
|
@ -22,11 +22,13 @@ describe JwtController do
|
|||
|
||||
context 'when using authorized request' do
|
||||
context 'using CI token' do
|
||||
let(:project) { create(:empty_project, runners_token: 'token') }
|
||||
let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } }
|
||||
let(:build) { create(:ci_build, :running) }
|
||||
let(:project) { build.project }
|
||||
let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
|
||||
|
||||
context 'project with enabled CI' do
|
||||
subject! { get '/jwt/auth', parameters, headers }
|
||||
|
||||
it { expect(service_class).to have_received(:new).with(project, nil, parameters) }
|
||||
end
|
||||
|
||||
|
@ -43,13 +45,31 @@ describe JwtController do
|
|||
|
||||
context 'using User login' do
|
||||
let(:user) { create(:user) }
|
||||
let(:headers) { { authorization: credentials('user', 'password') } }
|
||||
|
||||
before { expect(Gitlab::Auth).to receive(:find_with_user_password).with('user', 'password').and_return(user) }
|
||||
let(:headers) { { authorization: credentials(user.username, user.password) } }
|
||||
|
||||
subject! { get '/jwt/auth', parameters, headers }
|
||||
|
||||
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
|
||||
|
||||
context 'using invalid login' do
|
||||
|
|
|
@ -14,6 +14,7 @@ describe 'Git LFS API and storage' do
|
|||
end
|
||||
let(:authorization) { }
|
||||
let(:sendfile) { }
|
||||
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
|
||||
|
||||
let(:sample_oid) { lfs_object.oid }
|
||||
let(:sample_size) { lfs_object.size }
|
||||
|
@ -244,14 +245,63 @@ describe 'Git LFS API and storage' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when CI is authorized' do
|
||||
context 'when build is authorized as' do
|
||||
let(:authorization) { authorize_ci_project }
|
||||
|
||||
let(:update_permissions) do
|
||||
project.lfs_objects << lfs_object
|
||||
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
|
||||
project.team << [user, :reporter]
|
||||
project.lfs_objects << lfs_object
|
||||
end
|
||||
|
||||
it_behaves_like 'responds with a file'
|
||||
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
|
||||
|
||||
it_behaves_like 'responds with a file'
|
||||
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
|
||||
|
||||
|
@ -431,10 +481,62 @@ describe 'Git LFS API and storage' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when CI is authorized' do
|
||||
context 'when build is authorized as' do
|
||||
let(:authorization) { authorize_ci_project }
|
||||
|
||||
it_behaves_like 'an authorized requests'
|
||||
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'
|
||||
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
|
||||
|
@ -583,11 +685,37 @@ describe 'Git LFS API and storage' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when CI is authorized' do
|
||||
context 'when build is authorized' do
|
||||
let(:authorization) { authorize_ci_project }
|
||||
|
||||
it 'responds with 401' do
|
||||
expect(response).to have_http_status(401)
|
||||
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
|
||||
|
@ -609,14 +737,6 @@ describe 'Git LFS API and storage' do
|
|||
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
|
||||
|
||||
describe 'unsupported' do
|
||||
|
@ -779,10 +899,51 @@ describe 'Git LFS API and storage' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when CI is authenticated' do
|
||||
context 'when build is authorized' do
|
||||
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
|
||||
|
||||
context 'for unauthenticated' do
|
||||
|
@ -839,10 +1000,42 @@ describe 'Git LFS API and storage' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when CI is authenticated' do
|
||||
context 'when build is authorized' do
|
||||
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
|
||||
|
||||
context 'for unauthenticated' do
|
||||
|
@ -897,7 +1090,7 @@ describe 'Git LFS API and storage' do
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def authorize_user
|
||||
|
|
|
@ -6,8 +6,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
|
|||
let(:current_params) { {} }
|
||||
let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
|
||||
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
|
||||
allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
|
||||
|
@ -189,13 +195,22 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
|
|||
end
|
||||
end
|
||||
|
||||
context 'project authorization' do
|
||||
context 'build authorized as user' do
|
||||
let(:current_project) { create(:empty_project) }
|
||||
|
||||
context 'allow to use scope-less authentication' do
|
||||
it_behaves_like 'a valid token'
|
||||
let(:current_user) { create(:user) }
|
||||
let(:authentication_abilities) do
|
||||
[
|
||||
:build_read_container_image,
|
||||
:build_create_container_image
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
current_project.team << [current_user, :developer]
|
||||
end
|
||||
|
||||
it_behaves_like 'a valid token'
|
||||
|
||||
context 'allow to pull and push images' do
|
||||
let(:current_params) do
|
||||
{ scope: "repository:#{current_project.path_with_namespace}:pull,push" }
|
||||
|
@ -214,12 +229,44 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
|
|||
|
||||
context 'allow for public' do
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
|
||||
it_behaves_like 'a pullable'
|
||||
end
|
||||
|
||||
context 'disallow for private' do
|
||||
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'
|
||||
end
|
||||
end
|
||||
|
||||
context 'for private' do
|
||||
let(:project) { create(:empty_project, :private) }
|
||||
it_behaves_like 'an inaccessible'
|
||||
|
||||
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'
|
||||
end
|
||||
|
||||
context 'when you are member' do
|
||||
before do
|
||||
project.team << [current_user, :developer]
|
||||
end
|
||||
|
||||
it_behaves_like 'a pullable'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -230,6 +277,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
|
|||
|
||||
context 'disallow for all' do
|
||||
let(:project) { create(:empty_project, :public) }
|
||||
|
||||
before do
|
||||
project.team << [current_user, :developer]
|
||||
end
|
||||
|
||||
it_behaves_like 'an inaccessible'
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue