Commit 051c3641 authored by Nick Thomas's avatar Nick Thomas

Make search redaction more robust

Instead of stubbing out the policy framework, stub out the results
given to us by our subclasses. This allows us to test the redaction
logic itself. In so doing, we find a few mistakes.

By applying redaction only to a few known classes, it turns out we're
skipping it in a number of desirable cases. It's better to apply it
unconditionally, providing a fallback for cases where a plain array has
been returned to us.
parent 2e147c78
...@@ -3,11 +3,6 @@ ...@@ -3,11 +3,6 @@
class SearchService class SearchService
include Gitlab::Allowable include Gitlab::Allowable
REDACTABLE_RESULTS = [
ActiveRecord::Relation,
Gitlab::Search::FoundBlob
].freeze
SEARCH_TERM_LIMIT = 64 SEARCH_TERM_LIMIT = 64
SEARCH_CHAR_LIMIT = 4096 SEARCH_CHAR_LIMIT = 4096
...@@ -68,10 +63,6 @@ class SearchService ...@@ -68,10 +63,6 @@ class SearchService
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page])) @search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page]))
end end
def redactable_results
REDACTABLE_RESULTS
end
private private
def visible_result?(object) def visible_result?(object)
...@@ -80,12 +71,9 @@ class SearchService ...@@ -80,12 +71,9 @@ class SearchService
Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object) Ability.allowed?(current_user, :"read_#{object.to_ability_name}", object)
end end
def redact_unauthorized_results(results) def redact_unauthorized_results(results_collection)
return results unless redactable_results.any? { |redactable| results.is_a?(redactable) } results = results_collection.to_a
permitted_results = results.select { |object| visible_result?(object) }
permitted_results = results.select do |object|
visible_result?(object)
end
filtered_results = (results - permitted_results).each_with_object({}) do |object, memo| filtered_results = (results - permitted_results).each_with_object({}) do |object, memo|
memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name } memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name }
...@@ -93,13 +81,13 @@ class SearchService ...@@ -93,13 +81,13 @@ class SearchService
log_redacted_search_results(filtered_results.values) if filtered_results.any? log_redacted_search_results(filtered_results.values) if filtered_results.any?
return results.id_not_in(filtered_results.keys) if results.is_a?(ActiveRecord::Relation) return results_collection.id_not_in(filtered_results.keys) if results_collection.is_a?(ActiveRecord::Relation)
Kaminari.paginate_array( Kaminari.paginate_array(
permitted_results, permitted_results,
total_count: results.total_count, total_count: results_collection.total_count,
limit: results.limit_value, limit: results_collection.limit_value,
offset: results.offset_value offset: results_collection.offset_value
) )
end end
......
---
title: Make search redaction more robust
merge_request: 29166
author:
type: changed
...@@ -2,21 +2,12 @@ ...@@ -2,21 +2,12 @@
module EE module EE
module SearchService module SearchService
EE_REDACTABLE_RESULTS = [
Kaminari::PaginatableArray,
Elasticsearch::Model::Response::Records
].freeze
# This is a proper method instead of a `delegate` in order to # This is a proper method instead of a `delegate` in order to
# avoid adding unnecessary methods to Search::SnippetService # avoid adding unnecessary methods to Search::SnippetService
def use_elasticsearch? def use_elasticsearch?
search_service.use_elasticsearch? search_service.use_elasticsearch?
end end
def redactable_results
super + EE_REDACTABLE_RESULTS
end
def valid_query_length? def valid_query_length?
return true if use_elasticsearch? return true if use_elasticsearch?
......
...@@ -18,15 +18,6 @@ module Gitlab ...@@ -18,15 +18,6 @@ module Gitlab
@group = group @group = group
end end
def objects(scope, page = nil)
case scope
when 'users'
users.page(page).per(per_page)
else
super
end
end
def generic_search_results def generic_search_results
@generic_search_results ||= Gitlab::GroupSearchResults.new(current_user, limit_projects, group, query, default_project_filter: default_project_filter) @generic_search_results ||= Gitlab::GroupSearchResults.new(current_user, limit_projects, group, query, default_project_filter: default_project_filter)
end end
......
...@@ -19,23 +19,6 @@ module Gitlab ...@@ -19,23 +19,6 @@ module Gitlab
@public_and_internal_projects = false @public_and_internal_projects = false
end end
def objects(scope, page = nil)
case scope
when 'notes'
notes.page(page).per(per_page).records
when 'blobs'
blobs(page: page, per_page: per_page)
when 'wiki_blobs'
wiki_blobs(page: page, per_page: per_page)
when 'commits'
commits(page: page, per_page: per_page)
when 'users'
users.page(page).per(per_page)
else
super
end
end
def generic_search_results def generic_search_results
@generic_search_results ||= Gitlab::ProjectSearchResults.new(current_user, project, query, repository_ref) @generic_search_results ||= Gitlab::ProjectSearchResults.new(current_user, project, query, repository_ref)
end end
......
...@@ -3,16 +3,16 @@ ...@@ -3,16 +3,16 @@
require 'spec_helper' require 'spec_helper'
describe SearchService do describe SearchService do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:accessible_group) { create(:group, :private) } let_it_be(:accessible_group) { create(:group, :private) }
let(:inaccessible_group) { create(:group, :private) } let_it_be(:inaccessible_group) { create(:group, :private) }
let!(:group_member) { create(:group_member, group: accessible_group, user: user) } let_it_be(:group_member) { create(:group_member, group: accessible_group, user: user) }
let!(:accessible_project) { create(:project, :private, name: 'accessible_project') } let_it_be(:accessible_project) { create(:project, :repository, :private, name: 'accessible_project') }
let(:note) { create(:note_on_issue, project: accessible_project) } let_it_be(:note) { create(:note_on_issue, project: accessible_project) }
let!(:inaccessible_project) { create(:project, :private, name: 'inaccessible_project') } let_it_be(:inaccessible_project) { create(:project, :repository, :private, name: 'inaccessible_project') }
let(:snippet) { create(:snippet, author: user) } let(:snippet) { create(:snippet, author: user) }
let(:group_project) { create(:project, group: accessible_group, name: 'group_project') } let(:group_project) { create(:project, group: accessible_group, name: 'group_project') }
...@@ -298,67 +298,129 @@ describe SearchService do ...@@ -298,67 +298,129 @@ describe SearchService do
end end
context 'redacting search results' do context 'redacting search results' do
shared_examples 'it redacts incorrect results' do let(:search) { 'anything' }
before do
allow(Ability).to receive(:allowed?).and_return(allowed)
end
context 'when allowed' do subject(:result) { search_service.search_objects }
let(:allowed) { true }
it 'does nothing' do def found_blob(project)
expect(results).not_to be_empty Gitlab::Search::FoundBlob.new(project: project)
expect(results).to all(be_an(model_class)) end
end
end
context 'when disallowed' do def found_wiki_page(project)
let(:allowed) { false } Gitlab::Search::FoundWikiPage.new(found_blob(project))
end
it 'does nothing' do before do
expect(results).to be_empty expect(search_service)
end .to receive(:search_results)
end .and_return(double('search results', objects: unredacted_results))
end
def ar_relation(klass, *objects)
klass.id_in(objects.map(&:id))
end
def kaminari_array(*objects)
Kaminari.paginate_array(objects).page(1).per(20)
end end
context 'issues' do context 'issues' do
let(:issue) { create(:issue, project: accessible_project) } let(:readable) { create(:issue, project: accessible_project) }
let(:unreadable) { create(:issue, project: inaccessible_project) }
let(:unredacted_results) { ar_relation(Issue, readable, unreadable) }
let(:scope) { 'issues' } let(:scope) { 'issues' }
let(:model_class) { Issue }
let(:ability) { :read_issue }
let(:search) { issue.title }
let(:results) { subject.search_objects }
it_behaves_like 'it redacts incorrect results' it 'redacts the inaccessible issue' do
expect(result).to contain_exactly(readable)
end
end end
context 'notes' do context 'notes' do
let(:note) { create(:note_on_commit, project: accessible_project) } let(:readable) { create(:note_on_commit, project: accessible_project) }
let(:unreadable) { create(:note_on_commit, project: inaccessible_project) }
let(:unredacted_results) { ar_relation(Note, readable, unreadable) }
let(:scope) { 'notes' } let(:scope) { 'notes' }
let(:model_class) { Note }
let(:ability) { :read_note }
let(:search) { note.note }
let(:results) do
described_class.new(
user,
project_id: accessible_project.id,
scope: scope,
search: note.note
).search_objects
end
it_behaves_like 'it redacts incorrect results' it 'redacts the inaccessible note' do
expect(result).to contain_exactly(readable)
end
end end
context 'merge_requests' do context 'merge_requests' do
let(:readable) { create(:merge_request, source_project: accessible_project, author: user) }
let(:unreadable) { create(:merge_request, source_project: inaccessible_project) }
let(:unredacted_results) { ar_relation(MergeRequest, readable, unreadable) }
let(:scope) { 'merge_requests' } let(:scope) { 'merge_requests' }
let(:model_class) { MergeRequest }
let(:ability) { :read_merge_request }
let(:merge_request) { create(:merge_request, source_project: accessible_project, author: user) }
let(:search) { merge_request.title }
let(:results) { subject.search_objects }
it_behaves_like 'it redacts incorrect results' it 'redacts the inaccessible merge request' do
expect(result).to contain_exactly(readable)
end
end
context 'project repository blobs' do
let(:readable) { found_blob(accessible_project) }
let(:unreadable) { found_blob(inaccessible_project) }
let(:unredacted_results) { kaminari_array(readable, unreadable) }
let(:scope) { 'blobs' }
it 'redacts the inaccessible blob' do
expect(result).to contain_exactly(readable)
end
end
context 'project wiki blobs' do
let(:readable) { found_wiki_page(accessible_project) }
let(:unreadable) { found_wiki_page(inaccessible_project) }
let(:unredacted_results) { kaminari_array(readable, unreadable) }
let(:scope) { 'wiki_blobs' }
it 'redacts the inaccessible blob' do
expect(result).to contain_exactly(readable)
end
end
context 'project snippets' do
let(:readable) { create(:project_snippet, project: accessible_project) }
let(:unreadable) { create(:project_snippet, project: inaccessible_project) }
let(:unredacted_results) { ar_relation(ProjectSnippet, readable, unreadable) }
let(:scope) { 'snippet_blobs' }
it 'redacts the inaccessible snippet' do
expect(result).to contain_exactly(readable)
end
end
context 'personal snippets' do
let(:readable) { create(:personal_snippet, :private, author: user) }
let(:unreadable) { create(:personal_snippet, :private) }
let(:unredacted_results) { ar_relation(PersonalSnippet, readable, unreadable) }
let(:scope) { 'snippet_blobs' }
it 'redacts the inaccessible snippet' do
expect(result).to contain_exactly(readable)
end
end
context 'commits' do
let(:readable) { accessible_project.commit }
let(:unreadable) { inaccessible_project.commit }
let(:unredacted_results) { kaminari_array(readable, unreadable) }
let(:scope) { 'commits' }
it 'redacts the inaccessible commit' do
expect(result).to contain_exactly(readable)
end
end
context 'users' do
let(:other_user) { create(:user) }
let(:unredacted_results) { ar_relation(User, user, other_user) }
let(:scope) { 'users' }
it 'passes the users through' do
# Users are always visible to everyone
expect(result).to contain_exactly(user, other_user)
end
end end
end 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