Commit 505dc808 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Use a permissions of user to access all dependent projects from CI jobs (this...

Use a permissions of user to access all dependent projects from CI jobs (this also includes a container images, and in future LFS files)
parent 45afdbef
...@@ -11,7 +11,7 @@ class JwtController < ApplicationController ...@@ -11,7 +11,7 @@ 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 result = service.new(@project, @user, auth_params).execute(access_type: @access_type)
render json: result, status: result[:http_status] render json: result, status: result[:http_status]
end end
...@@ -21,10 +21,10 @@ class JwtController < ApplicationController ...@@ -21,10 +21,10 @@ 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 # if it's possible we first try to authenticate project with login and password
@project = authenticate_project(login, password) @project, @user, @access_type = authenticate_build(login, password)
return if @project return if @project
@user = authenticate_user(login, password) @user, @access_type = authenticate_user(login, password)
return if @user return if @user
render_403 render_403
...@@ -35,15 +35,17 @@ class JwtController < ApplicationController ...@@ -35,15 +35,17 @@ class JwtController < ApplicationController
params.permit(:service, :scope, :account, :client_id) params.permit(:service, :scope, :account, :client_id)
end end
def authenticate_project(login, password) def authenticate_build(login, password)
if login == 'gitlab-ci-token' return unless login == 'gitlab-ci-token'
Project.with_builds_enabled.find_by(runners_token: password) return unless password
end
build = Ci::Build.running.find_by(token: password)
return build.project, build.user, :restricted if build
end end
def authenticate_user(login, password) def authenticate_user(login, password)
user = Gitlab::Auth.find_with_user_password(login, password) user = Gitlab::Auth.find_with_user_password(login, password)
Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login) Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
user return user, :full
end end
end end
...@@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper include KerberosSpnegoHelper
attr_reader :user attr_reader :user, :access_type
# 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
...@@ -34,6 +34,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -34,6 +34,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@user = auth_result.user @user = auth_result.user
end end
@access_type = auth_result.access_type
if ci? || user if ci? || user
return # Allow access return # Allow access
end end
...@@ -118,6 +120,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -118,6 +120,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController
@ci.present? @ci.present?
end end
def full?
@access_type == :full
end
def restricted?
@access_type == :restricted
end
def verify_workhorse_api! def verify_workhorse_api!
Gitlab::Workhorse.verify_api_request!(request.headers) Gitlab::Workhorse.verify_api_request!(request.headers)
end end
......
...@@ -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', access_type: access_type)
end end
def access_check def access_check
......
...@@ -25,13 +25,25 @@ module LfsHelper ...@@ -25,13 +25,25 @@ 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? || privileged_user_can_download_code? || restricted_user_can_download_code?
end
def privileged_user_can_download_code?
full? && user && user.can?(:download_code, project)
end
def restricted_user_can_download_code?
restricted? && user && user.can?(:restricted_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) privileged_user_can_push_code?
end
def privileged_user_can_push_code?
full? && user && user.can?(: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
...@@ -172,7 +177,7 @@ module Ci ...@@ -172,7 +177,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
...@@ -340,12 +345,8 @@ module Ci ...@@ -340,12 +345,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?
......
...@@ -1138,12 +1138,6 @@ class Project < ActiveRecord::Base ...@@ -1138,12 +1138,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 direct member of a group
def restricted_reporter_access!
can! :restricted_download_code
can! :restricted_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
...@@ -130,10 +136,11 @@ class ProjectPolicy < BasePolicy ...@@ -130,10 +136,11 @@ class ProjectPolicy < BasePolicy
def team_access!(user) def team_access!(user)
access = project.team.max_member_access(user.id) access = project.team.max_member_access(user.id)
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
developer_access! if access >= Gitlab::Access::DEVELOPER restricted_reporter_access! if access >= Gitlab::Access::REPORTER
master_access! if access >= Gitlab::Access::MASTER developer_access! if access >= Gitlab::Access::DEVELOPER
master_access! if access >= Gitlab::Access::MASTER
end end
def archived_access! def archived_access!
......
...@@ -4,7 +4,9 @@ module Auth ...@@ -4,7 +4,9 @@ module Auth
AUDIENCE = 'container_registry' AUDIENCE = 'container_registry'
def execute def execute(access_type: access_type)
@access_type = access_type
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) restricted_user_can_pull?(requested_project) || privileged_user_can_pull?(requested_project)
when 'push' when 'push'
requested_project == project || can?(current_user, :create_container_image, requested_project) restricted_user_can_push?(requested_project) || privileged_user_can_push?(requested_project)
else else
false false
end end
...@@ -85,5 +87,37 @@ module Auth ...@@ -85,5 +87,37 @@ module Auth
def registry def registry
Gitlab.config.registry Gitlab.config.registry
end end
private
def restricted_user_can_pull?(requested_project)
return false unless restricted?
# Restricted can:
# 1. pull from it's own project (for ex. a build)
# 2. read images from dependent projects if he is a team member
requested_project == project || can?(current_user, :restricted_read_container_image, requested_project)
end
def privileged_user_can_pull?(requested_project)
full? && can?(current_user, :read_container_image, requested_project)
end
def restricted_user_can_push?(requested_project)
# Restricted can push only to project to from which he originates
restricted? && requested_project == project
end
def privileged_user_can_push?(requested_project)
full? && can?(current_user, :create_container_image, requested_project)
end
def full?
@access_type == :full
end
def restricted?
@access_type == :restricted
end
end end
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
...@@ -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?
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?
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)
......
module Gitlab module Gitlab
module Auth module Auth
Result = Struct.new(:user, :type) Result = Struct.new(:user, :type, :access_type)
class << self class << self
def find_for_git_client(login, password, project:, ip:) def find_for_git_client(login, password, project:, ip:)
...@@ -64,9 +64,7 @@ module Gitlab ...@@ -64,9 +64,7 @@ module Gitlab
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")
...@@ -77,12 +75,13 @@ module Gitlab ...@@ -77,12 +75,13 @@ module Gitlab
def populate_result(login, password) def populate_result(login, password)
result = result =
build_access_token_check(login, password) ||
user_with_password_for_git(login, password) || user_with_password_for_git(login, password) ||
oauth_access_token_check(login, password) || oauth_access_token_check(login, password) ||
personal_access_token_check(login, password) personal_access_token_check(login, password)
if result if result
result.type = nil unless result.user result.type = nil unless result.user && result.type != :ci
if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap
result.type = :missing_personal_token result.type = :missing_personal_token
...@@ -94,7 +93,7 @@ module Gitlab ...@@ -94,7 +93,7 @@ module Gitlab
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 Result.new(user, :gitlab_or_ldap, :full) if user
end end
def oauth_access_token_check(login, password) def oauth_access_token_check(login, password)
...@@ -102,7 +101,7 @@ module Gitlab ...@@ -102,7 +101,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) Result.new(user, :oauth, :full)
end end
end end
end end
...@@ -111,7 +110,23 @@ module Gitlab ...@@ -111,7 +110,23 @@ 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 Result.new(user, :personal_token, :full) if 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
if build.user
# If user is assigned to build, use restricted credentials of user
Result.new(build.user, :build, :restricted)
else
# Otherwise use generic CI credentials (backward compatibility)
Result.new(nil, :ci, :restricted)
end end
end end
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, :access_type
def initialize(actor, project, protocol) def initialize(actor, project, protocol, access_type: access_type)
@actor = actor @actor = actor
@project = project @project = project
@protocol = protocol @protocol = protocol
@access_type = access_type
@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 privileged_user_can_download_code? || restricted_user_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 privileged_user_can_download_code?
access_type == :full && user_access.can_do_action?(:download_code)
end
def restricted_user_can_download_code?
access_type == :restricted && user_access.can_do_action?(:restricted_download_code)
end
def user_push_access_check(changes) def user_push_access_check(changes)
unless access_type == :full
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
......
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