Commit 9c8e9ebf authored by Peter Leitzen's avatar Peter Leitzen

Anonymize GitLab user/group names on Status Pages

* Implement MentionAnonymizationFilter for StatusPage
* Check that sensitive information is not shown at all
parent 366a3f9e
---
title: Anonymize GitLab user/group names on Status Detail Pages
merge_request: 27273
author:
type: added
# frozen_string_literal: true
module StatusPage
module Filter
# HTML filter that replaces mention links with an anonymized plain version.
#
# This filter should be run before any references are redacted, before
# +Banzai::Filter::ReferenceRedactorFilter+, so it's easier to find and
# anonymize `user` references.
class MentionAnonymizationFilter < HTML::Pipeline::Filter
LINK_CSS_SELECTOR = "a.gfm[data-reference-type='user']"
# Static for now. In https://gitlab.com/gitlab-org/gitlab/-/issues/209114
# we'll map names with a more sophisticated approach.
ANONYMIZED_NAME = 'Incident Responder'
def call
doc.css(LINK_CSS_SELECTOR).each do |link_node|
link_node.replace(ANONYMIZED_NAME)
end
doc.to_html
end
end
end
end
...@@ -4,7 +4,12 @@ module StatusPage ...@@ -4,7 +4,12 @@ module StatusPage
module Pipeline module Pipeline
class PostProcessPipeline < ::Banzai::Pipeline::PostProcessPipeline class PostProcessPipeline < ::Banzai::Pipeline::PostProcessPipeline
def self.filters def self.filters
super + [StatusPage::Filter::ImageFilter] @filters ||= super
.dup
.insert_before(::Banzai::Filter::ReferenceRedactorFilter,
StatusPage::Filter::MentionAnonymizationFilter)
.concat(::Banzai::FilterArray[StatusPage::Filter::ImageFilter])
.freeze
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe StatusPage::Filter::MentionAnonymizationFilter do
include FilterSpecHelper
it 'replaces user link with anonymized text' do
original_html = "Hi #{user_link('alice')}, #{user_link('bob')} is calling."
context = {}
doc = filter(original_html, context)
expect(doc.to_s)
.to eq('Hi Incident Responder, Incident Responder is calling.')
end
private
def user_link(username)
name = username.capitalize
%{<a href="/#{username}" data-user="1" data-reference-type="user" data-container="body" data-placement="top" data-html="true" class="gfm gfm-project_member js-user-link" title="#{name}">@#{username}</a>}
end
end
...@@ -4,8 +4,15 @@ require 'spec_helper' ...@@ -4,8 +4,15 @@ require 'spec_helper'
describe StatusPage::Pipeline::PostProcessPipeline do describe StatusPage::Pipeline::PostProcessPipeline do
describe '.filters' do describe '.filters' do
let(:expected_filters) do
[StatusPage::Filter::MentionAnonymizationFilter] +
::Banzai::Pipeline::PostProcessPipeline.filters +
[StatusPage::Filter::ImageFilter]
end
subject { described_class.filters } subject { described_class.filters }
it { is_expected.to eq(::Banzai::Pipeline::PostProcessPipeline.filters.push(StatusPage::Filter::ImageFilter)) } it { is_expected.to eq(expected_filters) }
it { is_expected.to be_frozen }
end end
end end
...@@ -5,10 +5,12 @@ ...@@ -5,10 +5,12 @@
# - field: The entity field/AR attribute which contains the GFM reference # - field: The entity field/AR attribute which contains the GFM reference
# - value: The resulting JSON value # - value: The resulting JSON value
RSpec.shared_examples 'reference links for status page' do RSpec.shared_examples 'reference links for status page' do
let_it_be(:project, reload: true) { create(:project) } let(:project) { object.project }
let(:author) { object.author }
let(:gfm_reference) { reference.to_reference(full: true) } let(:gfm_reference) { reference.to_reference(full: true) }
before do before do
project.add_guest(author) unless project.team.member?(author)
project.update!(visibility_level: project_visibility) project.update!(visibility_level: project_visibility)
object.update!(field => gfm_reference) object.update!(field => gfm_reference)
...@@ -23,7 +25,7 @@ RSpec.shared_examples 'reference links for status page' do ...@@ -23,7 +25,7 @@ RSpec.shared_examples 'reference links for status page' do
aggregate_failures do aggregate_failures do
expect(value).to include(gfm_reference) expect(value).to include(gfm_reference)
expect(value).to include('<a ') expect(value).to include('<a ')
expect(value).to include(%{title="#{reference.title}"}) expect(value).to include(reference.title)
end end
end end
end end
...@@ -33,7 +35,7 @@ RSpec.shared_examples 'reference links for status page' do ...@@ -33,7 +35,7 @@ RSpec.shared_examples 'reference links for status page' do
aggregate_failures do aggregate_failures do
expect(value).to include(gfm_reference) expect(value).to include(gfm_reference)
expect(value).not_to include('<a ') expect(value).not_to include('<a ')
expect(value).not_to include(%{title="#{reference.title}"}) expect(value).not_to include(reference.title)
end end
end end
end end
...@@ -63,4 +65,57 @@ RSpec.shared_examples 'reference links for status page' do ...@@ -63,4 +65,57 @@ RSpec.shared_examples 'reference links for status page' do
include_examples 'plain reference' include_examples 'plain reference'
end end
end end
describe 'mentions' do
let(:project_visibility) { Project::PUBLIC }
shared_examples 'mention anonymization' do
let(:anonymized_name) { 'Incident Responder' }
it 'anonymizes mention' do
aggregate_failures do
expect(value).to include(anonymized_name)
expect(value).not_to include('<a ')
expect(value).not_to include(reference.name)
end
end
end
context 'with username' do
let(:reference) { project.creator }
include_examples 'mention anonymization'
end
context 'with arbitrary username' do
let(:reference) do
double(:reference, to_reference: '@non_existing_mention')
end
it 'shows the mention' do
expect(value).to include(reference.to_reference)
end
end
context 'with @all' do
let(:reference) do
double(:reference, name: 'All Project and Group Members',
to_reference: '@all')
end
include_examples 'mention anonymization'
end
context 'with groups' do
where(:group_visibility) do
%i[public internal private]
end
with_them do
let(:reference) { create(:group, group_visibility) }
include_examples 'mention anonymization'
end
end
end
end end
...@@ -6,7 +6,7 @@ FactoryBot.define do ...@@ -6,7 +6,7 @@ FactoryBot.define do
path { name.downcase.gsub(/\s/, '_') } path { name.downcase.gsub(/\s/, '_') }
type { 'Group' } type { 'Group' }
owner { nil } owner { nil }
project_creation_level { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS} project_creation_level { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS }
after(:create) do |group| after(:create) do |group|
if group.owner if group.owner
...@@ -17,15 +17,15 @@ FactoryBot.define do ...@@ -17,15 +17,15 @@ FactoryBot.define do
end end
trait :public do trait :public do
visibility_level { Gitlab::VisibilityLevel::PUBLIC} visibility_level { Gitlab::VisibilityLevel::PUBLIC }
end end
trait :internal do trait :internal do
visibility_level {Gitlab::VisibilityLevel::INTERNAL} visibility_level {Gitlab::VisibilityLevel::INTERNAL }
end end
trait :private do trait :private do
visibility_level { Gitlab::VisibilityLevel::PRIVATE} visibility_level { Gitlab::VisibilityLevel::PRIVATE }
end end
trait :with_avatar do trait :with_avatar do
...@@ -49,7 +49,7 @@ FactoryBot.define do ...@@ -49,7 +49,7 @@ FactoryBot.define do
end end
trait :owner_subgroup_creation_only do trait :owner_subgroup_creation_only do
subgroup_creation_level { ::Gitlab::Access::OWNER_SUBGROUP_ACCESS} subgroup_creation_level { ::Gitlab::Access::OWNER_SUBGROUP_ACCESS }
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