diff --git a/app/models/group.rb b/app/models/group.rb index eec41422dcdd509a88231107424ce47e517a0e7a..ea17559d073399b23c8cde0c5b8d5345f0a4bff0 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1132,6 +1132,11 @@ def supports_group_work_items? false end + # overriden in EE + def enterprise_user_settings_available?(user = nil) + false + end + def create_group_level_work_items_feature_flag_enabled? ::Feature.enabled?(:create_group_level_work_items, self, type: :wip) && supports_group_work_items? end diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 2cf2211c989d9116306b2d8d5caaaaca20c3fe1e..d5abcfab34247c5154bf9e4384f5369fa3e30081 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -34,6 +34,17 @@ = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/subgroup_creation_level', f: f, group: @group = render_if_exists 'groups/settings/prevent_forking', f: f, group: @group + + - if @group.enterprise_user_settings_available?(current_user) + %fieldset.form-group.gl-form-group + %legend.col-form-label.col-form-label + = s_('GroupSettings|Enterprise users') + .label-description + = s_('GroupSettings|Settings that apply only to enterprise users associated with this group.') + = render_if_exists 'groups/settings/enterprise_users_pats', f: f, group: @group + = render_if_exists 'groups/settings/hide_email_on_profile', f: f, group: @group + = render_if_exists 'groups/settings/extensions_marketplace', f: f, group: @group + = render_if_exists 'groups/settings/personal_access_tokens', f: f, group: @group = render 'groups/settings/resource_access_token_creation', f: f, group: @group = render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group @@ -44,7 +55,6 @@ = render_if_exists 'groups/settings/placeholder_confirmation_bypass', f: f, group: @group = render_if_exists 'groups/settings/remove_dormant_members', f: f, group: @group = render_if_exists 'groups/settings/disable_invite_members', f: f, group: @group - = render_if_exists 'groups/settings/extensions_marketplace', f: f, group: @group = render_if_exists 'groups/settings/pages_access_control', f: f, group: @group %h5= _('Customer relations') diff --git a/db/migrate/20250728235237_add_hide_email_to_namespace_setting.rb b/db/migrate/20250728235237_add_hide_email_to_namespace_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..018da4a084a8f5c004f7372cc3fbd10565cb2e1f --- /dev/null +++ b/db/migrate/20250728235237_add_hide_email_to_namespace_setting.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddHideEmailToNamespaceSetting < Gitlab::Database::Migration[2.3] + milestone '18.3' + + def change + add_column :namespace_settings, :hide_email_on_profile, :boolean, default: false, null: false + end +end diff --git a/db/schema_migrations/20250728235237 b/db/schema_migrations/20250728235237 new file mode 100644 index 0000000000000000000000000000000000000000..066fe831bbb5ab4e531c80338651ba0480e9fdd9 --- /dev/null +++ b/db/schema_migrations/20250728235237 @@ -0,0 +1 @@ +88be319b0a2fd60531704e55e11131d62f8c2c9d8459d559a3bd4ee0ee3f8367 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index fc63b1cb29aba6b51dd8a94285cf8e2182b76697..f48384fe1783a3dbcc7c7478e7b1834143188e65 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -18626,6 +18626,7 @@ CREATE TABLE namespace_settings ( lock_web_based_commit_signing_enabled boolean DEFAULT false NOT NULL, allow_enterprise_bypass_placeholder_confirmation boolean DEFAULT false NOT NULL, enterprise_bypass_expires_at timestamp with time zone, + hide_email_on_profile boolean DEFAULT false NOT NULL, CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT check_namespace_settings_security_policies_is_hash CHECK ((jsonb_typeof(security_policies) = 'object'::text)), CONSTRAINT namespace_settings_unique_project_download_limit_alertlist_size CHECK ((cardinality(unique_project_download_limit_alertlist) <= 100)), diff --git a/ee/app/controllers/concerns/ee/groups/params.rb b/ee/app/controllers/concerns/ee/groups/params.rb index 07cefd106aebfcf9e6328f24d479ec6294f29d88..4361fa479d657b68f56ebe73fc2846148c4c8d3b 100644 --- a/ee/app/controllers/concerns/ee/groups/params.rb +++ b/ee/app/controllers/concerns/ee/groups/params.rb @@ -93,6 +93,8 @@ def group_params_ee params_ee << :disable_invite_members end + params_ee << :hide_email_on_profile if current_group&.enterprise_user_settings_available?(current_user) + if enterprise_bypass_placeholders_allowed? params_ee << :allow_enterprise_bypass_placeholder_confirmation params_ee << :enterprise_bypass_expires_at diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb index dd58dc0d3e04619decafc8277870e1f74245235f..60a317fc0a381cf34c85f9083b03211c62d16021 100644 --- a/ee/app/models/ee/group.rb +++ b/ee/app/models/ee/group.rb @@ -114,6 +114,9 @@ module Group delegate :user_cap_enabled?, to: :namespace_settings delegate :disable_personal_access_tokens=, to: :namespace_settings + delegate :hide_email_on_profile=, to: :namespace_settings + delegate :hide_email_on_profile?, to: :namespace_settings + delegate :enterprise_users_extensions_marketplace_enabled=, to: :namespace_settings delegate :wiki_access_level, :wiki_access_level=, to: :group_feature, allow_nil: true @@ -1081,6 +1084,12 @@ def groups_for_extended_webhook_execution_on_token_expiry .where(namespace_settings: { extended_grat_expiry_webhooks_execute: true }) end + def enterprise_user_settings_available?(user = nil) + root? && + domain_verification_available? && + Ability.allowed?(user, :owner_access, self) + end + def virtual_registry_policy_subject ::VirtualRegistries::Packages::Policies::Group.new(self) end diff --git a/ee/app/views/groups/settings/_enterprise_users_pats.html.haml b/ee/app/views/groups/settings/_enterprise_users_pats.html.haml index dc138a9ba142132422925c9a94e26ec9c8b08330..ee10ad58326c7be38f8da6795f07201bd69de248 100644 --- a/ee/app/views/groups/settings/_enterprise_users_pats.html.haml +++ b/ee/app/views/groups/settings/_enterprise_users_pats.html.haml @@ -2,7 +2,7 @@ = f.gitlab_ui_checkbox_component :disable_personal_access_tokens, checkbox_options: { checked: group.disable_personal_access_tokens? } do |c| - c.with_label do - = s_('GroupSettings|Disable personal access tokens for enterprise users') + = s_('GroupSettings|Disable personal access tokens') - c.with_help_text do - learn_more_link = link_to(_('Learn more'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'disable-personal-access-tokens-for-enterprise-users')) - = s_("GroupSettings|If enabled, enterprise user accounts will not be able to use personal access tokens. %{learn_more_link}.").html_safe % { learn_more_link: learn_more_link } + = s_("GroupSettings|If enabled, enterprise users cannot use personal access tokens. %{learn_more_link}.").html_safe % { learn_more_link: learn_more_link } diff --git a/ee/app/views/groups/settings/_extensions_marketplace.html.haml b/ee/app/views/groups/settings/_extensions_marketplace.html.haml index a83439b5a27faae0697e05e73f1481ebc0961a5e..8107d03d52100ca8d864b64b3f8af081164230ea 100644 --- a/ee/app/views/groups/settings/_extensions_marketplace.html.haml +++ b/ee/app/views/groups/settings/_extensions_marketplace.html.haml @@ -3,11 +3,8 @@ - return unless group.can_manage_extensions_marketplace_for_enterprise_users? -%h5= _('Web IDE and workspaces') - -.form-group.gl-mb-3 - = f.gitlab_ui_checkbox_component :enterprise_users_extensions_marketplace_enabled, checkbox_options: { checked: group.enterprise_users_extensions_marketplace_enabled? } do |c| - - c.with_label do - = s_('GroupSettings|Enable extension marketplace') - - c.with_help_text do - = s_("GroupSettings|Enterprise user accounts can use the extension marketplace in the Web IDE and workspaces.") += f.gitlab_ui_checkbox_component :enterprise_users_extensions_marketplace_enabled, checkbox_options: { checked: group.enterprise_users_extensions_marketplace_enabled? } do |c| + - c.with_label do + = s_('GroupSettings|Enable extension marketplace') + - c.with_help_text do + = s_("GroupSettings|Enterprise user accounts can use the extension marketplace in the Web IDE and workspaces.") diff --git a/ee/app/views/groups/settings/_hide_email_on_profile.html.haml b/ee/app/views/groups/settings/_hide_email_on_profile.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..98aea8edb3ecf9738d79a5791fc7dfb83899c22e --- /dev/null +++ b/ee/app/views/groups/settings/_hide_email_on_profile.html.haml @@ -0,0 +1,7 @@ +- return unless group.root? && group.domain_verification_available? && can?(current_user, :owner_access, group) + += f.gitlab_ui_checkbox_component :hide_email_on_profile, checkbox_options: { checked: group.hide_email_on_profile? } do |c| + - c.with_label do + = s_('GroupSettings|Hide email address from public profile') + - c.with_help_text do + = s_("GroupSettings|If enabled, hides email addresses on public profile pages for enterprise users.") diff --git a/ee/app/views/groups/settings/_personal_access_tokens.html.haml b/ee/app/views/groups/settings/_personal_access_tokens.html.haml index 0afd9735d528e517f9e186c81fd0fce78554f24e..872542ed2602fc56fe08d3fda27c1755da55760c 100644 --- a/ee/app/views/groups/settings/_personal_access_tokens.html.haml +++ b/ee/app/views/groups/settings/_personal_access_tokens.html.haml @@ -3,5 +3,4 @@ %h5= s_('AccessTokens|Personal access tokens') .form-group.gl-mb-3 - = render_if_exists 'groups/settings/enterprise_users_pats', f: f, group: @group = render_if_exists 'groups/settings/service_accounts_pats_expiration_enforced', f: f, group: @group diff --git a/ee/spec/features/groups/group_settings_spec.rb b/ee/spec/features/groups/group_settings_spec.rb index 8953d6fb4cf0c479295c665ca905229efe8db923..0fea98a69c29794e10679475603e2775742f5582 100644 --- a/ee/spec/features/groups/group_settings_spec.rb +++ b/ee/spec/features/groups/group_settings_spec.rb @@ -515,6 +515,7 @@ def service_access_token_expiration_enforced_selector context 'for SaaS', :saas do before do + stub_licensed_features(domain_verification: true, disable_personal_access_tokens: true) stub_saas_features(disable_personal_access_tokens: true) end diff --git a/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb b/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb index 27b13e2e7541272ae7b25181793d33a6b9e459ad..86f99b2ec714e64b7286cea34b81167706acaf25 100644 --- a/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb +++ b/ee/spec/lib/namespaces/namespace_setting_changes_auditor_spec.rb @@ -121,7 +121,7 @@ default_branch_protection_defaults allow_merge_without_pipeline auto_ban_user_on_excessive_projects_download lock_math_rendering_limits_enabled enable_auto_assign_gitlab_duo_pro_seats early_access_program_participant lock_duo_features_enabled allow_merge_without_pipeline only_allow_merge_if_pipeline_succeeds - lock_spp_repository_pipeline_access spp_repository_pipeline_access archived + lock_spp_repository_pipeline_access hide_email_on_profile spp_repository_pipeline_access archived resource_access_token_notify_inherited lock_resource_access_token_notify_inherited pipeline_variables_default_role extended_grat_expiry_webhooks_execute force_pages_access_control jwt_ci_cd_job_token_enabled jwt_ci_cd_job_token_opted_out require_dpop_for_manage_api_endpoints diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb index 42cf28256fb40542b2e286fc5e597af5999420ff..0d3daa53f95c464bf717bc84a726ef4aa960360e 100644 --- a/ee/spec/models/ee/group_spec.rb +++ b/ee/spec/models/ee/group_spec.rb @@ -3043,6 +3043,24 @@ def webhook_headers end end + describe '#hide_email_on_profile?' do + it 'returns false by default' do + expect(group.hide_email_on_profile?).to be_falsey + end + + it 'returns true when enabled' do + group.update!(hide_email_on_profile: true) + expect(group.hide_email_on_profile?).to be_truthy + end + end + + describe '#hide_email_on_profile=' do + it 'delegates to namespace_settings' do + expect(group.namespace_settings).to receive(:hide_email_on_profile=).with(true) + group.hide_email_on_profile = true + end + end + context 'when setting extended_grat_expiry_webhooks_execute is disabled' do before do group.namespace_settings.update!(extended_grat_expiry_webhooks_execute: false) @@ -4285,6 +4303,49 @@ def webhook_headers end end + describe '#enterprise_user_settings_available?' do + let_it_be(:current_user) { create(:user) } + let_it_be(:root_group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: root_group) } + + context 'when all conditions are met' do + before do + allow(root_group).to receive(:domain_verification_available?).and_return(true) + allow(Ability).to receive(:allowed?).with(current_user, :owner_access, root_group).and_return(true) + end + + it 'returns true for root group' do + expect(root_group.enterprise_user_settings_available?(current_user)).to be_truthy + end + + it 'returns false for subgroup' do + expect(subgroup.enterprise_user_settings_available?(current_user)).to be_falsey + end + end + + context 'when domain verification is not available' do + before do + allow(root_group).to receive(:domain_verification_available?).and_return(false) + allow(Ability).to receive(:allowed?).with(current_user, :owner_access, root_group).and_return(true) + end + + it 'returns false' do + expect(root_group.enterprise_user_settings_available?(current_user)).to be_falsey + end + end + + context 'when user does not have owner access' do + before do + allow(root_group).to receive(:domain_verification_available?).and_return(true) + allow(Ability).to receive(:allowed?).with(current_user, :owner_access, root_group).and_return(false) + end + + it 'returns false' do + expect(root_group.enterprise_user_settings_available?(current_user)).to be_falsey + end + end + end + describe '#virtual_registry_policy_subject' do subject { group.virtual_registry_policy_subject } diff --git a/ee/spec/requests/groups_controller_spec.rb b/ee/spec/requests/groups_controller_spec.rb index cd7f1a191dde9956f30e7dc6f87735e8be97f8ea..8e6410432b8ed0aa4b3f766fba3b43e138f24ffc 100644 --- a/ee/spec/requests/groups_controller_spec.rb +++ b/ee/spec/requests/groups_controller_spec.rb @@ -617,6 +617,65 @@ end end end + + context 'when setting hide_email_on_profile' do + let(:params) { { group: { hide_email_on_profile: true } } } + + before do + group.add_owner(user) + allow(Group).to receive(:find_by_full_path).and_return(group) + allow(group).to receive(:domain_verification_available?).and_return(true) + end + + it 'successfully updates the setting for top-level group owners' do + expect { request }.to change { + group.reload.namespace_settings.hide_email_on_profile? + }.from(false).to(true) + + expect(response).to have_gitlab_http_status(:found) + end + + context 'and user is not a group owner' do + before do + group.owners.delete(user) + group.add_maintainer(user) + end + + it 'does not change the setting and returns not found' do + expect { request }.not_to change { + group.reload.namespace_settings.hide_email_on_profile? + }.from(false) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when domain verification is not enabled' do + before do + allow(group).to receive(:domain_verification_available?).and_return(false) + end + + it 'does not change the setting' do + expect { request }.not_to change { + group.reload.namespace_settings.hide_email_on_profile? + }.from(false) + + expect(response).to have_gitlab_http_status(:found) + end + end + + context 'when group is not a root group' do + let(:group) { create(:group, :nested) } + + it 'does not change the setting' do + expect { request }.not_to change { + group.reload.namespace_settings.hide_email_on_profile? + }.from(false) + + expect(response).to have_gitlab_http_status(:found) + end + end + end end describe 'PUT #transfer', :saas do diff --git a/ee/spec/views/groups/settings/_permissions.html.haml_spec.rb b/ee/spec/views/groups/settings/_permissions.html.haml_spec.rb index 3d72e361d111a895d0f40dc3799e441d38ae39c0..26e23c20129d640cabbd1c3848fa7bbe91de898c 100644 --- a/ee/spec/views/groups/settings/_permissions.html.haml_spec.rb +++ b/ee/spec/views/groups/settings/_permissions.html.haml_spec.rb @@ -50,31 +50,96 @@ end end - context 'for extensions marketplace settings' do - let_it_be(:section_title) { _('Web IDE and workspaces') } - let_it_be(:checkbox_label) { s_('GroupSettings|Enable extension marketplace') } + context 'for enterprise users section' do + let_it_be(:section_title) { s_('GroupSettings|Enterprise users') } + let_it_be(:section_description) do + s_('GroupSettings|Settings that apply only to enterprise users associated with this group.') + end - context 'when cannot manage extensions marketplace for enterprise users' do - it 'renders nothing', :aggregate_failures do - allow(group).to receive(:can_manage_extensions_marketplace_for_enterprise_users?).and_return(false) + context 'when group is not root' do + before do + allow(group).to receive(:root?).and_return(false) + end + it 'does not render enterprise users section' do render - expect(rendered).to render_template('groups/settings/_extensions_marketplace') expect(rendered).not_to have_content(section_title) - expect(rendered).not_to have_field(checkbox_label, type: 'checkbox') + expect(rendered).not_to have_content(section_description) end end - context 'when can manage extensions marketplace for enterprise users' do - it 'renders checkbox', :aggregate_failures do - allow(group).to receive(:can_manage_extensions_marketplace_for_enterprise_users?).and_return(true) + context 'when domain verification is not available' do + before do + allow(group).to receive(:root?).and_return(true) + allow(group).to receive(:domain_verification_available?).and_return(false) + end + it 'does not render enterprise users section' do + render + + expect(rendered).not_to have_content(section_title) + expect(rendered).not_to have_content(section_description) + end + end + + context 'when user does not have owner access' do + before do + allow(group).to receive(:root?).and_return(true) + allow(group).to receive(:domain_verification_available?).and_return(true) + allow(view).to receive(:can?).with(anything, :owner_access, group).and_return(false) + end + + it 'does not render enterprise users section' do + render + + expect(rendered).not_to have_content(section_title) + expect(rendered).not_to have_content(section_description) + end + end + + context 'when all conditions are met' do + before do + allow(group).to receive(:enterprise_user_settings_available?).and_return(true) + end + + it 'renders enterprise users section with description' do render - expect(rendered).to render_template('groups/settings/_extensions_marketplace') expect(rendered).to have_content(section_title) - expect(rendered).to have_unchecked_field(checkbox_label, type: 'checkbox') + expect(rendered).to have_content(section_description) + end + + it 'renders enterprise user partials' do + render + + expect(rendered).to render_template('groups/settings/_enterprise_users_pats') + expect(rendered).to render_template('groups/settings/_hide_email_on_profile') + expect(rendered).to render_template('groups/settings/_extensions_marketplace') + end + + context 'when extensions marketplace can be managed' do + before do + allow(group).to receive(:can_manage_extensions_marketplace_for_enterprise_users?).and_return(true) + end + + it 'renders extensions marketplace checkbox' do + render + + expect(rendered).to have_unchecked_field(s_('GroupSettings|Enable extension marketplace'), type: 'checkbox') + end + end + + context 'when extensions marketplace cannot be managed' do + before do + allow(group).to receive(:can_manage_extensions_marketplace_for_enterprise_users?).and_return(false) + end + + it 'does not render extensions marketplace checkbox' do + render + + expect(rendered).not_to have_field(s_('GroupSettings|Enable extension marketplace'), type: 'checkbox') + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ed2091644c6cd94aec6b720fdac91c6cffaebfd2..fa9212744830ff45b067c477269d399adaac9520 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31119,7 +31119,7 @@ msgstr "" msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group" msgstr "" -msgid "GroupSettings|Disable personal access tokens for enterprise users" +msgid "GroupSettings|Disable personal access tokens" msgstr "" msgid "GroupSettings|Disable user invitations to groups and projects within %{group}" @@ -31152,6 +31152,9 @@ msgstr "" msgid "GroupSettings|Enterprise user accounts can use the extension marketplace in the Web IDE and workspaces." msgstr "" +msgid "GroupSettings|Enterprise users" +msgstr "" + msgid "GroupSettings|Experiment" msgstr "" @@ -31176,15 +31179,21 @@ msgstr "" msgid "GroupSettings|Group path cannot be longer than %{length} characters." msgstr "" +msgid "GroupSettings|Hide email address from public profile" +msgstr "" + msgid "GroupSettings|How do I manage group SSH certificates?" msgstr "" -msgid "GroupSettings|If enabled, enterprise user accounts will not be able to use personal access tokens. %{learn_more_link}." +msgid "GroupSettings|If enabled, enterprise users cannot use personal access tokens. %{learn_more_link}." msgstr "" msgid "GroupSettings|If enabled, group access tokens expiry webhooks execute 60, 30, and 7 days before the token expires. If disabled, these webhooks only execute 7 days before the token expires." msgstr "" +msgid "GroupSettings|If enabled, hides email addresses on public profile pages for enterprise users." +msgstr "" + msgid "GroupSettings|If enabled, individual user accounts will be able to use only issued SSH certificates for Git access. It doesn't apply to service accounts, deploy keys, and other types of internal accounts." msgstr "" @@ -31257,6 +31266,9 @@ msgstr "" msgid "GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group." msgstr "" +msgid "GroupSettings|Settings that apply only to enterprise users associated with this group." +msgstr "" + msgid "GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found." msgstr "" @@ -70330,9 +70342,6 @@ msgstr "" msgid "Web IDE" msgstr "" -msgid "Web IDE and workspaces" -msgstr "" - msgid "Web terminal" msgstr ""