Commit dee24a2b authored by Manoj M J's avatar Manoj M J Committed by Kushal Pandya

Add licensed feature policies

This change adds licensed feature  policies
parent 6f1d54d3
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
%fieldset %fieldset
= render 'shared/default_branch_protection', f: f, selected_level: @application_setting.default_branch_protection = render 'shared/default_branch_protection', f: f, selected_level: @application_setting.default_branch_protection
= render_if_exists 'admin/application_settings/group_owners_can_manage_default_branch_protection_setting', form: f
.form-group .form-group
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold' = f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
......
...@@ -28,6 +28,21 @@ For more details, see [Protected branches](../../project/protected_branches.md). ...@@ -28,6 +28,21 @@ For more details, see [Protected branches](../../project/protected_branches.md).
To change this setting for a specific group, see [Default branch protection for groups](../../group/index.md#changing-the-default-branch-protection-of-a-group) To change this setting for a specific group, see [Default branch protection for groups](../../group/index.md#changing-the-default-branch-protection-of-a-group)
### Disable group owners from updating default branch protection **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211944) in GitLab 13.0.
By default, group owners are allowed to override the branch protection set at the global level.
In [GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can disable this privilege of group owners.
To do this:
1. Uncheck the **Allow owners to manage default branch protection in groups** checkbox.
NOTE: **Note:**
GitLab administrators can still update the default branch protection of a group.
## Default project creation protection ## Default project creation protection
Project creation protection specifies which roles can create projects. Project creation protection specifies which roles can create projects.
......
...@@ -196,6 +196,9 @@ To change this setting for a specific group: ...@@ -196,6 +196,9 @@ To change this setting for a specific group:
To change this setting globally, see [Default branch protection](../admin_area/settings/visibility_and_access_controls.md#default-branch-protection). To change this setting globally, see [Default branch protection](../admin_area/settings/visibility_and_access_controls.md#default-branch-protection).
NOTE: **Note:**
In [GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can choose to [disable group owners from updating the default branch protection](../admin_area/settings/visibility_and_access_controls.md#disable-group-owners-from-updating-default-branch-protection-premium-only).
## Add projects to a group ## Add projects to a group
There are two different ways to add a new project to a group: There are two different ways to add a new project to a group:
......
...@@ -58,6 +58,10 @@ module EE ...@@ -58,6 +58,10 @@ module EE
attrs << :npm_package_requests_forwarding attrs << :npm_package_requests_forwarding
end end
if License.feature_available?(:default_branch_protection_restriction_in_groups)
attrs << :group_owners_can_manage_default_branch_protection
end
attrs attrs
end end
......
...@@ -92,6 +92,7 @@ module EE ...@@ -92,6 +92,7 @@ module EE
%i[ %i[
email_additional_text email_additional_text
file_template_project_id file_template_project_id
group_owners_can_manage_default_branch_protection
default_project_deletion_protection default_project_deletion_protection
deletion_adjourned_period deletion_adjourned_period
updating_name_disabled_for_users updating_name_disabled_for_users
......
...@@ -59,6 +59,7 @@ class License < ApplicationRecord ...@@ -59,6 +59,7 @@ class License < ApplicationRecord
custom_project_templates custom_project_templates
cycle_analytics_for_groups cycle_analytics_for_groups
db_load_balancing db_load_balancing
default_branch_protection_restriction_in_groups
default_project_deletion_protection default_project_deletion_protection
dependency_proxy dependency_proxy
deploy_board deploy_board
...@@ -201,6 +202,7 @@ class License < ApplicationRecord ...@@ -201,6 +202,7 @@ class License < ApplicationRecord
custom_file_templates custom_file_templates
custom_project_templates custom_project_templates
db_load_balancing db_load_balancing
default_branch_protection_restriction_in_groups
elastic_search elastic_search
enterprise_templates enterprise_templates
extended_audit_events extended_audit_events
......
...@@ -18,6 +18,14 @@ module EE ...@@ -18,6 +18,14 @@ module EE
condition(:license_block) { License.block_changes? } condition(:license_block) { License.block_changes? }
rule { auditor }.enable :read_all_resources rule { auditor }.enable :read_all_resources
condition(:allow_to_manage_default_branch_protection) do
# When un-licensed: Always allow access.
# When licensed: Allow or deny access based on the
# `group_owners_can_manage_default_branch_protection` setting.
!License.feature_available?(:default_branch_protection_restriction_in_groups) ||
::Gitlab::CurrentSettings.group_owners_can_manage_default_branch_protection
end
end end
end end
end end
...@@ -26,6 +26,10 @@ module EE ...@@ -26,6 +26,10 @@ module EE
rule { ~anonymous }.policy do rule { ~anonymous }.policy do
enable :view_productivity_analytics enable :view_productivity_analytics
end end
rule { ~(admin | allow_to_manage_default_branch_protection) }.policy do
prevent :create_group_with_default_branch_protection
end
end end
end end
end end
...@@ -177,6 +177,10 @@ module EE ...@@ -177,6 +177,10 @@ module EE
rule { ~group_timelogs_available }.prevent :read_group_timelogs rule { ~group_timelogs_available }.prevent :read_group_timelogs
rule { can?(:read_cluster) & cluster_health_available }.enable :read_cluster_health rule { can?(:read_cluster) & cluster_health_available }.enable :read_cluster_health
rule { ~(admin | allow_to_manage_default_branch_protection) }.policy do
prevent :update_default_branch_protection
end
end end
override :lookup_access_level! override :lookup_access_level!
......
- return unless License.feature_available?(:default_branch_protection_restriction_in_groups)
- f = local_assigns.fetch(:form)
.form-group.form-check
= f.check_box :group_owners_can_manage_default_branch_protection, class: 'form-check-input'
= f.label :group_owners_can_manage_default_branch_protection, class: 'form-check-label' do
= _('Allow owners to manage default branch protection per group')
---
title: Provide instance level setting to enable or disable 'default branch protection'
at the group level for group owners
merge_request: 28997
author:
type: added
...@@ -19,6 +19,7 @@ module EE ...@@ -19,6 +19,7 @@ module EE
expose :deletion_adjourned_period, if: ->(_instance, _opts) { ::License.feature_available?(:adjourned_deletion_for_projects_and_groups) } expose :deletion_adjourned_period, if: ->(_instance, _opts) { ::License.feature_available?(:adjourned_deletion_for_projects_and_groups) }
expose :updating_name_disabled_for_users, if: ->(_instance, _opts) { ::License.feature_available?(:disable_name_update_for_users) } expose :updating_name_disabled_for_users, if: ->(_instance, _opts) { ::License.feature_available?(:disable_name_update_for_users) }
expose :npm_package_requests_forwarding, if: ->(_instance, _opts) { ::License.feature_available?(:packages) } expose :npm_package_requests_forwarding, if: ->(_instance, _opts) { ::License.feature_available?(:packages) }
expose :group_owners_can_manage_default_branch_protection, if: ->(_instance, _opts) { ::License.feature_available?(:default_branch_protection_restriction_in_groups) }
end end
end end
end end
......
...@@ -42,6 +42,7 @@ module EE ...@@ -42,6 +42,7 @@ module EE
optional :prevent_merge_requests_author_approval, type: Grape::API::Boolean, desc: 'Disable Merge request author ability to approve request.' optional :prevent_merge_requests_author_approval, type: Grape::API::Boolean, desc: 'Disable Merge request author ability to approve request.'
optional :prevent_merge_requests_committers_approval, type: Grape::API::Boolean, desc: 'Disable Merge request committer ability to approve request.' optional :prevent_merge_requests_committers_approval, type: Grape::API::Boolean, desc: 'Disable Merge request committer ability to approve request.'
optional :npm_package_requests_forwarding, type: Grape::API::Boolean, desc: 'NPM package requests are forwarded to npmjs.org if not found on GitLab.' optional :npm_package_requests_forwarding, type: Grape::API::Boolean, desc: 'NPM package requests are forwarded to npmjs.org if not found on GitLab.'
optional :group_owners_can_manage_default_branch_protection, type: Grape::API::Boolean, desc: 'Allow owners to manage default branch protection in groups'
end end
end end
......
...@@ -43,6 +43,10 @@ module EE ...@@ -43,6 +43,10 @@ module EE
attrs = attrs.except(:npm_package_requests_forwarding) attrs = attrs.except(:npm_package_requests_forwarding)
end end
unless License.feature_available?(:default_branch_protection_restriction_in_groups)
attrs = attrs.except(:group_owners_can_manage_default_branch_protection)
end
attrs attrs
end end
end end
......
...@@ -111,6 +111,13 @@ describe Admin::ApplicationSettingsController do ...@@ -111,6 +111,13 @@ describe Admin::ApplicationSettingsController do
it_behaves_like 'settings for licensed features' it_behaves_like 'settings for licensed features'
end end
context 'updating `group_owners_can_manage_default_branch_protection` setting' do
let(:settings) { { group_owners_can_manage_default_branch_protection: false } }
let(:feature) { :default_branch_protection_restriction_in_groups }
it_behaves_like 'settings for licensed features'
end
context 'updating npm packages request forwarding setting' do context 'updating npm packages request forwarding setting' do
let(:settings) { { npm_package_requests_forwarding: true } } let(:settings) { { npm_package_requests_forwarding: true } }
let(:feature) { :packages } let(:feature) { :packages }
......
...@@ -247,6 +247,67 @@ describe GroupsController do ...@@ -247,6 +247,67 @@ describe GroupsController do
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:found)
end end
end end
context 'when creating a group with `default_branch_protection` attribute' do
using RSpec::Parameterized::TableSyntax
let(:params) do
{ group: { name: 'new_group', path: 'new_group', default_branch_protection: Gitlab::Access::PROTECTION_NONE } }
end
subject { post :create, params: params }
shared_examples_for 'creates the group with the expected `default_branch_protection` value' do
it 'creates the group with the expected `default_branch_protection` value' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(Group.last.default_branch_protection).to eq(default_branch_protection)
end
end
context 'authenticated as an admin', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
where(:feature_enabled, :setting_enabled, :default_branch_protection) do
false | false | Gitlab::Access::PROTECTION_NONE
false | true | Gitlab::Access::PROTECTION_NONE
true | false | Gitlab::Access::PROTECTION_NONE
false | false | Gitlab::Access::PROTECTION_NONE
end
with_them do
before do
sign_in(user)
stub_licensed_features(default_branch_protection_restriction_in_groups: feature_enabled)
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: setting_enabled)
end
it_behaves_like 'creates the group with the expected `default_branch_protection` value'
end
end
context 'authenticated a normal user' do
where(:feature_enabled, :setting_enabled, :default_branch_protection) do
false | false | Gitlab::Access::PROTECTION_NONE
false | true | Gitlab::Access::PROTECTION_NONE
true | false | Gitlab::Access::PROTECTION_FULL
false | false | Gitlab::Access::PROTECTION_NONE
end
with_them do
before do
sign_in(user)
stub_licensed_features(default_branch_protection_restriction_in_groups: feature_enabled)
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: setting_enabled)
end
it_behaves_like 'creates the group with the expected `default_branch_protection` value'
end
end
end
end end
describe 'PUT #update' do describe 'PUT #update' do
...@@ -369,5 +430,67 @@ describe GroupsController do ...@@ -369,5 +430,67 @@ describe GroupsController do
end end
end end
end end
context 'when `default_branch_protection` is specified' do
using RSpec::Parameterized::TableSyntax
let(:params) do
{ id: group.to_param, group: { default_branch_protection: Gitlab::Access::PROTECTION_NONE } }
end
subject { put :update, params: params }
shared_examples_for 'updates the attribute' do
it 'updates the attribute' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(group.reload.default_branch_protection).to eq(default_branch_protection)
end
end
context 'authenticated as admin', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
where(:feature_enabled, :setting_enabled, :default_branch_protection) do
false | false | Gitlab::Access::PROTECTION_NONE
false | true | Gitlab::Access::PROTECTION_NONE
true | false | Gitlab::Access::PROTECTION_NONE
false | false | Gitlab::Access::PROTECTION_NONE
end
with_them do
before do
sign_in(user)
stub_licensed_features(default_branch_protection_restriction_in_groups: feature_enabled)
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: setting_enabled)
end
it_behaves_like 'updates the attribute'
end
end
context 'authenticated as group owner' do
where(:feature_enabled, :setting_enabled, :default_branch_protection) do
false | false | Gitlab::Access::PROTECTION_NONE
false | true | Gitlab::Access::PROTECTION_NONE
true | false | Gitlab::Access::PROTECTION_FULL
false | false | Gitlab::Access::PROTECTION_NONE
end
with_them do
before do
group.add_owner(user)
sign_in(user)
stub_licensed_features(default_branch_protection_restriction_in_groups: feature_enabled)
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: setting_enabled)
end
it_behaves_like 'updates the attribute'
end
end
end
end end
end end
...@@ -74,4 +74,102 @@ describe GlobalPolicy do ...@@ -74,4 +74,102 @@ describe GlobalPolicy do
it { expect(described_class.new(create(:admin), [user])).to be_disallowed(:update_max_pages_size) } it { expect(described_class.new(create(:admin), [user])).to be_disallowed(:update_max_pages_size) }
end end
describe 'create_group_with_default_branch_protection' do
context 'for an admin' do
let(:current_user) { create(:admin) }
context 'when the `default_branch_protection_restriction_in_groups` feature is available' do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: true)
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is enabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: true)
end
it { is_expected.to be_allowed(:create_group_with_default_branch_protection) }
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is disabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: false)
end
it { is_expected.to be_allowed(:create_group_with_default_branch_protection) }
end
end
context 'when the `default_branch_protection_restriction_in_groups` feature is not available' do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: false)
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is enabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: true)
end
it { is_expected.to be_allowed(:create_group_with_default_branch_protection) }
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is disabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: false)
end
it { is_expected.to be_allowed(:create_group_with_default_branch_protection) }
end
end
end
context 'for a normal user' do
let(:current_user) { create(:user) }
context 'when the `default_branch_protection_restriction_in_groups` feature is available' do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: true)
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is enabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: true)
end
it { is_expected.to be_allowed(:create_group_with_default_branch_protection) }
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is disabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: false)
end
it { is_expected.to be_disallowed(:create_group_with_default_branch_protection) }
end
end
context 'when the `default_branch_protection_restriction_in_groups` feature is not available' do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: false)
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is enabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: true)
end
it { is_expected.to be_allowed(:create_group_with_default_branch_protection) }
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is disabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: false)
end
it { is_expected.to be_allowed(:create_group_with_default_branch_protection) }
end
end
end
end
end end
...@@ -739,4 +739,102 @@ describe GroupPolicy do ...@@ -739,4 +739,102 @@ describe GroupPolicy do
end end
end end
end end
describe 'update_default_branch_protection' do
context 'for an admin' do
let(:current_user) { admin }
context 'when the `default_branch_protection_restriction_in_groups` feature is available' do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: true)
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is enabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: true)
end
it { is_expected.to be_allowed(:update_default_branch_protection) }
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is disabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: false)
end
it { is_expected.to be_allowed(:update_default_branch_protection) }
end
end
context 'when the `default_branch_protection_restriction_in_groups` feature is not available' do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: false)
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is enabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: true)
end
it { is_expected.to be_allowed(:update_default_branch_protection) }
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is disabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: false)
end
it { is_expected.to be_allowed(:update_default_branch_protection) }
end
end
end
context 'for an owner' do
let(:current_user) { owner }
context 'when the `default_branch_protection_restriction_in_groups` feature is available' do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: true)
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is enabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: true)
end
it { is_expected.to be_allowed(:update_default_branch_protection) }
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is disabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: false)
end
it { is_expected.to be_disallowed(:update_default_branch_protection) }
end
end
context 'when the `default_branch_protection_restriction_in_groups` feature is not available' do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: false)
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is enabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: true)
end
it { is_expected.to be_allowed(:update_default_branch_protection) }
end
context 'when the setting `group_owners_can_manage_default_branch_protection` is disabled' do
before do
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: false)
end
it { is_expected.to be_allowed(:update_default_branch_protection) }
end
end
end
end
end end
...@@ -158,6 +158,60 @@ describe API::Groups do ...@@ -158,6 +158,60 @@ describe API::Groups do
end end
end end
end end
context 'default_branch_protection' do
using RSpec::Parameterized::TableSyntax
let(:params) { { default_branch_protection: Gitlab::Access::PROTECTION_NONE } }
context 'authenticated as an admin' do
let(:user) { admin }
where(:feature_enabled, :setting_enabled, :default_branch_protection) do
false | false | Gitlab::Access::PROTECTION_NONE
false | true | Gitlab::Access::PROTECTION_NONE
true | false | Gitlab::Access::PROTECTION_NONE
false | false | Gitlab::Access::PROTECTION_NONE
end
with_them do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: feature_enabled)
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: setting_enabled)
end
it 'updates the attribute as expected' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['default_branch_protection']).to eq(default_branch_protection)
end
end
end
context 'authenticated a normal user' do
where(:feature_enabled, :setting_enabled, :default_branch_protection) do
false | false | Gitlab::Access::PROTECTION_NONE
false | true | Gitlab::Access::PROTECTION_NONE
true | false | Gitlab::Access::PROTECTION_FULL
false | false | Gitlab::Access::PROTECTION_NONE
end
with_them do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: feature_enabled)
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: setting_enabled)
end
it 'updates the attribute as expected' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['default_branch_protection']).to eq(default_branch_protection)
end
end
end
end
end end
describe "POST /groups" do describe "POST /groups" do
...@@ -197,6 +251,64 @@ describe API::Groups do ...@@ -197,6 +251,64 @@ describe API::Groups do
end end
end end
end end
context 'when creating a group with `default_branch_protection` attribute' do
using RSpec::Parameterized::TableSyntax
let(:params) { attributes_for_group_api(default_branch_protection: Gitlab::Access::PROTECTION_NONE) }
subject do
post api("/groups", user), params: params
end
context 'authenticated as an admin' do
let(:user) { admin }
where(:feature_enabled, :setting_enabled, :default_branch_protection) do
false | false | Gitlab::Access::PROTECTION_NONE
false | true | Gitlab::Access::PROTECTION_NONE
true | false | Gitlab::Access::PROTECTION_NONE
false | false | Gitlab::Access::PROTECTION_NONE
end
with_them do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: feature_enabled)
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: setting_enabled)
end
it 'creates the group with the expected `default_branch_protection` value' do
subject
expect(response).to have_gitlab_http_status(:created)
expect(json_response['default_branch_protection']).to eq(default_branch_protection)
end
end
end
context 'authenticated a normal user' do
where(:feature_enabled, :setting_enabled, :default_branch_protection) do
false | false | Gitlab::Access::PROTECTION_NONE
false | true | Gitlab::Access::PROTECTION_NONE
true | false | Gitlab::Access::PROTECTION_FULL
false | false | Gitlab::Access::PROTECTION_NONE
end
with_them do
before do
stub_licensed_features(default_branch_protection_restriction_in_groups: feature_enabled)
stub_ee_application_setting(group_owners_can_manage_default_branch_protection: setting_enabled)
end
it 'creates the group with the expected `default_branch_protection` value' do
subject
expect(response).to have_gitlab_http_status(:created)
expect(json_response['default_branch_protection']).to eq(default_branch_protection)
end
end
end
end
end end
describe 'POST /groups/:id/ldap_sync' do describe 'POST /groups/:id/ldap_sync' do
......
...@@ -143,6 +143,13 @@ describe API::Settings, 'EE Settings' do ...@@ -143,6 +143,13 @@ describe API::Settings, 'EE Settings' do
it_behaves_like 'settings for licensed features' it_behaves_like 'settings for licensed features'
end end
context 'group_owners_can_manage_default_branch_protection setting' do
let(:settings) { { group_owners_can_manage_default_branch_protection: false } }
let(:feature) { :default_branch_protection_restriction_in_groups }
it_behaves_like 'settings for licensed features'
end
context 'deletion adjourned period' do context 'deletion adjourned period' do
let(:settings) { { deletion_adjourned_period: 5 } } let(:settings) { { deletion_adjourned_period: 5 } }
let(:feature) { :adjourned_deletion_for_projects_and_groups } let(:feature) { :adjourned_deletion_for_projects_and_groups }
......
...@@ -1809,6 +1809,9 @@ msgstr "" ...@@ -1809,6 +1809,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access." msgid "Allow only the selected protocols to be used for Git access."
msgstr "" msgstr ""
msgid "Allow owners to manage default branch protection per group"
msgstr ""
msgid "Allow owners to manually add users outside of LDAP" msgid "Allow owners to manually add users outside of LDAP"
msgstr "" msgstr ""
......
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