Commit 79038226 authored by Yorick Peterse's avatar Yorick Peterse

Merge dev.gitlab.org@master into GitLab.com@master

parents 6fa15983 c1a3ac01
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 12.4.1
### Security (6 changes)
- Do not display project labels that are not visible for user accessing group labels.
- Do not index system notes for issue update.
- Redact search results based on Ability.allowed?.
- Do not show private cross references in epic notes.
- Filter out packages the user does'nt have permission to see at group level.
- Fixes a Open Redirect issue in `InternalRedirect`.
## 12.4.0 ## 12.4.0
### Security (2 changes) ### Security (2 changes)
......
...@@ -2,6 +2,26 @@ ...@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 12.4.1
### Security (14 changes)
- Standardize error response when route is missing.
- Do not display project labels that are not visible for user accessing group labels.
- Show cross-referenced label and milestones in issues' activities only to authorized users.
- Show cross-referenced label and milestones in issues' activities only to authorized users.
- Analyze incoming GraphQL queries and check for recursion.
- Disallow unprivileged users from commenting on private repository commits.
- Don't allow maintainers of a target project to delete the source branch of a merge request from a fork.
- Require Maintainer permission on group where project is transferred to.
- Don't leak private members in project member autocomplete suggestions.
- Return 404 on LFS request if project doesn't exist.
- Mask sentry auth token in Error Tracking dashboard.
- Fixes a Open Redirect issue in `InternalRedirect`.
- Remove deploy access level when project/group link is deleted.
- Sanitize all wiki markup formats with GitLab sanitization pipelines.
## 12.4.0 ## 12.4.0
### Security (14 changes) ### Security (14 changes)
......
...@@ -5,6 +5,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; ...@@ -5,6 +5,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import sanitize from 'sanitize-html';
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
const highlighter = function(element, text, matches) { const highlighter = function(element, text, matches) {
...@@ -74,7 +75,7 @@ export default class ProjectFindFile { ...@@ -74,7 +75,7 @@ export default class ProjectFindFile {
findFile() { findFile() {
var result, searchText; var result, searchText;
searchText = this.inputElement.val(); searchText = sanitize(this.inputElement.val());
result = result =
searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
return this.renderList(result, searchText); return this.renderList(result, searchText);
......
...@@ -17,7 +17,7 @@ class ApplicationController < ActionController::Base ...@@ -17,7 +17,7 @@ class ApplicationController < ActionController::Base
include Gitlab::Tracking::ControllerConcern include Gitlab::Tracking::ControllerConcern
include Gitlab::Experimentation::ControllerConcern include Gitlab::Experimentation::ControllerConcern
before_action :authenticate_user! before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms? before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration
...@@ -98,7 +98,9 @@ class ApplicationController < ActionController::Base ...@@ -98,7 +98,9 @@ class ApplicationController < ActionController::Base
if current_user if current_user
not_found not_found
else else
authenticate_user! store_location_for(:user, request.fullpath) unless request.xhr?
redirect_to new_user_session_path, alert: I18n.t('devise.failure.unauthenticated')
end end
end end
......
...@@ -6,7 +6,7 @@ module InternalRedirect ...@@ -6,7 +6,7 @@ module InternalRedirect
def safe_redirect_path(path) def safe_redirect_path(path)
return unless path return unless path
# Verify that the string starts with a `/` and a known route character. # Verify that the string starts with a `/` and a known route character.
return unless path =~ %r{^/[-\w].*$} return unless path =~ %r{\A/[-\w].*\z}
uri = URI(path) uri = URI(path)
# Ignore anything path of the redirect except for the path, querystring and, # Ignore anything path of the redirect except for the path, querystring and,
......
...@@ -34,6 +34,7 @@ module LfsRequest ...@@ -34,6 +34,7 @@ module LfsRequest
end end
def lfs_check_access! def lfs_check_access!
return render_lfs_not_found unless project
return if download_request? && lfs_download_access? return if download_request? && lfs_download_access?
return if upload_request? && lfs_upload_access? return if upload_request? && lfs_upload_access?
......
...@@ -51,7 +51,7 @@ class LabelsFinder < UnionFinder ...@@ -51,7 +51,7 @@ class LabelsFinder < UnionFinder
end end
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id)) unless only_group_labels? label_ids << Label.where(project_id: ids_user_can_read_labels(projects)) unless only_group_labels?
end end
label_ids label_ids
...@@ -188,4 +188,10 @@ class LabelsFinder < UnionFinder ...@@ -188,4 +188,10 @@ class LabelsFinder < UnionFinder
groups.select { |group| authorized_to_read_labels?(group) } groups.select { |group| authorized_to_read_labels?(group) }
end end
end end
# rubocop: disable CodeReuse/ActiveRecord
def ids_user_can_read_labels(projects)
Project.where(id: projects.select(:id)).ids_with_issuables_available_for(current_user)
end
# rubocop: enable CodeReuse/ActiveRecord
end end
...@@ -18,15 +18,15 @@ class GitlabSchema < GraphQL::Schema ...@@ -18,15 +18,15 @@ class GitlabSchema < GraphQL::Schema
use Gitlab::Graphql::GenericTracing use Gitlab::Graphql::GenericTracing
query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new
query_analyzer Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer.new
query(Types::QueryType)
default_max_page_size 100
max_complexity DEFAULT_MAX_COMPLEXITY max_complexity DEFAULT_MAX_COMPLEXITY
max_depth DEFAULT_MAX_DEPTH max_depth DEFAULT_MAX_DEPTH
mutation(Types::MutationType) query Types::QueryType
mutation Types::MutationType
default_max_page_size 100
class << self class << self
def multiplex(queries, **kwargs) def multiplex(queries, **kwargs)
......
...@@ -133,15 +133,7 @@ module MarkupHelper ...@@ -133,15 +133,7 @@ module MarkupHelper
issuable_state_filter_enabled: true issuable_state_filter_enabled: true
) )
html = html = markup_unsafe(wiki_page.path, text, context)
case wiki_page.format
when :markdown
markdown_unsafe(text, context)
when :asciidoc
asciidoc_unsafe(text)
else
wiki_page.formatted_content.html_safe
end
prepare_for_rendering(html, context) prepare_for_rendering(html, context)
end end
......
...@@ -13,7 +13,9 @@ module Mentionable ...@@ -13,7 +13,9 @@ module Mentionable
def self.other_patterns def self.other_patterns
[ [
Commit.reference_pattern, Commit.reference_pattern,
MergeRequest.reference_pattern MergeRequest.reference_pattern,
Label.reference_pattern,
Milestone.reference_pattern
] ]
end end
......
...@@ -16,6 +16,7 @@ class Discussion ...@@ -16,6 +16,7 @@ class Discussion
:commit_id, :commit_id,
:for_commit?, :for_commit?,
:for_merge_request?, :for_merge_request?,
:noteable_ability_name,
:to_ability_name, :to_ability_name,
:editable?, :editable?,
:visible_for?, :visible_for?,
......
...@@ -8,6 +8,7 @@ class Member < ApplicationRecord ...@@ -8,6 +8,7 @@ class Member < ApplicationRecord
include Gitlab::Access include Gitlab::Access
include Presentable include Presentable
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include FromUnion
attr_accessor :raw_invite_token attr_accessor :raw_invite_token
......
...@@ -69,6 +69,14 @@ class MergeRequest < ApplicationRecord ...@@ -69,6 +69,14 @@ class MergeRequest < ApplicationRecord
has_many :merge_request_assignees has_many :merge_request_assignees
has_many :assignees, class_name: "User", through: :merge_request_assignees has_many :assignees, class_name: "User", through: :merge_request_assignees
KNOWN_MERGE_PARAMS = [
:auto_merge_strategy,
:should_remove_source_branch,
:force_remove_source_branch,
:commit_message,
:squash_commit_message,
:sha
].freeze
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
after_create :ensure_merge_request_diff after_create :ensure_merge_request_diff
......
...@@ -261,6 +261,10 @@ class Milestone < ApplicationRecord ...@@ -261,6 +261,10 @@ class Milestone < ApplicationRecord
group || project group || project
end end
def to_ability_name
model_name.singular
end
def group_milestone? def group_milestone?
group_id.present? group_id.present?
end end
......
...@@ -361,6 +361,10 @@ class Note < ApplicationRecord ...@@ -361,6 +361,10 @@ class Note < ApplicationRecord
end end
def to_ability_name def to_ability_name
model_name.singular
end
def noteable_ability_name
for_snippet? ? noteable.class.name.underscore : noteable_type.demodulize.underscore for_snippet? ? noteable.class.name.underscore : noteable_type.demodulize.underscore
end end
......
...@@ -614,11 +614,11 @@ class Project < ApplicationRecord ...@@ -614,11 +614,11 @@ class Project < ApplicationRecord
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end end
# Returns ids of projects with milestones available for given user # Returns ids of projects with issuables available for given user
# #
# Used on queries to find milestones which user can see # Used on queries to find milestones or labels which user can see
# For example: Milestone.where(project_id: ids_with_milestone_available_for(user)) # For example: Milestone.where(project_id: ids_with_issuables_available_for(user))
def ids_with_milestone_available_for(user) def ids_with_issuables_available_for(user)
with_issues_enabled = with_issues_available_for_user(user).select(:id) with_issues_enabled = with_issues_available_for_user(user).select(:id)
with_merge_requests_enabled = with_merge_requests_available_for_user(user).select(:id) with_merge_requests_enabled = with_merge_requests_available_for_user(user).select(:id)
...@@ -1265,6 +1265,10 @@ class Project < ApplicationRecord ...@@ -1265,6 +1265,10 @@ class Project < ApplicationRecord
end end
end end
def to_ability_name
model_name.singular
end
# rubocop: disable CodeReuse/ServiceClass # rubocop: disable CodeReuse/ServiceClass
def execute_hooks(data, hooks_scope = :push_hooks) def execute_hooks(data, hooks_scope = :push_hooks)
run_after_commit_or_now do run_after_commit_or_now do
......
...@@ -10,6 +10,7 @@ class SystemNoteMetadata < ApplicationRecord ...@@ -10,6 +10,7 @@ class SystemNoteMetadata < ApplicationRecord
commit cross_reference commit cross_reference
close duplicate close duplicate
moved merge moved merge
label milestone
].freeze ].freeze
ICON_TYPES = %w[ ICON_TYPES = %w[
......
...@@ -121,6 +121,12 @@ class WikiPage ...@@ -121,6 +121,12 @@ class WikiPage
@version ||= @page.version @version ||= @page.version
end end
def path
return unless persisted?
@path ||= @page.path
end
def versions(options = {}) def versions(options = {})
return [] unless persisted? return [] unless persisted?
......
...@@ -4,4 +4,5 @@ class CommitPolicy < BasePolicy ...@@ -4,4 +4,5 @@ class CommitPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
rule { can?(:download_code) }.enable :read_commit rule { can?(:download_code) }.enable :read_commit
rule { ~can?(:read_commit) }.prevent :create_note
end end
...@@ -131,6 +131,8 @@ class GroupPolicy < BasePolicy ...@@ -131,6 +131,8 @@ class GroupPolicy < BasePolicy
rule { owner | admin }.enable :read_statistics rule { owner | admin }.enable :read_statistics
rule { maintainer & can?(:create_projects) }.enable :transfer_projects
def access_level def access_level
return GroupMember::NO_ACCESS if @user.nil? return GroupMember::NO_ACCESS if @user.nil?
......
...@@ -15,6 +15,8 @@ class NamespacePolicy < BasePolicy ...@@ -15,6 +15,8 @@ class NamespacePolicy < BasePolicy
end end
rule { personal_project & ~can_create_personal_project }.prevent :create_projects rule { personal_project & ~can_create_personal_project }.prevent :create_projects
rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects
end end
NamespacePolicy.prepend_if_ee('EE::NamespacePolicy') NamespacePolicy.prepend_if_ee('EE::NamespacePolicy')
...@@ -9,7 +9,7 @@ class NotePolicy < BasePolicy ...@@ -9,7 +9,7 @@ class NotePolicy < BasePolicy
condition(:editable, scope: :subject) { @subject.editable? } condition(:editable, scope: :subject) { @subject.editable? }
condition(:can_read_noteable) { can?(:"read_#{@subject.to_ability_name}") } condition(:can_read_noteable) { can?(:"read_#{@subject.noteable_ability_name}") }
condition(:is_visible) { @subject.visible_for?(@user) } condition(:is_visible) { @subject.visible_for?(@user) }
......
...@@ -3,12 +3,13 @@ ...@@ -3,12 +3,13 @@
module AutoMerge module AutoMerge
class BaseService < ::BaseService class BaseService < ::BaseService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include MergeRequests::AssignsMergeParams
def execute(merge_request) def execute(merge_request)
merge_request.merge_params.merge!(params) assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy))
merge_request.auto_merge_enabled = true merge_request.auto_merge_enabled = true
merge_request.merge_user = current_user merge_request.merge_user = current_user
merge_request.auto_merge_strategy = strategy
return :failed unless merge_request.save return :failed unless merge_request.save
...@@ -21,7 +22,7 @@ module AutoMerge ...@@ -21,7 +22,7 @@ module AutoMerge
end end
def update(merge_request) def update(merge_request)
merge_request.merge_params.merge!(params) assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy))
return :failed unless merge_request.save return :failed unless merge_request.save
......
# frozen_string_literal: true
module MergeRequests
module AssignsMergeParams
def self.included(klass)
raise "#{self} can not be included in #{klass} without implementing #current_user" unless klass.method_defined?(:current_user)
end
def assign_allowed_merge_params(merge_request, merge_params)
known_merge_params = merge_params.to_h.with_indifferent_access.slice(*MergeRequest::KNOWN_MERGE_PARAMS)
# Not checking `MergeRequest#can_remove_source_branch` as that includes
# other checks that aren't needed here.
known_merge_params.delete(:force_remove_source_branch) unless current_user.can?(:push_code, merge_request.source_project)
merge_request.merge_params.merge!(known_merge_params)
# Delete the known params now that they're assigned, so we don't try to
# assign them through an `#assign_attributes` later.
# They could be coming in as strings or symbols
merge_params.to_h.with_indifferent_access.except!(*MergeRequest::KNOWN_MERGE_PARAMS)
end
end
end
...@@ -32,7 +32,7 @@ module ErrorTracking ...@@ -32,7 +32,7 @@ module ErrorTracking
project_slug: 'proj' project_slug: 'proj'
) )
setting.token = params[:token] setting.token = token(setting)
setting.enabled = true setting.enabled = true
end end
end end
...@@ -40,5 +40,12 @@ module ErrorTracking ...@@ -40,5 +40,12 @@ module ErrorTracking
def can_read? def can_read?
can?(current_user, :read_sentry_issue, project) can?(current_user, :read_sentry_issue, project)
end end
def token(setting)
# Use param token if not masked, otherwise use database token
return params[:token] unless /\A\*+\z/.match?(params[:token])
setting.token
end
end end
end end
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module MergeRequests module MergeRequests
class BaseService < ::IssuableBaseService class BaseService < ::IssuableBaseService
include MergeRequests::AssignsMergeParams
def create_note(merge_request, state = merge_request.state) def create_note(merge_request, state = merge_request.state)
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, state, nil) SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, state, nil)
end end
...@@ -29,6 +31,18 @@ module MergeRequests ...@@ -29,6 +31,18 @@ module MergeRequests
private private
def create(merge_request)
self.params = assign_allowed_merge_params(merge_request, params)
super
end
def update(merge_request)
self.params = assign_allowed_merge_params(merge_request, params)
super
end
def handle_wip_event(merge_request) def handle_wip_event(merge_request)
if wip_event = params.delete(:wip_event) if wip_event = params.delete(:wip_event)
# We update the title that is provided in the params or we use the mr title # We update the title that is provided in the params or we use the mr title
......
...@@ -24,6 +24,8 @@ module MergeRequests ...@@ -24,6 +24,8 @@ module MergeRequests
merge_request.source_project.remove_source_branch_after_merge? merge_request.source_project.remove_source_branch_after_merge?
end end
self.params = assign_allowed_merge_params(merge_request, params)
filter_params(merge_request) filter_params(merge_request)
# merge_request.assign_attributes(...) below is a Rails # merge_request.assign_attributes(...) below is a Rails
......
...@@ -9,7 +9,6 @@ module MergeRequests ...@@ -9,7 +9,6 @@ module MergeRequests
merge_request.target_project = @project merge_request.target_project = @project
merge_request.source_project = @source_project merge_request.source_project = @source_project
merge_request.source_branch = params[:source_branch] merge_request.source_branch = params[:source_branch]
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
create(merge_request) create(merge_request)
end end
......
...@@ -16,10 +16,6 @@ module MergeRequests ...@@ -16,10 +16,6 @@ module MergeRequests
params.delete(:force_remove_source_branch) params.delete(:force_remove_source_branch)
end end
if params.has_key?(:force_remove_source_branch)
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
end
handle_wip_event(merge_request) handle_wip_event(merge_request)
update_task_event(merge_request) || update(merge_request) update_task_event(merge_request) || update(merge_request)
end end
......
...@@ -281,7 +281,7 @@ class NotificationService ...@@ -281,7 +281,7 @@ class NotificationService
end end
def send_new_note_notifications(note) def send_new_note_notifications(note)
notify_method = "note_#{note.to_ability_name}_email".to_sym notify_method = "note_#{note.noteable_ability_name}_email".to_sym
recipients = NotificationRecipientService.build_new_note_recipients(note) recipients = NotificationRecipientService.build_new_note_recipients(note)
recipients.each do |recipient| recipients.each do |recipient|
......
...@@ -36,15 +36,17 @@ module Projects ...@@ -36,15 +36,17 @@ module Projects
organization_slug: settings.dig(:project, :organization_slug) organization_slug: settings.dig(:project, :organization_slug)
) )
{ params = {
error_tracking_setting_attributes: { error_tracking_setting_attributes: {
api_url: api_url, api_url: api_url,
token: settings[:token],
enabled: settings[:enabled], enabled: settings[:enabled],
project_name: settings.dig(:project, :name), project_name: settings.dig(:project, :name),
organization_name: settings.dig(:project, :organization_name) organization_name: settings.dig(:project, :organization_name)
} }
} }
params[:error_tracking_setting_attributes][:token] = settings[:token] unless /\A\*+\z/.match?(settings[:token]) # Don't update token if we receive masked value
params
end end
def grafana_integration_params def grafana_integration_params
......
...@@ -7,16 +7,69 @@ module Projects ...@@ -7,16 +7,69 @@ module Projects
def execute(noteable) def execute(noteable)
@noteable = noteable @noteable = noteable
participants = noteable_owner + participants_in_noteable + all_members + groups + project_members participants =
noteable_owner +
participants_in_noteable +
all_members +
groups +
project_members
participants.uniq participants.uniq
end end
def project_members def project_members
@project_members ||= sorted(project.team.members) @project_members ||= sorted(get_project_members)
end
def get_project_members
members = Member.from_union([project_members_through_ancestral_groups,
project_members_through_invited_groups,
individual_project_members])
User.id_in(members.select(:user_id))
end end
def all_members def all_members
[{ username: "all", name: "All Project and Group Members", count: project_members.count }] [{ username: "all", name: "All Project and Group Members", count: project_members.count }]
end end
private
def project_members_through_invited_groups
groups_with_ancestors_ids = Gitlab::ObjectHierarchy
.new(visible_groups)
.base_and_ancestors
.pluck_primary_key
GroupMember
.active_without_invites_and_requests
.with_source_id(groups_with_ancestors_ids)
end
def visible_groups
visible_groups = project.invited_groups
unless project_owner?
visible_groups = visible_groups.public_or_visible_to_user(current_user)
end
visible_groups
end
def project_members_through_ancestral_groups
project.group.present? ? project.group.members_with_parents : Member.none
end
def individual_project_members
project.project_members
end
def project_owner?
if project.group.present?
project.group.owners.include?(current_user)
else
project.namespace.owner == current_user
end
end
end end
end end
...@@ -98,7 +98,7 @@ module Projects ...@@ -98,7 +98,7 @@ module Projects
@new_namespace && @new_namespace &&
can?(current_user, :change_namespace, project) && can?(current_user, :change_namespace, project) &&
@new_namespace.id != project.namespace_id && @new_namespace.id != project.namespace_id &&
current_user.can?(:create_projects, @new_namespace) current_user.can?(:transfer_projects, @new_namespace)
end end
def update_namespace_and_visibility(to_namespace) def update_namespace_and_visibility(to_namespace)
......
...@@ -17,4 +17,4 @@ ...@@ -17,4 +17,4 @@
project: error_tracking_setting_project_json, project: error_tracking_setting_project_json,
api_host: setting.api_host, api_host: setting.api_host,
enabled: setting.enabled.to_json, enabled: setting.enabled.to_json,
token: setting.token } } token: setting.token.present? ? '*' * 12 : nil } }
---
title: Standardize error response when route is missing
merge_request:
author:
type: security
---
title: Do not display project labels that are not visible for user accessing group labels
merge_request:
author:
type: security
---
title: Show cross-referenced label and milestones in issues' activities only to authorized users
merge_request:
author:
type: security
---
title: Analyze incoming GraphQL queries and check for recursion
merge_request:
author:
type: security
---
title: Disallow unprivileged users from commenting on private repository commits
merge_request:
author:
type: security
---
title: Don't allow maintainers of a target project to delete the source branch of
a merge request from a fork
merge_request:
author:
type: security
---
title: Require Maintainer permission on group where project is transferred to
merge_request:
author:
type: security
---
title: "Don't leak private members in project member autocomplete suggestions"
type: security
---
title: Return 404 on LFS request if project doesn't exist
merge_request:
author:
type: security
---
title: Mask sentry auth token in Error Tracking dashboard
author:
type: security
---
title: Remove deploy access level when project/group link is deleted
merge_request:
author:
type: security
---
title: Sanitize search text to prevent XSS
merge_request:
author:
type: security
---
title: Sanitize all wiki markup formats with GitLab sanitization pipelines
merge_request:
author:
type: security
...@@ -44,6 +44,10 @@ Admins are able to share projects with any group in the system. ...@@ -44,6 +44,10 @@ Admins are able to share projects with any group in the system.
In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Maintainer' or 'Owner') will only have 'Developer' access to 'Project Acme'. In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Maintainer' or 'Owner') will only have 'Developer' access to 'Project Acme'.
## Sharing public project with private group
When sharing a public project with a private group, owners and maintainers of the project will see the name of the group in the `members` page. Owners will also have the possibility to see members of the private group they don't have access to when mentioning them in the issue or merge request.
## Share project with group lock ## Share project with group lock
It is possible to prevent projects in a group from [sharing It is possible to prevent projects in a group from [sharing
......
...@@ -5,14 +5,18 @@ module EE ...@@ -5,14 +5,18 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
before_destroy :delete_branch_protection before_destroy :delete_related_access_levels
end end
def delete_branch_protection def delete_related_access_levels
if group.present? && project.present? return unless group.present? && project.present?
project.protected_branches.merge_access_by_group(group).destroy_all # rubocop: disable DestroyAll
project.protected_branches.push_access_by_group(group).destroy_all # rubocop: disable DestroyAll # For protected branches
end project.protected_branches.merge_access_by_group(group).destroy_all # rubocop: disable DestroyAll
project.protected_branches.push_access_by_group(group).destroy_all # rubocop: disable DestroyAll
# For protected environments
project.protected_environments.deploy_access_levels_by_group(group).delete_all
end end
end end
end end
...@@ -14,7 +14,7 @@ module EE ...@@ -14,7 +14,7 @@ module EE
EE_TYPES_WITH_CROSS_REFERENCES = %w[ EE_TYPES_WITH_CROSS_REFERENCES = %w[
relate unrelate relate unrelate
epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic
epic_issue_moved issue_changed_epic epic_issue_moved issue_changed_epic relate_epic unrelate_epic
].freeze ].freeze
override :icon_types override :icon_types
......
...@@ -19,6 +19,11 @@ class ProtectedEnvironment < ApplicationRecord ...@@ -19,6 +19,11 @@ class ProtectedEnvironment < ApplicationRecord
' AND protected_environments.project_id = environments.project_id') ' AND protected_environments.project_id = environments.project_id')
end end
scope :deploy_access_levels_by_group, -> (group) do
ProtectedEnvironment::DeployAccessLevel
.joins(:protected_environment).where(group: group)
end
def accessible_to?(user) def accessible_to?(user)
deploy_access_levels deploy_access_levels
.any? { |deploy_access_level| deploy_access_level.check_access(user) } .any? { |deploy_access_level| deploy_access_level.check_access(user) }
......
...@@ -34,11 +34,13 @@ module Elastic ...@@ -34,11 +34,13 @@ module Elastic
private private
# rubocop: disable CodeReuse/ActiveRecord
def update_issue_notes(record, changed_fields) def update_issue_notes(record, changed_fields)
if changed_fields && (changed_fields & ISSUE_TRACKED_FIELDS).any? if changed_fields && (changed_fields & ISSUE_TRACKED_FIELDS).any?
import_association(Note, query: -> { where(noteable: record) }) import_association(Note, query: -> { searchable.where(noteable: record) })
end end
end end
# rubocop: enable CodeReuse/ActiveRecord
def initial_index_project(project) def initial_index_project(project)
# Enqueue the repository indexing jobs immediately so they run in parallel # Enqueue the repository indexing jobs immediately so they run in parallel
......
---
title: Do not index system notes for issue update
merge_request:
author:
type: security
---
title: Redact search results based on Ability.allowed?
merge_request:
author:
type: security
---
title: Do not show private cross references in epic notes
merge_request:
author:
type: security
---
title: Fixes a Open Redirect issue in `InternalRedirect`.
merge_request:
author:
type: security
...@@ -167,9 +167,26 @@ module Gitlab ...@@ -167,9 +167,26 @@ module Gitlab
def eager_load(es_result, page, eager:) def eager_load(es_result, page, eager:)
paginated_base = es_result.page(page).per(per_page) paginated_base = es_result.page(page).per(per_page)
relation = paginated_base.records.includes(eager) # rubocop:disable CodeReuse/ActiveRecord relation = paginated_base.records.includes(eager) # rubocop:disable CodeReuse/ActiveRecord
filtered_results = []
permitted_results = relation.select do |o|
ability = :"read_#{o.to_ability_name}"
if Ability.allowed?(current_user, ability, o)
true
else
# Redact any search result the user may not have access to. This
# could be due to incorrect data in the index or a bug in our query
# so we log this as an error.
filtered_results << { ability: ability, id: o.id, class_name: o.class.name }
false
end
end
if filtered_results.any?
logger.error(message: "redacted_search_results", filtered: filtered_results, current_user_id: current_user&.id, query: query)
end
Kaminari.paginate_array( Kaminari.paginate_array(
relation, permitted_results,
total_count: paginated_base.total_count, total_count: paginated_base.total_count,
limit: per_page, limit: per_page,
offset: per_page * (page - 1) offset: per_page * (page - 1)
...@@ -363,6 +380,10 @@ module Gitlab ...@@ -363,6 +380,10 @@ module Gitlab
def per_page def per_page
20 20
end end
def logger
@logger ||= Gitlab::ProjectServiceLogger.build
end
end end
end end
end end
...@@ -56,6 +56,16 @@ describe Groups::BoardsController do ...@@ -56,6 +56,16 @@ describe Groups::BoardsController do
let(:parent) { group } let(:parent) { group }
it_behaves_like 'returns recently visited boards' it_behaves_like 'returns recently visited boards'
context 'unauthenticated' do
it 'returns a 401' do
sign_out(user)
list_boards(recent: true)
expect(response).to have_gitlab_http_status(401)
end
end
end end
describe 'GET show' do describe 'GET show' do
......
...@@ -31,6 +31,16 @@ describe Projects::BoardsController do ...@@ -31,6 +31,16 @@ describe Projects::BoardsController do
let(:parent) { project } let(:parent) { project }
it_behaves_like 'returns recently visited boards' it_behaves_like 'returns recently visited boards'
context 'unauthenticated' do
it 'returns a 302' do
sign_out(user)
list_boards(recent: true)
expect(response).to have_gitlab_http_status(302)
end
end
end end
describe 'GET show' do describe 'GET show' do
......
...@@ -72,10 +72,10 @@ describe Projects::ManagedLicensesController do ...@@ -72,10 +72,10 @@ describe Projects::ManagedLicensesController do
context 'with no logged in user' do context 'with no logged in user' do
let(:user) { unlogged_user } let(:user) { unlogged_user }
it 'returns an unauthorized status' do it 'returns a redirect' do
subject subject
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:redirect)
end end
end end
...@@ -122,10 +122,10 @@ describe Projects::ManagedLicensesController do ...@@ -122,10 +122,10 @@ describe Projects::ManagedLicensesController do
context 'with no logged in user' do context 'with no logged in user' do
let(:user) { unlogged_user } let(:user) { unlogged_user }
it 'returns an unauthorized status' do it 'returns a redirect' do
subject subject
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:redirect)
end end
end end
...@@ -235,10 +235,10 @@ describe Projects::ManagedLicensesController do ...@@ -235,10 +235,10 @@ describe Projects::ManagedLicensesController do
new_software_license_policy_attributes new_software_license_policy_attributes
end end
it 'returns an unauthorized status' do it 'returns a redirect' do
expect { subject }.not_to change { project.software_license_policies.count } expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:redirect)
end end
end end
...@@ -347,10 +347,10 @@ describe Projects::ManagedLicensesController do ...@@ -347,10 +347,10 @@ describe Projects::ManagedLicensesController do
new_software_license_policy_attributes new_software_license_policy_attributes
end end
it 'returns an unauthorized status' do it 'returns a redirect' do
expect { subject }.not_to change { project.software_license_policies.count } expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:redirect)
end end
end end
...@@ -452,10 +452,10 @@ describe Projects::ManagedLicensesController do ...@@ -452,10 +452,10 @@ describe Projects::ManagedLicensesController do
new_software_license_policy_attributes new_software_license_policy_attributes
end end
it 'returns an unauthorized status' do it 'returns a redirect' do
expect { subject }.not_to change { project.software_license_policies.count } expect { subject }.not_to change { project.software_license_policies.count }
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:redirect)
end end
end end
......
...@@ -506,10 +506,10 @@ describe Projects::Settings::OperationsController do ...@@ -506,10 +506,10 @@ describe Projects::Settings::OperationsController do
sign_out(user) sign_out(user)
end end
it 'returns unauthorized status' do it 'returns a redirect' do
reset_alerting_token reset_alerting_token
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:redirect)
end end
end end
......
...@@ -43,8 +43,10 @@ describe 'Global elastic search', :elastic do ...@@ -43,8 +43,10 @@ describe 'Global elastic search', :elastic do
let(:object) { :project } let(:object) { :project }
let(:creation_args) { { namespace: user.namespace } } let(:creation_args) { { namespace: user.namespace } }
let(:path) { search_path(search: 'project*', scope: 'projects') } let(:path) { search_path(search: 'project*', scope: 'projects') }
# Each Project requires 4 extra queries: one for each "count" (forks, open MRs, open Issues) and one for access level # Each Project requires 5 extra queries: one for each "count" (forks,
let(:query_count_multiplier) { 4 } # open MRs, open Issues) and twice for access level. This should be fixed
# per https://gitlab.com/gitlab-org/gitlab/issues/34457
let(:query_count_multiplier) { 5 }
it_behaves_like 'an efficient database result' it_behaves_like 'an efficient database result'
end end
......
...@@ -215,6 +215,45 @@ describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need_inlin ...@@ -215,6 +215,45 @@ describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need_inlin
expect(results.objects('notes')).to be_empty expect(results.objects('notes')).to be_empty
expect(results.notes_count).to eq 0 expect(results.notes_count).to eq 0
end end
it 'redacts issue comments on public projects where issue has lower access_level' do
project_1.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
results = described_class.new(user, 'foo', limit_project_ids)
expect(results.send(:logger))
.to receive(:error)
.with(hash_including(message: "redacted_search_results", filtered: array_including([
{ class_name: "Note", id: @note_1.id, ability: :read_note },
{ class_name: "Note", id: @note_2.id, ability: :read_note }
])))
expect(results.notes_count).to eq(2) # 2 because redacting only happens when we instantiate the results
expect(results.objects('notes')).to be_empty
end
it 'redacts commit comments when user is a guest on a private project' do
project_1.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
project_1.add_guest(user)
note_on_commit = create(
:note_on_commit,
project: project_1,
note: 'foo note on commit'
)
Gitlab::Elastic::Helper.refresh_index
results = described_class.new(user, 'foo', limit_project_ids)
expect(results.send(:logger))
.to receive(:error)
.with(hash_including(message: "redacted_search_results", filtered: array_including([
{ class_name: "Note", id: note_on_commit.id, ability: :read_note }
])))
expect(results.notes_count).to eq(3) # 3 because redacting only happens when we instantiate the results
expect(results.objects('notes')).to match_array([@note_1, @note_2])
end
end end
describe 'confidential issues' do describe 'confidential issues' do
...@@ -869,15 +908,20 @@ describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need_inlin ...@@ -869,15 +908,20 @@ describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need_inlin
context 'when project_ids is not present' do context 'when project_ids is not present' do
context 'when project_ids is :any' do context 'when project_ids is :any' do
it 'returns all milestones' do it 'returns all milestones and redacts them when the user has no access' do
results = described_class.new(user, 'project', :any) results = described_class.new(user, 'project', :any)
expect(results.send(:logger))
.to receive(:error)
.with(hash_including(message: "redacted_search_results", filtered: [{ class_name: "Milestone", id: milestone_2.id, ability: :read_milestone }]))
milestones = results.objects('milestones') milestones = results.objects('milestones')
expect(results.milestones_count).to eq(4) # 4 because redacting only happens when we instantiate the results
expect(milestones).to include(milestone_1) expect(milestones).to include(milestone_1)
expect(milestones).to include(milestone_2) expect(milestones).not_to include(milestone_2)
expect(milestones).to include(milestone_3) expect(milestones).to include(milestone_3)
expect(milestones).to include(milestone_4) expect(milestones).to include(milestone_4)
expect(results.milestones_count).to eq(4)
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe ProjectGroupLink do
describe '#destroy' do
let(:project) { create(:project) }
let(:group) { create(:group) }
let(:user) { create(:user) }
let!(:project_group_link) { create(:project_group_link, project: project, group: group) }
it 'removes related protected environment deploy access levels' do
params = attributes_for(:protected_environment,
deploy_access_levels_attributes: [{ group_id: group.id }, { user_id: user.id }])
protected_environment = ProtectedEnvironments::CreateService.new(project, user, params).execute
expect { project_group_link.destroy! }.to change(ProtectedEnvironment::DeployAccessLevel, :count).by(-1)
expect(protected_environment.deploy_access_levels.find_by_group_id(group)).to be_nil
expect(protected_environment.deploy_access_levels.find_by_user_id(user)).to be_persisted
end
end
end
...@@ -72,4 +72,17 @@ describe Note do ...@@ -72,4 +72,17 @@ describe Note do
end end
end end
end end
describe '#cross_reference?' do
[:relate_epic, :unrelate_epic].each do |type|
it "delegates #{type} system note to the cross-reference regex" do
note = create(:note, :system)
create(:system_note_metadata, note: note, action: type)
expect(note).to receive(:matches_cross_reference_regex?).and_return(false)
note.cross_reference?
end
end
end
end end
...@@ -116,6 +116,21 @@ describe ProtectedEnvironment do ...@@ -116,6 +116,21 @@ describe ProtectedEnvironment do
end end
end end
describe '.deploy_access_levels_by_group' do
let(:group) { create(:group) }
let(:project) { create(:project) }
let(:environment) { create(:environment, project: project, name: 'production') }
let(:protected_environment) { create(:protected_environment, project: project, name: 'production') }
it 'returns matching deploy access levels for the given group' do
_deploy_access_level_for_different_group = create_deploy_access_level(group: create(:group))
_deploy_access_level_for_user = create_deploy_access_level(user: create(:user))
deploy_access_level = create_deploy_access_level(group: group)
expect(described_class.deploy_access_levels_by_group(group)).to contain_exactly(deploy_access_level)
end
end
def create_deploy_access_level(**opts) def create_deploy_access_level(**opts)
protected_environment.deploy_access_levels.create(**opts) protected_environment.deploy_access_levels.create(**opts)
end end
......
...@@ -320,4 +320,50 @@ describe Elastic::IndexRecordService, :elastic do ...@@ -320,4 +320,50 @@ describe Elastic::IndexRecordService, :elastic do
expect(Project.elastic_search('project_1').present?).to eq(false) expect(Project.elastic_search('project_1').present?).to eq(false)
end end
context 'when updating an Issue' do
context 'when changing the confidential value' do
it 'updates issue notes excluding system notes' do
issue = nil
Sidekiq::Testing.disable! do
issue = create(:issue, confidential: false)
subject.execute(issue.project, true)
subject.execute(issue, false)
create(:note, note: 'the_normal_note', noteable: issue, project: issue.project)
create(:note, note: 'the_system_note', system: true, noteable: issue, project: issue.project)
end
options = { project_ids: [issue.project.id] }
Sidekiq::Testing.inline! do
expect(subject.execute(issue, false, 'changed_fields' => ['confidential'])).to eq(true)
Gitlab::Elastic::Helper.refresh_index
end
expect(Note.elastic_search('the_normal_note', options: options).present?).to eq(true)
expect(Note.elastic_search('the_system_note', options: options).present?).to eq(false)
end
end
context 'when changing the title' do
it 'does not update issue notes' do
issue = nil
Sidekiq::Testing.disable! do
issue = create(:issue, confidential: false)
subject.execute(issue.project, true)
subject.execute(issue, false)
create(:note, note: 'the_normal_note', noteable: issue, project: issue.project)
end
options = { project_ids: [issue.project.id] }
Sidekiq::Testing.inline! do
expect(subject.execute(issue, false, 'changed_fields' => ['title'])).to eq(true)
Gitlab::Elastic::Helper.refresh_index
end
expect(Note.elastic_search('the_normal_note', options: options).present?).to eq(false)
end
end
end
end end
...@@ -5,16 +5,6 @@ require 'spec_helper' ...@@ -5,16 +5,6 @@ require 'spec_helper'
shared_examples 'returns recently visited boards' do shared_examples 'returns recently visited boards' do
let(:boards) { create_list(:board, 8, resource_parent: parent) } let(:boards) { create_list(:board, 8, resource_parent: parent) }
context 'unauthenticated' do
it 'returns a 401' do
sign_out(user)
list_boards(recent: true)
expect(response).to have_gitlab_http_status(401)
end
end
it 'returns last 4 visited boards' do it 'returns last 4 visited boards' do
[0, 2, 5, 3, 7, 1].each_with_index do |board_index, i| [0, 2, 5, 3, 7, 1].each_with_index do |board_index, i|
visit_board(boards[board_index], Time.now + i.minutes) visit_board(boards[board_index], Time.now + i.minutes)
......
# frozen_string_literal: true
# Recursive queries, with relatively low effort, can quickly spiral out of control exponentially
# and may not be picked up by depth and complexity alone.
module Gitlab
module Graphql
module QueryAnalyzers
class RecursionAnalyzer
IGNORED_FIELDS = %w(node edges ofType).freeze
RECURSION_THRESHOLD = 2
def initial_value(query)
{
recurring_fields: {}
}
end
def call(memo, visit_type, irep_node)
return memo if skip_node?(irep_node)
node_name = irep_node.ast_node.name
times_encountered = memo[node_name] || 0
if visit_type == :enter
times_encountered += 1
memo[:recurring_fields][node_name] = times_encountered if recursion_too_deep?(node_name, times_encountered)
else
times_encountered -= 1
end
memo[node_name] = times_encountered
memo
end
def final_value(memo)
recurring_fields = memo[:recurring_fields]
recurring_fields = recurring_fields.select { |k, v| recursion_too_deep?(k, v) }
if recurring_fields.any?
GraphQL::AnalysisError.new("Recursive query - too many of fields '#{recurring_fields}' detected in single branch of the query")
end
end
private
def recursion_too_deep?(node_name, times_encountered)
return if IGNORED_FIELDS.include?(node_name)
times_encountered > recursion_threshold
end
def skip_node?(irep_node)
ast_node = irep_node.ast_node
!ast_node.is_a?(GraphQL::Language::Nodes::Field) || ast_node.selections.empty?
end
def recursion_threshold
RECURSION_THRESHOLD
end
end
end
end
end
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
def self.render(file_name, input, context) def self.render(file_name, input, context)
html = GitHub::Markup.render(file_name, input) html = GitHub::Markup.render(file_name, input)
.force_encoding(input.encoding) .force_encoding(input.encoding)
context[:pipeline] = :markup context[:pipeline] ||= :markup
html = Banzai.render(html, context) html = Banzai.render(html, context)
......
...@@ -163,7 +163,7 @@ module Gitlab ...@@ -163,7 +163,7 @@ module Gitlab
return Milestone.none if project_ids.nil? return Milestone.none if project_ids.nil?
authorized_project_ids_relation = authorized_project_ids_relation =
Project.where(id: project_ids).ids_with_milestone_available_for(current_user) Project.where(id: project_ids).ids_with_issuables_available_for(current_user)
milestones.where(project_id: authorized_project_ids_relation) milestones.where(project_id: authorized_project_ids_relation)
end end
......
...@@ -186,7 +186,7 @@ describe ApplicationController do ...@@ -186,7 +186,7 @@ describe ApplicationController do
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
it 'redirects to login page via authenticate_user! if not authenticated' do it 'redirects to login page if not authenticated' do
get :index get :index
expect(response).to redirect_to new_user_session_path expect(response).to redirect_to new_user_session_path
......
...@@ -19,7 +19,8 @@ describe InternalRedirect do ...@@ -19,7 +19,8 @@ describe InternalRedirect do
[ [
'Hello world', 'Hello world',
'//example.com/hello/world', '//example.com/hello/world',
'https://example.com/hello/world' 'https://example.com/hello/world',
"not-starting-with-a-slash\n/starting/with/slash"
] ]
end end
......
...@@ -16,13 +16,17 @@ describe LfsRequest do ...@@ -16,13 +16,17 @@ describe LfsRequest do
end end
def project def project
@project ||= Project.find(params[:id]) @project ||= Project.find_by(id: params[:id])
end end
def download_request? def download_request?
true true
end end
def upload_request?
false
end
def ci? def ci?
false false
end end
...@@ -49,4 +53,41 @@ describe LfsRequest do ...@@ -49,4 +53,41 @@ describe LfsRequest do
expect(assigns(:storage_project)).to eq(project) expect(assigns(:storage_project)).to eq(project)
end end
end end
context 'user is authenticated without access to lfs' do
before do
allow(controller).to receive(:authenticate_user)
allow(controller).to receive(:authentication_result) do
Gitlab::Auth::Result.new
end
end
context 'with access to the project' do
it 'returns 403' do
get :show, params: { id: project.id }
expect(response.status).to eq(403)
end
end
context 'without access to the project' do
context 'project does not exist' do
it 'returns 404' do
get :show, params: { id: 'does not exist' }
expect(response.status).to eq(404)
end
end
context 'project is private' do
let(:project) { create(:project, :private) }
it 'returns 404' do
get :show, params: { id: project.id }
expect(response.status).to eq(404)
end
end
end
end
end end
...@@ -8,6 +8,10 @@ describe Projects::AutocompleteSourcesController do ...@@ -8,6 +8,10 @@ describe Projects::AutocompleteSourcesController do
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
def members_by_username(username)
json_response.find { |member| member['username'] == username }
end
describe 'GET members' do describe 'GET members' do
before do before do
group.add_owner(user) group.add_owner(user)
...@@ -17,22 +21,21 @@ describe Projects::AutocompleteSourcesController do ...@@ -17,22 +21,21 @@ describe Projects::AutocompleteSourcesController do
it 'returns an array of member object' do it 'returns an array of member object' do
get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id } get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
all = json_response.find {|member| member["username"] == 'all'} expect(members_by_username('all').symbolize_keys).to include(
the_group = json_response.find {|member| member["username"] == group.full_path} username: 'all',
the_user = json_response.find {|member| member["username"] == user.username} name: 'All Project and Group Members',
count: 1)
expect(all.symbolize_keys).to include(username: 'all',
name: 'All Project and Group Members', expect(members_by_username(group.full_path).symbolize_keys).to include(
count: 1) type: group.class.name,
name: group.full_name,
expect(the_group.symbolize_keys).to include(type: group.class.name, avatar_url: group.avatar_url,
name: group.full_name, count: 1)
avatar_url: group.avatar_url,
count: 1) expect(members_by_username(user.username).symbolize_keys).to include(
type: user.class.name,
expect(the_user.symbolize_keys).to include(type: user.class.name, name: user.name,
name: user.name, avatar_url: user.avatar_url)
avatar_url: user.avatar_url)
end end
end end
......
...@@ -142,7 +142,7 @@ describe Projects::CommitsController do ...@@ -142,7 +142,7 @@ describe Projects::CommitsController do
context 'token authentication' do context 'token authentication' do
context 'public project' do context 'public project' do
it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do it_behaves_like 'authenticates sessionless user', :show, :atom, { public: true, ignore_incrementing: true } do
before do before do
public_project = create(:project, :repository, :public) public_project = create(:project, :repository, :public)
...@@ -152,7 +152,7 @@ describe Projects::CommitsController do ...@@ -152,7 +152,7 @@ describe Projects::CommitsController do
end end
context 'private project' do context 'private project' do
it_behaves_like 'authenticates sessionless user', :show, :atom, public: false do it_behaves_like 'authenticates sessionless user', :show, :atom, { public: false, ignore_incrementing: true } do
before do before do
private_project = create(:project, :repository, :private) private_project = create(:project, :repository, :private)
private_project.add_maintainer(user) private_project.add_maintainer(user)
......
...@@ -146,7 +146,7 @@ describe Projects::ErrorTrackingController do ...@@ -146,7 +146,7 @@ describe Projects::ErrorTrackingController do
it 'redirects to sign-in page' do it 'redirects to sign-in page' do
post :list_projects, params: list_projects_params post :list_projects, params: list_projects_params
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:redirect)
end end
end end
......
...@@ -1441,7 +1441,7 @@ describe Projects::IssuesController do ...@@ -1441,7 +1441,7 @@ describe Projects::IssuesController do
context 'private project with token authentication' do context 'private project with token authentication' do
let(:private_project) { create(:project, :private) } let(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user', :index, :atom do it_behaves_like 'authenticates sessionless user', :index, :atom, ignore_incrementing: true do
before do before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
...@@ -1449,7 +1449,7 @@ describe Projects::IssuesController do ...@@ -1449,7 +1449,7 @@ describe Projects::IssuesController do
end end
end end
it_behaves_like 'authenticates sessionless user', :calendar, :ics do it_behaves_like 'authenticates sessionless user', :calendar, :ics, ignore_incrementing: true do
before do before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
......
...@@ -41,7 +41,7 @@ describe Projects::TagsController do ...@@ -41,7 +41,7 @@ describe Projects::TagsController do
context 'private project with token authentication' do context 'private project with token authentication' do
let(:private_project) { create(:project, :repository, :private) } let(:private_project) { create(:project, :repository, :private) }
it_behaves_like 'authenticates sessionless user', :index, :atom do it_behaves_like 'authenticates sessionless user', :index, :atom, ignore_incrementing: true do
before do before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace) default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
......
...@@ -1149,7 +1149,7 @@ describe ProjectsController do ...@@ -1149,7 +1149,7 @@ describe ProjectsController do
context 'private project with token authentication' do context 'private project with token authentication' do
let(:private_project) { create(:project, :private) } let(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user', :show, :atom do it_behaves_like 'authenticates sessionless user', :show, :atom, ignore_incrementing: true do
before do before do
default_params.merge!(id: private_project, namespace_id: private_project.namespace) default_params.merge!(id: private_project, namespace_id: private_project.namespace)
......
...@@ -819,7 +819,10 @@ describe 'Pipelines', :js do ...@@ -819,7 +819,10 @@ describe 'Pipelines', :js do
context 'when project is private' do context 'when project is private' do
let(:project) { create(:project, :private, :repository) } let(:project) { create(:project, :private, :repository) }
it { expect(page).to have_content 'You need to sign in' } it 'redirects the user to sign_in and displays the flash alert' do
expect(page).to have_content 'You need to sign in'
expect(page.current_path).to eq("/users/sign_in")
end
end end
end end
......
...@@ -15,7 +15,7 @@ describe 'User views tags', :feature do ...@@ -15,7 +15,7 @@ describe 'User views tags', :feature do
it do it do
visit project_tags_path(project, format: :atom) visit project_tags_path(project, format: :atom)
expect(page).to have_gitlab_http_status(401) expect(page.current_path).to eq("/users/sign_in")
end end
end end
......
...@@ -128,6 +128,89 @@ describe LabelsFinder do ...@@ -128,6 +128,89 @@ describe LabelsFinder do
expect(finder.execute).to eq [private_subgroup_label_1] expect(finder.execute).to eq [private_subgroup_label_1]
end end
end end
context 'when including labels from group projects with limited visibility' do
let(:finder) { described_class.new(user, group_id: group_4.id) }
let(:group_4) { create(:group) }
let(:limited_visibility_project) { create(:project, :public, group: group_4) }
let(:visible_project) { create(:project, :public, group: group_4) }
let!(:group_label_1) { create(:group_label, group: group_4) }
let!(:limited_visibility_label) { create(:label, project: limited_visibility_project) }
let!(:visible_label) { create(:label, project: visible_project) }
shared_examples 'with full visibility' do
it 'returns all projects labels' do
expect(finder.execute).to eq [group_label_1, limited_visibility_label, visible_label]
end
end
shared_examples 'with limited visibility' do
it 'returns only authorized projects labels' do
expect(finder.execute).to eq [group_label_1, visible_label]
end
end
context 'when merge requests and issues are not visible for non members' do
before do
limited_visibility_project.project_feature.update!(
merge_requests_access_level: ProjectFeature::PRIVATE,
issues_access_level: ProjectFeature::PRIVATE
)
end
context 'when user is not a group member' do
it_behaves_like 'with limited visibility'
end
context 'when user is a group member' do
before do
group_4.add_developer(user)
end
it_behaves_like 'with full visibility'
end
end
context 'when merge requests are not visible for non members' do
before do
limited_visibility_project.project_feature.update!(
merge_requests_access_level: ProjectFeature::PRIVATE
)
end
context 'when user is not a group member' do
it_behaves_like 'with full visibility'
end
context 'when user is a group member' do
before do
group_4.add_developer(user)
end
it_behaves_like 'with full visibility'
end
end
context 'when issues are not visible for non members' do
before do
limited_visibility_project.project_feature.update!(
issues_access_level: ProjectFeature::PRIVATE
)
end
context 'when user is not a group member' do
it_behaves_like 'with full visibility'
end
context 'when user is a group member' do
before do
group_4.add_developer(user)
end
it_behaves_like 'with full visibility'
end
end
end
end end
context 'filtering by project_id' do context 'filtering by project_id' do
......
query allSchemaTypes {
__schema {
types {
fields {
type{
fields {
type {
fields {
type {
fields {
type {
fields {
type {
fields {
type {
fields {
type {
fields {
type {
fields {
type {
fields {
type {
fields {
type {
fields {
type {
fields {
name
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
\ No newline at end of file
{
project(fullPath: "gitlab-org/gitlab-ce") {
group {
projects {
edges {
node {
group {
projects {
edges {
node {
group {
projects {
edges {
node {
group {
projects {
edges {
node {
group {
projects {
edges {
node {
group {
description
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
query allSchemaTypes {
__schema {
types {
fields {
type {
fields {
type {
name
}
}
}
}
}
}
}
...@@ -3,6 +3,9 @@ import $ from 'jquery'; ...@@ -3,6 +3,9 @@ import $ from 'jquery';
import ProjectFindFile from '~/project_find_file'; import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import sanitize from 'sanitize-html';
jest.mock('sanitize-html', () => jest.fn(val => val));
const BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`; const BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`;
const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`; const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`;
...@@ -38,31 +41,31 @@ describe('ProjectFindFile', () => { ...@@ -38,31 +41,31 @@ describe('ProjectFindFile', () => {
href: el.querySelector('a').href, href: el.querySelector('a').href,
})); }));
const files = [
'fileA.txt',
'fileB.txt',
'fi#leC.txt',
'folderA/fileD.txt',
'folder#B/fileE.txt',
'folde?rC/fil#F.txt',
];
beforeEach(() => { beforeEach(() => {
// Create a mock adapter for stubbing axios API requests // Create a mock adapter for stubbing axios API requests
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
element = $(TEMPLATE); element = $(TEMPLATE);
mock.onGet(FILE_FIND_URL).replyOnce(200, files);
getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
}); });
afterEach(() => { afterEach(() => {
// Reset the mock adapter // Reset the mock adapter
mock.restore(); mock.restore();
sanitize.mockClear();
}); });
it('loads and renders elements from remote server', done => { it('loads and renders elements from remote server', done => {
const files = [
'fileA.txt',
'fileB.txt',
'fi#leC.txt',
'folderA/fileD.txt',
'folder#B/fileE.txt',
'folde?rC/fil#F.txt',
];
mock.onGet(FILE_FIND_URL).replyOnce(200, files);
getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
setImmediate(() => { setImmediate(() => {
expect(findFiles()).toEqual( expect(findFiles()).toEqual(
files.map(text => ({ files.map(text => ({
...@@ -74,4 +77,14 @@ describe('ProjectFindFile', () => { ...@@ -74,4 +77,14 @@ describe('ProjectFindFile', () => {
done(); done();
}); });
}); });
it('sanitizes search text', done => {
const searchText = element.find('.file-finder-input').val();
setImmediate(() => {
expect(sanitize).toHaveBeenCalledTimes(1);
expect(sanitize).toHaveBeenCalledWith(searchText);
done();
});
});
}); });
...@@ -3,18 +3,18 @@ ...@@ -3,18 +3,18 @@
require 'spec_helper' require 'spec_helper'
describe MarkupHelper do describe MarkupHelper do
let!(:project) { create(:project, :repository) } set(:project) { create(:project, :repository) }
set(:user) do
let(:user) { create(:user, username: 'gfm') } user = create(:user, username: 'gfm')
let(:commit) { project.commit }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:snippet) { create(:project_snippet, project: project) }
before do
# Ensure the generated reference links aren't redacted
project.add_maintainer(user) project.add_maintainer(user)
user
end
set(:issue) { create(:issue, project: project) }
set(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
set(:snippet) { create(:project_snippet, project: project) }
let(:commit) { project.commit }
before do
# Helper expects a @project instance variable # Helper expects a @project instance variable
helper.instance_variable_set(:@project, project) helper.instance_variable_set(:@project, project)
...@@ -44,8 +44,8 @@ describe MarkupHelper do ...@@ -44,8 +44,8 @@ describe MarkupHelper do
describe "override default project" do describe "override default project" do
let(:actual) { issue.to_reference } let(:actual) { issue.to_reference }
let(:second_project) { create(:project, :public) } set(:second_project) { create(:project, :public) }
let(:second_issue) { create(:issue, project: second_project) } set(:second_issue) { create(:issue, project: second_project) }
it 'links to the issue' do it 'links to the issue' do
expected = urls.project_issue_path(second_project, second_issue) expected = urls.project_issue_path(second_project, second_issue)
...@@ -55,7 +55,7 @@ describe MarkupHelper do ...@@ -55,7 +55,7 @@ describe MarkupHelper do
describe 'uploads' do describe 'uploads' do
let(:text) { "![ImageTest](/uploads/test.png)" } let(:text) { "![ImageTest](/uploads/test.png)" }
let(:group) { create(:group) } set(:group) { create(:group) }
subject { helper.markdown(text) } subject { helper.markdown(text) }
...@@ -77,7 +77,7 @@ describe MarkupHelper do ...@@ -77,7 +77,7 @@ describe MarkupHelper do
end end
describe "with a group in the context" do describe "with a group in the context" do
let(:project_in_group) { create(:project, group: group) } set(:project_in_group) { create(:project, group: group) }
before do before do
helper.instance_variable_set(:@group, group) helper.instance_variable_set(:@group, group)
...@@ -235,38 +235,48 @@ describe MarkupHelper do ...@@ -235,38 +235,48 @@ describe MarkupHelper do
end end
describe '#render_wiki_content' do describe '#render_wiki_content' do
let(:wiki) { double('WikiPage', path: "file.#{extension}") }
let(:context) do
{
pipeline: :wiki, project: project, project_wiki: wiki,
page_slug: 'nested/page', issuable_state_filter_enabled: true
}
end
before do before do
@wiki = double('WikiPage') expect(wiki).to receive(:content).and_return('wiki content')
allow(@wiki).to receive(:content).and_return('wiki content') expect(wiki).to receive(:slug).and_return('nested/page')
allow(@wiki).to receive(:slug).and_return('nested/page') helper.instance_variable_set(:@project_wiki, wiki)
helper.instance_variable_set(:@project_wiki, @wiki)
end end
it "uses Wiki pipeline for markdown files" do context 'when file is Markdown' do
allow(@wiki).to receive(:format).and_return(:markdown) let(:extension) { 'md' }
expect(helper).to receive(:markdown_unsafe).with('wiki content', it 'renders using #markdown_unsafe helper method' do
pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page", expect(helper).to receive(:markdown_unsafe).with('wiki content', context)
issuable_state_filter_enabled: true)
helper.render_wiki_content(@wiki) helper.render_wiki_content(wiki)
end
end end
it "uses Asciidoctor for asciidoc files" do context 'when file is Asciidoc' do
allow(@wiki).to receive(:format).and_return(:asciidoc) let(:extension) { 'adoc' }
expect(helper).to receive(:asciidoc_unsafe).with('wiki content') it 'renders using Gitlab::Asciidoc' do
expect(Gitlab::Asciidoc).to receive(:render)
helper.render_wiki_content(@wiki) helper.render_wiki_content(wiki)
end
end end
it "uses the Gollum renderer for all other file types" do context 'any other format' do
allow(@wiki).to receive(:format).and_return(:rdoc) let(:extension) { 'foo' }
formatted_content_stub = double('formatted_content')
expect(formatted_content_stub).to receive(:html_safe)
allow(@wiki).to receive(:formatted_content).and_return(formatted_content_stub)
helper.render_wiki_content(@wiki) it 'renders all other formats using Gitlab::OtherMarkup' do
expect(Gitlab::OtherMarkup).to receive(:render)
helper.render_wiki_content(wiki)
end
end end
end end
......
...@@ -227,6 +227,14 @@ describe Milestone do ...@@ -227,6 +227,14 @@ describe Milestone do
end end
end end
describe '#to_ability_name' do
it 'returns milestone' do
milestone = build(:milestone)
expect(milestone.to_ability_name).to eq('milestone')
end
end
describe '.search' do describe '.search' do
let(:milestone) { create(:milestone, title: 'foo', description: 'bar') } let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }
......
...@@ -379,6 +379,63 @@ describe Note do ...@@ -379,6 +379,63 @@ describe Note do
expect(label_note.cross_reference?).to be_falsy expect(label_note.cross_reference?).to be_falsy
end end
end end
context 'when system note metadata is not present' do
let(:note) { build(:note, :system) }
before do
allow(note).to receive(:system_note_metadata).and_return(nil)
end
it 'delegates to the system note service' do
expect(SystemNotes::IssuablesService).to receive(:cross_reference?).with(note.note)
note.cross_reference?
end
end
context 'with a system note' do
let(:issue) { create(:issue, project: create(:project, :repository)) }
let(:note) { create(:system_note, note: "test", noteable: issue, project: issue.project) }
shared_examples 'system_note_metadata includes note action' do
it 'delegates to the cross-reference regex' do
expect(note).to receive(:matches_cross_reference_regex?)
note.cross_reference?
end
end
context 'with :label action' do
let!(:metadata) {create(:system_note_metadata, note: note, action: :label)}
it_behaves_like 'system_note_metadata includes note action'
it { expect(note.cross_reference?).to be_falsy }
context 'with cross reference label note' do
let(:label) { create(:label, project: issue.project)}
let(:note) { create(:system_note, note: "added #{label.to_reference} label", noteable: issue, project: issue.project) }
it { expect(note.cross_reference?).to be_truthy }
end
end
context 'with :milestone action' do
let!(:metadata) {create(:system_note_metadata, note: note, action: :milestone)}
it_behaves_like 'system_note_metadata includes note action'
it { expect(note.cross_reference?).to be_falsy }
context 'with cross reference milestone note' do
let(:milestone) { create(:milestone, project: issue.project)}
let(:note) { create(:system_note, note: "added #{milestone.to_reference} milestone", noteable: issue, project: issue.project) }
it { expect(note.cross_reference?).to be_truthy }
end
end
end
end end
describe 'clear_blank_line_code!' do describe 'clear_blank_line_code!' do
...@@ -578,24 +635,30 @@ describe Note do ...@@ -578,24 +635,30 @@ describe Note do
end end
describe '#to_ability_name' do describe '#to_ability_name' do
it 'returns snippet for a project snippet note' do it 'returns note' do
expect(build(:note_on_project_snippet).to_ability_name).to eq('project_snippet') expect(build(:note).to_ability_name).to eq('note')
end
end
describe '#noteable_ability_name' do
it 'returns project_snippet for a project snippet note' do
expect(build(:note_on_project_snippet).noteable_ability_name).to eq('project_snippet')
end end
it 'returns personal_snippet for a personal snippet note' do it 'returns personal_snippet for a personal snippet note' do
expect(build(:note_on_personal_snippet).to_ability_name).to eq('personal_snippet') expect(build(:note_on_personal_snippet).noteable_ability_name).to eq('personal_snippet')
end end
it 'returns merge_request for an MR note' do it 'returns merge_request for an MR note' do
expect(build(:note_on_merge_request).to_ability_name).to eq('merge_request') expect(build(:note_on_merge_request).noteable_ability_name).to eq('merge_request')
end end
it 'returns issue for an issue note' do it 'returns issue for an issue note' do
expect(build(:note_on_issue).to_ability_name).to eq('issue') expect(build(:note_on_issue).noteable_ability_name).to eq('issue')
end end
it 'returns issue for a commit note' do it 'returns commit for a commit note' do
expect(build(:note_on_commit).to_ability_name).to eq('commit') expect(build(:note_on_commit).noteable_ability_name).to eq('commit')
end end
end end
......
...@@ -3416,7 +3416,7 @@ describe Project do ...@@ -3416,7 +3416,7 @@ describe Project do
end end
end end
describe '.ids_with_milestone_available_for' do describe '.ids_with_issuables_available_for' do
let!(:user) { create(:user) } let!(:user) { create(:user) }
it 'returns project ids with milestones available for user' do it 'returns project ids with milestones available for user' do
...@@ -3426,7 +3426,7 @@ describe Project do ...@@ -3426,7 +3426,7 @@ describe Project do
project_4 = create(:project, :public) project_4 = create(:project, :public)
project_4.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE ) project_4.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE )
project_ids = described_class.ids_with_milestone_available_for(user).pluck(:id) project_ids = described_class.ids_with_issuables_available_for(user).pluck(:id)
expect(project_ids).to include(project_2.id, project_3.id) expect(project_ids).to include(project_2.id, project_3.id)
expect(project_ids).not_to include(project_1.id, project_4.id) expect(project_ids).not_to include(project_1.id, project_4.id)
...@@ -4444,6 +4444,14 @@ describe Project do ...@@ -4444,6 +4444,14 @@ describe Project do
end end
end end
describe '#to_ability_name' do
it 'returns project' do
project = build(:project_empty_repo)
expect(project.to_ability_name).to eq('project')
end
end
describe '#execute_hooks' do describe '#execute_hooks' do
let(:data) { { ref: 'refs/heads/master', data: 'data' } } let(:data) { { ref: 'refs/heads/master', data: 'data' } }
it 'executes active projects hooks with the specified scope' do it 'executes active projects hooks with the specified scope' do
......
...@@ -358,6 +358,23 @@ describe WikiPage do ...@@ -358,6 +358,23 @@ describe WikiPage do
end end
end end
describe '#path' do
let(:path) { 'mypath.md' }
let(:wiki_page) { instance_double('Gitlab::Git::WikiPage', path: path).as_null_object }
it 'returns the path when persisted' do
page = described_class.new(wiki, wiki_page, true)
expect(page.path).to eq(path)
end
it 'returns nil when not persisted' do
page = described_class.new(wiki, wiki_page, false)
expect(page.path).to be_nil
end
end
describe '#directory' do describe '#directory' do
context 'when the page is at the root directory' do context 'when the page is at the root directory' do
it 'returns an empty string' do it 'returns an empty string' do
......
...@@ -8,28 +8,42 @@ describe CommitPolicy do ...@@ -8,28 +8,42 @@ describe CommitPolicy do
let(:commit) { project.repository.head_commit } let(:commit) { project.repository.head_commit }
let(:policy) { described_class.new(user, commit) } let(:policy) { described_class.new(user, commit) }
shared_examples 'can read commit and create a note' do
it 'can read commit' do
expect(policy).to be_allowed(:read_commit)
end
it 'can create a note' do
expect(policy).to be_allowed(:create_note)
end
end
shared_examples 'cannot read commit nor create a note' do
it 'can not read commit' do
expect(policy).to be_disallowed(:read_commit)
end
it 'can not create a note' do
expect(policy).to be_disallowed(:create_note)
end
end
context 'when project is public' do context 'when project is public' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
it 'can read commit and create a note' do it_behaves_like 'can read commit and create a note'
expect(policy).to be_allowed(:read_commit)
end
context 'when repository access level is private' do context 'when repository access level is private' do
let(:project) { create(:project, :public, :repository, :repository_private) } let(:project) { create(:project, :public, :repository, :repository_private) }
it 'can not read commit and create a note' do it_behaves_like 'cannot read commit nor create a note'
expect(policy).to be_disallowed(:read_commit)
end
context 'when the user is a project member' do context 'when the user is a project member' do
before do before do
project.add_developer(user) project.add_developer(user)
end end
it 'can read commit and create a note' do it_behaves_like 'can read commit and create a note'
expect(policy).to be_allowed(:read_commit)
end
end end
end end
end end
...@@ -37,9 +51,7 @@ describe CommitPolicy do ...@@ -37,9 +51,7 @@ describe CommitPolicy do
context 'when project is private' do context 'when project is private' do
let(:project) { create(:project, :private, :repository) } let(:project) { create(:project, :private, :repository) }
it 'can not read commit and create a note' do it_behaves_like 'cannot read commit nor create a note'
expect(policy).to be_disallowed(:read_commit)
end
context 'when the user is a project member' do context 'when the user is a project member' do
before do before do
...@@ -50,6 +62,18 @@ describe CommitPolicy do ...@@ -50,6 +62,18 @@ describe CommitPolicy do
expect(policy).to be_allowed(:read_commit) expect(policy).to be_allowed(:read_commit)
end end
end end
context 'when the user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like 'cannot read commit nor create a note'
it 'cannot download code' do
expect(policy).to be_disallowed(:download_code)
end
end
end end
end end
end end
...@@ -356,6 +356,88 @@ describe GroupPolicy do ...@@ -356,6 +356,88 @@ describe GroupPolicy do
end end
end end
context 'transfer_projects' do
shared_examples_for 'allowed to transfer projects' do
before do
group.update(project_creation_level: project_creation_level)
end
it { is_expected.to be_allowed(:transfer_projects) }
end
shared_examples_for 'not allowed to transfer projects' do
before do
group.update(project_creation_level: project_creation_level)
end
it { is_expected.to be_disallowed(:transfer_projects) }
end
context 'reporter' do
let(:current_user) { reporter }
it_behaves_like 'not allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::NO_ONE_PROJECT_ACCESS }
end
it_behaves_like 'not allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS }
end
it_behaves_like 'not allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS }
end
end
context 'developer' do
let(:current_user) { developer }
it_behaves_like 'not allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::NO_ONE_PROJECT_ACCESS }
end
it_behaves_like 'not allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS }
end
it_behaves_like 'not allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS }
end
end
context 'maintainer' do
let(:current_user) { maintainer }
it_behaves_like 'not allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::NO_ONE_PROJECT_ACCESS }
end
it_behaves_like 'allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS }
end
it_behaves_like 'allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS }
end
end
context 'owner' do
let(:current_user) { owner }
it_behaves_like 'not allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::NO_ONE_PROJECT_ACCESS }
end
it_behaves_like 'allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS }
end
it_behaves_like 'allowed to transfer projects' do
let(:project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS }
end
end
end
context "create_projects" do context "create_projects" do
context 'when group has no project creation level set' do context 'when group has no project creation level set' do
before_all do before_all do
......
...@@ -8,7 +8,7 @@ describe NamespacePolicy do ...@@ -8,7 +8,7 @@ describe NamespacePolicy do
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, owner: owner) } let(:namespace) { create(:namespace, owner: owner) }
let(:owner_permissions) { [:create_projects, :admin_namespace, :read_namespace, :read_statistics] } let(:owner_permissions) { [:create_projects, :admin_namespace, :read_namespace, :read_statistics, :transfer_projects] }
subject { described_class.new(current_user, namespace) } subject { described_class.new(current_user, namespace) }
...@@ -33,6 +33,7 @@ describe NamespacePolicy do ...@@ -33,6 +33,7 @@ describe NamespacePolicy do
let(:owner) { create(:user, projects_limit: 0) } let(:owner) { create(:user, projects_limit: 0) }
it { is_expected.to be_disallowed(:create_projects) } it { is_expected.to be_disallowed(:create_projects) }
it { is_expected.to be_disallowed(:transfer_projects) }
end end
end end
......
...@@ -15,7 +15,7 @@ describe 'GitlabSchema configurations' do ...@@ -15,7 +15,7 @@ describe 'GitlabSchema configurations' do
subject subject
expect(graphql_errors.flatten.first['message']).to include('which exceeds max complexity of 1') expect_graphql_errors_to_include /which exceeds max complexity of 1/
end end
end end
end end
...@@ -23,12 +23,11 @@ describe 'GitlabSchema configurations' do ...@@ -23,12 +23,11 @@ describe 'GitlabSchema configurations' do
describe '#max_depth' do describe '#max_depth' do
context 'when query depth is too high' do context 'when query depth is too high' do
it 'shows error' do it 'shows error' do
errors = { "message" => "Query has depth of 2, which exceeds max depth of 1" }
allow(GitlabSchema).to receive(:max_query_depth).and_return 1 allow(GitlabSchema).to receive(:max_query_depth).and_return 1
subject subject
expect(graphql_errors.flatten).to include(errors) expect_graphql_errors_to_include /exceeds max depth/
end end
end end
...@@ -38,7 +37,42 @@ describe 'GitlabSchema configurations' do ...@@ -38,7 +37,42 @@ describe 'GitlabSchema configurations' do
subject subject
expect(Array.wrap(graphql_errors).compact).to be_empty expect_graphql_errors_to_be_empty
end
end
end
end
context 'depth, complexity and recursion checking' do
context 'unauthenticated recursive queries' do
context 'a not-quite-recursive-enough introspective query' do
it 'succeeds' do
query = File.read(Rails.root.join('spec/fixtures/api/graphql/small-recursive-introspection.graphql'))
post_graphql(query, current_user: nil)
expect_graphql_errors_to_be_empty
end
end
context 'a deep but simple recursive introspective query' do
it 'fails due to recursion' do
query = File.read(Rails.root.join('spec/fixtures/api/graphql/recursive-introspection.graphql'))
post_graphql(query, current_user: nil)
expect_graphql_errors_to_include [/Recursive query/]
end
end
context 'a deep recursive non-introspective query' do
it 'fails due to recursion, complexity and depth' do
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
query = File.read(Rails.root.join('spec/fixtures/api/graphql/recursive-query.graphql'))
post_graphql(query, current_user: nil)
expect_graphql_errors_to_include [/Recursive query/, /exceeds max complexity/, /exceeds max depth/]
end end
end end
end end
...@@ -88,7 +122,7 @@ describe 'GitlabSchema configurations' do ...@@ -88,7 +122,7 @@ describe 'GitlabSchema configurations' do
# Expect errors for each query # Expect errors for each query
expect(graphql_errors.size).to eq(3) expect(graphql_errors.size).to eq(3)
graphql_errors.each do |single_query_errors| graphql_errors.each do |single_query_errors|
expect(single_query_errors.first['message']).to include('which exceeds max complexity of 4') expect_graphql_errors_to_include(/which exceeds max complexity of 4/)
end end
end end
end end
...@@ -105,12 +139,12 @@ describe 'GitlabSchema configurations' do ...@@ -105,12 +139,12 @@ describe 'GitlabSchema configurations' do
end end
context 'when IntrospectionQuery' do context 'when IntrospectionQuery' do
it 'is not too complex' do it 'is not too complex nor recursive' do
query = File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) query = File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql'))
post_graphql(query, current_user: nil) post_graphql(query, current_user: nil)
expect(graphql_errors).to be_nil expect_graphql_errors_to_be_empty
end end
end end
......
...@@ -1741,6 +1741,38 @@ describe API::MergeRequests do ...@@ -1741,6 +1741,38 @@ describe API::MergeRequests do
expect(json_response['state']).to eq('closed') expect(json_response['state']).to eq('closed')
expect(json_response['force_remove_source_branch']).to be_truthy expect(json_response['force_remove_source_branch']).to be_truthy
end end
context 'with a merge request across forks' do
let(:fork_owner) { create(:user) }
let(:source_project) { fork_project(project, fork_owner) }
let(:target_project) { project }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
target_project: target_project,
source_branch: 'fixes',
merge_params: { 'force_remove_source_branch' => false })
end
it 'is true for an authorized user' do
put api("/projects/#{target_project.id}/merge_requests/#{merge_request.iid}", fork_owner), params: { state_event: 'close', remove_source_branch: true }
expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq('closed')
expect(json_response['force_remove_source_branch']).to be true
end
it 'is false for an unauthorized user' do
expect do
put api("/projects/#{target_project.id}/merge_requests/#{merge_request.iid}", target_project.owner), params: { state_event: 'close', remove_source_branch: true }
end.not_to change { merge_request.reload.merge_params }
expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq('closed')
expect(json_response['force_remove_source_branch']).to be false
end
end
end end
context "to close a MR" do context "to close a MR" do
......
...@@ -2684,6 +2684,22 @@ describe API::Projects do ...@@ -2684,6 +2684,22 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(400)
end end
end end
context 'when authenticated as developer' do
before do
group.add_developer(user)
end
context 'target namespace allows developers to create projects' do
let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
it 'fails transferring the project to the target namespace' do
put api("/projects/#{project.id}/transfer", user), params: { namespace: group.id }
expect(response).to have_gitlab_http_status(400)
end
end
end
end end
it_behaves_like 'custom attributes endpoints', 'projects' do it_behaves_like 'custom attributes endpoints', 'projects' do
......
...@@ -51,7 +51,7 @@ describe AutoMerge::BaseService do ...@@ -51,7 +51,7 @@ describe AutoMerge::BaseService do
expect(merge_request.merge_params['commit_message']).to eq("Merge branch 'patch-12' into 'master'") expect(merge_request.merge_params['commit_message']).to eq("Merge branch 'patch-12' into 'master'")
expect(merge_request.merge_params['sha']).to eq('200fcc9c260f7219eaf0daba87d818f0922c5b18') expect(merge_request.merge_params['sha']).to eq('200fcc9c260f7219eaf0daba87d818f0922c5b18')
expect(merge_request.merge_params['should_remove_source_branch']).to eq(false) expect(merge_request.merge_params['should_remove_source_branch']).to eq(false)
expect(merge_request.merge_params['squash']).to eq(false) expect(merge_request.squash).to eq(false)
expect(merge_request.merge_params['squash_commit_message']).to eq('Update README.md') expect(merge_request.merge_params['squash_commit_message']).to eq('Update README.md')
end end
end end
...@@ -108,7 +108,6 @@ describe AutoMerge::BaseService do ...@@ -108,7 +108,6 @@ describe AutoMerge::BaseService do
'commit_message' => "Merge branch 'patch-12' into 'master'", 'commit_message' => "Merge branch 'patch-12' into 'master'",
'sha' => "200fcc9c260f7219eaf0daba87d818f0922c5b18", 'sha' => "200fcc9c260f7219eaf0daba87d818f0922c5b18",
'should_remove_source_branch' => false, 'should_remove_source_branch' => false,
'squash' => false,
'squash_commit_message' => "Update README.md" 'squash_commit_message' => "Update README.md"
} }
end end
......
...@@ -59,8 +59,9 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do ...@@ -59,8 +59,9 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
it 'sets the params, merge_user, and flag' do it 'sets the params, merge_user, and flag' do
expect(merge_request).to be_valid expect(merge_request).to be_valid
expect(merge_request.merge_when_pipeline_succeeds).to be_truthy expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
expect(merge_request.merge_params).to include commit_message: 'Awesome message' expect(merge_request.merge_params).to include 'commit_message' => 'Awesome message'
expect(merge_request.merge_user).to be user expect(merge_request.merge_user).to be user
expect(merge_request.auto_merge_strategy).to eq AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
end end
it 'creates a system note' do it 'creates a system note' do
...@@ -73,7 +74,7 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do ...@@ -73,7 +74,7 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
end end
context 'already approved' do context 'already approved' do
let(:service) { described_class.new(project, user, new_key: true) } let(:service) { described_class.new(project, user, should_remove_source_branch: true) }
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
before do before do
...@@ -90,7 +91,7 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do ...@@ -90,7 +91,7 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds) expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds)
service.execute(mr_merge_if_green_enabled) service.execute(mr_merge_if_green_enabled)
expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key) expect(mr_merge_if_green_enabled.merge_params).to have_key('should_remove_source_branch')
end end
end end
end end
......
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