Commit da2e2b9f authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents 322c6da5 e4c61726
...@@ -2,6 +2,70 @@ ...@@ -2,6 +2,70 @@
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.7.2 (2019-01-29)
### Security (24 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
- Sanitize user full name to clean up any URL to prevent mail clients from auto-linking URLs. !2828
- Fixed XSS content in KaTex links.
- Disallows unauthorized users from accessing the pipelines section.
- Verify that LFS upload requests are genuine.
- Extract GitLab Pages using RubyZip.
- 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.
- Add subresources removal to member destroy service.
- Add more LFS validations to prevent forgery.
- 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.
- Group guests are no longer able to see merge requests they don't have access to at group level.
- 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.7.1 (2019-01-28)
### Security (24 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
- Sanitize user full name to clean up any URL to prevent mail clients from auto-linking URLs. !2828
- Fixed XSS content in KaTex links.
- Disallows unauthorized users from accessing the pipelines section.
- Verify that LFS upload requests are genuine.
- Extract GitLab Pages using RubyZip.
- 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.
- Add subresources removal to member destroy service.
- Add more LFS validations to prevent forgery.
- 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.
- Group guests are no longer able to see merge requests they don't have access to at group level.
- 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.
## 11.7.0 (2019-01-22) ## 11.7.0 (2019-01-22)
### Security (14 changes, 1 of them is from the community) ### Security (14 changes, 1 of them is from the community)
...@@ -188,6 +252,10 @@ entry. ...@@ -188,6 +252,10 @@ entry.
- Update url placeholder for the sentry configuration page. !24338 - Update url placeholder for the sentry configuration page. !24338
## 11.6.8 (2019-01-30)
- No changes.
## 11.6.5 (2019-01-17) ## 11.6.5 (2019-01-17)
### Fixed (5 changes) ### Fixed (5 changes)
...@@ -528,6 +596,33 @@ entry. ...@@ -528,6 +596,33 @@ entry.
- Enable Rubocop on lib/gitlab. (gfyoung) - Enable Rubocop on lib/gitlab. (gfyoung)
## 11.5.8 (2019-01-28)
### 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
- Fixed XSS content in KaTex links.
- Verify that LFS upload requests are genuine.
- Extract GitLab Pages using RubyZip.
- 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.
- Disallows unauthorized users from accessing the pipelines section.
- Add more LFS validations to prevent forgery.
- 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.
## 11.5.5 (2018-12-20) ## 11.5.5 (2018-12-20)
### Security (1 change) ### Security (1 change)
......
1.14.0 1.14.1
\ No newline at end of file
8.1.0 8.1.1
\ No newline at end of file
...@@ -60,6 +60,7 @@ gem 'u2f', '~> 0.2.1' ...@@ -60,6 +60,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'
......
...@@ -1177,6 +1177,7 @@ DEPENDENCIES ...@@ -1177,6 +1177,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)
rugged (~> 0.27) rugged (~> 0.27)
sanitize (~> 4.6) sanitize (~> 4.6)
sass (~> 3.5) sass (~> 3.5)
......
...@@ -28,10 +28,10 @@ export default { ...@@ -28,10 +28,10 @@ export default {
}, },
computed: { computed: {
statusHtml() { statusHtml() {
if (this.user.status.emoji && this.user.status.message) { if (this.user.status.emoji && this.user.status.message_html) {
return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message}`; return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
} else if (this.user.status.message) { } else if (this.user.status.message_html) {
return this.user.status.message; return this.user.status.message_html;
} }
return ''; return '';
}, },
......
...@@ -35,7 +35,9 @@ module MembershipActions ...@@ -35,7 +35,9 @@ module MembershipActions
respond_to do |format| respond_to do |format|
format.html do format.html do
message = "User was successfully removed from #{source_type}." source = source_type == 'group' ? 'group and any subresources' : source_type
message = "User was successfully removed from #{source}."
redirect_to members_page_url, notice: message redirect_to members_page_url, notice: message
end end
......
...@@ -8,7 +8,7 @@ class Import::BitbucketController < Import::BaseController ...@@ -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 ...@@ -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
......
...@@ -83,7 +83,7 @@ class Import::GithubController < Import::BaseController ...@@ -83,7 +83,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 ...@@ -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]
...@@ -249,14 +249,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -249,14 +249,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 ...@@ -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,9 +39,12 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -39,9 +39,12 @@ 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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 = []
......
...@@ -36,6 +36,14 @@ module EmailsHelper ...@@ -36,6 +36,14 @@ module EmailsHelper
nil nil
end end
def sanitize_name(name)
if name =~ URI::DEFAULT_PARSER.regexp[:URI_REF]
name.tr('.', '_')
else
name
end
end
def password_reset_token_valid_time def password_reset_token_valid_time
valid_hours = Devise.reset_password_within / 60 / 60 valid_hours = Devise.reset_password_within / 60 / 60
if valid_hours >= 24 if valid_hours >= 24
......
# 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
...@@ -18,12 +18,13 @@ module MembersHelper ...@@ -18,12 +18,13 @@ module MembersHelper
"remove #{member.user.name} from" "remove #{member.user.name} from"
end end
"#{text} #{action} the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?" "#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
end end
def remove_member_title(member) def remove_member_title(member)
action = member.request? ? 'Deny access request' : 'Remove user' action = member.request? ? 'Deny access request' : 'Remove user'
"#{action} from #{member.real_source_type.humanize(capitalize: false)}"
"#{action} from #{source_text(member)}"
end end
def leave_confirmation_message(member_source) def leave_confirmation_message(member_source)
...@@ -35,4 +36,14 @@ module MembersHelper ...@@ -35,4 +36,14 @@ module MembersHelper
options = params.slice(:search, :sort).merge(options).permit! options = params.slice(:search, :sort).merge(options).permit!
"#{request.path}?#{options.to_param}" "#{request.path}?#{options.to_param}"
end end
private
def source_text(member)
type = member.real_source_type.humanize(capitalize: false)
return type if member.request? || member.invite? || type != 'group'
'group and any subresources'
end
end end
...@@ -307,7 +307,8 @@ module ProjectsHelper ...@@ -307,7 +307,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
...@@ -315,19 +316,24 @@ module ProjectsHelper ...@@ -315,19 +316,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 ...@@ -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 ...@@ -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 ...@@ -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
...@@ -304,7 +305,9 @@ class Commit ...@@ -304,7 +305,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 ...@@ -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 = 13 CACHE_COMMONMARK_VERSION = 14
# 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
......
# 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
...@@ -78,12 +78,15 @@ class Member < ActiveRecord::Base ...@@ -78,12 +78,15 @@ class Member < ActiveRecord::Base
scope :owners, -> { active.where(access_level: OWNER) } scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated
scope :with_user, -> (user) { where(user: user) }
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
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) { where(source: [project] + project.ancestors) }
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?
......
...@@ -12,6 +12,8 @@ class GroupMember < Member ...@@ -12,6 +12,8 @@ class GroupMember < Member
validates :source_type, format: { with: /\ANamespace\z/ } validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) }
after_create :update_two_factor_requirement, unless: :invite? after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite?
......
...@@ -12,6 +12,10 @@ class ProjectMember < Member ...@@ -12,6 +12,10 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) } default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) } scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_namespaces, ->(groups) do
joins('INNER JOIN projects ON projects.id = members.source_id')
.where('projects.namespace_id in (?)', groups.select(:id))
end
class << self class << self
# Add users to projects with passed access option # Add users to projects with passed access option
......
...@@ -377,8 +377,10 @@ class Project < ActiveRecord::Base ...@@ -377,8 +377,10 @@ class Project < ActiveRecord::Base
# "enabled" here means "not disabled". It includes private features! # "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) { scope :with_feature_enabled, ->(feature) {
access_level_attribute = ProjectFeature.access_level_attribute(feature) access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] }) enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))
with_project_feature.where(enabled_feature)
} }
# Picks a feature where the level is exactly that given. # Picks a feature where the level is exactly that given.
...@@ -465,7 +467,8 @@ class Project < ActiveRecord::Base ...@@ -465,7 +467,8 @@ class Project < ActiveRecord::Base
# logged in users to more efficiently get private projects with the given # logged in users to more efficiently get private projects with the given
# feature. # feature.
def self.with_feature_available_for_user(feature, user) def self.with_feature_available_for_user(feature, user)
visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
min_access_level = ProjectFeature.required_minimum_access_level(feature)
if user&.admin? if user&.admin?
with_feature_enabled(feature) with_feature_enabled(feature)
...@@ -473,10 +476,15 @@ class Project < ActiveRecord::Base ...@@ -473,10 +476,15 @@ class Project < ActiveRecord::Base
column = ProjectFeature.quoted_access_level_column(feature) column = ProjectFeature.quoted_access_level_column(feature)
with_project_feature with_project_feature
.where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", .where(
visible, "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
ProjectFeature::PRIVATE, " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
user.authorizations_for_projects) {
private: Gitlab::VisibilityLevel::PRIVATE,
public_visible: ProjectFeature::ENABLED,
private_visible: ProjectFeature::PRIVATE,
authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
})
else else
with_feature_access_level(feature, visible) with_feature_access_level(feature, visible)
end end
...@@ -530,6 +538,7 @@ class Project < ActiveRecord::Base ...@@ -530,6 +538,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
...@@ -569,6 +578,14 @@ class Project < ActiveRecord::Base ...@@ -569,6 +578,14 @@ class Project < ActiveRecord::Base
end end
end end
def all_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, hierarchy_order: nil) def ancestors_upto(top = nil, hierarchy_order: nil)
......
...@@ -23,11 +23,11 @@ class ProjectFeature < ActiveRecord::Base ...@@ -23,11 +23,11 @@ class ProjectFeature < ActiveRecord::Base
PUBLIC = 30 PUBLIC = 30
FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
class << self class << self
def access_level_attribute(feature) def access_level_attribute(feature)
feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name) feature = ensure_feature!(feature)
raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
"#{feature}_access_level".to_sym "#{feature}_access_level".to_sym
end end
...@@ -38,6 +38,21 @@ class ProjectFeature < ActiveRecord::Base ...@@ -38,6 +38,21 @@ class ProjectFeature < ActiveRecord::Base
"#{table}.#{attribute}" "#{table}.#{attribute}"
end end
def required_minimum_access_level(feature)
feature = ensure_feature!(feature)
PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST)
end
private
def ensure_feature!(feature)
feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
feature
end
end end
# Default scopes force us to unscope here since a service may need to check # Default scopes force us to unscope here since a service may need to check
......
...@@ -74,6 +74,14 @@ class ProjectTeam ...@@ -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,6 +199,10 @@ class ProjectTeam ...@@ -191,6 +199,10 @@ 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
ProjectTeam.prepend(EE::ProjectTeam) ProjectTeam.prepend(EE::ProjectTeam)
...@@ -754,8 +754,12 @@ class User < ApplicationRecord ...@@ -754,8 +754,12 @@ class User < ApplicationRecord
# #
# Example use: # Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)` # `Project.where('EXISTS(?)', user.authorizations_for_projects)`
def authorizations_for_projects def authorizations_for_projects(min_access_level: nil)
project_authorizations.select(1).where('project_authorizations.project_id = projects.id') authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
return authorizations unless min_access_level.present?
authorizations.where('project_authorizations.access_level >= ?', min_access_level)
end end
# Returns the projects this user has reporter (or greater) access to, limited # Returns the projects this user has reporter (or greater) access to, limited
......
...@@ -10,6 +10,15 @@ module Ci ...@@ -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 ...@@ -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 ...@@ -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,7 +28,10 @@ class PersonalSnippetPolicy < BasePolicy ...@@ -28,7 +28,10 @@ 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
rule { full_private_access }.enable :read_personal_snippet rule { full_private_access }.enable :read_personal_snippet
end end
...@@ -108,6 +108,10 @@ class ProjectPolicy < BasePolicy ...@@ -108,6 +108,10 @@ class ProjectPolicy < BasePolicy
condition(:has_clusters, scope: :subject) { clusterable_has_clusters? } condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
condition(:can_have_multiple_clusters) { multiple_clusters_available? } condition(:can_have_multiple_clusters) { multiple_clusters_available? }
condition(:internal_builds_disabled) do
!@subject.builds_enabled?
end
features = %w[ features = %w[
merge_requests merge_requests
issues issues
...@@ -196,7 +200,6 @@ class ProjectPolicy < BasePolicy ...@@ -196,7 +200,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
...@@ -235,6 +238,7 @@ class ProjectPolicy < BasePolicy ...@@ -235,6 +238,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
...@@ -314,13 +318,12 @@ class ProjectPolicy < BasePolicy ...@@ -314,13 +318,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))
...@@ -328,11 +331,22 @@ class ProjectPolicy < BasePolicy ...@@ -328,11 +331,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
prevent(*create_read_update_admin_destroy(:release)) prevent(*create_read_update_admin_destroy(:release))
end end
...@@ -359,7 +373,6 @@ class ProjectPolicy < BasePolicy ...@@ -359,7 +373,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
...@@ -378,7 +391,6 @@ class ProjectPolicy < BasePolicy ...@@ -378,7 +391,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
...@@ -393,7 +405,7 @@ class ProjectPolicy < BasePolicy ...@@ -393,7 +405,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
rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
......
...@@ -43,6 +43,8 @@ class ProjectSnippetPolicy < BasePolicy ...@@ -43,6 +43,8 @@ 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
ProjectSnippetPolicy.prepend(EE::ProjectSnippetPolicy) ProjectSnippetPolicy.prepend(EE::ProjectSnippetPolicy)
# 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
# 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 ...@@ -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
......
...@@ -59,7 +59,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -59,7 +59,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
......
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
module Members module Members
class DestroyService < Members::BaseService class DestroyService < Members::BaseService
def execute(member, skip_authorization: false) def execute(member, skip_authorization: false, skip_subresources: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member) raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
@skip_auth = skip_authorization
return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user) return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy member.destroy
...@@ -15,6 +17,7 @@ module Members ...@@ -15,6 +17,7 @@ module Members
notification_service.decline_access_request(member) notification_service.decline_access_request(member)
end end
delete_subresources(member) unless skip_subresources
enqueue_delete_todos(member) enqueue_delete_todos(member)
after_execute(member: member) after_execute(member: member)
...@@ -24,6 +27,29 @@ module Members ...@@ -24,6 +27,29 @@ module Members
private private
def delete_subresources(member)
return unless member.is_a?(GroupMember) && member.user && member.group
delete_project_members(member)
delete_subgroup_members(member) if Group.supports_nested_objects?
end
def delete_project_members(member)
groups = member.group.self_and_descendants
ProjectMember.in_namespaces(groups).with_user(member.user).each do |project_member|
self.class.new(current_user).execute(project_member, skip_authorization: @skip_auth)
end
end
def delete_subgroup_members(member)
groups = member.group.descendants
GroupMember.in_groups(groups).with_user(member.user).each do |group_member|
self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
end
end
def can_destroy_member?(member) def can_destroy_member?(member)
can?(current_user, destroy_member_permission(member), member) can?(current_user, destroy_member_permission(member), member)
end end
......
...@@ -11,7 +11,7 @@ module Notes ...@@ -11,7 +11,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
...@@ -36,19 +36,8 @@ module Notes ...@@ -36,19 +36,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
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 end
...@@ -373,7 +373,8 @@ class NotificationService ...@@ -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(
......
# 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,16 @@ module Projects ...@@ -24,8 +24,16 @@ module Projects
import_data import_data
success success
rescue => e rescue Gitlab::UrlBlocker::BlockedUrlError => e
Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type })
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)
Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type })
error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{message}")
end end
private private
...@@ -35,7 +43,7 @@ module Projects ...@@ -35,7 +43,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 +94,11 @@ module Projects ...@@ -86,11 +94,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 ...@@ -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 ...@@ -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 @@ ...@@ -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)
# rubocop: disable CodeReuse/ActiveRecord attr_reader :lfs_download_object
def execute(oid, url) delegate :oid, :size, :credentials, :sanitized_url, to: :lfs_download_object, prefix: :lfs
return unless project&.lfs_enabled? && oid.present? && url.present?
return if LfsObject.exists?(oid: oid) def initialize(project, lfs_download_object)
super(project)
sanitized_uri = sanitize_url!(url) @lfs_download_object = lfs_download_object
end
with_tmp_file(oid) do |file| # rubocop: disable CodeReuse/ActiveRecord
download_and_save_file(file, sanitized_uri) def execute
lfs_object = LfsObject.new(oid: oid, size: file.size, file: file) 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)
project.all_lfs_objects << lfs_object wrap_download_errors do
download_lfs_file!
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?
end
def headers(sanitized_uri) raise_size_error! if file.size != lfs_size
query_options.tap do |headers| raise_oid_error! if digester.hexdigest != lfs_oid
credentials = sanitized_uri.credentials end
if credentials[:user].present? || credentials[:password].present? def download_headers
{ stream_body: true }.tap do |headers|
if lfs_credentials[:user].present? || lfs_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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
...@@ -7,8 +7,6 @@ module ProtectedBranches ...@@ -7,8 +7,6 @@ module ProtectedBranches
@merge_params = AccessLevelParams.new(:merge, params) @merge_params = AccessLevelParams.new(:merge, params)
@unprotect_params = AccessLevelParams.new(:unprotect, params) @unprotect_params = AccessLevelParams.new(:unprotect, params)
verify_params!
protected_branch_params = { protected_branch_params = {
name: params[:name], name: params[:name],
push_access_levels_attributes: @push_params.access_levels, push_access_levels_attributes: @push_params.access_levels,
...@@ -18,12 +16,6 @@ module ProtectedBranches ...@@ -18,12 +16,6 @@ module ProtectedBranches
::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute ::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
end end
private
def verify_params!
# EE-only
end
end end
end end
......
#content #content
= email_default_heading("#{@resource.user.name}, you've added an additional email!") = email_default_heading("#{sanitize_name(@resource.user.name)}, you've added an additional email!")
%p Click the link below to confirm your email address (#{@resource.email}) %p Click the link below to confirm your email address (#{@resource.email})
#cta #cta
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
......
...@@ -302,17 +302,31 @@ ...@@ -302,17 +302,31 @@
- if project_nav_tab? :wiki - if project_nav_tab? :wiki
= nav_link(controller: :wikis) do = nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), class: 'shortcuts-wiki qa-wiki-link' do = link_to wiki_url, class: 'shortcuts-wiki qa-wiki-link' 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
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<% discussion = note.discussion if note.part_of_discussion? -%> <% discussion = note.discussion if note.part_of_discussion? -%>
<% if discussion && !discussion.individual_note? -%> <% if discussion && !discussion.individual_note? -%>
<%= note.author_name -%> <%= sanitize_name(note.author_name) -%>
<% if discussion.new_discussion? -%> <% if discussion.new_discussion? -%>
<%= " started a new discussion" -%> <%= " started a new discussion" -%>
<% else -%> <% else -%>
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<% elsif Gitlab::CurrentSettings.email_author_in_body -%> <% elsif Gitlab::CurrentSettings.email_author_in_body -%>
<%= "#{note.author_name} commented:" -%> <%= "#{sanitize_name(note.author_name)} commented:" -%>
<% end -%> <% end -%>
......
...@@ -3,7 +3,7 @@ Auto DevOps pipeline was disabled for <%= @project.name %> ...@@ -3,7 +3,7 @@ Auto DevOps pipeline was disabled for <%= @project.name %>
The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>). The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>).
<% if @pipeline.user -%> <% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%> <% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%> <% end -%>
......
%p %p
Issue was closed by #{@updated_by.name} Issue was closed by #{sanitize_name(@updated_by.name)}
Issue was closed by #{@updated_by.name} Issue was closed by #{sanitize_name(@updated_by.name)}
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)} Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
%p %p
Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name} Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)}
Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name} Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)}
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to') = merge_path_description(@merge_request, 'to')
Author: #{@merge_request.author_name} Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{@merge_request.assignee_name} Assignee: #{sanitize_name(@merge_request.assignee_name)}
%p %p
Issue was #{@issue_status} by #{@updated_by.name} Issue was #{@issue_status} by #{sanitize_name(@updated_by.name)}
Issue was <%= @issue_status %> by <%= @updated_by.name %> Issue was <%= @issue_status %> by <%= sanitize_name(@updated_by.name) %>
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>. <%= sanitize_name(member.user.name) %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= polymorphic_url([member_source, :members]) %> <%= polymorphic_url([member_source, :members]) %>
<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>. <%= member.invite_email %>, now known as <%= sanitize_name(member.user.name) %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
<%= member_source.web_url %> <%= member_source.web_url %>
You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>. You have been invited <%= "by #{sanitize_name(member.created_by.name)} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>.
Accept invitation: <%= invite_url(@token) %> Accept invitation: <%= invite_url(@token) %>
Decline invitation: <%= decline_invite_url(@token) %> Decline invitation: <%= decline_invite_url(@token) %>
%p %p
Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name} Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)}
Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name} Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)}
Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to') = merge_path_description(@merge_request, 'to')
Author: #{@merge_request.author_name} Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{@merge_request.assignee_name} Assignee: #{sanitize_name(@merge_request.assignee_name)}
...@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m ...@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to') = merge_path_description(@merge_request, 'to')
Author: #{@merge_request.author_name} Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{@merge_request.assignee_name} Assignee: #{sanitize_name(@merge_request.assignee_name)}
...@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m ...@@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to') = merge_path_description(@merge_request, 'to')
Author: #{@merge_request.author_name} Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{@merge_request.assignee_name} Assignee: #{sanitize_name(@merge_request.assignee_name)}
%p %p
Hi #{@user.name}! Hi #{sanitize_name(@user.name)}!
%p %p
A new GPG key was added to your account: A new GPG key was added to your account:
%p %p
......
Hi <%= @user.name %>! Hi <%= sanitize_name(@user.name) %>!
A new GPG key was added to your account: A new GPG key was added to your account:
......
New Issue was created. New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %> Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= @issue.assignee_list %> Assignee: <%= @issue.assignee_list %>
<%= @issue.description %> <%= @issue.description %>
You have been mentioned in an issue. You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %> Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= @issue.assignee_list %> Assignee: <%= sanitize_name(@issue.assignee_list) %>
<%= @issue.description %> <%= @issue.description %>
...@@ -3,7 +3,7 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %> ...@@ -3,7 +3,7 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %>
<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
<%= merge_path_description(@merge_request, 'to') %> <%= merge_path_description(@merge_request, 'to') %>
Author: <%= @merge_request.author_name %> Author: <%= sanitize_name(@merge_request.author_name) %>
Assignee: <%= @merge_request.assignee_name %> Assignee: <%= sanitize_name(@merge_request.assignee_name) %>
<%= @merge_request.description %> <%= @merge_request.description %>
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- if @merge_request.assignee_id.present? - if @merge_request.assignee_id.present?
%p %p
Assignee: #{@merge_request.assignee_name} Assignee: #{sanitize_name(@merge_request.assignee_name)}
= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter = render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
......
%p %p
Hi #{@user.name}! Hi #{sanitize_name(@user.name)}!
%p %p
A new public key was added to your account: A new public key was added to your account:
%p %p
......
Hi <%= @user.name %>! Hi <%= sanitize_name(@user.name) %>!
A new public key was added to your account: A new public key was added to your account:
......
%p %p
Hi #{@user['name']}! Hi #{sanitize_name(@user['name'])}!
%p %p
- if Gitlab::CurrentSettings.allow_signup? - if Gitlab::CurrentSettings.allow_signup?
Your account has been created successfully. Your account has been created successfully.
......
Hi <%= @user.name %>! Hi <%= sanitize_name(@user.name) %>!
The Administrator created an account for you. Now you are a member of the company GitLab application. The Administrator created an account for you. Now you are a member of the company GitLab application.
......
...@@ -10,20 +10,20 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) ...@@ -10,20 +10,20 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%> <% commit = @pipeline.commit -%>
<% if commit.author -%> <% if commit.author -%>
Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%> <% else -%>
Commit Author: <%= commit.author_name %> Commit Author: <%= commit.author_name %>
<% end -%> <% end -%>
<% if commit.different_committer? -%> <% if commit.different_committer? -%>
<% if commit.committer -%> <% if commit.committer -%>
Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> ) Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%> <% else -%>
Committed by: <%= commit.committer_name %> Committed by: <%= commit.committer_name %>
<% end -%> <% end -%>
<% end -%> <% end -%>
<% if @pipeline.user -%> <% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%> <% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%> <% end -%>
......
...@@ -10,13 +10,13 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) ...@@ -10,13 +10,13 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%> <% commit = @pipeline.commit -%>
<% if commit.author -%> <% if commit.author -%>
Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%> <% else -%>
Commit Author: <%= commit.author_name %> Commit Author: <%= commit.author_name %>
<% end -%> <% end -%>
<% if commit.different_committer? -%> <% if commit.different_committer? -%>
<% if commit.committer -%> <% if commit.committer -%>
Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> ) Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%> <% else -%>
Committed by: <%= commit.committer_name %> Committed by: <%= commit.committer_name %>
<% end -%> <% end -%>
...@@ -25,7 +25,7 @@ Committed by: <%= commit.committer_name %> ...@@ -25,7 +25,7 @@ Committed by: <%= commit.committer_name %>
<% job_count = @pipeline.total_size -%> <% job_count = @pipeline.total_size -%>
<% stage_count = @pipeline.stages_count -%> <% stage_count = @pipeline.stages_count -%>
<% if @pipeline.user -%> <% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%> <% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%> <% end -%>
......
%h3 %h3
= @updated_by_user.name = sanitize_name(@updated_by_user.name)
pushed new commits to merge request pushed new commits to merge request
= link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)) = link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
......
#{@updated_by_user.name} pushed new commits to merge request #{@merge_request.to_reference} #{sanitize_name(@updated_by_user.name)} pushed new commits to merge request #{@merge_request.to_reference}
\ \
#{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))} #{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))}
\ \
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Assignee changed Assignee changed
- if @previous_assignees.any? - if @previous_assignees.any?
from from
%strong= @previous_assignees.map(&:name).to_sentence %strong= sanitize_name(@previous_assignees.map(&:name).to_sentence)
to to
- if @issue.assignees.any? - if @issue.assignees.any?
%strong= @issue.assignee_list %strong= @issue.assignee_list
......
...@@ -2,5 +2,5 @@ Reassigned Issue <%= @issue.iid %> ...@@ -2,5 +2,5 @@ Reassigned Issue <%= @issue.iid %>
<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %> <%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%> Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %> to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
Assignee changed Assignee changed
- if @previous_assignee - if @previous_assignee
from from
%strong= @previous_assignee.name %strong= sanitize_name(@previous_assignee.name)
to to
- if @merge_request.assignee_id - if @merge_request.assignee_id
%strong= @merge_request.assignee_name %strong= sanitize_name(@merge_request.assignee_name)
- else - else
%strong Unassigned %strong Unassigned
...@@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %> ...@@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %>
<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %> <%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%> Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%>
to <%= "#{@merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'}" %> to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %>
%p %p
All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{@resolved_by.name} All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{sanitize_name(@resolved_by.name)}
All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= @resolved_by.name %> All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= sanitize_name(@resolved_by.name) %>
<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
= 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)
- 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 @@ ...@@ -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,10 +9,7 @@ ...@@ -9,10 +9,7 @@
.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"
- else
.block-connector
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true
.limited-width-notes .limited-width-notes
......
...@@ -6,6 +6,7 @@ ...@@ -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)
...@@ -26,7 +27,7 @@ ...@@ -26,7 +27,7 @@
%span.commit-row-message.d-block.d-sm-none %span.commit-row-message.d-block.d-sm-none
&middot; &middot;
= 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?
...@@ -52,7 +53,7 @@ ...@@ -52,7 +53,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) } }
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
%ul.content-list.related-items-list %ul.content-list.related-items-list
- 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.list-item.py-0.px-0 %li.list-item.py-0.px-0
.item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3 .item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3
.item-contents .item-contents
...@@ -25,7 +26,7 @@ ...@@ -25,7 +26,7 @@
= merge_request.target_project.full_path = merge_request.target_project.full_path
= merge_request.to_reference = merge_request.to_reference
%span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2 %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2
- if merge_request.head_pipeline - if merge_request.can_read_pipeline?
= render_pipeline_status(merge_request.head_pipeline, tooltip_placement: 'bottom') = render_pipeline_status(merge_request.head_pipeline, tooltip_placement: 'bottom')
- elsif has_any_head_pipeline - elsif has_any_head_pipeline
= icon('blank fw') = icon('blank fw')
......
...@@ -6,7 +6,7 @@ ...@@ -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 @@ ...@@ -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,7 +6,6 @@ ...@@ -6,7 +6,6 @@
= 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')
......
%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
......
- @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 @@ ...@@ -2,7 +2,7 @@
- breadcrumb_title @page.human_title - breadcrumb_title @page.human_title
- wiki_breadcrumb_dropdown_links(@page.slug) - wiki_breadcrumb_dropdown_links(@page.slug)
- page_title @page.human_title, _("Wiki") - page_title @page.human_title, _("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" }
......
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
title: _('Issues'), data: { container: 'body', placement: 'top' } do title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'append-right-4') = sprite_icon('issues', size: 14, css_class: 'append-right-4')
= number_with_delimiter(project.open_issues_count) = number_with_delimiter(project.open_issues_count)
- if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
%span.icon-wrapper.pipeline-status %span.icon-wrapper.pipeline-status
= render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top') = render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top')
.updated-note .updated-note
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment