diff --git a/ee/app/models/authz/custom_ability/definition.rb b/ee/app/models/authz/custom_ability/definition.rb index db2dec610209b960fdccbce0fdc90e5cb441ccca..2b146e57f78abd2c8757c613095f2b7aecc26d42 100644 --- a/ee/app/models/authz/custom_ability/definition.rb +++ b/ee/app/models/authz/custom_ability/definition.rb @@ -27,7 +27,9 @@ def project_ability_enabled? end def admin_ability_enabled? - attributes.fetch(:admin_ability, false) + return false if group_ability_enabled? || project_ability_enabled? + + exists? end private diff --git a/ee/app/models/members/member_role.rb b/ee/app/models/members/member_role.rb index 3dd9d8e7bf1f6c55f5c7a76ef8b7fbda07e5d8a4..a8d7a3257c05b151dfec219c19bc14501e910ce6 100644 --- a/ee/app/models/members/member_role.rb +++ b/ee/app/models/members/member_role.rb @@ -93,23 +93,23 @@ def all_customizable_permissions end def all_customizable_standard_permissions - MemberRole.all_customizable_permissions.select { |_k, v| !v[:admin_ability] } + Gitlab::CustomRoles::Definition.standard end def all_customizable_admin_permissions - MemberRole.all_customizable_permissions.select { |_k, v| v[:admin_ability] } + Gitlab::CustomRoles::Definition.admin end def all_customizable_project_permissions - MemberRole.all_customizable_permissions.select { |_k, v| v[:project_ability] }.keys + MemberRole.all_customizable_standard_permissions.select { |_k, v| v[:project_ability] }.keys end def all_customizable_group_permissions - MemberRole.all_customizable_permissions.select { |_k, v| v[:group_ability] }.keys + MemberRole.all_customizable_standard_permissions.select { |_k, v| v[:group_ability] }.keys end def all_customizable_admin_permission_keys - MemberRole.all_customizable_permissions.select { |_k, v| v[:admin_ability] }.keys + Gitlab::CustomRoles::Definition.admin.keys end def customizable_permissions_exempt_from_consuming_seat diff --git a/ee/config/custom_abilities/read_admin_cicd.yml b/ee/config/custom_abilities/admin/read_admin_cicd.yml similarity index 78% rename from ee/config/custom_abilities/read_admin_cicd.yml rename to ee/config/custom_abilities/admin/read_admin_cicd.yml index cc8a510be64d98ba487459cd6be402ccf9e716f8..f50c707a41337237e56c59ae2c5e2fe4a981f835 100644 --- a/ee/config/custom_abilities/read_admin_cicd.yml +++ b/ee/config/custom_abilities/admin/read_admin_cicd.yml @@ -6,7 +6,3 @@ introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/507960 introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177233 feature_category: admin milestone: '17.9' -group_ability: false -project_ability: false -admin_ability: true -requirements: [] diff --git a/ee/config/custom_abilities/read_admin_dashboard.yml b/ee/config/custom_abilities/admin/read_admin_dashboard.yml similarity index 79% rename from ee/config/custom_abilities/read_admin_dashboard.yml rename to ee/config/custom_abilities/admin/read_admin_dashboard.yml index c267b2ce6590cbfbbdd910ad1cb0e3c249ae92a5..43a6c9c433960a505ae0e2712c4798325c5c5b68 100644 --- a/ee/config/custom_abilities/read_admin_dashboard.yml +++ b/ee/config/custom_abilities/admin/read_admin_dashboard.yml @@ -6,7 +6,3 @@ introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/501549 introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171581 feature_category: admin milestone: '17.6' -group_ability: false -project_ability: false -admin_ability: true -requirements: [] diff --git a/ee/config/custom_abilities/read_admin_monitoring.yml b/ee/config/custom_abilities/admin/read_admin_monitoring.yml similarity index 83% rename from ee/config/custom_abilities/read_admin_monitoring.yml rename to ee/config/custom_abilities/admin/read_admin_monitoring.yml index f981229e153d3f083a161d36e03e726f66e82a65..b8b4bfbdef5ee772c12b968fb51b1a9cc2c7a243 100644 --- a/ee/config/custom_abilities/read_admin_monitoring.yml +++ b/ee/config/custom_abilities/admin/read_admin_monitoring.yml @@ -6,7 +6,3 @@ introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/507959 introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179439 feature_category: admin milestone: '17.9' -group_ability: false -project_ability: false -admin_ability: true -requirements: [] diff --git a/ee/config/custom_abilities/read_admin_subscription.yml b/ee/config/custom_abilities/admin/read_admin_subscription.yml similarity index 79% rename from ee/config/custom_abilities/read_admin_subscription.yml rename to ee/config/custom_abilities/admin/read_admin_subscription.yml index 84384cf778c746bbab0f7022bd5afff9dfa30b6e..263a3dbcd7e3d4c5836155f875771db072cf7fe4 100644 --- a/ee/config/custom_abilities/read_admin_subscription.yml +++ b/ee/config/custom_abilities/admin/read_admin_subscription.yml @@ -6,7 +6,3 @@ introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/507961 introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178230 feature_category: admin milestone: '17.9' -admin_ability: true -group_ability: false -project_ability: false -requirements: [] diff --git a/ee/config/custom_abilities/admin/type_schema.json b/ee/config/custom_abilities/admin/type_schema.json new file mode 100644 index 0000000000000000000000000000000000000000..88ea91f3c2829baa06f7d34f286707aada09e739 --- /dev/null +++ b/ee/config/custom_abilities/admin/type_schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/custom_abilities/type_schema.json", + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "type": "string", + "description": "Human readable title for the custom ability" + }, + "name": { + "type": "string", + "description": "Unique name for the custom ability" + }, + "description": { + "type": "string", + "description": "Human readable description of this custom ability" + }, + "introduced_by_issue": { + "type": "string", + "format": "uri", + "description": "URL to GitLab issue that added this custom ability", + "qt-uri-protocols": [ + "https" + ] + }, + "introduced_by_mr": { + "type": "string", + "format": "uri", + "description": "URL to GitLab merge request that added this custom ability", + "qt-uri-protocols": [ + "https" + ] + }, + "feature_category": { + "type": "string", + "description": "The feature category of this this custom ability. For example, vulnerability_management" + }, + "milestone": { + "type": "string", + "description": "Milestone that introduced this custom ability. For example, 15.8", + "pattern": "\\A[0-9]+\\.[0-9]+\\z" + } + }, + "required": [ + "description", + "feature_category", + "introduced_by_issue", + "introduced_by_mr", + "milestone", + "name", + "title" + ], + "title": "GitLabCustomPermission" +} diff --git a/ee/config/custom_abilities/type_schema.json b/ee/config/custom_abilities/type_schema.json index bc101704ba9c95b1cb870ca13bca2359689264a4..3428732cfb490fc3f776d2f71087aaccfa8b2f15 100644 --- a/ee/config/custom_abilities/type_schema.json +++ b/ee/config/custom_abilities/type_schema.json @@ -10,11 +10,11 @@ }, "name": { "type": "string", - "description": "Unique identifying name for this custom ability" + "description": "Unique name for the custom ability" }, "description": { "type": "string", - "description": "A human-readable description of this custom ability" + "description": "Human readable description of this custom ability" }, "introduced_by_issue": { "type": "string", @@ -39,11 +39,7 @@ "milestone": { "type": "string", "description": "Milestone that introduced this custom ability. For example, 15.8", - "pattern": "^[0-9]+\\.[0-9]+$" - }, - "admin_ability": { - "type": "boolean", - "description": "Indicate whether this ability is checked at the admin level." + "pattern": "\\A[0-9]+\\.[0-9]+\\z" }, "group_ability": { "type": "boolean", @@ -154,11 +150,6 @@ "enum": [ false ] - }, - "admin_ability": { - "enum": [ - false - ] } } }, diff --git a/ee/lib/gitlab/custom_roles/definition.rb b/ee/lib/gitlab/custom_roles/definition.rb index 9adc9d6758df059d66ef8267e9207831a91737af..314fdf43eeb9762082add89f9bd8a6654bc03fbe 100644 --- a/ee/lib/gitlab/custom_roles/definition.rb +++ b/ee/lib/gitlab/custom_roles/definition.rb @@ -9,26 +9,43 @@ class << self attr_accessor :definitions def all - load_abilities! if @definitions.nil? + standard.merge(admin) + end + + def admin + @admin_definitions ||= load_definitions(admin_path) + end - @definitions + def standard + @standard_definitions ||= load_definitions(standard_path) end def load_abilities! - @definitions = {} + @standard_definitions = load_definitions(standard_path) + @admin_definitions = load_definitions(admin_path) + end + + private + + def standard_path + Rails.root.join("ee/config/custom_abilities/*.yml") + end + + def admin_path + Rails.root.join("ee/config/custom_abilities/admin/*.yml") + end + + def load_definitions(path) + definitions = {} Dir.glob(path).each do |file| definition = load_from_file(file) - name = definition[:name].to_sym - @definitions[name] = definition + name = definition[:name].to_sym + definitions[name] = definition end - end - - private - def path - Rails.root.join("ee/config/custom_abilities/*.yml") + definitions end def load_from_file(path) diff --git a/ee/spec/factories/member_roles.rb b/ee/spec/factories/member_roles.rb index 6b3b0f06271631e1b7e24c23e58a1bd21749d4cc..87b0840ef5281439c2204a36f5ba7f77684d232d 100644 --- a/ee/spec/factories/member_roles.rb +++ b/ee/spec/factories/member_roles.rb @@ -12,18 +12,22 @@ trait(role) { base_access_level { value } } end - Gitlab::CustomRoles::Definition.all.each_value do |attributes| + Gitlab::CustomRoles::Definition.standard.each_value do |attributes| trait attributes[:name].to_sym do send(attributes[:name].to_sym) { true } attributes.fetch(:requirements, []).each do |requirement| send(requirement.to_sym) { true } end + end + end - if attributes[:admin_ability] - namespace { nil } - base_access_level { nil } - end + Gitlab::CustomRoles::Definition.admin.each_value do |attributes| + trait attributes[:name].to_sym do + send(attributes[:name].to_sym) { true } + + namespace { nil } + base_access_level { nil } end end diff --git a/ee/spec/lib/gitlab/custom_roles/definition_spec.rb b/ee/spec/lib/gitlab/custom_roles/definition_spec.rb index c9b9311ac92c2d65d0bb84dce19eeed50d61cf08..7f8b1c67efc2d56b05767cbd597da1f80d4aee1f 100644 --- a/ee/spec/lib/gitlab/custom_roles/definition_spec.rb +++ b/ee/spec/lib/gitlab/custom_roles/definition_spec.rb @@ -3,60 +3,106 @@ require 'spec_helper' RSpec.describe Gitlab::CustomRoles::Definition, feature_category: :permissions do - yaml_files = Dir.glob(Rails.root.join("ee/config/custom_abilities/*.yml")) + standard_yaml_files = Dir.glob(Rails.root.join("ee/config/custom_abilities/*.yml")) + admin_yaml_files = Dir.glob(Rails.root.join("ee/config/custom_abilities/admin/*.yml")) + + def defined_abilities(files) + files.map { |file| File.basename(file, '.yml').to_sym } + end + + let_it_be(:defined_standard_abilities) { defined_abilities(standard_yaml_files) } + let_it_be(:defined_admin_abilities) { defined_abilities(admin_yaml_files) } + let_it_be(:all_defined_abilities) { defined_standard_abilities + defined_admin_abilities } describe '.all' do subject(:abilities) { described_class.all } - let_it_be(:defined_abilities) do - yaml_files.map do |file| - File.basename(file, '.yml').to_sym - end + it 'returns the defined abilities' do + expect(abilities.keys).to match_array(all_defined_abilities) end - context 'when initialized' do - it 'does not reload the abilities from the yaml files' do - expect(described_class).not_to receive(:load_abilities!) - - abilities - end + it 'does not have overlapping role names' do + admin_abilities = described_class.admin.keys + standard_abilities = described_class.standard.keys - it 'returns the defined abilities' do - expect(abilities.keys).to match_array(defined_abilities) - end + expect(standard_abilities & admin_abilities).to be_empty end context 'when not initialized' do before do - described_class.instance_variable_set(:@definitions, nil) + described_class.instance_variable_set(:@standard_definitions, nil) + described_class.instance_variable_set(:@admin_definitions, nil) end it 'reloads the abilities from the yaml files' do - expect(described_class).to receive(:load_abilities!) + expect(described_class).to receive(:load_definitions) + .with(described_class.send(:standard_path)).and_call_original + expect(described_class).to receive(:load_definitions) + .with(described_class.send(:admin_path)).and_call_original abilities end it 'returns the defined abilities' do - expect(abilities.keys).to match_array(defined_abilities) + expect(abilities.keys).to match_array(all_defined_abilities) end end end + describe '.admin' do + subject(:abilities) { described_class.admin } + + it 'returns the defined abilities' do + expect(abilities.keys).to match_array(defined_admin_abilities) + end + end + + describe '.standard' do + subject(:abilities) { described_class.standard } + + it 'returns the defined abilities' do + expect(abilities.keys).to match_array(defined_standard_abilities) + end + end + + describe '.load_abilities!' do + before do + described_class.instance_variable_set(:@standard_definitions, { old: 'ability' }) + described_class.instance_variable_set(:@admin_definitions, { old: 'ability' }) + end + + it 'returns the defined abilities' do + expect { described_class.load_abilities! }.to change { described_class.admin.keys } + .from(%i[old]).to(defined_admin_abilities).and change { described_class.standard.keys } + .from(%i[old]).to(defined_standard_abilities) + end + end + describe 'validations' do - def validate(data) + let(:validator) { JSONSchemer.schema(Pathname.new(Rails.root.join(validator_path))) } + + def validate(ability_file) + data = YAML.load_file(ability_file) validator.validate(data).pluck('error') end - let_it_be(:validator) do - JSONSchemer.schema(Pathname.new(Rails.root.join('ee/config/custom_abilities/type_schema.json'))) + describe 'for standard abilities' do + let(:validator_path) { 'ee/config/custom_abilities/type_schema.json' } + + standard_yaml_files.each do |ability_file| + it "validates #{ability_file}" do + expect(validate(ability_file)).to be_empty + end + end end - yaml_files.each do |ability_file| - it "validates #{ability_file}" do - data = YAML.load_file(ability_file) + describe 'for admin abilities' do + let(:validator_path) { 'ee/config/custom_abilities/admin/type_schema.json' } - expect(validate(data)).to be_empty + admin_yaml_files.each do |ability_file| + it "validates #{ability_file}" do + expect(validate(ability_file)).to be_empty + end end end end diff --git a/ee/spec/models/authz/custom_ability/definition_spec.rb b/ee/spec/models/authz/custom_ability/definition_spec.rb index f81ef16473c0458ab2b0c82cfedfd48595d5dfcc..0954336609efbafbeee240655348b8c0029dda94 100644 --- a/ee/spec/models/authz/custom_ability/definition_spec.rb +++ b/ee/spec/models/authz/custom_ability/definition_spec.rb @@ -14,8 +14,7 @@ admin_group_member: { name: 'admin_group_member', group_ability: group_ability, - project_ability: project_ability, - admin_ability: admin_ability + project_ability: project_ability } } end @@ -91,12 +90,21 @@ end describe '#admin_ability_enabled?' do + let(:group_ability) { false } + let(:project_ability) { false } + subject { definition.admin_ability_enabled? } it { is_expected.to be_truthy } - context 'when admin ability is restricted' do - let(:admin_ability) { false } + context 'when it is a group ability' do + let(:group_ability) { true } + + it { is_expected.to be_falsey } + end + + context 'when it is a project ability' do + let(:project_ability) { true } it { is_expected.to be_falsey } end diff --git a/ee/spec/requests/api/graphql/member_role/permissions_list_spec.rb b/ee/spec/requests/api/graphql/member_role/permissions_list_spec.rb index 0d2f4a33c0fdd34db2f93ab6f7bf6279bdb6036f..c06f98c8d18b52d1f95b83b667861efb4cf15063 100644 --- a/ee/spec/requests/api/graphql/member_role/permissions_list_spec.rb +++ b/ee/spec/requests/api/graphql/member_role/permissions_list_spec.rb @@ -22,6 +22,33 @@ QUERY end + let(:mock_permissions) do + { + admin_ability_one: { + title: 'Admin something', + description: 'Allows admin access to do something.', + project_ability: true, + available_from_access_level: 50, + enabled_for_project_access_levels: [50] + }, + admin_ability_two: { + title: 'Admin something else', + description: 'Allows admin access to do something else.', + requirements: [:read_ability_two], + group_ability: true, + enabled_for_group_access_levels: [40, 50] + }, + read_ability_two: { + title: 'Read something else', + description: 'Allows read access to do something else.', + group_ability: true, + project_ability: true, + enabled_for_group_access_levels: [20, 30, 40, 50], + enabled_for_project_access_levels: [20, 30, 40, 50] + } + } + end + let(:query) do graphql_query_for('memberRolePermissions', fields) end @@ -44,31 +71,9 @@ def reset_enum! end before do - allow(MemberRole).to receive(:all_customizable_permissions).and_return( - { - admin_ability_one: { - title: 'Admin something', - description: 'Allows admin access to do something.', - project_ability: true, - available_from_access_level: 50, - enabled_for_project_access_levels: [50] - }, - admin_ability_two: { - title: 'Admin something else', - description: 'Allows admin access to do something else.', - requirements: [:read_ability_two], - group_ability: true, - enabled_for_group_access_levels: [40, 50] - }, - read_ability_two: { - title: 'Read something else', - description: 'Allows read access to do something else.', - group_ability: true, - project_ability: true, - enabled_for_group_access_levels: [20, 30, 40, 50], - enabled_for_project_access_levels: [20, 30, 40, 50] - } - } + allow(MemberRole).to receive_messages( + all_customizable_permissions: mock_permissions, + all_customizable_standard_permissions: mock_permissions ) redefine_enum! @@ -84,7 +89,7 @@ def reset_enum! it_behaves_like 'a working graphql query' - it 'returns all customizable ablities' do + it 'returns all customizable ablities', :unlimited_max_formatted_output_length do expected_result = [ { 'availableFor' => ['project'], 'description' => 'Allows admin access to do something.', 'name' => 'Admin something', 'requirements' => nil, 'value' => 'ADMIN_ABILITY_ONE', diff --git a/tooling/custom_roles/docs/templates/custom_abilities.md.erb b/tooling/custom_roles/docs/templates/custom_abilities.md.erb index 1f33eb970855744ebd5791fae83f87bb2d3e5397..c5775e6eb6ca02f9ec523c17999b958b4e948148 100644 --- a/tooling/custom_roles/docs/templates/custom_abilities.md.erb +++ b/tooling/custom_roles/docs/templates/custom_abilities.md.erb @@ -21,7 +21,7 @@ <% end %> <% def scope(ability) %> <% scopes = [] %> -<% scopes << 'Instance' if ability[:admin_ability]%> +<% scopes << 'Instance' unless ability[:group_ability] || ability[:project_ability]%> <% scopes << 'Group' if ability[:group_ability]%> <% scopes << 'Project' if ability[:project_ability]%> <% return scopes.join(',<br> ') %>