From f773bb71bab318cf6726557ad78fabe210254bb7 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Mon, 22 May 2023 15:48:33 +0800 Subject: [PATCH 01/18] Hide projects created by banned users Hide projects created by banned users Changelog: changed --- app/finders/projects_finder.rb | 9 +++++ app/models/project.rb | 12 +++++++ app/policies/project_policy.rb | 8 +++++ .../hide_projects_of_banned_users.yml | 8 +++++ spec/finders/projects_finder_spec.rb | 20 ++++++++--- spec/models/project_spec.rb | 35 +++++++++++++++++++ spec/policies/project_policy_spec.rb | 22 ++++++++++++ 7 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 config/feature_flags/development/hide_projects_of_banned_users.yml diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 57a9538db150f9..a99f2d72f9cc04 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -53,8 +53,11 @@ def execute init_collection end + collection = without_created_by_banned_user(collection) + use_cte = params.delete(:use_cte) collection = Project.wrap_with_cte(collection) if use_cte + collection = filter_projects(collection) sort(collection) @@ -282,6 +285,12 @@ def finder_params { min_access_level: params[:min_access_level] } end + + def without_created_by_banned_user(projects) + return projects if current_user && current_user.can?(:read_all_resources) + + projects.without_created_by_banned_user + end end ProjectsFinder.prepend_mod_with('ProjectsFinder') diff --git a/app/models/project.rb b/app/models/project.rb index 452a5c8973c395..5066809a505d4d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -904,6 +904,14 @@ def self.inactive scope :for_group_and_its_ancestor_groups, ->(group) { where(namespace_id: group.self_and_ancestors.select(:id)) } scope :is_importing, -> { with_import_state.where(import_state: { status: %w[started scheduled] }) } + scope :without_created_by_banned_user, -> do + if Feature.enabled?(:hide_projects_of_banned_users) + where_not_exists(Users::BannedUser.where('projects.creator_id = banned_users.user_id')) + else + all + end + end + class << self # Searches for a list of projects based on the query given in `query`. # @@ -3167,6 +3175,10 @@ def pending_delete_or_hidden? pending_delete? || hidden? end + def created_by_banned_user? + creator.banned? + end + def content_editor_on_issues_feature_flag_enabled? group&.content_editor_on_issues_feature_flag_enabled? || Feature.enabled?(:content_editor_on_issues, self) end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index c70dc2887100c3..c099aeb61a19ed 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -259,6 +259,10 @@ class ProjectPolicy < BasePolicy condition(:namespace_catalog_available) { namespace_catalog_available? } + condition(:created_by_banned_user, scope: :subject) do + Feature.enabled?(:hide_projects_of_banned_users) && @subject.created_by_banned_user? + end + # `:read_project` may be prevented in EE, but `:read_project_for_iids` should # not. rule { guest | admin }.enable :read_project_for_iids @@ -909,6 +913,10 @@ class ProjectPolicy < BasePolicy enable :read_model_experiments end + rule { created_by_banned_user & ~admin }.policy do + prevent :read_project + end + private def user_is_user? diff --git a/config/feature_flags/development/hide_projects_of_banned_users.yml b/config/feature_flags/development/hide_projects_of_banned_users.yml new file mode 100644 index 00000000000000..e88be2a25c7e38 --- /dev/null +++ b/config/feature_flags/development/hide_projects_of_banned_users.yml @@ -0,0 +1,8 @@ +--- +name: hide_projects_of_banned_users +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121488 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412621 +milestone: '16.1' +type: development +group: group::anti-abuse +default_enabled: false diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 3d108951c64efc..f53dd1773f1e5f 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ProjectsFinder do +RSpec.describe ProjectsFinder, feature_category: :projects do include AdminModeHelper describe '#execute' do @@ -25,6 +25,10 @@ create(:project, :private, name: 'D', path: 'D') end + let_it_be(:banned_user_project) do + create(:project, :public, name: 'Project created by a banned user', creator: create(:user, :banned)) + end + let(:params) { {} } let(:current_user) { user } let(:project_ids_relation) { nil } @@ -488,15 +492,23 @@ describe 'with admin user' do let(:user) { create(:admin) } - context 'admin mode enabled' do + context 'with admin mode enabled' do before do enable_admin_mode!(current_user) end - it { is_expected.to match_array([public_project, internal_project, private_project, shared_project]) } + it do + is_expected.to match_array([ + public_project, + internal_project, + private_project, + shared_project, + banned_user_project + ]) + end end - context 'admin mode disabled' do + context 'with admin mode disabled' do it { is_expected.to match_array([public_project, internal_project]) } end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f44331521e971f..b9107d790432e3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -9028,6 +9028,41 @@ def has_external_wiki end end + describe '.without_created_by_banned_user' do + let_it_be(:project_1) { create(:project) } + let_it_be(:project_2) { create(:project, creator: create(:user, :banned)) } + + it 'excludes projects created by banned users' do + expect(described_class.without_created_by_banned_user).to match_array([project_1]) + end + + context 'when hide_projects_of_banned_users FF is disabled' do + before do + stub_feature_flags(hide_projects_of_banned_users: false) + end + + it 'includes projects created by banned users' do + expect(described_class.without_created_by_banned_user).to match_array([project_1, project_2]) + end + end + end + + describe '#created_by_banned_user?' do + subject { project.created_by_banned_user? } + + context 'when creator is banned' do + let_it_be(:project) { create(:project, creator: create(:user, :banned)) } + + it { is_expected.to eq true } + end + + context 'when creator is not banned' do + let_it_be(:project) { create(:project) } + + it { is_expected.to eq false } + end + end + it_behaves_like 'something that has web-hooks' do let_it_be_with_reload(:object) { create(:project) } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index ee8d811971a07d..914818d80cb195 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -3309,6 +3309,28 @@ def permissions_abilities(role) end end + describe 'when creator has been banned' do + let_it_be(:project) { create(:project, :public, creator: create(:user, :banned)) } + + let(:current_user) { guest } + + it { expect_disallowed(:read_project) } + + context 'when current user is an admin', :enable_admin_mode do + let(:current_user) { admin } + + it { expect_allowed(:read_project) } + end + + context 'when hide_projects_of_banned_users FF is disabled' do + before do + stub_feature_flags(hide_projects_of_banned_users: false) + end + + it { expect_allowed(:read_project) } + end + end + private def project_subject(project_type) -- GitLab From 3778b565feb6e0351b74f5da2044307104a82c48 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Tue, 23 May 2023 16:39:53 +0800 Subject: [PATCH 02/18] revert whitespace change --- app/finders/projects_finder.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index a99f2d72f9cc04..13119e43803006 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -57,7 +57,6 @@ def execute use_cte = params.delete(:use_cte) collection = Project.wrap_with_cte(collection) if use_cte - collection = filter_projects(collection) sort(collection) -- GitLab From 63a6e4c88551047c90224ace45505f579be06be9 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Thu, 1 Jun 2023 14:32:42 +0800 Subject: [PATCH 03/18] Update docs --- doc/user/admin_area/moderate_users.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/user/admin_area/moderate_users.md b/doc/user/admin_area/moderate_users.md index ee6d360ac1d794..cc5c13e48ba4fe 100644 --- a/doc/user/admin_area/moderate_users.md +++ b/doc/user/admin_area/moderate_users.md @@ -229,8 +229,9 @@ Users can also be activated using the [GitLab API](../../api/users.md#activate-u > - Ban and unban users [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/327353) in GitLab 14.8. Feature flag `ban_user_feature_flag` removed. > - Hiding merge requests of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107836) in GitLab 15.8 [with a flag](../../administration/feature_flags.md) named `hide_merge_requests_from_banned_users`. Disabled by default. > - Hiding comments of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112973) in GitLab 15.11 [with a flag](../../administration/feature_flags.md) named `hidden_notes`. Disabled by default. +> - Hiding projects of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121488) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `hide_projects_of_banned_users`. Disabled by default. -GitLab administrators can ban and unban users. Banned users are blocked, and their issues, merge requests, and comments are hidden. +GitLab administrators can ban and unban users. Banned users are blocked, and their projects, issues, merge requests, and comments are hidden. ### Ban a user -- GitLab From 561d5488255b42da86f1c550851a1ed2ae024702 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Thu, 1 Jun 2023 14:47:17 +0800 Subject: [PATCH 04/18] Fix feature_category --- spec/finders/projects_finder_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index f53dd1773f1e5f..0cba1452a89229 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ProjectsFinder, feature_category: :projects do +RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do include AdminModeHelper describe '#execute' do -- GitLab From 5e266a777d5cedb307f6513eb661838eeae1dc96 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Thu, 1 Jun 2023 17:33:26 +0800 Subject: [PATCH 05/18] Update condition to be creator AND owner --- app/models/project.rb | 7 +++++- app/models/project_authorization.rb | 2 ++ app/models/users/banned_user.rb | 1 + spec/factories/project_authorizations.rb | 4 ++++ spec/models/project_authorization_spec.rb | 8 +++++++ spec/models/project_spec.rb | 26 +++++++++++++++++------ spec/models/users/banned_user_spec.rb | 7 ++++++ 7 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 5066809a505d4d..a2f7ae7791e935 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -906,7 +906,12 @@ def self.inactive scope :without_created_by_banned_user, -> do if Feature.enabled?(:hide_projects_of_banned_users) - where_not_exists(Users::BannedUser.where('projects.creator_id = banned_users.user_id')) + where_not_exists( + Users::BannedUser.joins(:project_authorizations) + .where('projects.creator_id = banned_users.user_id') + .where('project_authorizations.project_id = projects.id') + .where(project_authorizations: { access_level: Gitlab::Access::OWNER }) + ) else all end diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index cb578496f26133..d8174220e29caa 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -9,6 +9,8 @@ class ProjectAuthorization < ApplicationRecord belongs_to :user belongs_to :project + belongs_to :banned_user, class_name: '::Users::BannedUser', primary_key: :user_id, + foreign_key: :user_id, inverse_of: :project_authorizations validates :project, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true diff --git a/app/models/users/banned_user.rb b/app/models/users/banned_user.rb index 8a62744c7d6ee2..f3f0f7b6721367 100644 --- a/app/models/users/banned_user.rb +++ b/app/models/users/banned_user.rb @@ -7,6 +7,7 @@ class BannedUser < ApplicationRecord belongs_to :user has_one :credit_card_validation, class_name: '::Users::CreditCardValidation', primary_key: 'user_id', foreign_key: 'user_id', inverse_of: :banned_user + has_many :project_authorizations, primary_key: 'user_id', foreign_key: 'user_id', inverse_of: :banned_user validates :user, presence: true validates :user_id, uniqueness: { message: N_("banned user already exists") } diff --git a/spec/factories/project_authorizations.rb b/spec/factories/project_authorizations.rb index ffdf5576f842f4..1726da55c994bb 100644 --- a/spec/factories/project_authorizations.rb +++ b/spec/factories/project_authorizations.rb @@ -6,4 +6,8 @@ project access_level { Gitlab::Access::REPORTER } end + + trait :owner do + access_level { Gitlab::Access::OWNER } + end end diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb index dc4922d8114765..f913c15614eac4 100644 --- a/spec/models/project_authorization_spec.rb +++ b/spec/models/project_authorization_spec.rb @@ -56,6 +56,14 @@ describe 'relations' do it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:project) } + + it do + is_expected.to belong_to(:banned_user) + .class_name('::Users::BannedUser') + .with_primary_key('user_id') + .with_foreign_key('user_id') + .inverse_of(:project_authorizations) + end end describe 'validations' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index b9107d790432e3..54c4bb3264706f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -9029,11 +9029,25 @@ def has_external_wiki end describe '.without_created_by_banned_user' do - let_it_be(:project_1) { create(:project) } - let_it_be(:project_2) { create(:project, creator: create(:user, :banned)) } + let_it_be(:visible_project) { create(:project) } - it 'excludes projects created by banned users' do - expect(described_class.without_created_by_banned_user).to match_array([project_1]) + subject(:results) { described_class.without_created_by_banned_user } + + context 'when project creator is also an owner' do + let_it_be(:hidden_project) { create(:project, creator: create(:user, :banned)) } + let_it_be(:project_auth) { create(:project_authorization, :owner, user: hidden_project.creator, project: hidden_project) } + + it 'is excluded' do + expect(results).to match_array([visible_project]) + end + end + + context 'when project creator is no longer an owner' do + let_it_be(:transferred_project) { create(:project, creator: create(:user, :banned)) } + + it 'is included' do + expect(results).to match_array([visible_project, transferred_project]) + end end context 'when hide_projects_of_banned_users FF is disabled' do @@ -9041,8 +9055,8 @@ def has_external_wiki stub_feature_flags(hide_projects_of_banned_users: false) end - it 'includes projects created by banned users' do - expect(described_class.without_created_by_banned_user).to match_array([project_1, project_2]) + it 'returns all' do + expect(results).to match_array(Project.all) end end end diff --git a/spec/models/users/banned_user_spec.rb b/spec/models/users/banned_user_spec.rb index b55c4821d05ce6..0b59db08f53a9b 100644 --- a/spec/models/users/banned_user_spec.rb +++ b/spec/models/users/banned_user_spec.rb @@ -5,6 +5,13 @@ RSpec.describe Users::BannedUser do describe 'relationships' do it { is_expected.to belong_to(:user) } + + it do + is_expected.to have_many(:project_authorizations) + .with_primary_key('user_id') + .with_foreign_key('user_id') + .inverse_of(:banned_user) + end end describe 'validations' do -- GitLab From 4e91ac5399b4faf75c806e0b9141c013f362e46b Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Sun, 4 Jun 2023 08:54:45 +0800 Subject: [PATCH 06/18] Update what happens when user is banned --- .../javascripts/admin/users/components/actions/ban.vue | 2 +- locale/gitlab.pot | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/users/components/actions/ban.vue b/app/assets/javascripts/admin/users/components/actions/ban.vue index d7bdceb4798e11..36dcde619cff13 100644 --- a/app/assets/javascripts/admin/users/components/actions/ban.vue +++ b/app/assets/javascripts/admin/users/components/actions/ban.vue @@ -12,7 +12,7 @@ const messageHtml = `
  • ${s__("AdminUsers|The user can't log in.")}
  • ${s__("AdminUsers|The user can't access git repositories.")}
  • ${s__( - 'AdminUsers|Issues and merge requests authored by this user are hidden from other users.', + 'AdminUsers|Projects, issues, merge requests, and comments of this user are hidden from other users.', )}
  • ${s__('AdminUsers|You can unban their account in the future. Their data remains intact.')}

    diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fbe56269f88fb3..457e9b493a1a50 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3849,9 +3849,6 @@ msgstr "" msgid "AdminUsers|Is using seat" msgstr "" -msgid "AdminUsers|Issues and merge requests authored by this user are hidden from other users." -msgstr "" - msgid "AdminUsers|It's you!" msgstr "" @@ -3897,6 +3894,9 @@ msgstr "" msgid "AdminUsers|Private profile" msgstr "" +msgid "AdminUsers|Projects, issues, merge requests, and comments of this user are hidden from other users." +msgstr "" + msgid "AdminUsers|Reactivating a user will:" msgstr "" -- GitLab From 1b42578b8b62e4c1457978dd85684ddc744c1158 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Tue, 6 Jun 2023 15:00:18 +0800 Subject: [PATCH 07/18] Update condition to be creator AND owner --- app/finders/projects_finder.rb | 6 +-- app/models/project.rb | 6 +-- app/policies/project_policy.rb | 6 +-- spec/models/project_spec.rb | 73 +++++++++++++++++++--------- spec/policies/project_policy_spec.rb | 8 ++- 5 files changed, 66 insertions(+), 33 deletions(-) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 13119e43803006..d8f1bd0c0445ae 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -53,7 +53,7 @@ def execute init_collection end - collection = without_created_by_banned_user(collection) + collection = without_created_and_owned_by_banned_user(collection) use_cte = params.delete(:use_cte) collection = Project.wrap_with_cte(collection) if use_cte @@ -285,10 +285,10 @@ def finder_params { min_access_level: params[:min_access_level] } end - def without_created_by_banned_user(projects) + def without_created_and_owned_by_banned_user(projects) return projects if current_user && current_user.can?(:read_all_resources) - projects.without_created_by_banned_user + projects.without_created_and_owned_by_banned_user end end diff --git a/app/models/project.rb b/app/models/project.rb index a2f7ae7791e935..7bf5ed558511a0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -904,7 +904,7 @@ def self.inactive scope :for_group_and_its_ancestor_groups, ->(group) { where(namespace_id: group.self_and_ancestors.select(:id)) } scope :is_importing, -> { with_import_state.where(import_state: { status: %w[started scheduled] }) } - scope :without_created_by_banned_user, -> do + scope :without_created_and_owned_by_banned_user, -> do if Feature.enabled?(:hide_projects_of_banned_users) where_not_exists( Users::BannedUser.joins(:project_authorizations) @@ -3180,8 +3180,8 @@ def pending_delete_or_hidden? pending_delete? || hidden? end - def created_by_banned_user? - creator.banned? + def created_and_owned_by_banned_user? + creator.banned? && team.owner?(creator) end def content_editor_on_issues_feature_flag_enabled? diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index c099aeb61a19ed..b5d1133445fe4c 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -259,8 +259,8 @@ class ProjectPolicy < BasePolicy condition(:namespace_catalog_available) { namespace_catalog_available? } - condition(:created_by_banned_user, scope: :subject) do - Feature.enabled?(:hide_projects_of_banned_users) && @subject.created_by_banned_user? + condition(:created_and_owned_by_banned_user, scope: :subject) do + Feature.enabled?(:hide_projects_of_banned_users) && @subject.created_and_owned_by_banned_user? end # `:read_project` may be prevented in EE, but `:read_project_for_iids` should @@ -913,7 +913,7 @@ class ProjectPolicy < BasePolicy enable :read_model_experiments end - rule { created_by_banned_user & ~admin }.policy do + rule { created_and_owned_by_banned_user & ~admin }.policy do prevent :read_project end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 54c4bb3264706f..1d5f0e10428108 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -9028,46 +9028,75 @@ def has_external_wiki end end - describe '.without_created_by_banned_user' do - let_it_be(:visible_project) { create(:project) } + describe '.without_created_and_owned_by_banned_user' do + let_it_be(:other_project) { create(:project) } - subject(:results) { described_class.without_created_by_banned_user } + subject(:results) { described_class.without_created_and_owned_by_banned_user } - context 'when project creator is also an owner' do - let_it_be(:hidden_project) { create(:project, creator: create(:user, :banned)) } - let_it_be(:project_auth) { create(:project_authorization, :owner, user: hidden_project.creator, project: hidden_project) } + context 'when project creator is not banned' do + let_it_be(:project_of_active_user) { create(:project, creator: create(:user)) } - it 'is excluded' do - expect(results).to match_array([visible_project]) + it 'includes the project' do + expect(results).to match_array([other_project, project_of_active_user]) end end - context 'when project creator is no longer an owner' do - let_it_be(:transferred_project) { create(:project, creator: create(:user, :banned)) } + context 'when project creator is banned' do + let_it_be(:banned_user) { create(:user, :banned) } + let_it_be(:project_of_banned_user) { create(:project, creator: banned_user) } - it 'is included' do - expect(results).to match_array([visible_project, transferred_project]) - end - end + context 'when project creator is also an owner' do + let_it_be(:project_auth) do + project = project_of_banned_user + create(:project_authorization, :owner, user: project.creator, project: project) + end - context 'when hide_projects_of_banned_users FF is disabled' do - before do - stub_feature_flags(hide_projects_of_banned_users: false) + it 'excludes the project' do + expect(results).to match_array([other_project]) + end + + context 'when hide_projects_of_banned_users FF is disabled' do + before do + stub_feature_flags(hide_projects_of_banned_users: false) + end + + it 'includes the project' do + expect(results).to match_array([other_project, project_of_banned_user]) + end + end end - it 'returns all' do - expect(results).to match_array(Project.all) + context 'when project creator is not an owner' do + it 'includes the project' do + expect(results).to match_array([other_project, project_of_banned_user]) + end end end end - describe '#created_by_banned_user?' do - subject { project.created_by_banned_user? } + describe '#created_and_owned_by_banned_user?' do + subject { project.created_and_owned_by_banned_user? } context 'when creator is banned' do - let_it_be(:project) { create(:project, creator: create(:user, :banned)) } + let_it_be(:creator) { create(:user, :banned) } + let_it_be(:project) { create(:project, creator: creator) } + + let(:creator_is_owner) { true } + + before do + team_double = instance_double(ProjectTeam) + + allow(team_double).to receive(:owner?).with(creator).and_return(creator_is_owner) + allow(project).to receive(:team).and_return(team_double) + end it { is_expected.to eq true } + + context 'when creator is not an owner' do + let(:creator_is_owner) { false } + + it { is_expected.to eq false } + end end context 'when creator is not banned' do diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 914818d80cb195..1cffad012341ee 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -3309,11 +3309,15 @@ def permissions_abilities(role) end end - describe 'when creator has been banned' do - let_it_be(:project) { create(:project, :public, creator: create(:user, :banned)) } + describe 'when project is created and owned by a banned user' do + let_it_be(:project) { create(:project, :public) } let(:current_user) { guest } + before do + allow(project).to receive(:created_and_owned_by_banned_user?).and_return(true) + end + it { expect_disallowed(:read_project) } context 'when current user is an admin', :enable_admin_mode do -- GitLab From 9dfed4899e92fe290b60d97f9f74bd2d4e478a8e Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Wed, 7 Jun 2023 11:10:51 +0800 Subject: [PATCH 08/18] Fix N+1 problems --- app/controllers/profiles/notifications_controller.rb | 2 +- app/models/project.rb | 2 +- app/policies/project_policy.rb | 2 +- spec/models/project_spec.rb | 11 +++++------ 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index b663a75f04a57f..1477f8e0aacfa7 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -45,7 +45,7 @@ def project_notifications_with_preloaded_associations projects = project_notifications.map(&:source) ActiveRecord::Associations::Preloader.new( records: projects, - associations: { namespace: [:route, :owner], group: [] } + associations: { namespace: [:route, :owner], group: [], creator: [] } ).call Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute diff --git a/app/models/project.rb b/app/models/project.rb index 7bf5ed558511a0..f53ac9abc236de 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3181,7 +3181,7 @@ def pending_delete_or_hidden? end def created_and_owned_by_banned_user? - creator.banned? && team.owner?(creator) + creator.banned? && team.max_member_access(creator.id) >= Gitlab::Access::OWNER end def content_editor_on_issues_feature_flag_enabled? diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index b5d1133445fe4c..3ba2b23929f1ff 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -913,7 +913,7 @@ class ProjectPolicy < BasePolicy enable :read_model_experiments end - rule { created_and_owned_by_banned_user & ~admin }.policy do + rule { ~admin & created_and_owned_by_banned_user }.policy do prevent :read_project end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1d5f0e10428108..c6be55b3428c8e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -9083,15 +9083,14 @@ def has_external_wiki let(:creator_is_owner) { true } - before do - team_double = instance_double(ProjectTeam) + context 'when creator is an owner' do + let_it_be(:project_auth) do + create(:project_authorization, :owner, user: project.creator, project: project) + end - allow(team_double).to receive(:owner?).with(creator).and_return(creator_is_owner) - allow(project).to receive(:team).and_return(team_double) + it { is_expected.to eq true } end - it { is_expected.to eq true } - context 'when creator is not an owner' do let(:creator_is_owner) { false } -- GitLab From 020d9fa15d4442518aac61c4ba6c1bee98d46cd6 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Tue, 13 Jun 2023 17:02:05 +0800 Subject: [PATCH 09/18] Update comment on how many extra queries are expected --- spec/graphql/resolvers/users/participants_resolver_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb index 63a14daabba72d..22111626c5b033 100644 --- a/spec/graphql/resolvers/users/participants_resolver_spec.rb +++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb @@ -137,7 +137,8 @@ # 1 extra query per source (3 emojis + 2 notes) to fetch participables collection # 2 extra queries to load work item widgets collection - expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(7) + # 1 extra query to load the project creator to check if they are banned + expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(8) end it 'does not execute N+1 for system note metadata relation' do -- GitLab From 3bf4354be3e136328d60e544c15992c896cca8b6 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Wed, 14 Jun 2023 17:07:48 +0800 Subject: [PATCH 10/18] Fix N+1 problem in ee/spec/requests/api/search_spec.rb --- ee/lib/gitlab/elastic/search_results.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/lib/gitlab/elastic/search_results.rb b/ee/lib/gitlab/elastic/search_results.rb index 17614606c964d6..fa3b9c5f1a8c97 100644 --- a/ee/lib/gitlab/elastic/search_results.rb +++ b/ee/lib/gitlab/elastic/search_results.rb @@ -30,7 +30,7 @@ def objects(scope, page: 1, per_page: DEFAULT_PER_PAGE, preload_method: nil) case scope when 'projects' - eager_load(projects, page, per_page, preload_method, [:route, :namespace, :topics]) + eager_load(projects, page, per_page, preload_method, [:route, :namespace, :topics, :creator]) when 'issues' eager_load(issues, page, per_page, preload_method, project: [:route, :namespace], labels: [], timelogs: [], assignees: []) when 'merge_requests' -- GitLab From 4a2f0d370fba5f7ee38f4784f7dcd25ff8d819e1 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Mon, 19 Jun 2023 14:23:43 +0800 Subject: [PATCH 11/18] Use lonely operator --- app/finders/projects_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index d8f1bd0c0445ae..1e394c2910a04e 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -286,7 +286,7 @@ def finder_params end def without_created_and_owned_by_banned_user(projects) - return projects if current_user && current_user.can?(:read_all_resources) + return projects if current_user&.can?(:read_all_resources) projects.without_created_and_owned_by_banned_user end -- GitLab From 6187ba0a8a20daad4647a3a85973fc159f237a5b Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Mon, 19 Jun 2023 14:27:18 +0800 Subject: [PATCH 12/18] Update milestone in FF definition file --- .../feature_flags/development/hide_projects_of_banned_users.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/feature_flags/development/hide_projects_of_banned_users.yml b/config/feature_flags/development/hide_projects_of_banned_users.yml index e88be2a25c7e38..374782c359a3e1 100644 --- a/config/feature_flags/development/hide_projects_of_banned_users.yml +++ b/config/feature_flags/development/hide_projects_of_banned_users.yml @@ -2,7 +2,7 @@ name: hide_projects_of_banned_users introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121488 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412621 -milestone: '16.1' +milestone: '16.2' type: development group: group::anti-abuse default_enabled: false -- GitLab From 2c935b067ed8247bd391a72044f49fc69c96433b Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Mon, 19 Jun 2023 14:27:55 +0800 Subject: [PATCH 13/18] Update milestone in docs --- doc/user/admin_area/moderate_users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/admin_area/moderate_users.md b/doc/user/admin_area/moderate_users.md index cc5c13e48ba4fe..f7a8150dacf9d8 100644 --- a/doc/user/admin_area/moderate_users.md +++ b/doc/user/admin_area/moderate_users.md @@ -229,7 +229,7 @@ Users can also be activated using the [GitLab API](../../api/users.md#activate-u > - Ban and unban users [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/327353) in GitLab 14.8. Feature flag `ban_user_feature_flag` removed. > - Hiding merge requests of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107836) in GitLab 15.8 [with a flag](../../administration/feature_flags.md) named `hide_merge_requests_from_banned_users`. Disabled by default. > - Hiding comments of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112973) in GitLab 15.11 [with a flag](../../administration/feature_flags.md) named `hidden_notes`. Disabled by default. -> - Hiding projects of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121488) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `hide_projects_of_banned_users`. Disabled by default. +> - Hiding projects of banned users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121488) in GitLab 16.2 [with a flag](../../administration/feature_flags.md) named `hide_projects_of_banned_users`. Disabled by default. GitLab administrators can ban and unban users. Banned users are blocked, and their projects, issues, merge requests, and comments are hidden. -- GitLab From b26f7b0dc4719772e0795629f83d5375f75bfeef Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Mon, 19 Jun 2023 14:49:55 +0800 Subject: [PATCH 14/18] Update spec --- spec/models/project_spec.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index c6be55b3428c8e..1fdee5ac7eb03f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -9081,7 +9081,7 @@ def has_external_wiki let_it_be(:creator) { create(:user, :banned) } let_it_be(:project) { create(:project, creator: creator) } - let(:creator_is_owner) { true } + it { is_expected.to eq false } context 'when creator is an owner' do let_it_be(:project_auth) do @@ -9090,12 +9090,6 @@ def has_external_wiki it { is_expected.to eq true } end - - context 'when creator is not an owner' do - let(:creator_is_owner) { false } - - it { is_expected.to eq false } - end end context 'when creator is not banned' do -- GitLab From 98e917d6c6853b266cc2d7ee4528535185987692 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Wed, 21 Jun 2023 13:59:53 +0800 Subject: [PATCH 15/18] Fix owner check operator from >= to == --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index f53ac9abc236de..8fc37d70287dc8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3181,7 +3181,7 @@ def pending_delete_or_hidden? end def created_and_owned_by_banned_user? - creator.banned? && team.max_member_access(creator.id) >= Gitlab::Access::OWNER + creator.banned? && team.max_member_access(creator.id) == Gitlab::Access::OWNER end def content_editor_on_issues_feature_flag_enabled? -- GitLab From 17a89a2dacc2751a1146f107acc5c99107ed8ccd Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Wed, 21 Jun 2023 15:04:06 +0800 Subject: [PATCH 16/18] Move feature flag state check from Project scope to ProjectsFinder --- app/finders/projects_finder.rb | 4 +++- app/models/project.rb | 16 ++++++---------- spec/finders/projects_finder_spec.rb | 12 +++++++++++- spec/models/project_spec.rb | 10 ---------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 1e394c2910a04e..0209ad6f2d7452 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -53,7 +53,9 @@ def execute init_collection end - collection = without_created_and_owned_by_banned_user(collection) + if Feature.enabled?(:hide_projects_of_banned_users) + collection = without_created_and_owned_by_banned_user(collection) + end use_cte = params.delete(:use_cte) collection = Project.wrap_with_cte(collection) if use_cte diff --git a/app/models/project.rb b/app/models/project.rb index 8fc37d70287dc8..c8070e65ae8cba 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -905,16 +905,12 @@ def self.inactive scope :is_importing, -> { with_import_state.where(import_state: { status: %w[started scheduled] }) } scope :without_created_and_owned_by_banned_user, -> do - if Feature.enabled?(:hide_projects_of_banned_users) - where_not_exists( - Users::BannedUser.joins(:project_authorizations) - .where('projects.creator_id = banned_users.user_id') - .where('project_authorizations.project_id = projects.id') - .where(project_authorizations: { access_level: Gitlab::Access::OWNER }) - ) - else - all - end + where_not_exists( + Users::BannedUser.joins(:project_authorizations) + .where('projects.creator_id = banned_users.user_id') + .where('project_authorizations.project_id = projects.id') + .where(project_authorizations: { access_level: Gitlab::Access::OWNER }) + ) end class << self diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 0cba1452a89229..a795df4dec6a1e 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -26,7 +26,9 @@ end let_it_be(:banned_user_project) do - create(:project, :public, name: 'Project created by a banned user', creator: create(:user, :banned)) + create(:project, :public, name: 'Project created by a banned user', creator: create(:user, :banned)).tap do |p| + create(:project_authorization, :owner, user: p.creator, project: p) + end end let(:params) { {} } @@ -510,6 +512,14 @@ context 'with admin mode disabled' do it { is_expected.to match_array([public_project, internal_project]) } + + context 'when hide_projects_of_banned_users FF is disabled' do + before do + stub_feature_flags(hide_projects_of_banned_users: false) + end + + it { is_expected.to match_array([public_project, internal_project, banned_user_project]) } + end end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1fdee5ac7eb03f..9dad1032c578a0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -9054,16 +9054,6 @@ def has_external_wiki it 'excludes the project' do expect(results).to match_array([other_project]) end - - context 'when hide_projects_of_banned_users FF is disabled' do - before do - stub_feature_flags(hide_projects_of_banned_users: false) - end - - it 'includes the project' do - expect(results).to match_array([other_project, project_of_banned_user]) - end - end end context 'when project creator is not an owner' do -- GitLab From e4f5df6e8db9059c57510698ffeca0dedfcb1279 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Thu, 22 Jun 2023 11:25:48 +0800 Subject: [PATCH 17/18] Use join with raw SQL instead of defining associations --- app/models/project.rb | 5 +++-- app/models/project_authorization.rb | 2 -- app/models/users/banned_user.rb | 1 - spec/models/project_authorization_spec.rb | 8 -------- spec/models/users/banned_user_spec.rb | 7 ------- 5 files changed, 3 insertions(+), 20 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index c8070e65ae8cba..c68755bd42c105 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -906,8 +906,9 @@ def self.inactive scope :without_created_and_owned_by_banned_user, -> do where_not_exists( - Users::BannedUser.joins(:project_authorizations) - .where('projects.creator_id = banned_users.user_id') + Users::BannedUser.joins( + 'INNER JOIN project_authorizations ON project_authorizations.user_id = banned_users.user_id' + ).where('projects.creator_id = banned_users.user_id') .where('project_authorizations.project_id = projects.id') .where(project_authorizations: { access_level: Gitlab::Access::OWNER }) ) diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index d8174220e29caa..cb578496f26133 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -9,8 +9,6 @@ class ProjectAuthorization < ApplicationRecord belongs_to :user belongs_to :project - belongs_to :banned_user, class_name: '::Users::BannedUser', primary_key: :user_id, - foreign_key: :user_id, inverse_of: :project_authorizations validates :project, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true diff --git a/app/models/users/banned_user.rb b/app/models/users/banned_user.rb index f3f0f7b6721367..8a62744c7d6ee2 100644 --- a/app/models/users/banned_user.rb +++ b/app/models/users/banned_user.rb @@ -7,7 +7,6 @@ class BannedUser < ApplicationRecord belongs_to :user has_one :credit_card_validation, class_name: '::Users::CreditCardValidation', primary_key: 'user_id', foreign_key: 'user_id', inverse_of: :banned_user - has_many :project_authorizations, primary_key: 'user_id', foreign_key: 'user_id', inverse_of: :banned_user validates :user, presence: true validates :user_id, uniqueness: { message: N_("banned user already exists") } diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb index f913c15614eac4..dc4922d8114765 100644 --- a/spec/models/project_authorization_spec.rb +++ b/spec/models/project_authorization_spec.rb @@ -56,14 +56,6 @@ describe 'relations' do it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:project) } - - it do - is_expected.to belong_to(:banned_user) - .class_name('::Users::BannedUser') - .with_primary_key('user_id') - .with_foreign_key('user_id') - .inverse_of(:project_authorizations) - end end describe 'validations' do diff --git a/spec/models/users/banned_user_spec.rb b/spec/models/users/banned_user_spec.rb index 0b59db08f53a9b..b55c4821d05ce6 100644 --- a/spec/models/users/banned_user_spec.rb +++ b/spec/models/users/banned_user_spec.rb @@ -5,13 +5,6 @@ RSpec.describe Users::BannedUser do describe 'relationships' do it { is_expected.to belong_to(:user) } - - it do - is_expected.to have_many(:project_authorizations) - .with_primary_key('user_id') - .with_foreign_key('user_id') - .inverse_of(:banned_user) - end end describe 'validations' do -- GitLab From 58ee26fe763b3db9613206bf75e89af2e448e646 Mon Sep 17 00:00:00 2001 From: eugielimpin Date: Mon, 26 Jun 2023 15:01:55 +0800 Subject: [PATCH 18/18] Return banned user projects only for admins Auditors can `read_all_resources`. Only admins can `admin_all_resources` --- app/finders/projects_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 0209ad6f2d7452..e6ee4355fd4961 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -288,7 +288,7 @@ def finder_params end def without_created_and_owned_by_banned_user(projects) - return projects if current_user&.can?(:read_all_resources) + return projects if current_user&.can?(:admin_all_resources) projects.without_created_and_owned_by_banned_user end -- GitLab