New upstream version 11.5.10+dfsg
This commit is contained in:
parent
86ace60a9c
commit
b16c5f7852
148 changed files with 2472 additions and 424 deletions
39
CHANGELOG.md
39
CHANGELOG.md
|
@ -2,6 +2,45 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 11.5.10 (2019-01-30)
|
||||||
|
|
||||||
|
- No changes.
|
||||||
|
|
||||||
|
## 11.5.9 (2019-01-29)
|
||||||
|
|
||||||
|
### Security (21 changes)
|
||||||
|
|
||||||
|
- Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770
|
||||||
|
- Don't process MR refs for guests in the notes. !2771
|
||||||
|
- Add more LFS validations to prevent forgery.
|
||||||
|
- Verify that LFS upload requests are genuine.
|
||||||
|
- Fixed XSS content in KaTex links.
|
||||||
|
- Prevent awarding emojis to notes whose parent is not visible to user.
|
||||||
|
- Prevent unauthorized replies when discussion is locked or confidential.
|
||||||
|
- Disable git v2 protocol temporarily.
|
||||||
|
- Fix showing ci status for guest users when public pipline are not set.
|
||||||
|
- Fix contributed projects info still visible when user enable private profile.
|
||||||
|
- Extract GitLab Pages using RubyZip.
|
||||||
|
- Disallows unauthorized users from accessing the pipelines section.
|
||||||
|
- Use common error for unauthenticated users when creating issues.
|
||||||
|
- Fix slow regex in project reference pattern.
|
||||||
|
- Fix private user email being visible in push (and tag push) webhooks.
|
||||||
|
- Fix wiki access rights when external wiki is enabled.
|
||||||
|
- Fix path disclosure on project import error.
|
||||||
|
- Restrict project import visibility based on its group.
|
||||||
|
- Expose CI/CD trigger token only to the trigger owner.
|
||||||
|
- Notify only users who can access the project on project move.
|
||||||
|
- Alias GitHub and BitBucket OAuth2 callback URLs.
|
||||||
|
|
||||||
|
### Fixed (1 change)
|
||||||
|
|
||||||
|
- Fix uninitialized constant with GitLab Pages.
|
||||||
|
|
||||||
|
|
||||||
|
## 11.5.8 (2019-01-28)
|
||||||
|
|
||||||
|
- Unreleased due to quality assurance failure.
|
||||||
|
|
||||||
## 11.5.7 (2019-01-15)
|
## 11.5.7 (2019-01-15)
|
||||||
|
|
||||||
### Security (1 change)
|
### Security (1 change)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.129.0
|
0.129.1
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
7.1.3
|
7.1.4
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -66,6 +66,7 @@ gem 'u2f', '~> 0.2.1'
|
||||||
|
|
||||||
# GitLab Pages
|
# GitLab Pages
|
||||||
gem 'validates_hostname', '~> 1.0.6'
|
gem 'validates_hostname', '~> 1.0.6'
|
||||||
|
gem 'rubyzip', '~> 1.2.2', require: 'zip'
|
||||||
|
|
||||||
# Browser detection
|
# Browser detection
|
||||||
gem 'browser', '~> 2.5'
|
gem 'browser', '~> 2.5'
|
||||||
|
|
|
@ -1107,6 +1107,7 @@ DEPENDENCIES
|
||||||
ruby-prof (~> 0.17.0)
|
ruby-prof (~> 0.17.0)
|
||||||
ruby-progressbar
|
ruby-progressbar
|
||||||
ruby_parser (~> 3.8)
|
ruby_parser (~> 3.8)
|
||||||
|
rubyzip (~> 1.2.2)
|
||||||
rufus-scheduler (~> 3.4)
|
rufus-scheduler (~> 3.4)
|
||||||
rugged (~> 0.27)
|
rugged (~> 0.27)
|
||||||
sanitize (~> 4.6)
|
sanitize (~> 4.6)
|
||||||
|
|
|
@ -1117,6 +1117,7 @@ DEPENDENCIES
|
||||||
ruby-prof (~> 0.17.0)
|
ruby-prof (~> 0.17.0)
|
||||||
ruby-progressbar
|
ruby-progressbar
|
||||||
ruby_parser (~> 3.8)
|
ruby_parser (~> 3.8)
|
||||||
|
rubyzip (~> 1.2.2)
|
||||||
rufus-scheduler (~> 3.4)
|
rufus-scheduler (~> 3.4)
|
||||||
rugged (~> 0.27)
|
rugged (~> 0.27)
|
||||||
sanitize (~> 4.6)
|
sanitize (~> 4.6)
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
11.5.7
|
11.5.10
|
||||||
|
|
|
@ -8,7 +8,7 @@ class Import::BitbucketController < Import::BaseController
|
||||||
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
|
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
|
||||||
|
|
||||||
def callback
|
def callback
|
||||||
response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
|
response = client.auth_code.get_token(params[:code], redirect_uri: users_import_bitbucket_callback_url)
|
||||||
|
|
||||||
session[:bitbucket_token] = response.token
|
session[:bitbucket_token] = response.token
|
||||||
session[:bitbucket_expires_at] = response.expires_at
|
session[:bitbucket_expires_at] = response.expires_at
|
||||||
|
@ -89,7 +89,7 @@ class Import::BitbucketController < Import::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def go_to_bitbucket_for_permissions
|
def go_to_bitbucket_for_permissions
|
||||||
redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
|
redirect_to client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def bitbucket_unauthorized
|
def bitbucket_unauthorized
|
||||||
|
|
|
@ -86,7 +86,7 @@ class Import::GithubController < Import::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def callback_import_url
|
def callback_import_url
|
||||||
public_send("callback_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
|
public_send("users_import_#{provider}_callback_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
|
||||||
end
|
end
|
||||||
|
|
||||||
def provider_unauthorized
|
def provider_unauthorized
|
||||||
|
|
|
@ -19,7 +19,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
|
|
||||||
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
|
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
|
||||||
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
|
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
|
||||||
prepend_before_action :authenticate_new_issue!, only: [:new]
|
prepend_before_action :authenticate_user!, only: [:new]
|
||||||
prepend_before_action :store_uri, only: [:new, :show]
|
prepend_before_action :store_uri, only: [:new, :show]
|
||||||
|
|
||||||
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
|
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
|
||||||
|
@ -229,14 +229,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
] + [{ label_ids: [], assignee_ids: [] }]
|
] + [{ label_ids: [], assignee_ids: [] }]
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_new_issue!
|
|
||||||
return if current_user
|
|
||||||
|
|
||||||
notice = "Please sign in to create the new issue."
|
|
||||||
|
|
||||||
redirect_to new_user_session_path, notice: notice
|
|
||||||
end
|
|
||||||
|
|
||||||
def store_uri
|
def store_uri
|
||||||
if request.get? && !request.xhr?
|
if request.get? && !request.xhr?
|
||||||
store_location_for :user, request.fullpath
|
store_location_for :user, request.fullpath
|
||||||
|
|
|
@ -5,7 +5,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
|
||||||
include WorkhorseRequest
|
include WorkhorseRequest
|
||||||
include SendFileUpload
|
include SendFileUpload
|
||||||
|
|
||||||
skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize]
|
skip_before_action :verify_workhorse_api!, only: :download
|
||||||
|
|
||||||
def download
|
def download
|
||||||
lfs_object = LfsObject.find_by_oid(oid)
|
lfs_object = LfsObject.find_by_oid(oid)
|
||||||
|
|
|
@ -39,8 +39,11 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pipeline_variables
|
def set_pipeline_variables
|
||||||
@pipelines = @merge_request.all_pipelines
|
@pipelines =
|
||||||
@pipeline = @merge_request.head_pipeline
|
if can?(current_user, :read_pipeline, @project)
|
||||||
@statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0
|
@merge_request.all_pipelines
|
||||||
|
else
|
||||||
|
Ci::Pipeline.none
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||||
before_action :whitelist_query_limiting, only: [:create, :retry]
|
before_action :whitelist_query_limiting, only: [:create, :retry]
|
||||||
before_action :pipeline, except: [:index, :new, :create, :charts]
|
before_action :pipeline, except: [:index, :new, :create, :charts]
|
||||||
before_action :authorize_read_pipeline!
|
before_action :authorize_read_pipeline!
|
||||||
|
before_action :authorize_read_build!, only: [:index]
|
||||||
before_action :authorize_create_pipeline!, only: [:new, :create]
|
before_action :authorize_create_pipeline!, only: [:new, :create]
|
||||||
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
|
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,9 @@ module Projects
|
||||||
|
|
||||||
def define_triggers_variables
|
def define_triggers_variables
|
||||||
@triggers = @project.triggers
|
@triggers = @project.triggers
|
||||||
|
.present(current_user: current_user)
|
||||||
@trigger = ::Ci::Trigger.new
|
@trigger = ::Ci::Trigger.new
|
||||||
|
.present(current_user: current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def define_badges_variables
|
def define_badges_variables
|
||||||
|
|
|
@ -66,12 +66,11 @@ class Projects::TriggersController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def trigger
|
def trigger
|
||||||
@trigger ||= project.triggers.find(params[:id]) || render_404
|
@trigger ||= project.triggers.find(params[:id])
|
||||||
|
.present(current_user: current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def trigger_params
|
def trigger_params
|
||||||
params.require(:trigger).permit(
|
params.require(:trigger).permit(:description)
|
||||||
:description
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,9 @@ class ContributedProjectsFinder < UnionFinder
|
||||||
# Returns an ActiveRecord::Relation.
|
# Returns an ActiveRecord::Relation.
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def execute(current_user = nil)
|
def execute(current_user = nil)
|
||||||
|
# Do not show contributed projects if the user profile is private.
|
||||||
|
return Project.none unless can_read_profile?(current_user)
|
||||||
|
|
||||||
segments = all_projects(current_user)
|
segments = all_projects(current_user)
|
||||||
|
|
||||||
find_union(segments, Project).includes(:namespace).order_id_desc
|
find_union(segments, Project).includes(:namespace).order_id_desc
|
||||||
|
@ -22,6 +25,10 @@ class ContributedProjectsFinder < UnionFinder
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def can_read_profile?(current_user)
|
||||||
|
Ability.allowed?(current_user, :read_user_profile, @user)
|
||||||
|
end
|
||||||
|
|
||||||
def all_projects(current_user)
|
def all_projects(current_user)
|
||||||
projects = []
|
projects = []
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module ExternalWikiHelper
|
|
||||||
def get_project_wiki_path(project)
|
|
||||||
external_wiki_service = project.external_wiki
|
|
||||||
if external_wiki_service
|
|
||||||
external_wiki_service.properties['external_wiki_url']
|
|
||||||
else
|
|
||||||
project_wiki_path(project, :home)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -278,7 +278,8 @@ module ProjectsHelper
|
||||||
nav_tabs << :container_registry
|
nav_tabs << :container_registry
|
||||||
end
|
end
|
||||||
|
|
||||||
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
|
# Pipelines feature is tied to presence of builds
|
||||||
|
if can?(current_user, :read_build, project)
|
||||||
nav_tabs << :pipelines
|
nav_tabs << :pipelines
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -286,19 +287,24 @@ module ProjectsHelper
|
||||||
nav_tabs << :operations
|
nav_tabs << :operations
|
||||||
end
|
end
|
||||||
|
|
||||||
if project.external_issue_tracker
|
|
||||||
nav_tabs << :external_issue_tracker
|
|
||||||
end
|
|
||||||
|
|
||||||
tab_ability_map.each do |tab, ability|
|
tab_ability_map.each do |tab, ability|
|
||||||
if can?(current_user, ability, project)
|
if can?(current_user, ability, project)
|
||||||
nav_tabs << tab
|
nav_tabs << tab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
nav_tabs << external_nav_tabs(project)
|
||||||
|
|
||||||
nav_tabs.flatten
|
nav_tabs.flatten
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def external_nav_tabs(project)
|
||||||
|
[].tap do |tabs|
|
||||||
|
tabs << :external_issue_tracker if project.external_issue_tracker
|
||||||
|
tabs << :external_wiki if project.has_external_wiki?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def tab_ability_map
|
def tab_ability_map
|
||||||
{
|
{
|
||||||
environments: :read_environment,
|
environments: :read_environment,
|
||||||
|
|
|
@ -4,6 +4,7 @@ module Ci
|
||||||
class Trigger < ActiveRecord::Base
|
class Trigger < ActiveRecord::Base
|
||||||
extend Gitlab::Ci::Model
|
extend Gitlab::Ci::Model
|
||||||
include IgnorableColumn
|
include IgnorableColumn
|
||||||
|
include Presentable
|
||||||
|
|
||||||
ignore_column :deleted_at
|
ignore_column :deleted_at
|
||||||
|
|
||||||
|
@ -29,7 +30,7 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
def short_token
|
def short_token
|
||||||
token[0...4]
|
token[0...4] if token.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def legacy?
|
def legacy?
|
||||||
|
|
|
@ -11,6 +11,7 @@ class Commit
|
||||||
include Mentionable
|
include Mentionable
|
||||||
include Referable
|
include Referable
|
||||||
include StaticModel
|
include StaticModel
|
||||||
|
include Presentable
|
||||||
include ::Gitlab::Utils::StrongMemoize
|
include ::Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
attr_mentionable :safe_message, pipeline: :single_line
|
attr_mentionable :safe_message, pipeline: :single_line
|
||||||
|
@ -313,7 +314,9 @@ class Commit
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_pipeline
|
def last_pipeline
|
||||||
@last_pipeline ||= pipelines.last
|
strong_memoize(:last_pipeline) do
|
||||||
|
pipelines.last
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def status(ref = nil)
|
def status(ref = nil)
|
||||||
|
|
|
@ -15,7 +15,7 @@ module CacheMarkdownField
|
||||||
# Increment this number every time the renderer changes its output
|
# Increment this number every time the renderer changes its output
|
||||||
CACHE_REDCARPET_VERSION = 3
|
CACHE_REDCARPET_VERSION = 3
|
||||||
CACHE_COMMONMARK_VERSION_START = 10
|
CACHE_COMMONMARK_VERSION_START = 10
|
||||||
CACHE_COMMONMARK_VERSION = 12
|
CACHE_COMMONMARK_VERSION = 13
|
||||||
|
|
||||||
# changes to these attributes cause the cache to be invalidates
|
# changes to these attributes cause the cache to be invalidates
|
||||||
INVALIDATED_BY = %w[author project].freeze
|
INVALIDATED_BY = %w[author project].freeze
|
||||||
|
|
22
app/models/lfs_download_object.rb
Normal file
22
app/models/lfs_download_object.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class LfsDownloadObject
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
attr_accessor :oid, :size, :link
|
||||||
|
delegate :sanitized_url, :credentials, to: :sanitized_uri
|
||||||
|
|
||||||
|
validates :oid, format: { with: /\A\h{64}\z/ }
|
||||||
|
validates :size, numericality: { greater_than_or_equal_to: 0 }
|
||||||
|
validates :link, public_url: { protocols: %w(http https) }
|
||||||
|
|
||||||
|
def initialize(oid:, size:, link:)
|
||||||
|
@oid = oid
|
||||||
|
@size = size
|
||||||
|
@link = link
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitized_uri
|
||||||
|
@sanitized_uri ||= Gitlab::UrlSanitizer.new(link)
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,6 +7,7 @@ class Member < ActiveRecord::Base
|
||||||
include Expirable
|
include Expirable
|
||||||
include Gitlab::Access
|
include Gitlab::Access
|
||||||
include Presentable
|
include Presentable
|
||||||
|
include FromUnion
|
||||||
|
|
||||||
attr_accessor :raw_invite_token
|
attr_accessor :raw_invite_token
|
||||||
|
|
||||||
|
@ -83,6 +84,14 @@ class Member < ActiveRecord::Base
|
||||||
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
|
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
|
||||||
scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
|
scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
|
||||||
|
|
||||||
|
scope :on_project_and_ancestors, ->(project) do
|
||||||
|
if project.group
|
||||||
|
from_union([GroupMember.where(source_id: project.group.self_and_ancestors), project.project_members])
|
||||||
|
else
|
||||||
|
project.project_members
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
|
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
|
||||||
|
|
||||||
after_create :send_invite, if: :invite?, unless: :importing?
|
after_create :send_invite, if: :invite?, unless: :importing?
|
||||||
|
|
|
@ -492,6 +492,7 @@ class Project < ActiveRecord::Base
|
||||||
|
|
||||||
def reference_pattern
|
def reference_pattern
|
||||||
%r{
|
%r{
|
||||||
|
(?<!#{Gitlab::PathRegex::PATH_START_CHAR})
|
||||||
((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
|
((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
|
||||||
(?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
|
(?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
|
||||||
}x
|
}x
|
||||||
|
@ -531,6 +532,14 @@ class Project < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pipelines
|
||||||
|
if builds_enabled?
|
||||||
|
super
|
||||||
|
else
|
||||||
|
super.external
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# returns all ancestor-groups upto but excluding the given namespace
|
# returns all ancestor-groups upto but excluding the given namespace
|
||||||
# when no namespace is given, all ancestors upto the top are returned
|
# when no namespace is given, all ancestors upto the top are returned
|
||||||
def ancestors_upto(top = nil)
|
def ancestors_upto(top = nil)
|
||||||
|
|
|
@ -74,6 +74,14 @@ class ProjectTeam
|
||||||
end
|
end
|
||||||
alias_method :users, :members
|
alias_method :users, :members
|
||||||
|
|
||||||
|
# `members` method uses project_authorizations table which
|
||||||
|
# is updated asynchronously, on project move it still contains
|
||||||
|
# old members who may not have access to the new location,
|
||||||
|
# so we filter out only members of project or project's group
|
||||||
|
def members_in_project_and_ancestors
|
||||||
|
members.where(id: member_user_ids)
|
||||||
|
end
|
||||||
|
|
||||||
def guests
|
def guests
|
||||||
@guests ||= fetch_members(Gitlab::Access::GUEST)
|
@guests ||= fetch_members(Gitlab::Access::GUEST)
|
||||||
end
|
end
|
||||||
|
@ -191,4 +199,8 @@ class ProjectTeam
|
||||||
def group
|
def group
|
||||||
project.group
|
project.group
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def member_user_ids
|
||||||
|
Member.on_project_and_ancestors(project).select(:user_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,15 @@ module Ci
|
||||||
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
|
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
condition(:external_pipeline, scope: :subject, score: 0) do
|
||||||
|
@subject.external?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Disallow users without permissions from accessing internal pipelines
|
||||||
|
rule { ~can?(:read_build) & ~external_pipeline }.policy do
|
||||||
|
prevent :read_pipeline
|
||||||
|
end
|
||||||
|
|
||||||
rule { protected_ref }.prevent :update_pipeline
|
rule { protected_ref }.prevent :update_pipeline
|
||||||
|
|
||||||
rule { can?(:public_access) & branch_allows_collaboration }.policy do
|
rule { can?(:public_access) & branch_allows_collaboration }.policy do
|
||||||
|
|
|
@ -18,6 +18,7 @@ class IssuePolicy < IssuablePolicy
|
||||||
prevent :read_issue_iid
|
prevent :read_issue_iid
|
||||||
prevent :update_issue
|
prevent :update_issue
|
||||||
prevent :admin_issue
|
prevent :admin_issue
|
||||||
|
prevent :create_note
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { locked }.policy do
|
rule { locked }.policy do
|
||||||
|
|
|
@ -18,6 +18,7 @@ class NotePolicy < BasePolicy
|
||||||
prevent :read_note
|
prevent :read_note
|
||||||
prevent :admin_note
|
prevent :admin_note
|
||||||
prevent :resolve_note
|
prevent :resolve_note
|
||||||
|
prevent :award_emoji
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { is_author }.policy do
|
rule { is_author }.policy do
|
||||||
|
|
|
@ -28,5 +28,8 @@ class PersonalSnippetPolicy < BasePolicy
|
||||||
|
|
||||||
rule { anonymous }.prevent :comment_personal_snippet
|
rule { anonymous }.prevent :comment_personal_snippet
|
||||||
|
|
||||||
rule { can?(:comment_personal_snippet) }.enable :award_emoji
|
rule { can?(:comment_personal_snippet) }.policy do
|
||||||
|
enable :create_note
|
||||||
|
enable :award_emoji
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -103,6 +103,10 @@ class ProjectPolicy < BasePolicy
|
||||||
@subject.feature_available?(:merge_requests, @user)
|
@subject.feature_available?(:merge_requests, @user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
condition(:internal_builds_disabled) do
|
||||||
|
!@subject.builds_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
features = %w[
|
features = %w[
|
||||||
merge_requests
|
merge_requests
|
||||||
issues
|
issues
|
||||||
|
@ -190,7 +194,6 @@ class ProjectPolicy < BasePolicy
|
||||||
enable :read_build
|
enable :read_build
|
||||||
enable :read_container_image
|
enable :read_container_image
|
||||||
enable :read_pipeline
|
enable :read_pipeline
|
||||||
enable :read_pipeline_schedule
|
|
||||||
enable :read_environment
|
enable :read_environment
|
||||||
enable :read_deployment
|
enable :read_deployment
|
||||||
enable :read_merge_request
|
enable :read_merge_request
|
||||||
|
@ -226,6 +229,7 @@ class ProjectPolicy < BasePolicy
|
||||||
enable :update_build
|
enable :update_build
|
||||||
enable :create_pipeline
|
enable :create_pipeline
|
||||||
enable :update_pipeline
|
enable :update_pipeline
|
||||||
|
enable :read_pipeline_schedule
|
||||||
enable :create_pipeline_schedule
|
enable :create_pipeline_schedule
|
||||||
enable :create_merge_request_from
|
enable :create_merge_request_from
|
||||||
enable :create_wiki
|
enable :create_wiki
|
||||||
|
@ -299,13 +303,12 @@ class ProjectPolicy < BasePolicy
|
||||||
prevent(*create_read_update_admin_destroy(:project_snippet))
|
prevent(*create_read_update_admin_destroy(:project_snippet))
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { wiki_disabled & ~has_external_wiki }.policy do
|
rule { wiki_disabled }.policy do
|
||||||
prevent(*create_read_update_admin_destroy(:wiki))
|
prevent(*create_read_update_admin_destroy(:wiki))
|
||||||
prevent(:download_wiki_code)
|
prevent(:download_wiki_code)
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { builds_disabled | repository_disabled }.policy do
|
rule { builds_disabled | repository_disabled }.policy do
|
||||||
prevent(*create_update_admin_destroy(:pipeline))
|
|
||||||
prevent(*create_read_update_admin_destroy(:build))
|
prevent(*create_read_update_admin_destroy(:build))
|
||||||
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
|
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
|
||||||
prevent(*create_read_update_admin_destroy(:environment))
|
prevent(*create_read_update_admin_destroy(:environment))
|
||||||
|
@ -313,11 +316,22 @@ class ProjectPolicy < BasePolicy
|
||||||
prevent(*create_read_update_admin_destroy(:deployment))
|
prevent(*create_read_update_admin_destroy(:deployment))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# There's two separate cases when builds_disabled is true:
|
||||||
|
# 1. When internal CI is disabled - builds_disabled && internal_builds_disabled
|
||||||
|
# - We do not prevent the user from accessing Pipelines to allow him to access external CI
|
||||||
|
# 2. When the user is not allowed to access CI - builds_disabled && ~internal_builds_disabled
|
||||||
|
# - We prevent the user from accessing Pipelines
|
||||||
|
rule { (builds_disabled & ~internal_builds_disabled) | repository_disabled }.policy do
|
||||||
|
prevent(*create_read_update_admin_destroy(:pipeline))
|
||||||
|
prevent(*create_read_update_admin_destroy(:commit_status))
|
||||||
|
end
|
||||||
|
|
||||||
rule { repository_disabled }.policy do
|
rule { repository_disabled }.policy do
|
||||||
prevent :push_code
|
prevent :push_code
|
||||||
prevent :download_code
|
prevent :download_code
|
||||||
prevent :fork_project
|
prevent :fork_project
|
||||||
prevent :read_commit_status
|
prevent :read_commit_status
|
||||||
|
prevent :read_pipeline
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { container_registry_disabled }.policy do
|
rule { container_registry_disabled }.policy do
|
||||||
|
@ -343,7 +357,6 @@ class ProjectPolicy < BasePolicy
|
||||||
enable :read_merge_request
|
enable :read_merge_request
|
||||||
enable :read_note
|
enable :read_note
|
||||||
enable :read_pipeline
|
enable :read_pipeline
|
||||||
enable :read_pipeline_schedule
|
|
||||||
enable :read_commit_status
|
enable :read_commit_status
|
||||||
enable :read_container_image
|
enable :read_container_image
|
||||||
enable :download_code
|
enable :download_code
|
||||||
|
@ -361,7 +374,6 @@ class ProjectPolicy < BasePolicy
|
||||||
|
|
||||||
rule { public_builds & can?(:guest_access) }.policy do
|
rule { public_builds & can?(:guest_access) }.policy do
|
||||||
enable :read_pipeline
|
enable :read_pipeline
|
||||||
enable :read_pipeline_schedule
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# These rules are included to allow maintainers of projects to push to certain
|
# These rules are included to allow maintainers of projects to push to certain
|
||||||
|
@ -376,7 +388,7 @@ class ProjectPolicy < BasePolicy
|
||||||
end.enable :read_issue_iid
|
end.enable :read_issue_iid
|
||||||
|
|
||||||
rule do
|
rule do
|
||||||
(can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
|
(~guest & can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request)
|
||||||
end.enable :read_merge_request_iid
|
end.enable :read_merge_request_iid
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -44,4 +44,6 @@ class ProjectSnippetPolicy < BasePolicy
|
||||||
enable :update_project_snippet
|
enable :update_project_snippet
|
||||||
enable :admin_project_snippet
|
enable :admin_project_snippet
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rule { ~can?(:read_project_snippet) }.prevent :create_note
|
||||||
end
|
end
|
||||||
|
|
19
app/presenters/ci/trigger_presenter.rb
Normal file
19
app/presenters/ci/trigger_presenter.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Ci
|
||||||
|
class TriggerPresenter < Gitlab::View::Presenter::Delegated
|
||||||
|
presents :trigger
|
||||||
|
|
||||||
|
def has_token_exposed?
|
||||||
|
can?(current_user, :admin_trigger, trigger)
|
||||||
|
end
|
||||||
|
|
||||||
|
def token
|
||||||
|
if has_token_exposed?
|
||||||
|
trigger.token
|
||||||
|
else
|
||||||
|
trigger.short_token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
app/presenters/commit_presenter.rb
Normal file
13
app/presenters/commit_presenter.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CommitPresenter < Gitlab::View::Presenter::Simple
|
||||||
|
presents :commit
|
||||||
|
|
||||||
|
def status_for(ref)
|
||||||
|
can?(current_user, :read_commit_status, commit.project) && commit.status(ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
def any_pipelines?
|
||||||
|
can?(current_user, :read_pipeline, commit.project) && commit.pipelines.any?
|
||||||
|
end
|
||||||
|
end
|
|
@ -170,6 +170,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
|
||||||
source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
|
source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def can_read_pipeline?
|
||||||
|
pipeline && can?(current_user, :read_pipeline, pipeline)
|
||||||
|
end
|
||||||
|
|
||||||
def mergeable_discussions_state
|
def mergeable_discussions_state
|
||||||
# This avoids calling MergeRequest#mergeable_discussions_state without
|
# This avoids calling MergeRequest#mergeable_discussions_state without
|
||||||
# considering the state of the MR first. If a MR isn't mergeable, we can
|
# considering the state of the MR first. If a MR isn't mergeable, we can
|
||||||
|
|
|
@ -54,7 +54,7 @@ class MergeRequestWidgetEntity < IssuableEntity
|
||||||
end
|
end
|
||||||
|
|
||||||
expose :merge_commit_message
|
expose :merge_commit_message
|
||||||
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
|
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? }
|
||||||
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
|
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
|
||||||
|
|
||||||
# Booleans
|
# Booleans
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Notes
|
||||||
if in_reply_to_discussion_id.present?
|
if in_reply_to_discussion_id.present?
|
||||||
discussion = find_discussion(in_reply_to_discussion_id)
|
discussion = find_discussion(in_reply_to_discussion_id)
|
||||||
|
|
||||||
unless discussion
|
unless discussion && can?(current_user, :create_note, discussion.noteable)
|
||||||
note = Note.new
|
note = Note.new
|
||||||
note.errors.add(:base, 'Discussion to reply to cannot be found')
|
note.errors.add(:base, 'Discussion to reply to cannot be found')
|
||||||
return note
|
return note
|
||||||
|
@ -34,19 +34,8 @@ module Notes
|
||||||
if project
|
if project
|
||||||
project.notes.find_discussion(discussion_id)
|
project.notes.find_discussion(discussion_id)
|
||||||
else
|
else
|
||||||
discussion = Note.find_discussion(discussion_id)
|
Note.find_discussion(discussion_id)
|
||||||
noteable = discussion.noteable
|
|
||||||
|
|
||||||
return nil unless noteable_without_project?(noteable)
|
|
||||||
|
|
||||||
discussion
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def noteable_without_project?(noteable)
|
|
||||||
return true if noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
|
|
||||||
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -373,7 +373,8 @@ class NotificationService
|
||||||
end
|
end
|
||||||
|
|
||||||
def project_was_moved(project, old_path_with_namespace)
|
def project_was_moved(project, old_path_with_namespace)
|
||||||
recipients = notifiable_users(project.team.members, :mention, project: project)
|
recipients = project.private? ? project.team.members_in_project_and_ancestors : project.team.members
|
||||||
|
recipients = notifiable_users(recipients, :mention, project: project)
|
||||||
|
|
||||||
recipients.each do |recipient|
|
recipients.each do |recipient|
|
||||||
mailer.project_was_moved_email(
|
mailer.project_was_moved_email(
|
||||||
|
|
14
app/services/projects/import_error_filter.rb
Normal file
14
app/services/projects/import_error_filter.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Projects
|
||||||
|
# Used by project imports, it removes any potential paths
|
||||||
|
# included in an error message that could be stored in the DB
|
||||||
|
class ImportErrorFilter
|
||||||
|
ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/
|
||||||
|
FILTER_MESSAGE = '[FILTERED]'
|
||||||
|
|
||||||
|
def self.filter_message(message)
|
||||||
|
message.gsub(ERROR_MESSAGE_FILTER, FILTER_MESSAGE)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -24,8 +24,12 @@ module Projects
|
||||||
import_data
|
import_data
|
||||||
|
|
||||||
success
|
success
|
||||||
rescue => e
|
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
||||||
error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{e.message}")
|
error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{e.message}")
|
||||||
|
rescue => e
|
||||||
|
message = Projects::ImportErrorFilter.filter_message(e.message)
|
||||||
|
|
||||||
|
error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{message}")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -35,7 +39,7 @@ module Projects
|
||||||
begin
|
begin
|
||||||
Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS)
|
Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS)
|
||||||
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
||||||
raise Error, "Blocked import URL: #{e.message}"
|
raise e, "Blocked import URL: #{e.message}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -86,11 +90,11 @@ module Projects
|
||||||
|
|
||||||
return unless project.lfs_enabled?
|
return unless project.lfs_enabled?
|
||||||
|
|
||||||
oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
|
lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
|
||||||
download_service = Projects::LfsPointers::LfsDownloadService.new(project)
|
|
||||||
|
|
||||||
oids_to_download.each do |oid, link|
|
lfs_objects_to_download.each do |lfs_download_object|
|
||||||
download_service.execute(oid, link)
|
Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object)
|
||||||
|
.execute
|
||||||
end
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
# Right now, to avoid aborting the importing process, we silently fail
|
# Right now, to avoid aborting the importing process, we silently fail
|
||||||
|
|
|
@ -41,16 +41,17 @@ module Projects
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_response_links(objects_response)
|
def parse_response_links(objects_response)
|
||||||
objects_response.each_with_object({}) do |entry, link_list|
|
objects_response.each_with_object([]) do |entry, link_list|
|
||||||
begin
|
begin
|
||||||
oid = entry['oid']
|
|
||||||
link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
|
link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
|
||||||
|
|
||||||
raise DownloadLinkNotFound unless link
|
raise DownloadLinkNotFound unless link
|
||||||
|
|
||||||
link_list[oid] = add_credentials(link)
|
link_list << LfsDownloadObject.new(oid: entry['oid'],
|
||||||
rescue DownloadLinkNotFound, URI::InvalidURIError
|
size: entry['size'],
|
||||||
Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.")
|
link: add_credentials(link))
|
||||||
|
rescue DownloadLinkNotFound, Addressable::URI::InvalidURIError
|
||||||
|
log_error("Link for Lfs Object with oid #{entry['oid']} not found or invalid.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -70,7 +71,7 @@ module Projects
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_credentials(link)
|
def add_credentials(link)
|
||||||
uri = URI.parse(link)
|
uri = Addressable::URI.parse(link)
|
||||||
|
|
||||||
if should_add_credentials?(uri)
|
if should_add_credentials?(uri)
|
||||||
uri.user = remote_uri.user
|
uri.user = remote_uri.user
|
||||||
|
|
|
@ -4,68 +4,93 @@
|
||||||
module Projects
|
module Projects
|
||||||
module LfsPointers
|
module LfsPointers
|
||||||
class LfsDownloadService < BaseService
|
class LfsDownloadService < BaseService
|
||||||
VALID_PROTOCOLS = %w[http https].freeze
|
SizeError = Class.new(StandardError)
|
||||||
|
OidError = Class.new(StandardError)
|
||||||
|
|
||||||
|
attr_reader :lfs_download_object
|
||||||
|
delegate :oid, :size, :credentials, :sanitized_url, to: :lfs_download_object, prefix: :lfs
|
||||||
|
|
||||||
|
def initialize(project, lfs_download_object)
|
||||||
|
super(project)
|
||||||
|
|
||||||
|
@lfs_download_object = lfs_download_object
|
||||||
|
end
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def execute(oid, url)
|
def execute
|
||||||
return unless project&.lfs_enabled? && oid.present? && url.present?
|
return unless project&.lfs_enabled? && lfs_download_object
|
||||||
|
return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
|
||||||
|
return if LfsObject.exists?(oid: lfs_oid)
|
||||||
|
|
||||||
return if LfsObject.exists?(oid: oid)
|
wrap_download_errors do
|
||||||
|
download_lfs_file!
|
||||||
sanitized_uri = sanitize_url!(url)
|
|
||||||
|
|
||||||
with_tmp_file(oid) do |file|
|
|
||||||
download_and_save_file(file, sanitized_uri)
|
|
||||||
lfs_object = LfsObject.new(oid: oid, size: file.size, file: file)
|
|
||||||
|
|
||||||
project.all_lfs_objects << lfs_object
|
|
||||||
end
|
end
|
||||||
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
|
||||||
Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded: #{e.message}")
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}")
|
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sanitize_url!(url)
|
def wrap_download_errors(&block)
|
||||||
Gitlab::UrlSanitizer.new(url).tap do |sanitized_uri|
|
yield
|
||||||
# Just validate that HTTP/HTTPS protocols are used. The
|
rescue SizeError, OidError, StandardError => e
|
||||||
# subsequent Gitlab::HTTP.get call will do network checks
|
error("LFS file with oid #{lfs_oid} could't be downloaded from #{lfs_sanitized_url}: #{e.message}")
|
||||||
# based on the settings.
|
end
|
||||||
Gitlab::UrlBlocker.validate!(sanitized_uri.sanitized_url,
|
|
||||||
protocols: VALID_PROTOCOLS)
|
def download_lfs_file!
|
||||||
|
with_tmp_file do |tmp_file|
|
||||||
|
download_and_save_file!(tmp_file)
|
||||||
|
project.all_lfs_objects << LfsObject.new(oid: lfs_oid,
|
||||||
|
size: lfs_size,
|
||||||
|
file: tmp_file)
|
||||||
|
|
||||||
|
success
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def download_and_save_file(file, sanitized_uri)
|
def download_and_save_file!(file)
|
||||||
response = Gitlab::HTTP.get(sanitized_uri.sanitized_url, headers(sanitized_uri)) do |fragment|
|
digester = Digest::SHA256.new
|
||||||
|
response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment|
|
||||||
|
digester << fragment
|
||||||
file.write(fragment)
|
file.write(fragment)
|
||||||
|
|
||||||
|
raise_size_error! if file.size > lfs_size
|
||||||
end
|
end
|
||||||
|
|
||||||
raise StandardError, "Received error code #{response.code}" unless response.success?
|
raise StandardError, "Received error code #{response.code}" unless response.success?
|
||||||
|
|
||||||
|
raise_size_error! if file.size != lfs_size
|
||||||
|
raise_oid_error! if digester.hexdigest != lfs_oid
|
||||||
end
|
end
|
||||||
|
|
||||||
def headers(sanitized_uri)
|
def download_headers
|
||||||
query_options.tap do |headers|
|
{ stream_body: true }.tap do |headers|
|
||||||
credentials = sanitized_uri.credentials
|
if lfs_credentials[:user].present? || lfs_credentials[:password].present?
|
||||||
|
|
||||||
if credentials[:user].present? || credentials[:password].present?
|
|
||||||
# Using authentication headers in the request
|
# Using authentication headers in the request
|
||||||
headers[:http_basic_authentication] = [credentials[:user], credentials[:password]]
|
headers[:basic_auth] = { username: lfs_credentials[:user], password: lfs_credentials[:password] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def query_options
|
def with_tmp_file
|
||||||
{ stream_body: true }
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_tmp_file(oid)
|
|
||||||
create_tmp_storage_dir
|
create_tmp_storage_dir
|
||||||
|
|
||||||
File.open(File.join(tmp_storage_dir, oid), 'wb') { |file| yield file }
|
File.open(tmp_filename, 'wb') do |file|
|
||||||
|
begin
|
||||||
|
yield file
|
||||||
|
rescue StandardError => e
|
||||||
|
# If the lfs file is successfully downloaded it will be removed
|
||||||
|
# when it is added to the project's lfs files.
|
||||||
|
# Nevertheless if any excetion raises the file would remain
|
||||||
|
# in the file system. Here we ensure to remove it
|
||||||
|
File.unlink(file) if File.exist?(file)
|
||||||
|
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def tmp_filename
|
||||||
|
File.join(tmp_storage_dir, lfs_oid)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_tmp_storage_dir
|
def create_tmp_storage_dir
|
||||||
|
@ -79,6 +104,20 @@ module Projects
|
||||||
def storage_dir
|
def storage_dir
|
||||||
@storage_dir ||= Gitlab.config.lfs.storage_path
|
@storage_dir ||= Gitlab.config.lfs.storage_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def raise_size_error!
|
||||||
|
raise SizeError, 'Size mistmatch'
|
||||||
|
end
|
||||||
|
|
||||||
|
def raise_oid_error!
|
||||||
|
raise OidError, 'Oid mismatch'
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(message, http_status = nil)
|
||||||
|
log_error(message)
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,11 @@ module Projects
|
||||||
|
|
||||||
BLOCK_SIZE = 32.kilobytes
|
BLOCK_SIZE = 32.kilobytes
|
||||||
MAX_SIZE = 1.terabyte
|
MAX_SIZE = 1.terabyte
|
||||||
SITE_PATH = 'public/'.freeze
|
PUBLIC_DIR = 'public'.freeze
|
||||||
|
|
||||||
|
# this has to be invalid group name,
|
||||||
|
# as it shares the namespace with groups
|
||||||
|
TMP_EXTRACT_PATH = '@pages.tmp'.freeze
|
||||||
|
|
||||||
attr_reader :build
|
attr_reader :build
|
||||||
|
|
||||||
|
@ -27,12 +31,11 @@ module Projects
|
||||||
raise InvalidStateError, 'pages are outdated' unless latest?
|
raise InvalidStateError, 'pages are outdated' unless latest?
|
||||||
|
|
||||||
# Create temporary directory in which we will extract the artifacts
|
# Create temporary directory in which we will extract the artifacts
|
||||||
FileUtils.mkdir_p(tmp_path)
|
make_secure_tmp_dir(tmp_path) do |archive_path|
|
||||||
Dir.mktmpdir(nil, tmp_path) do |archive_path|
|
|
||||||
extract_archive!(archive_path)
|
extract_archive!(archive_path)
|
||||||
|
|
||||||
# Check if we did extract public directory
|
# Check if we did extract public directory
|
||||||
archive_public_path = File.join(archive_path, 'public')
|
archive_public_path = File.join(archive_path, PUBLIC_DIR)
|
||||||
raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
|
raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
|
||||||
raise InvalidStateError, 'pages are outdated' unless latest?
|
raise InvalidStateError, 'pages are outdated' unless latest?
|
||||||
|
|
||||||
|
@ -85,22 +88,18 @@ module Projects
|
||||||
raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
|
raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
|
||||||
|
|
||||||
# Calculate page size after extract
|
# Calculate page size after extract
|
||||||
public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
|
public_entry = build.artifacts_metadata_entry(PUBLIC_DIR + '/', recursive: true)
|
||||||
|
|
||||||
if public_entry.total_size > max_size
|
if public_entry.total_size > max_size
|
||||||
raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
|
raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Requires UnZip at least 6.00 Info-ZIP.
|
|
||||||
# -qq be (very) quiet
|
|
||||||
# -n never overwrite existing files
|
|
||||||
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
|
|
||||||
site_path = File.join(SITE_PATH, '*')
|
|
||||||
build.artifacts_file.use_file do |artifacts_path|
|
build.artifacts_file.use_file do |artifacts_path|
|
||||||
unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path}))
|
SafeZip::Extract.new(artifacts_path)
|
||||||
raise FailedToExtractError, 'pages failed to extract'
|
.extract(directories: [PUBLIC_DIR], to: temp_path)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
rescue SafeZip::Extract::Error => e
|
||||||
|
raise FailedToExtractError, e.message
|
||||||
end
|
end
|
||||||
|
|
||||||
def deploy_page!(archive_public_path)
|
def deploy_page!(archive_public_path)
|
||||||
|
@ -139,7 +138,7 @@ module Projects
|
||||||
end
|
end
|
||||||
|
|
||||||
def tmp_path
|
def tmp_path
|
||||||
@tmp_path ||= File.join(::Settings.pages.path, 'tmp')
|
@tmp_path ||= File.join(::Settings.pages.path, TMP_EXTRACT_PATH)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pages_path
|
def pages_path
|
||||||
|
@ -147,11 +146,11 @@ module Projects
|
||||||
end
|
end
|
||||||
|
|
||||||
def public_path
|
def public_path
|
||||||
@public_path ||= File.join(pages_path, 'public')
|
@public_path ||= File.join(pages_path, PUBLIC_DIR)
|
||||||
end
|
end
|
||||||
|
|
||||||
def previous_public_path
|
def previous_public_path
|
||||||
@previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}")
|
@previous_public_path ||= File.join(pages_path, "#{PUBLIC_DIR}.#{SecureRandom.hex}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def ref
|
def ref
|
||||||
|
@ -188,5 +187,15 @@ module Projects
|
||||||
def pages_deployments_failed_total_counter
|
def pages_deployments_failed_total_counter
|
||||||
@pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
|
@pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def make_secure_tmp_dir(tmp_path)
|
||||||
|
FileUtils.mkdir_p(tmp_path)
|
||||||
|
path = Dir.mktmpdir(nil, tmp_path)
|
||||||
|
begin
|
||||||
|
yield(path)
|
||||||
|
ensure
|
||||||
|
FileUtils.remove_entry_secure(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -264,19 +264,34 @@
|
||||||
%strong.fly-out-top-item-name
|
%strong.fly-out-top-item-name
|
||||||
= _('Registry')
|
= _('Registry')
|
||||||
|
|
||||||
- if project_nav_tab? :wiki
|
- if project_nav_tab?(:wiki)
|
||||||
|
- wiki_url = project_wiki_path(@project, :home)
|
||||||
= nav_link(controller: :wikis) do
|
= nav_link(controller: :wikis) do
|
||||||
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
|
= link_to wiki_url, class: 'shortcuts-wiki' do
|
||||||
.nav-icon-container
|
.nav-icon-container
|
||||||
= sprite_icon('book')
|
= sprite_icon('book')
|
||||||
%span.nav-item-name
|
%span.nav-item-name
|
||||||
= _('Wiki')
|
= _('Wiki')
|
||||||
%ul.sidebar-sub-level-items.is-fly-out-only
|
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||||
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
|
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
|
||||||
= link_to get_project_wiki_path(@project) do
|
= link_to wiki_url do
|
||||||
%strong.fly-out-top-item-name
|
%strong.fly-out-top-item-name
|
||||||
= _('Wiki')
|
= _('Wiki')
|
||||||
|
|
||||||
|
- if project_nav_tab?(:external_wiki)
|
||||||
|
- external_wiki_url = @project.external_wiki.external_wiki_url
|
||||||
|
= nav_link do
|
||||||
|
= link_to external_wiki_url, class: 'shortcuts-external_wiki' do
|
||||||
|
.nav-icon-container
|
||||||
|
= sprite_icon('issue-external')
|
||||||
|
%span.nav-item-name
|
||||||
|
= _('External Wiki')
|
||||||
|
%ul.sidebar-sub-level-items.is-fly-out-only
|
||||||
|
= nav_link(html_options: { class: "fly-out-top-item" } ) do
|
||||||
|
= link_to external_wiki_url do
|
||||||
|
%strong.fly-out-top-item-name
|
||||||
|
= _('External Wiki')
|
||||||
|
|
||||||
- if project_nav_tab? :snippets
|
- if project_nav_tab? :snippets
|
||||||
= nav_link(controller: :snippets) do
|
= nav_link(controller: :snippets) do
|
||||||
= link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
|
= link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
= icon('info-circle fw')
|
= icon('info-circle fw')
|
||||||
= succeed '.' do
|
= succeed '.' do
|
||||||
To learn more about this project, read
|
To learn more about this project, read
|
||||||
= link_to "the wiki", get_project_wiki_path(viewer.project)
|
= link_to "the wiki", project_wiki_path(viewer.project, :home)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
- any_pipelines = @commit.present(current_user: current_user).any_pipelines?
|
||||||
|
|
||||||
%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs
|
%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs
|
||||||
= nav_link(path: 'commit#show') do
|
= nav_link(path: 'commit#show') do
|
||||||
= link_to project_commit_path(@project, @commit.id) do
|
= link_to project_commit_path(@project, @commit.id) do
|
||||||
Changes
|
Changes
|
||||||
%span.badge.badge-pill= @diffs.size
|
%span.badge.badge-pill= @diffs.size
|
||||||
- if can?(current_user, :read_pipeline, @project)
|
- if any_pipelines
|
||||||
= nav_link(path: 'commit#pipelines') do
|
= nav_link(path: 'commit#pipelines') do
|
||||||
= link_to pipelines_project_commit_path(@project, @commit.id) do
|
= link_to pipelines_project_commit_path(@project, @commit.id) do
|
||||||
Pipelines
|
Pipelines
|
||||||
|
|
|
@ -74,8 +74,8 @@
|
||||||
%span.commit-info.merge-requests{ 'data-project-commit-path' => merge_requests_project_commit_path(@project, @commit.id, format: :json) }
|
%span.commit-info.merge-requests{ 'data-project-commit-path' => merge_requests_project_commit_path(@project, @commit.id, format: :json) }
|
||||||
= icon('spinner spin')
|
= icon('spinner spin')
|
||||||
|
|
||||||
- if @commit.last_pipeline
|
- last_pipeline = @commit.last_pipeline
|
||||||
- last_pipeline = @commit.last_pipeline
|
- if can?(current_user, :read_pipeline, last_pipeline)
|
||||||
.well-segment.pipeline-info
|
.well-segment.pipeline-info
|
||||||
.status-icon-container
|
.status-icon-container
|
||||||
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
|
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
|
||||||
|
|
|
@ -9,11 +9,8 @@
|
||||||
|
|
||||||
.container-fluid{ class: [limited_container_width, container_class] }
|
.container-fluid{ class: [limited_container_width, container_class] }
|
||||||
= render "commit_box"
|
= render "commit_box"
|
||||||
- if @commit.status
|
= render "ci_menu"
|
||||||
= render "ci_menu"
|
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true
|
||||||
- else
|
|
||||||
.block-connector
|
|
||||||
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment
|
|
||||||
|
|
||||||
.limited-width-notes
|
.limited-width-notes
|
||||||
= render "shared/notes/notes_with_form", :autocomplete => true
|
= render "shared/notes/notes_with_form", :autocomplete => true
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
- merge_request = local_assigns.fetch(:merge_request, nil)
|
- merge_request = local_assigns.fetch(:merge_request, nil)
|
||||||
- project = local_assigns.fetch(:project) { merge_request&.project }
|
- project = local_assigns.fetch(:project) { merge_request&.project }
|
||||||
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
|
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
|
||||||
|
- commit_status = commit.present(current_user: current_user).status_for(ref)
|
||||||
|
|
||||||
- link = commit_path(project, commit, merge_request: merge_request)
|
- link = commit_path(project, commit, merge_request: merge_request)
|
||||||
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
|
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
|
||||||
|
@ -22,7 +23,7 @@
|
||||||
%span.commit-row-message.d-block.d-sm-none
|
%span.commit-row-message.d-block.d-sm-none
|
||||||
·
|
·
|
||||||
= commit.short_id
|
= commit.short_id
|
||||||
- if commit.status(ref)
|
- if commit_status
|
||||||
.d-block.d-sm-none
|
.d-block.d-sm-none
|
||||||
= render_commit_status(commit, ref: ref)
|
= render_commit_status(commit, ref: ref)
|
||||||
- if commit.description?
|
- if commit.description?
|
||||||
|
@ -45,7 +46,7 @@
|
||||||
- else
|
- else
|
||||||
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
|
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
|
||||||
|
|
||||||
- if commit.status(ref)
|
- if commit_status
|
||||||
= render_commit_status(commit, ref: ref)
|
= render_commit_status(commit, ref: ref)
|
||||||
|
|
||||||
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
|
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
|
||||||
|
|
|
@ -4,9 +4,10 @@
|
||||||
%ul.unstyled-list.related-merge-requests
|
%ul.unstyled-list.related-merge-requests
|
||||||
- has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
|
- has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
|
||||||
- @merge_requests.each do |merge_request|
|
- @merge_requests.each do |merge_request|
|
||||||
|
- merge_request = merge_request.present(current_user: current_user)
|
||||||
%li
|
%li
|
||||||
%span.merge-request-ci-status
|
%span.merge-request-ci-status
|
||||||
- if merge_request.head_pipeline
|
- if merge_request.can_read_pipeline?
|
||||||
= render_pipeline_status(merge_request.head_pipeline)
|
= render_pipeline_status(merge_request.head_pipeline)
|
||||||
- elsif has_any_head_pipeline
|
- elsif has_any_head_pipeline
|
||||||
= icon('blank fw')
|
= icon('blank fw')
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
%li
|
%li
|
||||||
- target = @project.repository.find_branch(branch).dereferenced_target
|
- target = @project.repository.find_branch(branch).dereferenced_target
|
||||||
- pipeline = @project.pipeline_for(branch, target.sha) if target
|
- pipeline = @project.pipeline_for(branch, target.sha) if target
|
||||||
- if pipeline
|
- if can?(current_user, :read_pipeline, pipeline)
|
||||||
%span.related-branch-ci-status
|
%span.related-branch-ci-status
|
||||||
= render_pipeline_status(pipeline)
|
= render_pipeline_status(pipeline)
|
||||||
%span.related-branch-info
|
%span.related-branch-info
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
%li.issuable-status.d-none.d-sm-inline-block
|
%li.issuable-status.d-none.d-sm-inline-block
|
||||||
= icon('ban')
|
= icon('ban')
|
||||||
CLOSED
|
CLOSED
|
||||||
- if merge_request.head_pipeline
|
- if can?(current_user, :read_pipeline, merge_request.head_pipeline)
|
||||||
%li.issuable-pipeline-status.d-none.d-sm-inline-block
|
%li.issuable-pipeline-status.d-none.d-sm-inline-block
|
||||||
= render_pipeline_status(merge_request.head_pipeline)
|
= render_pipeline_status(merge_request.head_pipeline)
|
||||||
- if merge_request.open? && merge_request.broken?
|
- if merge_request.open? && merge_request.broken?
|
||||||
|
|
|
@ -6,23 +6,22 @@
|
||||||
= preserve(markdown(commit.description, pipeline: :single_line))
|
= preserve(markdown(commit.description, pipeline: :single_line))
|
||||||
|
|
||||||
.info-well
|
.info-well
|
||||||
- if commit.status
|
.well-segment.pipeline-info
|
||||||
.well-segment.pipeline-info
|
.icon-container
|
||||||
.icon-container
|
= icon('clock-o')
|
||||||
= icon('clock-o')
|
= pluralize @pipeline.total_size, "job"
|
||||||
= pluralize @pipeline.total_size, "job"
|
- if @pipeline.ref
|
||||||
- if @pipeline.ref
|
from
|
||||||
from
|
- if @pipeline.ref_exists?
|
||||||
- if @pipeline.ref_exists?
|
= link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
|
||||||
= link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
|
- else
|
||||||
- else
|
%span.ref-name
|
||||||
%span.ref-name
|
= @pipeline.ref
|
||||||
= @pipeline.ref
|
- if @pipeline.duration
|
||||||
- if @pipeline.duration
|
in
|
||||||
in
|
= time_interval_in_words(@pipeline.duration)
|
||||||
= time_interval_in_words(@pipeline.duration)
|
- if @pipeline.queued_duration
|
||||||
- if @pipeline.queued_duration
|
= "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
|
||||||
= "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
|
|
||||||
|
|
||||||
.well-segment.branch-info
|
.well-segment.branch-info
|
||||||
.icon-container.commit-icon
|
.icon-container.commit-icon
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
%tr
|
%tr
|
||||||
%td
|
%td
|
||||||
- if can?(current_user, :admin_trigger, trigger)
|
- if trigger.has_token_exposed?
|
||||||
%span= trigger.token
|
%span= trigger.token
|
||||||
= clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard")
|
= clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard")
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- @no_container = true
|
- @no_container = true
|
||||||
- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project)
|
- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home)
|
||||||
- breadcrumb_title s_("Wiki|Pages")
|
- breadcrumb_title s_("Wiki|Pages")
|
||||||
- page_title s_("Wiki|Pages"), _("Wiki")
|
- page_title s_("Wiki|Pages"), _("Wiki")
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
- breadcrumb_title @page.title.capitalize
|
- breadcrumb_title @page.title.capitalize
|
||||||
- wiki_breadcrumb_dropdown_links(@page.slug)
|
- wiki_breadcrumb_dropdown_links(@page.slug)
|
||||||
- page_title @page.title.capitalize, _("Wiki")
|
- page_title @page.title.capitalize, _("Wiki")
|
||||||
- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project)
|
- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home)
|
||||||
|
|
||||||
.wiki-page-header.has-sidebar-toggle
|
.wiki-page-header.has-sidebar-toggle
|
||||||
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
|
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
.prepend-top-0
|
.prepend-top-0
|
||||||
- if project.archived
|
- if project.archived
|
||||||
%span.prepend-left-10.badge.badge-warning archived
|
%span.prepend-left-10.badge.badge-warning archived
|
||||||
- if can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
|
- if can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
|
||||||
%span.prepend-left-10
|
%span.prepend-left-10
|
||||||
= render_project_pipeline_status(project.pipeline_status)
|
= render_project_pipeline_status(project.pipeline_status)
|
||||||
- if forks
|
- if forks
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
# Alias import callbacks under the /users/auth endpoint so that
|
||||||
|
# the OAuth2 callback URL can be restricted under http://example.com/users/auth
|
||||||
|
# instead of http://example.com.
|
||||||
|
Devise.omniauth_providers.each do |provider|
|
||||||
|
next if provider == 'ldapmain'
|
||||||
|
|
||||||
|
get "/users/auth/-/import/#{provider}/callback", to: "import/#{provider}#callback", as: "users_import_#{provider}_callback"
|
||||||
|
end
|
||||||
|
|
||||||
namespace :import do
|
namespace :import do
|
||||||
resource :github, only: [:create, :new], controller: :github do
|
resource :github, only: [:create, :new], controller: :github do
|
||||||
post :personal_access_token
|
post :personal_access_token
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UpdateProjectImportVisibilityLevel < ActiveRecord::Migration
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
BATCH_SIZE = 100
|
||||||
|
|
||||||
|
PRIVATE = 0
|
||||||
|
INTERNAL = 10
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
class Namespace < ActiveRecord::Base
|
||||||
|
self.table_name = 'namespaces'
|
||||||
|
end
|
||||||
|
|
||||||
|
class Project < ActiveRecord::Base
|
||||||
|
include EachBatch
|
||||||
|
|
||||||
|
belongs_to :namespace
|
||||||
|
|
||||||
|
IMPORT_TYPE = 'gitlab_project'
|
||||||
|
|
||||||
|
scope :with_group_visibility, ->(visibility) do
|
||||||
|
joins(:namespace)
|
||||||
|
.where(namespaces: { type: 'Group', visibility_level: visibility })
|
||||||
|
.where(import_type: IMPORT_TYPE)
|
||||||
|
.where('projects.visibility_level > namespaces.visibility_level')
|
||||||
|
end
|
||||||
|
|
||||||
|
self.table_name = 'projects'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
# Update project's visibility to be the same as the group
|
||||||
|
# if it is more restrictive than `PUBLIC`.
|
||||||
|
update_projects_visibility(PRIVATE)
|
||||||
|
update_projects_visibility(INTERNAL)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# no-op: unrecoverable data migration
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def update_projects_visibility(visibility)
|
||||||
|
say_with_time("Updating project visibility to #{visibility} on #{Project::IMPORT_TYPE} imports.") do
|
||||||
|
Project.with_group_visibility(visibility).select(:id).each_batch(of: BATCH_SIZE) do |batch, _index|
|
||||||
|
batch_sql = Gitlab::Database.mysql? ? batch.pluck(:id).join(', ') : batch.select(:id).to_sql
|
||||||
|
|
||||||
|
say("Updating #{batch.size} items.", true)
|
||||||
|
|
||||||
|
execute("UPDATE projects SET visibility_level = '#{visibility}' WHERE id IN (#{batch_sql})")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,7 +11,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20181123042307) do
|
ActiveRecord::Schema.define(version: 20181219130552) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
|
@ -5,6 +5,13 @@ description: "Set and configure Git protocol v2"
|
||||||
# Configuring Git Protocol v2
|
# Configuring Git Protocol v2
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/46555) in GitLab 11.4.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/46555) in GitLab 11.4.
|
||||||
|
> [Temporarily disabled](https://gitlab.com/gitlab-org/gitlab-ce/issues/55769) in GitLab 11.5.8, 11.6.6, 11.7.1, and 11.8+
|
||||||
|
|
||||||
|
NOTE: **Note:**
|
||||||
|
Git protocol v2 support has been [temporarily disabled](https://gitlab.com/gitlab-org/gitlab-ce/issues/55769),
|
||||||
|
as a feature used to hide certain internal references does not function when it
|
||||||
|
is enabled, and this has a security impact. Once this problem has been resolved,
|
||||||
|
protocol v2 support will be re-enabled.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,25 @@ Gitaly. To use a custom Gitaly version in CI you need to update
|
||||||
GITALY_SERVER_VERSION. You can use the format `=revision` to use a
|
GITALY_SERVER_VERSION. You can use the format `=revision` to use a
|
||||||
non-tagged commit from https://gitlab.com/gitlab-org/gitaly in CI.
|
non-tagged commit from https://gitlab.com/gitlab-org/gitaly in CI.
|
||||||
|
|
||||||
|
To use a different Gitaly repository, e.g., if your changes are present
|
||||||
|
on a fork, you can specify a `GITALY_REPO_URL` environment variable when
|
||||||
|
running tests:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
GITALY_REPO_URL=https://gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb
|
||||||
|
```
|
||||||
|
|
||||||
|
If your fork of Gitaly is private, you can generate a [Deploy Token](../user/project/deploy_tokens/index.md)
|
||||||
|
and specify it in the URL:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
GITALY_REPO_URL=https://gitlab+deploy-token-1000:token-here@gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a custom Gitaly repository in CI, for instance if you want your
|
||||||
|
GitLab fork to always use your own Gitaly fork, set `GITALY_REPO_URL`
|
||||||
|
as a [CI environment variable](../ci/variables/README.md#variables).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[Return to Development documentation](README.md)
|
[Return to Development documentation](README.md)
|
||||||
|
|
|
@ -43,9 +43,13 @@ you to use.
|
||||||
| :--- | :---------- |
|
| :--- | :---------- |
|
||||||
| **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
|
| **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
|
||||||
| **Application description** | Fill this in if you wish. |
|
| **Application description** | Fill this in if you wish. |
|
||||||
| **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
|
| **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com/users/auth`. |
|
||||||
| **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
|
| **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
|
||||||
|
|
||||||
|
NOTE: Be sure to append `/users/auth` to the end of the callback URL
|
||||||
|
to prevent a [OAuth2 convert
|
||||||
|
redirect](http://tetraph.com/covert_redirect/) vulnerability.
|
||||||
|
|
||||||
NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will
|
NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will
|
||||||
see an "Invalid redirect_uri" message. For more details, see [the
|
see an "Invalid redirect_uri" message. For more details, see [the
|
||||||
Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html).
|
Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html).
|
||||||
|
|
|
@ -19,10 +19,15 @@ GitHub will generate an application ID and secret key for you to use.
|
||||||
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
|
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
|
||||||
- Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
|
- Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
|
||||||
- Application description: Fill this in if you wish.
|
- Application description: Fill this in if you wish.
|
||||||
- Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'. Please make sure the port is included if your GitLab instance is not configured on default port.
|
- Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth`. Please make sure the port is included if your GitLab instance is not configured on default port.
|
||||||
1. Select "Register application".
|
|
||||||
|
|
||||||
1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
|
NOTE: Be sure to append `/users/auth` to the end of the callback URL
|
||||||
|
to prevent a [OAuth2 convert
|
||||||
|
redirect](http://tetraph.com/covert_redirect/) vulnerability.
|
||||||
|
|
||||||
|
1. Select **Register application**.
|
||||||
|
|
||||||
|
1. You should now see a pair of **Client ID** and **Client Secret** near the top right of the page (see screenshot).
|
||||||
Keep this page open as you continue configuration.
|
Keep this page open as you continue configuration.
|
||||||

|

|
||||||
|
|
||||||
|
|
|
@ -1175,8 +1175,11 @@ module API
|
||||||
end
|
end
|
||||||
|
|
||||||
class Trigger < Grape::Entity
|
class Trigger < Grape::Entity
|
||||||
|
include ::API::Helpers::Presentable
|
||||||
|
|
||||||
expose :id
|
expose :id
|
||||||
expose :token, :description
|
expose :token
|
||||||
|
expose :description
|
||||||
expose :created_at, :updated_at, :last_used
|
expose :created_at, :updated_at, :last_used
|
||||||
expose :owner, using: Entities::UserBasic
|
expose :owner, using: Entities::UserBasic
|
||||||
end
|
end
|
||||||
|
|
29
lib/api/helpers/presentable.rb
Normal file
29
lib/api/helpers/presentable.rb
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module API
|
||||||
|
module Helpers
|
||||||
|
##
|
||||||
|
# This module makes it possible to use `app/presenters` with
|
||||||
|
# Grape Entities. It instantiates model presenter and passes
|
||||||
|
# options defined in the API endpoint to the presenter itself.
|
||||||
|
#
|
||||||
|
# present object, with: Entities::Something,
|
||||||
|
# current_user: current_user,
|
||||||
|
# another_option: 'my options'
|
||||||
|
#
|
||||||
|
# Example above will make `current_user` and `another_option`
|
||||||
|
# values available in the subclass of `Gitlab::View::Presenter`
|
||||||
|
# thorough a separate method in the presenter.
|
||||||
|
#
|
||||||
|
# The model class needs to have `::Presentable` module mixed in
|
||||||
|
# if you want to use `API::Helpers::Presentable`.
|
||||||
|
#
|
||||||
|
module Presentable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def initialize(object, options = {})
|
||||||
|
super(object.present(options), options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -76,7 +76,7 @@ module API
|
||||||
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
|
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
|
||||||
end
|
end
|
||||||
get ':id/pipelines/:pipeline_id' do
|
get ':id/pipelines/:pipeline_id' do
|
||||||
authorize! :read_pipeline, user_project
|
authorize! :read_pipeline, pipeline
|
||||||
|
|
||||||
present pipeline, with: Entities::Pipeline
|
present pipeline, with: Entities::Pipeline
|
||||||
end
|
end
|
||||||
|
@ -89,7 +89,7 @@ module API
|
||||||
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
|
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
|
||||||
end
|
end
|
||||||
post ':id/pipelines/:pipeline_id/retry' do
|
post ':id/pipelines/:pipeline_id/retry' do
|
||||||
authorize! :update_pipeline, user_project
|
authorize! :update_pipeline, pipeline
|
||||||
|
|
||||||
pipeline.retry_failed(current_user)
|
pipeline.retry_failed(current_user)
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ module API
|
||||||
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
|
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
|
||||||
end
|
end
|
||||||
post ':id/pipelines/:pipeline_id/cancel' do
|
post ':id/pipelines/:pipeline_id/cancel' do
|
||||||
authorize! :update_pipeline, user_project
|
authorize! :update_pipeline, pipeline
|
||||||
|
|
||||||
pipeline.cancel_running
|
pipeline.cancel_running
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ module API
|
||||||
|
|
||||||
triggers = user_project.triggers.includes(:trigger_requests)
|
triggers = user_project.triggers.includes(:trigger_requests)
|
||||||
|
|
||||||
present paginate(triggers), with: Entities::Trigger
|
present paginate(triggers), with: Entities::Trigger, current_user: current_user
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ module API
|
||||||
trigger = user_project.triggers.find(params.delete(:trigger_id))
|
trigger = user_project.triggers.find(params.delete(:trigger_id))
|
||||||
break not_found!('Trigger') unless trigger
|
break not_found!('Trigger') unless trigger
|
||||||
|
|
||||||
present trigger, with: Entities::Trigger
|
present trigger, with: Entities::Trigger, current_user: current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
desc 'Create a trigger' do
|
desc 'Create a trigger' do
|
||||||
|
@ -85,7 +85,7 @@ module API
|
||||||
declared_params(include_missing: false).merge(owner: current_user))
|
declared_params(include_missing: false).merge(owner: current_user))
|
||||||
|
|
||||||
if trigger.valid?
|
if trigger.valid?
|
||||||
present trigger, with: Entities::Trigger
|
present trigger, with: Entities::Trigger, current_user: current_user
|
||||||
else
|
else
|
||||||
render_validation_error!(trigger)
|
render_validation_error!(trigger)
|
||||||
end
|
end
|
||||||
|
@ -106,7 +106,7 @@ module API
|
||||||
break not_found!('Trigger') unless trigger
|
break not_found!('Trigger') unless trigger
|
||||||
|
|
||||||
if trigger.update(declared_params(include_missing: false))
|
if trigger.update(declared_params(include_missing: false))
|
||||||
present trigger, with: Entities::Trigger
|
present trigger, with: Entities::Trigger, current_user: current_user
|
||||||
else
|
else
|
||||||
render_validation_error!(trigger)
|
render_validation_error!(trigger)
|
||||||
end
|
end
|
||||||
|
@ -127,7 +127,7 @@ module API
|
||||||
|
|
||||||
if trigger.update(owner: current_user)
|
if trigger.update(owner: current_user)
|
||||||
status :ok
|
status :ok
|
||||||
present trigger, with: Entities::Trigger
|
present trigger, with: Entities::Trigger, current_user: current_user
|
||||||
else
|
else
|
||||||
render_validation_error!(trigger)
|
render_validation_error!(trigger)
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,10 @@ module Banzai
|
||||||
#
|
#
|
||||||
# Based on HTML::Pipeline::AutolinkFilter
|
# Based on HTML::Pipeline::AutolinkFilter
|
||||||
#
|
#
|
||||||
|
# Note that our CommonMark parser, `commonmarker` (using the autolink extension)
|
||||||
|
# handles standard autolinking, like http/https. We detect additional
|
||||||
|
# schemes (smb, rdar, etc).
|
||||||
|
#
|
||||||
# Context options:
|
# Context options:
|
||||||
# :autolink - Boolean, skips all processing done by this filter when false
|
# :autolink - Boolean, skips all processing done by this filter when false
|
||||||
# :link_attr - Hash of attributes for the generated links
|
# :link_attr - Hash of attributes for the generated links
|
||||||
|
@ -107,10 +111,13 @@ module Banzai
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# match has come from node.to_html above, so we know it's encoded
|
# Since this came from a Text node, make sure the new href is encoded.
|
||||||
# correctly.
|
# `commonmarker` percent encodes the domains of links it handles, so
|
||||||
|
# do the same (instead of using `normalized_encode`).
|
||||||
|
href_safe = Addressable::URI.encode(match).html_safe
|
||||||
|
|
||||||
html_safe_match = match.html_safe
|
html_safe_match = match.html_safe
|
||||||
options = link_options.merge(href: html_safe_match)
|
options = link_options.merge(href: href_safe)
|
||||||
|
|
||||||
content_tag(:a, html_safe_match, options) + dropped
|
content_tag(:a, html_safe_match, options) + dropped
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,17 +4,29 @@ module Banzai
|
||||||
module Filter
|
module Filter
|
||||||
# HTML Filter to modify the attributes of external links
|
# HTML Filter to modify the attributes of external links
|
||||||
class ExternalLinkFilter < HTML::Pipeline::Filter
|
class ExternalLinkFilter < HTML::Pipeline::Filter
|
||||||
SCHEMES = ['http', 'https', nil].freeze
|
SCHEMES = ['http', 'https', nil].freeze
|
||||||
|
RTLO = "\u202E".freeze
|
||||||
|
ENCODED_RTLO = '%E2%80%AE'.freeze
|
||||||
|
|
||||||
def call
|
def call
|
||||||
links.each do |node|
|
links.each do |node|
|
||||||
uri = uri(node['href'].to_s)
|
# URI.parse does stricter checking on the url than Addressable,
|
||||||
|
# such as on `mailto:` links. Since we've been using it, do an
|
||||||
|
# initial parse for validity and then use Addressable
|
||||||
|
# for IDN support, etc
|
||||||
|
uri = uri_strict(node['href'].to_s)
|
||||||
|
if uri
|
||||||
|
node.set_attribute('href', uri.to_s)
|
||||||
|
addressable_uri = addressable_uri(node['href'])
|
||||||
|
else
|
||||||
|
addressable_uri = nil
|
||||||
|
end
|
||||||
|
|
||||||
node.set_attribute('href', uri.to_s) if uri
|
unless internal_url?(addressable_uri)
|
||||||
|
punycode_autolink_node!(addressable_uri, node)
|
||||||
if SCHEMES.include?(uri&.scheme) && !internal_url?(uri)
|
sanitize_link_text!(node)
|
||||||
node.set_attribute('rel', 'nofollow noreferrer noopener')
|
add_malicious_tooltip!(addressable_uri, node)
|
||||||
node.set_attribute('target', '_blank')
|
add_nofollow!(addressable_uri, node)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,12 +35,18 @@ module Banzai
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def uri(href)
|
def uri_strict(href)
|
||||||
URI.parse(href)
|
URI.parse(href)
|
||||||
rescue URI::Error
|
rescue URI::Error
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def addressable_uri(href)
|
||||||
|
Addressable::URI.parse(href)
|
||||||
|
rescue Addressable::URI::InvalidURIError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def links
|
def links
|
||||||
query = 'descendant-or-self::a[@href and not(@href = "")]'
|
query = 'descendant-or-self::a[@href and not(@href = "")]'
|
||||||
doc.xpath(query)
|
doc.xpath(query)
|
||||||
|
@ -45,6 +63,57 @@ module Banzai
|
||||||
def internal_url
|
def internal_url
|
||||||
@internal_url ||= URI.parse(Gitlab.config.gitlab.url)
|
@internal_url ||= URI.parse(Gitlab.config.gitlab.url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Only replace an autolink with an IDN with it's punycode
|
||||||
|
# version if we need emailable links. Otherwise let it
|
||||||
|
# be shown normally and the tooltips will show the
|
||||||
|
# punycode version.
|
||||||
|
def punycode_autolink_node!(uri, node)
|
||||||
|
return unless uri
|
||||||
|
return unless context[:emailable_links]
|
||||||
|
|
||||||
|
unencoded_uri_str = Addressable::URI.unencode(node['href'])
|
||||||
|
|
||||||
|
if unencoded_uri_str == node.content && idn?(uri)
|
||||||
|
node.content = uri.normalize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# escape any right-to-left (RTLO) characters in link text
|
||||||
|
def sanitize_link_text!(node)
|
||||||
|
node.inner_html = node.inner_html.gsub(RTLO, ENCODED_RTLO)
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the domain is an international domain name (IDN),
|
||||||
|
# let's expose with a tooltip in case it's intended
|
||||||
|
# to be malicious. This is particularly useful for links
|
||||||
|
# where the link text is not the same as the actual link.
|
||||||
|
# We will continue to show the unicode version of the domain
|
||||||
|
# in autolinked link text, which could contain emojis, etc.
|
||||||
|
#
|
||||||
|
# Also show the tooltip if the url contains the RTLO character,
|
||||||
|
# as this is an indicator of a malicious link
|
||||||
|
def add_malicious_tooltip!(uri, node)
|
||||||
|
if idn?(uri) || has_encoded_rtlo?(uri)
|
||||||
|
node.add_class('has-tooltip')
|
||||||
|
node.set_attribute('title', uri.normalize)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_nofollow!(uri, node)
|
||||||
|
if SCHEMES.include?(uri&.scheme)
|
||||||
|
node.set_attribute('rel', 'nofollow noreferrer noopener')
|
||||||
|
node.set_attribute('target', '_blank')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def idn?(uri)
|
||||||
|
uri&.normalized_host&.start_with?('xn--')
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_encoded_rtlo?(uri)
|
||||||
|
uri&.to_s&.include?(ENCODED_RTLO)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,8 @@ module Banzai
|
||||||
|
|
||||||
def self.transform_context(context)
|
def self.transform_context(context)
|
||||||
super(context).merge(
|
super(context).merge(
|
||||||
only_path: false
|
only_path: false,
|
||||||
|
emailable_links: true
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,7 +85,7 @@ module Gitlab
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
user_name: user.name,
|
user_name: user.name,
|
||||||
user_username: user.username,
|
user_username: user.username,
|
||||||
user_email: user.email,
|
user_email: user.public_email,
|
||||||
user_avatar: user.avatar_url(only_path: false),
|
user_avatar: user.avatar_url(only_path: false),
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
project: project.hook_attrs,
|
project: project.hook_attrs,
|
||||||
|
|
|
@ -43,7 +43,7 @@ module Gitlab
|
||||||
raise ProjectNotFound unless author.can?(:read_project, project)
|
raise ProjectNotFound unless author.can?(:read_project, project)
|
||||||
end
|
end
|
||||||
|
|
||||||
raise UserNotAuthorizedError unless author.can?(permission, project || noteable)
|
raise UserNotAuthorizedError unless author.can?(permission, try(:noteable) || project)
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_record!(record:, invalid_exception:, record_name:)
|
def verify_record!(record:, invalid_exception:, record_name:)
|
||||||
|
|
|
@ -13,10 +13,12 @@ module Gitlab
|
||||||
@project = project
|
@project = project
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def lfs_download_object
|
||||||
|
LfsDownloadObject.new(oid: lfs_object.oid, size: lfs_object.size, link: lfs_object.link)
|
||||||
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
Projects::LfsPointers::LfsDownloadService
|
Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object).execute
|
||||||
.new(project)
|
|
||||||
.execute(lfs_object.oid, lfs_object.download_link)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,11 +9,11 @@ module Gitlab
|
||||||
|
|
||||||
attr_reader :attributes
|
attr_reader :attributes
|
||||||
|
|
||||||
expose_attribute :oid, :download_link
|
expose_attribute :oid, :link, :size
|
||||||
|
|
||||||
# Builds a lfs_object
|
# Builds a lfs_object
|
||||||
def self.from_api_response(lfs_object)
|
def self.from_api_response(lfs_object)
|
||||||
new({ oid: lfs_object[0], download_link: lfs_object[1] })
|
new({ oid: lfs_object.oid, link: lfs_object.link, size: lfs_object.size })
|
||||||
end
|
end
|
||||||
|
|
||||||
# Builds a new lfs_object using a Hash that was built from a JSON payload.
|
# Builds a new lfs_object using a Hash that was built from a JSON payload.
|
||||||
|
|
|
@ -103,7 +103,7 @@ module Gitlab
|
||||||
|
|
||||||
def project_params
|
def project_params
|
||||||
@project_params ||= begin
|
@project_params ||= begin
|
||||||
attrs = json_params.merge(override_params)
|
attrs = json_params.merge(override_params).merge(visibility_level)
|
||||||
|
|
||||||
# Cleaning all imported and overridden params
|
# Cleaning all imported and overridden params
|
||||||
Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs,
|
Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs,
|
||||||
|
@ -123,6 +123,13 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visibility_level
|
||||||
|
level = override_params['visibility_level'] || json_params['visibility_level'] || @project.visibility_level
|
||||||
|
level = @project.group.visibility_level if @project.group && level > @project.group.visibility_level
|
||||||
|
|
||||||
|
{ 'visibility_level' => level }
|
||||||
|
end
|
||||||
|
|
||||||
# Given a relation hash containing one or more models and its relationships,
|
# Given a relation hash containing one or more models and its relationships,
|
||||||
# loops through each model and each object from a model type and
|
# loops through each model and each object from a model type and
|
||||||
# and assigns its correspondent attributes hash from +tree_hash+
|
# and assigns its correspondent attributes hash from +tree_hash+
|
||||||
|
|
|
@ -6,6 +6,7 @@ module Gitlab
|
||||||
def initialize(project)
|
def initialize(project)
|
||||||
@project = project
|
@project = project
|
||||||
@errors = []
|
@errors = []
|
||||||
|
@logger = Gitlab::Import::Logger.build
|
||||||
end
|
end
|
||||||
|
|
||||||
def active_export_count
|
def active_export_count
|
||||||
|
@ -21,19 +22,14 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def error(error)
|
def error(error)
|
||||||
error_out(error.message, caller[0].dup)
|
log_error(message: error.message, caller: caller[0].dup)
|
||||||
add_error_message(error.message)
|
log_debug(backtrace: error.backtrace&.join("\n"))
|
||||||
|
|
||||||
# Debug:
|
add_error_message(error.message)
|
||||||
if error.backtrace
|
|
||||||
Rails.logger.error("Import/Export backtrace: #{error.backtrace.join("\n")}")
|
|
||||||
else
|
|
||||||
Rails.logger.error("No backtrace found")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_error_message(error_message)
|
def add_error_message(message)
|
||||||
@errors << error_message
|
@errors << filtered_error_message(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_export_in_progress?
|
def after_export_in_progress?
|
||||||
|
@ -50,8 +46,25 @@ module Gitlab
|
||||||
@project.disk_path
|
@project.disk_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_out(message, caller)
|
def log_error(details)
|
||||||
Rails.logger.error("Import/Export error raised on #{caller}: #{message}")
|
@logger.error(log_base_data.merge(details))
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_debug(details)
|
||||||
|
@logger.debug(log_base_data.merge(details))
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_base_data
|
||||||
|
{
|
||||||
|
importer: 'Import/Export',
|
||||||
|
import_jid: @project&.import_state&.import_jid,
|
||||||
|
project_id: @project&.id,
|
||||||
|
project_path: @project&.full_path
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_error_message(message)
|
||||||
|
Projects::ImportErrorFilter.filter_message(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_export_lock_file
|
def after_export_lock_file
|
||||||
|
|
|
@ -125,7 +125,8 @@ module Gitlab
|
||||||
# allow non-regex validations, etc), `NAMESPACE_FORMAT_REGEX_JS` serves as a Javascript-compatible version of
|
# allow non-regex validations, etc), `NAMESPACE_FORMAT_REGEX_JS` serves as a Javascript-compatible version of
|
||||||
# `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation
|
# `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation
|
||||||
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
|
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
|
||||||
PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze
|
PATH_START_CHAR = '[a-zA-Z0-9_\.]'.freeze
|
||||||
|
PATH_REGEX_STR = PATH_START_CHAR + '[a-zA-Z0-9_\-\.]*'.freeze
|
||||||
NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
|
NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
|
||||||
|
|
||||||
NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze
|
NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze
|
||||||
|
|
97
lib/safe_zip/entry.rb
Normal file
97
lib/safe_zip/entry.rb
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module SafeZip
|
||||||
|
class Entry
|
||||||
|
attr_reader :zip_archive, :zip_entry
|
||||||
|
attr_reader :path, :params
|
||||||
|
|
||||||
|
def initialize(zip_archive, zip_entry, params)
|
||||||
|
@zip_archive = zip_archive
|
||||||
|
@zip_entry = zip_entry
|
||||||
|
@params = params
|
||||||
|
@path = ::File.expand_path(zip_entry.name, params.extract_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def path_dir
|
||||||
|
::File.dirname(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def real_path_dir
|
||||||
|
::File.realpath(path_dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
def exist?
|
||||||
|
::File.exist?(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract
|
||||||
|
# do not extract if file is not part of target directory
|
||||||
|
return false unless matching_target_directory
|
||||||
|
|
||||||
|
# do not overwrite existing file
|
||||||
|
raise SafeZip::Extract::AlreadyExistsError, "File already exists #{zip_entry.name}" if exist?
|
||||||
|
|
||||||
|
create_path_dir
|
||||||
|
|
||||||
|
if zip_entry.file?
|
||||||
|
extract_file
|
||||||
|
elsif zip_entry.directory?
|
||||||
|
extract_dir
|
||||||
|
elsif zip_entry.symlink?
|
||||||
|
extract_symlink
|
||||||
|
else
|
||||||
|
raise SafeZip::Extract::UnsupportedEntryError, "File #{zip_entry.name} cannot be extracted"
|
||||||
|
end
|
||||||
|
rescue SafeZip::Extract::Error
|
||||||
|
raise
|
||||||
|
rescue => e
|
||||||
|
raise SafeZip::Extract::ExtractError, e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def extract_file
|
||||||
|
zip_archive.extract(zip_entry, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_dir
|
||||||
|
FileUtils.mkdir(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_symlink
|
||||||
|
source_path = read_symlink
|
||||||
|
real_source_path = expand_symlink(source_path)
|
||||||
|
|
||||||
|
# ensure that source path of symlink is within target directories
|
||||||
|
unless real_source_path.start_with?(matching_target_directory)
|
||||||
|
raise SafeZip::Extract::PermissionDeniedError, "Symlink cannot be created targeting: #{source_path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
::File.symlink(source_path, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_path_dir
|
||||||
|
# Create all directories, but ignore permissions
|
||||||
|
FileUtils.mkdir_p(path_dir)
|
||||||
|
|
||||||
|
# disallow to make path dirs to point to another directories
|
||||||
|
unless path_dir == real_path_dir
|
||||||
|
raise SafeZip::Extract::PermissionDeniedError, "Directory of #{zip_entry.name} points to another directory"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def matching_target_directory
|
||||||
|
params.matching_target_directory(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_symlink
|
||||||
|
zip_archive.read(zip_entry)
|
||||||
|
end
|
||||||
|
|
||||||
|
def expand_symlink(source_path)
|
||||||
|
::File.realpath(source_path, path_dir)
|
||||||
|
rescue
|
||||||
|
raise SafeZip::Extract::SymlinkSourceDoesNotExistError, "Symlink source #{source_path} does not exist"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
73
lib/safe_zip/extract.rb
Normal file
73
lib/safe_zip/extract.rb
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module SafeZip
|
||||||
|
class Extract
|
||||||
|
Error = Class.new(StandardError)
|
||||||
|
PermissionDeniedError = Class.new(Error)
|
||||||
|
SymlinkSourceDoesNotExistError = Class.new(Error)
|
||||||
|
UnsupportedEntryError = Class.new(Error)
|
||||||
|
AlreadyExistsError = Class.new(Error)
|
||||||
|
NoMatchingError = Class.new(Error)
|
||||||
|
ExtractError = Class.new(Error)
|
||||||
|
|
||||||
|
attr_reader :archive_path
|
||||||
|
|
||||||
|
def initialize(archive_file)
|
||||||
|
@archive_path = archive_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract(opts = {})
|
||||||
|
params = SafeZip::ExtractParams.new(**opts)
|
||||||
|
|
||||||
|
if Feature.enabled?(:safezip_use_rubyzip, default_enabled: true)
|
||||||
|
extract_with_ruby_zip(params)
|
||||||
|
else
|
||||||
|
legacy_unsafe_extract_with_system_zip(params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def extract_with_ruby_zip(params)
|
||||||
|
::Zip::File.open(archive_path) do |zip_archive|
|
||||||
|
# Extract all files in the following order:
|
||||||
|
# 1. Directories first,
|
||||||
|
# 2. Files next,
|
||||||
|
# 3. Symlinks last (or anything else)
|
||||||
|
extracted = extract_all_entries(zip_archive, params,
|
||||||
|
zip_archive.lazy.select(&:directory?))
|
||||||
|
|
||||||
|
extracted += extract_all_entries(zip_archive, params,
|
||||||
|
zip_archive.lazy.select(&:file?))
|
||||||
|
|
||||||
|
extracted += extract_all_entries(zip_archive, params,
|
||||||
|
zip_archive.lazy.reject(&:directory?).reject(&:file?))
|
||||||
|
|
||||||
|
raise NoMatchingError, 'No entries extracted' unless extracted > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_all_entries(zip_archive, params, entries)
|
||||||
|
entries.count do |zip_entry|
|
||||||
|
SafeZip::Entry.new(zip_archive, zip_entry, params)
|
||||||
|
.extract
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def legacy_unsafe_extract_with_system_zip(params)
|
||||||
|
# Requires UnZip at least 6.00 Info-ZIP.
|
||||||
|
# -n never overwrite existing files
|
||||||
|
args = %W(unzip -n -qq #{archive_path})
|
||||||
|
|
||||||
|
# We add * to end of directory, because we want to extract directory and all subdirectories
|
||||||
|
args += params.directories_wildcard
|
||||||
|
|
||||||
|
# Target directory where we extract
|
||||||
|
args += %W(-d #{params.extract_path})
|
||||||
|
|
||||||
|
unless system(*args)
|
||||||
|
raise Error, 'archive failed to extract'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
36
lib/safe_zip/extract_params.rb
Normal file
36
lib/safe_zip/extract_params.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module SafeZip
|
||||||
|
class ExtractParams
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
attr_reader :directories, :extract_path
|
||||||
|
|
||||||
|
def initialize(directories:, to:)
|
||||||
|
@directories = directories
|
||||||
|
@extract_path = ::File.realpath(to)
|
||||||
|
end
|
||||||
|
|
||||||
|
def matching_target_directory(path)
|
||||||
|
target_directories.find do |directory|
|
||||||
|
path.start_with?(directory)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def target_directories
|
||||||
|
strong_memoize(:target_directories) do
|
||||||
|
directories.map do |directory|
|
||||||
|
::File.join(::File.expand_path(directory, extract_path), '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def directories_wildcard
|
||||||
|
strong_memoize(:directories_wildcard) do
|
||||||
|
directories.map do |directory|
|
||||||
|
::File.join(directory, '*')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -2780,6 +2780,9 @@ msgstr ""
|
||||||
msgid "Explore public groups"
|
msgid "Explore public groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "External Wiki"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Facebook"
|
msgid "Facebook"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
"js-cookie": "^2.1.3",
|
"js-cookie": "^2.1.3",
|
||||||
"jszip": "^3.1.3",
|
"jszip": "^3.1.3",
|
||||||
"jszip-utils": "^0.0.2",
|
"jszip-utils": "^0.0.2",
|
||||||
"katex": "^0.9.0",
|
"katex": "^0.10.0",
|
||||||
"marked": "^0.3.12",
|
"marked": "^0.3.12",
|
||||||
"mermaid": "^8.0.0-rc.8",
|
"mermaid": "^8.0.0-rc.8",
|
||||||
"monaco-editor": "^0.14.3",
|
"monaco-editor": "^0.14.3",
|
||||||
|
|
|
@ -8,6 +8,7 @@ describe Import::BitbucketController do
|
||||||
let(:secret) { "sekrettt" }
|
let(:secret) { "sekrettt" }
|
||||||
let(:refresh_token) { SecureRandom.hex(15) }
|
let(:refresh_token) { SecureRandom.hex(15) }
|
||||||
let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } }
|
let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } }
|
||||||
|
let(:code) { SecureRandom.hex(8) }
|
||||||
|
|
||||||
def assign_session_tokens
|
def assign_session_tokens
|
||||||
session[:bitbucket_token] = token
|
session[:bitbucket_token] = token
|
||||||
|
@ -32,10 +33,16 @@ describe Import::BitbucketController do
|
||||||
expires_in: expires_in,
|
expires_in: expires_in,
|
||||||
refresh_token: refresh_token)
|
refresh_token: refresh_token)
|
||||||
allow_any_instance_of(OAuth2::Client)
|
allow_any_instance_of(OAuth2::Client)
|
||||||
.to receive(:get_token).and_return(access_token)
|
.to receive(:get_token)
|
||||||
|
.with(hash_including(
|
||||||
|
'grant_type' => 'authorization_code',
|
||||||
|
'code' => code,
|
||||||
|
redirect_uri: users_import_bitbucket_callback_url),
|
||||||
|
{})
|
||||||
|
.and_return(access_token)
|
||||||
stub_omniauth_provider('bitbucket')
|
stub_omniauth_provider('bitbucket')
|
||||||
|
|
||||||
get :callback
|
get :callback, code: code
|
||||||
|
|
||||||
expect(session[:bitbucket_token]).to eq(token)
|
expect(session[:bitbucket_token]).to eq(token)
|
||||||
expect(session[:bitbucket_refresh_token]).to eq(refresh_token)
|
expect(session[:bitbucket_refresh_token]).to eq(refresh_token)
|
||||||
|
|
|
@ -12,9 +12,15 @@ describe Import::GithubController do
|
||||||
|
|
||||||
it "redirects to GitHub for an access token if logged in with GitHub" do
|
it "redirects to GitHub for an access token if logged in with GitHub" do
|
||||||
allow(controller).to receive(:logged_in_with_provider?).and_return(true)
|
allow(controller).to receive(:logged_in_with_provider?).and_return(true)
|
||||||
expect(controller).to receive(:go_to_provider_for_permissions)
|
expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
|
||||||
|
allow_any_instance_of(Gitlab::LegacyGithubImport::Client)
|
||||||
|
.to receive(:authorize_url)
|
||||||
|
.with(users_import_github_callback_url)
|
||||||
|
.and_call_original
|
||||||
|
|
||||||
get :new
|
get :new
|
||||||
|
|
||||||
|
expect(response).to have_http_status(302)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ describe Projects::IssuesController do
|
||||||
it 'redirects to signin if not logged in' do
|
it 'redirects to signin if not logged in' do
|
||||||
get :new, namespace_id: project.namespace, project_id: project
|
get :new, namespace_id: project.namespace, project_id: project
|
||||||
|
|
||||||
expect(flash[:notice]).to eq 'Please sign in to create the new issue.'
|
expect(flash[:alert]).to eq 'You need to sign in or sign up before continuing.'
|
||||||
expect(response).to redirect_to(new_user_session_path)
|
expect(response).to redirect_to(new_user_session_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,14 @@ require 'spec_helper'
|
||||||
describe Projects::PipelineSchedulesController do
|
describe Projects::PipelineSchedulesController do
|
||||||
include AccessMatchersForController
|
include AccessMatchersForController
|
||||||
|
|
||||||
|
set(:user) { create(:user) }
|
||||||
set(:project) { create(:project, :public, :repository) }
|
set(:project) { create(:project, :public, :repository) }
|
||||||
set(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
|
set(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET #index' do
|
describe 'GET #index' do
|
||||||
render_views
|
render_views
|
||||||
|
|
||||||
|
@ -14,6 +19,10 @@ describe Projects::PipelineSchedulesController do
|
||||||
create(:ci_pipeline_schedule, :inactive, project: project)
|
create(:ci_pipeline_schedule, :inactive, project: project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
it 'renders the index view' do
|
it 'renders the index view' do
|
||||||
visit_pipelines_schedules
|
visit_pipelines_schedules
|
||||||
|
|
||||||
|
@ -21,7 +30,7 @@ describe Projects::PipelineSchedulesController do
|
||||||
expect(response).to render_template(:index)
|
expect(response).to render_template(:index)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'avoids N + 1 queries' do
|
it 'avoids N + 1 queries', :request_store do
|
||||||
control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count
|
control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count
|
||||||
|
|
||||||
create_list(:ci_pipeline_schedule, 2, project: project)
|
create_list(:ci_pipeline_schedule, 2, project: project)
|
||||||
|
|
|
@ -5,7 +5,7 @@ describe Projects::PipelinesController do
|
||||||
|
|
||||||
set(:user) { create(:user) }
|
set(:user) { create(:user) }
|
||||||
let(:project) { create(:project, :public, :repository) }
|
let(:project) { create(:project, :public, :repository) }
|
||||||
let(:feature) { ProjectFeature::DISABLED }
|
let(:feature) { ProjectFeature::ENABLED }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_not_protect_default_branch
|
stub_not_protect_default_branch
|
||||||
|
@ -184,6 +184,27 @@ describe Projects::PipelinesController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when builds are disabled' do
|
||||||
|
let(:feature) { ProjectFeature::DISABLED }
|
||||||
|
|
||||||
|
it 'users can not see internal pipelines' do
|
||||||
|
get_pipeline_json
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when pipeline is external' do
|
||||||
|
let(:pipeline) { create(:ci_pipeline, source: :external, project: project) }
|
||||||
|
|
||||||
|
it 'users can see the external pipeline' do
|
||||||
|
get_pipeline_json
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response['id']).to be(pipeline.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def get_pipeline_json
|
def get_pipeline_json
|
||||||
get :show, namespace_id: project.namespace, project_id: project, id: pipeline, format: :json
|
get :show, namespace_id: project.namespace, project_id: project, id: pipeline, format: :json
|
||||||
end
|
end
|
||||||
|
@ -316,16 +337,14 @@ describe Projects::PipelinesController do
|
||||||
format: :json
|
format: :json
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when builds are enabled' do
|
it 'retries a pipeline without returning any content' do
|
||||||
let(:feature) { ProjectFeature::ENABLED }
|
expect(response).to have_gitlab_http_status(:no_content)
|
||||||
|
expect(build.reload).to be_retried
|
||||||
it 'retries a pipeline without returning any content' do
|
|
||||||
expect(response).to have_gitlab_http_status(:no_content)
|
|
||||||
expect(build.reload).to be_retried
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when builds are disabled' do
|
context 'when builds are disabled' do
|
||||||
|
let(:feature) { ProjectFeature::DISABLED }
|
||||||
|
|
||||||
it 'fails to retry pipeline' do
|
it 'fails to retry pipeline' do
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
end
|
end
|
||||||
|
@ -343,16 +362,14 @@ describe Projects::PipelinesController do
|
||||||
format: :json
|
format: :json
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when builds are enabled' do
|
it 'cancels a pipeline without returning any content' do
|
||||||
let(:feature) { ProjectFeature::ENABLED }
|
expect(response).to have_gitlab_http_status(:no_content)
|
||||||
|
expect(pipeline.reload).to be_canceled
|
||||||
it 'cancels a pipeline without returning any content' do
|
|
||||||
expect(response).to have_gitlab_http_status(:no_content)
|
|
||||||
expect(pipeline.reload).to be_canceled
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when builds are disabled' do
|
context 'when builds are disabled' do
|
||||||
|
let(:feature) { ProjectFeature::DISABLED }
|
||||||
|
|
||||||
it 'fails to retry pipeline' do
|
it 'fails to retry pipeline' do
|
||||||
expect(response).to have_gitlab_http_status(:not_found)
|
expect(response).to have_gitlab_http_status(:not_found)
|
||||||
end
|
end
|
||||||
|
|
|
@ -206,6 +206,38 @@ describe UsersController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GET #contributed' do
|
||||||
|
let(:project) { create(:project, :public) }
|
||||||
|
let(:current_user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(current_user)
|
||||||
|
|
||||||
|
project.add_developer(public_user)
|
||||||
|
project.add_developer(private_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with public profile' do
|
||||||
|
it 'renders contributed projects' do
|
||||||
|
create(:push_event, project: project, author: public_user)
|
||||||
|
|
||||||
|
get :contributed, username: public_user.username
|
||||||
|
|
||||||
|
expect(assigns[:contributed_projects]).not_to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with private profile' do
|
||||||
|
it 'does not render contributed projects' do
|
||||||
|
create(:push_event, project: project, author: private_user)
|
||||||
|
|
||||||
|
get :contributed, username: private_user.username
|
||||||
|
|
||||||
|
expect(assigns[:contributed_projects]).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET #snippets' do
|
describe 'GET #snippets' do
|
||||||
before do
|
before do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
|
|
|
@ -144,6 +144,27 @@ describe 'Dashboard Projects' do
|
||||||
expect(page).to have_link('Commit: passed')
|
expect(page).to have_link('Commit: passed')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'guest user of project and project has private pipelines' do
|
||||||
|
let(:guest_user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.update(public_builds: false)
|
||||||
|
project.add_guest(guest_user)
|
||||||
|
sign_in(guest_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows that the last pipeline passed' do
|
||||||
|
visit dashboard_projects_path
|
||||||
|
|
||||||
|
page.within('.controls') do
|
||||||
|
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
|
||||||
|
expect(page).not_to have_css('.ci-status-link')
|
||||||
|
expect(page).not_to have_css('.ci-status-icon-success')
|
||||||
|
expect(page).not_to have_link('Commit: passed')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'last push widget', :use_clean_rails_memory_store_caching do
|
context 'last push widget', :use_clean_rails_memory_store_caching do
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe 'Math rendering', :js do
|
describe 'Math rendering', :js do
|
||||||
|
let!(:project) { create(:project, :public) }
|
||||||
|
|
||||||
it 'renders inline and display math correctly' do
|
it 'renders inline and display math correctly' do
|
||||||
description = <<~MATH
|
description = <<~MATH
|
||||||
This math is inline $`a^2+b^2=c^2`$.
|
This math is inline $`a^2+b^2=c^2`$.
|
||||||
|
@ -11,12 +13,26 @@ describe 'Math rendering', :js do
|
||||||
```
|
```
|
||||||
MATH
|
MATH
|
||||||
|
|
||||||
project = create(:project, :public)
|
|
||||||
issue = create(:issue, project: project, description: description)
|
issue = create(:issue, project: project, description: description)
|
||||||
|
|
||||||
visit project_issue_path(project, issue)
|
visit project_issue_path(project, issue)
|
||||||
|
|
||||||
expect(page).to have_selector('.katex .mord.mathit', text: 'b')
|
expect(page).to have_selector('.katex .mord.mathdefault', text: 'b')
|
||||||
expect(page).to have_selector('.katex-display .mord.mathit', text: 'b')
|
expect(page).to have_selector('.katex-display .mord.mathdefault', text: 'b')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'only renders non XSS links' do
|
||||||
|
description = <<~MATH
|
||||||
|
This link is valid $`\\href{javascript:alert('xss');}{xss}`$.
|
||||||
|
|
||||||
|
This link is valid $`\\href{https://gitlab.com}{Gitlab}`$.
|
||||||
|
MATH
|
||||||
|
|
||||||
|
issue = create(:issue, project: project, description: description)
|
||||||
|
|
||||||
|
visit project_issue_path(project, issue)
|
||||||
|
|
||||||
|
expect(page).to have_selector('.katex-error', text: "\href{javascript:alert('xss');}{xss}")
|
||||||
|
expect(page).to have_selector('.katex-html a', text: 'Gitlab')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,6 +15,9 @@ describe 'Projects > Settings > User changes default branch' do
|
||||||
let(:project) { create(:project, :repository, namespace: user.namespace) }
|
let(:project) { create(:project, :repository, namespace: user.namespace) }
|
||||||
|
|
||||||
it 'allows to change the default branch', :js do
|
it 'allows to change the default branch', :js do
|
||||||
|
# Otherwise, running JS may overwrite our change to project_default_branch
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
select2('fix', from: '#project_default_branch')
|
select2('fix', from: '#project_default_branch')
|
||||||
|
|
||||||
page.within '#default-branch-settings' do
|
page.within '#default-branch-settings' do
|
||||||
|
|
|
@ -452,9 +452,9 @@ describe "Internal Project Access" do
|
||||||
it { is_expected.to be_allowed_for(:owner).of(project) }
|
it { is_expected.to be_allowed_for(:owner).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:maintainer).of(project) }
|
it { is_expected.to be_allowed_for(:maintainer).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:developer).of(project) }
|
it { is_expected.to be_allowed_for(:developer).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:reporter).of(project) }
|
it { is_expected.to be_denied_for(:reporter).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:guest).of(project) }
|
it { is_expected.to be_denied_for(:guest).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:user) }
|
it { is_expected.to be_denied_for(:user) }
|
||||||
it { is_expected.to be_denied_for(:external) }
|
it { is_expected.to be_denied_for(:external) }
|
||||||
it { is_expected.to be_denied_for(:visitor) }
|
it { is_expected.to be_denied_for(:visitor) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -485,7 +485,7 @@ describe "Private Project Access" do
|
||||||
it { is_expected.to be_allowed_for(:owner).of(project) }
|
it { is_expected.to be_allowed_for(:owner).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:maintainer).of(project) }
|
it { is_expected.to be_allowed_for(:maintainer).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:developer).of(project) }
|
it { is_expected.to be_allowed_for(:developer).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:reporter).of(project) }
|
it { is_expected.to be_denied_for(:reporter).of(project) }
|
||||||
it { is_expected.to be_denied_for(:guest).of(project) }
|
it { is_expected.to be_denied_for(:guest).of(project) }
|
||||||
it { is_expected.to be_denied_for(:user) }
|
it { is_expected.to be_denied_for(:user) }
|
||||||
it { is_expected.to be_denied_for(:external) }
|
it { is_expected.to be_denied_for(:external) }
|
||||||
|
|
|
@ -272,11 +272,11 @@ describe "Public Project Access" do
|
||||||
it { is_expected.to be_allowed_for(:owner).of(project) }
|
it { is_expected.to be_allowed_for(:owner).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:maintainer).of(project) }
|
it { is_expected.to be_allowed_for(:maintainer).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:developer).of(project) }
|
it { is_expected.to be_allowed_for(:developer).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:reporter).of(project) }
|
it { is_expected.to be_denied_for(:reporter).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:guest).of(project) }
|
it { is_expected.to be_denied_for(:guest).of(project) }
|
||||||
it { is_expected.to be_allowed_for(:user) }
|
it { is_expected.to be_denied_for(:user) }
|
||||||
it { is_expected.to be_allowed_for(:external) }
|
it { is_expected.to be_denied_for(:external) }
|
||||||
it { is_expected.to be_allowed_for(:visitor) }
|
it { is_expected.to be_denied_for(:visitor) }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /:project_path/environments" do
|
describe "GET /:project_path/environments" do
|
||||||
|
|
|
@ -31,4 +31,16 @@ describe ContributedProjectsFinder do
|
||||||
|
|
||||||
it { is_expected.to match_array([private_project, internal_project, public_project]) }
|
it { is_expected.to match_array([private_project, internal_project, public_project]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'user with private profile' do
|
||||||
|
it 'does not return contributed projects' do
|
||||||
|
private_user = create(:user, private_profile: true)
|
||||||
|
public_project.add_maintainer(private_user)
|
||||||
|
create(:push_event, project: public_project, author: private_user)
|
||||||
|
|
||||||
|
projects = described_class.new(private_user).execute(current_user)
|
||||||
|
|
||||||
|
expect(projects).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
BIN
spec/fixtures/pages_non_writeable.zip
vendored
Normal file
BIN
spec/fixtures/pages_non_writeable.zip
vendored
Normal file
Binary file not shown.
BIN
spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zip
vendored
Normal file
BIN
spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zip
vendored
Normal file
Binary file not shown.
BIN
spec/fixtures/safe_zip/invalid-symlinks-outside.zip
vendored
Normal file
BIN
spec/fixtures/safe_zip/invalid-symlinks-outside.zip
vendored
Normal file
Binary file not shown.
BIN
spec/fixtures/safe_zip/valid-non-writeable.zip
vendored
Normal file
BIN
spec/fixtures/safe_zip/valid-non-writeable.zip
vendored
Normal file
Binary file not shown.
BIN
spec/fixtures/safe_zip/valid-simple.zip
vendored
Normal file
BIN
spec/fixtures/safe_zip/valid-simple.zip
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue