Commit 0cd97628 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents 49d3ea9c eb15869f
...@@ -92,8 +92,9 @@ gem 'kaminari', '~> 1.0' ...@@ -92,8 +92,9 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.8.8' gem 'hamlit', '~> 2.8.8'
# Files attachments # Files attachments
# Locked until https://github.com/carrierwaveuploader/carrierwave/pull/2332/files is merged. # Locked until https://github.com/carrierwaveuploader/carrierwave/pull/2332 and
# config/initializers/carrierwave_patch.rb can be removed once that change is released. # https://github.com/carrierwaveuploader/carrierwave/pull/2356 are merged.
# config/initializers/carrierwave_patch.rb can be removed once both changes are released.
gem 'carrierwave', '= 1.2.3' gem 'carrierwave', '= 1.2.3'
gem 'mini_magick' gem 'mini_magick'
......
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
# #
# This module depends on the finder implementing the following methods: # This module depends on the finder implementing the following methods:
# #
# - `#execute` should return an `ActiveRecord::Relation` # - `#execute` should return an `ActiveRecord::Relation` or the `model` needs to
# be defined in the call to `requires_cross_project_access`.
# - `#current_user` the user that requires access (or nil) # - `#current_user` the user that requires access (or nil)
module FinderWithCrossProjectAccess module FinderWithCrossProjectAccess
extend ActiveSupport::Concern extend ActiveSupport::Concern
...@@ -13,20 +14,35 @@ module FinderWithCrossProjectAccess ...@@ -13,20 +14,35 @@ module FinderWithCrossProjectAccess
prepended do prepended do
extend Gitlab::CrossProjectAccess::ClassMethods extend Gitlab::CrossProjectAccess::ClassMethods
cattr_accessor :finder_model
def self.requires_cross_project_access(*args)
super
self.finder_model = extract_model_from_arguments(args)
end
private
def self.extract_model_from_arguments(args)
args.detect { |argument| argument.is_a?(Hash) && argument[:model] }
&.fetch(:model)
end
end end
override :execute override :execute
def execute(*args) def execute(*args)
check = Gitlab::CrossProjectAccess.find_check(self) check = Gitlab::CrossProjectAccess.find_check(self)
original = super original = -> { super }
return original unless check return original.call unless check
return original if should_skip_cross_project_check || can_read_cross_project? return original.call if should_skip_cross_project_check || can_read_cross_project?
if check.should_run?(self) if check.should_run?(self)
original.model.none finder_model&.none || original.call.model.none
else else
original original.call
end end
end end
...@@ -48,8 +64,6 @@ module FinderWithCrossProjectAccess ...@@ -48,8 +64,6 @@ module FinderWithCrossProjectAccess
skip_cross_project_check { super } skip_cross_project_check { super }
end end
private
attr_accessor :should_skip_cross_project_check attr_accessor :should_skip_cross_project_check
def skip_cross_project_check def skip_cross_project_check
......
...@@ -3,22 +3,27 @@ ...@@ -3,22 +3,27 @@
class EventsFinder class EventsFinder
prepend FinderMethods prepend FinderMethods
prepend FinderWithCrossProjectAccess prepend FinderWithCrossProjectAccess
MAX_PER_PAGE = 100
attr_reader :source, :params, :current_user attr_reader :source, :params, :current_user
requires_cross_project_access unless: -> { source.is_a?(Project) } requires_cross_project_access unless: -> { source.is_a?(Project) }, model: Event
# Used to filter Events # Used to filter Events
# #
# Arguments: # Arguments:
# source - which user or project to looks for events on # source - which user or project to looks for events on
# current_user - only return events for projects visible to this user # current_user - only return events for projects visible to this user
# WARNING: does not consider project feature visibility!
# params: # params:
# action: string # action: string
# target_type: string # target_type: string
# before: datetime # before: datetime
# after: datetime # after: datetime
# # per_page: integer (max. 100)
# page: integer
# with_associations: boolean
# sort: 'asc' or 'desc'
def initialize(params = {}) def initialize(params = {})
@source = params.delete(:source) @source = params.delete(:source)
@current_user = params.delete(:current_user) @current_user = params.delete(:current_user)
...@@ -33,15 +38,18 @@ class EventsFinder ...@@ -33,15 +38,18 @@ class EventsFinder
events = by_target_type(events) events = by_target_type(events)
events = by_created_at_before(events) events = by_created_at_before(events)
events = by_created_at_after(events) events = by_created_at_after(events)
events = sort(events)
events = events.with_associations if params[:with_associations]
events paginated_filtered_by_user_visibility(events)
end end
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def by_current_user_access(events) def by_current_user_access(events)
events.merge(ProjectsFinder.new(current_user: current_user).execute) # rubocop: disable CodeReuse/Finder events.merge(Project.public_or_visible_to_user(current_user))
.joins(:project) .joins(:project)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -77,4 +85,31 @@ class EventsFinder ...@@ -77,4 +85,31 @@ class EventsFinder
events.where('events.created_at > ?', params[:after].end_of_day) events.where('events.created_at > ?', params[:after].end_of_day)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def sort(events)
return events unless params[:sort]
if params[:sort] == 'asc'
events.order_id_asc
else
events.order_id_desc
end
end
def paginated_filtered_by_user_visibility(events)
limited_events = events.page(page).per(per_page)
visible_events = limited_events.select { |event| event.visible_to_user?(current_user) }
Kaminari.paginate_array(visible_events, total_count: events.count)
end
def per_page
return MAX_PER_PAGE unless params[:per_page]
[params[:per_page], MAX_PER_PAGE].min
end
def page
params[:page] || 1
end
end end
---
title: Update specs to exclude possible false positive pass
merge_request: 23893
author: "@blackst0ne"
type: other
---
title: Hide confidential events in the API
merge_request: 23746
author:
type: other
---
title: Fix object storage not working properly with Google S3 compatibility
merge_request: 23858
author:
type: fixed
...@@ -23,6 +23,19 @@ module CarrierWave ...@@ -23,6 +23,19 @@ module CarrierWave
end end
end end
end end
# Fix for https://github.com/carrierwaveuploader/carrierwave/pull/2356
def acl_header
if fog_provider == 'AWS'
{ 'x-amz-acl' => @uploader.fog_public ? 'public-read' : 'private' }
else
{}
end
end
def fog_provider
@uploader.fog_credentials[:provider].to_s
end
end end
end end
end end
......
...@@ -18,29 +18,15 @@ module API ...@@ -18,29 +18,15 @@ module API
desc: 'Return events sorted in ascending and descending order' desc: 'Return events sorted in ascending and descending order'
end end
RedactedEvent = OpenStruct.new(target_title: 'Confidential event').freeze def present_events(events)
def redact_events(events)
events.map do |event|
if event.visible_to_user?(current_user)
event
else
RedactedEvent
end
end
end
# rubocop: disable CodeReuse/ActiveRecord
def present_events(events, redact: true)
events = events.reorder(created_at: params[:sort])
.with_associations
events = paginate(events) events = paginate(events)
events = redact_events(events) if redact
present events, with: Entities::Event present events, with: Entities::Event
end end
# rubocop: enable CodeReuse/ActiveRecord
def find_events(source)
EventsFinder.new(params.merge(source: source, current_user: current_user, with_associations: true)).execute
end
end end
resource :events do resource :events do
...@@ -55,16 +41,14 @@ module API ...@@ -55,16 +41,14 @@ module API
use :event_filter_params use :event_filter_params
use :sort_params use :sort_params
end end
# rubocop: disable CodeReuse/ActiveRecord
get do get do
authenticate! authenticate!
events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target) events = find_events(current_user)
# Since we're viewing our own events, redaction is unnecessary present_events(events)
present_events(events, redact: false)
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
params do params do
...@@ -82,16 +66,15 @@ module API ...@@ -82,16 +66,15 @@ module API
use :event_filter_params use :event_filter_params
use :sort_params use :sort_params
end end
# rubocop: disable CodeReuse/ActiveRecord
get ':id/events' do get ':id/events' do
user = find_user(params[:id]) user = find_user(params[:id])
not_found!('User') unless user not_found!('User') unless user
events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target) events = find_events(user)
present_events(events) present_events(events)
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
params do params do
...@@ -106,13 +89,12 @@ module API ...@@ -106,13 +89,12 @@ module API
use :event_filter_params use :event_filter_params
use :sort_params use :sort_params
end end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/events" do get ":id/events" do
events = EventsFinder.new(params.merge(source: user_project, current_user: current_user)).execute.preload(:author, :target) events = find_events(user_project)
present_events(events) present_events(events)
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
end end
end end
...@@ -115,4 +115,20 @@ describe FinderWithCrossProjectAccess do ...@@ -115,4 +115,20 @@ describe FinderWithCrossProjectAccess do
expect(finder.execute).to include(result) expect(finder.execute).to include(result)
end end
end end
context 'when specifying a model' do
let(:finder_class) do
Class.new do
prepend FinderWithCrossProjectAccess
requires_cross_project_access model: Project
end
end
context '.finder_model' do
it 'is set correctly' do
expect(finder_class.finder_model).to eq(Project)
end
end
end
end end
...@@ -14,6 +14,10 @@ describe EventsFinder do ...@@ -14,6 +14,10 @@ describe EventsFinder do
let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) } let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) }
let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) } let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) }
let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
context 'when targeting a user' do context 'when targeting a user' do
it 'returns events between specified dates filtered on action and type' do it 'returns events between specified dates filtered on action and type' do
events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute
...@@ -27,6 +31,19 @@ describe EventsFinder do ...@@ -27,6 +31,19 @@ describe EventsFinder do
expect(events).not_to include(opened_merge_request_event) expect(events).not_to include(opened_merge_request_event)
end end
it 'does not include events on confidential issues the user does not have access to' do
events = described_class.new(source: user, current_user: other_user).execute
expect(events).not_to include(confidential_event)
end
it 'includes confidential events user has access to' do
public_project.add_developer(other_user)
events = described_class.new(source: user, current_user: other_user).execute
expect(events).to include(confidential_event)
end
it 'returns nothing when the current user cannot read cross project' do it 'returns nothing when the current user cannot read cross project' do
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
......
...@@ -29,8 +29,9 @@ describe UserRecentEventsFinder do ...@@ -29,8 +29,9 @@ describe UserRecentEventsFinder do
end end
it 'does not include the events if the user cannot read cross project' do it 'does not include the events if the user cannot read cross project' do
expect(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?).and_call_original
expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false } expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false }
expect(finder.execute).to be_empty expect(finder.execute).to be_empty
end end
end end
......
...@@ -124,7 +124,7 @@ describe Gitlab::EncodingHelper do ...@@ -124,7 +124,7 @@ describe Gitlab::EncodingHelper do
end end
it 'returns empty string on conversion errors' do it 'returns empty string on conversion errors' do
expect { ext_class.encode_utf8('') }.not_to raise_error(ArgumentError) expect { ext_class.encode_utf8('') }.not_to raise_error
end end
context 'with strings that can be forcefully encoded into utf8' do context 'with strings that can be forcefully encoded into utf8' do
......
...@@ -137,7 +137,7 @@ describe Gitlab::Gpg do ...@@ -137,7 +137,7 @@ describe Gitlab::Gpg do
described_class.using_tmp_keychain do described_class.using_tmp_keychain do
end end
end end
end.not_to raise_error(ThreadError) end.not_to raise_error
end end
end end
end end
......
...@@ -43,7 +43,7 @@ shared_examples 'ChronicDurationAttribute writer' do ...@@ -43,7 +43,7 @@ shared_examples 'ChronicDurationAttribute writer' do
end end
it "doesn't raise exception" do it "doesn't raise exception" do
expect { subject.send("#{virtual_field}=", '-10m') }.not_to raise_error(ChronicDuration::DurationParseError) expect { subject.send("#{virtual_field}=", '-10m') }.not_to raise_error
end end
it "doesn't change value" do it "doesn't change value" do
...@@ -87,7 +87,7 @@ shared_examples 'ChronicDurationAttribute writer' do ...@@ -87,7 +87,7 @@ shared_examples 'ChronicDurationAttribute writer' do
end end
it "doesn't raise exception" do it "doesn't raise exception" do
expect { subject.send("#{virtual_field}=", nil) }.not_to raise_error(NoMethodError) expect { subject.send("#{virtual_field}=", nil) }.not_to raise_error
end end
end end
end end
......
...@@ -1489,7 +1489,7 @@ describe MergeRequest do ...@@ -1489,7 +1489,7 @@ describe MergeRequest do
it 'does not raises a NameError exception' do it 'does not raises a NameError exception' do
allow_any_instance_of(service_class_name.constantize).to receive(:execute).and_return(nil) allow_any_instance_of(service_class_name.constantize).to receive(:execute).and_return(nil)
expect { subject }.not_to raise_error(NameError) expect { subject }.not_to raise_error
end end
end end
end end
......
...@@ -182,6 +182,68 @@ describe API::Events do ...@@ -182,6 +182,68 @@ describe API::Events do
end end
end end
context 'with inaccessible events' do
let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: Event::CLOSED) }
it 'returns only accessible events' do
get api("/projects/#{public_project.id}/events", non_member)
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(1)
end
it 'returns all events when the user has access' do
get api("/projects/#{public_project.id}/events", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(2)
end
end
context 'pagination' do
let(:public_project) { create(:project, :public) }
before do
create(:event,
project: public_project,
target: create(:issue, project: public_project, title: 'Issue 1'),
action: Event::CLOSED,
created_at: Date.parse('2018-12-10'))
create(:event,
project: public_project,
target: create(:issue, confidential: true, project: public_project, title: 'Confidential event'),
action: Event::CLOSED,
created_at: Date.parse('2018-12-11'))
create(:event,
project: public_project,
target: create(:issue, project: public_project, title: 'Issue 2'),
action: Event::CLOSED,
created_at: Date.parse('2018-12-12'))
end
it 'correctly returns the second page without inaccessible events' do
get api("/projects/#{public_project.id}/events", user), per_page: 2, page: 2
titles = json_response.map { |event| event['target_title'] }
expect(titles.first).to eq('Issue 1')
expect(titles).not_to include('Confidential event')
end
it 'correctly returns the first page without inaccessible events' do
get api("/projects/#{public_project.id}/events", user), per_page: 2, page: 1
titles = json_response.map { |event| event['target_title'] }
expect(titles.first).to eq('Issue 2')
expect(titles).not_to include('Confidential event')
end
end
context 'when not permitted to read' do context 'when not permitted to read' do
it 'returns 404' do it 'returns 404' do
get api("/projects/#{private_project.id}/events", non_member) get api("/projects/#{private_project.id}/events", non_member)
......
require 'spec_helper'
describe 'Redacted events in API::Events' do
shared_examples 'private events are redacted' do
it 'redacts events the user does not have access to' do
expect_any_instance_of(Event).to receive(:visible_to_user?).and_call_original
get api(path), user
expect(response).to have_gitlab_http_status(200)
expect(json_response).to contain_exactly(
'project_id' => nil,
'action_name' => nil,
'target_id' => nil,
'target_iid' => nil,
'target_type' => nil,
'author_id' => nil,
'target_title' => 'Confidential event',
'created_at' => nil,
'author_username' => nil
)
end
end
describe '/users/:id/events' do
let(:project) { create(:project, :public) }
let(:path) { "/users/#{project.owner.id}/events" }
let(:issue) { create(:issue, :confidential, project: project) }
before do
EventCreateService.new.open_issue(issue, issue.author)
end
context 'unauthenticated user views another user with private events' do
let(:user) { nil }
include_examples 'private events are redacted'
end
context 'authenticated user without access views another user with private events' do
let(:user) { create(:user) }
include_examples 'private events are redacted'
end
end
describe '/projects/:id/events' do
let(:project) { create(:project, :public) }
let(:path) { "/projects/#{project.id}/events" }
let(:issue) { create(:issue, :confidential, project: project) }
before do
EventCreateService.new.open_issue(issue, issue.author)
end
context 'unauthenticated user views public project' do
let(:user) { nil }
include_examples 'private events are redacted'
end
context 'authenticated user without access views public project' do
let(:user) { create(:user) }
include_examples 'private events are redacted'
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment