From 0269a2b3b80567fb45a1117e2abb1aea07aef21d Mon Sep 17 00:00:00 2001 From: Hinam Mehra Date: Mon, 9 Dec 2024 15:09:55 +1100 Subject: [PATCH 1/3] Add read compliance dashboard as a custom ability - Update group and project policy - Disable `New framework` button when only read compliance ability is enabled and admin compliance ability is not Changelog: added EE: true --- .../json_schemas/member_role_permissions.json | 3 + doc/api/graphql/reference/index.md | 1 + doc/user/custom_roles/abilities.md | 1 + .../components/main_layout.vue | 7 +- .../compliance_dashboards_controller.rb | 4 ++ .../compliance_dashboards_controller.rb | 6 +- ee/app/policies/ee/group_policy.rb | 10 ++- ee/app/policies/ee/project_policy.rb | 9 ++- .../admin_compliance_framework.yml | 3 +- .../read_compliance_dashboard.yml | 13 ++++ .../menus/security_compliance_menu.rb | 5 +- .../components/main_layout_spec.js | 16 +++++ .../menus/security_compliance_menu_spec.rb | 62 ++++++++++++++--- .../menus/security_compliance_menu_spec.rb | 56 +++++++++++++-- ee/spec/policies/group_policy_spec.rb | 68 ++++++++++++++++++- ee/spec/policies/project_policy_spec.rb | 44 +++++++++++- .../request_spec.rb | 23 +++++-- .../read_compliance_dashboard/request_spec.rb | 61 +++++++++++++++++ .../menus/security_compliance_menu_spec.rb | 2 - 19 files changed, 352 insertions(+), 42 deletions(-) create mode 100644 ee/config/custom_abilities/read_compliance_dashboard.yml create mode 100644 ee/spec/requests/custom_roles/read_compliance_dashboard/request_spec.rb diff --git a/app/validators/json_schemas/member_role_permissions.json b/app/validators/json_schemas/member_role_permissions.json index 6a4095908b339e..cf42ece8ebffb8 100644 --- a/app/validators/json_schemas/member_role_permissions.json +++ b/app/validators/json_schemas/member_role_permissions.json @@ -61,6 +61,9 @@ "read_code": { "type": "boolean" }, + "read_compliance_dashboard": { + "type": "boolean" + }, "read_crm_contact": { "type": "boolean" }, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index ef49c43e85c6b5..f056d18bb7c30e 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -39850,6 +39850,7 @@ Member role permission. | `MANAGE_SECURITY_POLICY_LINK` | Allows linking security policy projects. | | `READ_ADMIN_DASHBOARD` | Read-only access to admin dashboard. | | `READ_CODE` | Allows read-only access to the source code in the user interface. Does not allow users to edit or download repository archives, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. You can download individual files because read-only access inherently grants the ability to make a local copy of the file. | +| `READ_COMPLIANCE_DASHBOARD` | Read compliance capabilities including adherence, violations, and frameworks for groups and projects. | | `READ_CRM_CONTACT` | Read CRM contact. | | `READ_DEPENDENCY` | Allows read-only access to the dependencies and licenses. | | `READ_RUNNERS` | Allows read-only access to group or project runners, including the runner fleet dashboard. | diff --git a/doc/user/custom_roles/abilities.md b/doc/user/custom_roles/abilities.md index c32f90bb9b6ab8..29130e803d18fe 100644 --- a/doc/user/custom_roles/abilities.md +++ b/doc/user/custom_roles/abilities.md @@ -40,6 +40,7 @@ These requirements are documented in the `Required permission` column in the fol | Name | Required permission | Description | Introduced in | Feature flag | Enabled in | |:-----|:------------|:------------------|:---------|:--------------|:---------| | [`admin_compliance_framework`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144183) | | Create, read, update, and delete compliance frameworks. Users with this permission can also assign a compliance framework label to a project, and set the default framework of a group. | GitLab [17.0](https://gitlab.com/gitlab-org/gitlab/-/issues/411502) | | | +| [`read_compliance_dashboard`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175066) | | Read compliance capabilities including adherence, violations, and frameworks for groups and projects. | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/465324) | | | ## Continuous delivery diff --git a/ee/app/assets/javascripts/compliance_dashboard/components/main_layout.vue b/ee/app/assets/javascripts/compliance_dashboard/components/main_layout.vue index d087db48362913..7839ee582f5fcb 100644 --- a/ee/app/assets/javascripts/compliance_dashboard/components/main_layout.vue +++ b/ee/app/assets/javascripts/compliance_dashboard/components/main_layout.vue @@ -11,6 +11,7 @@ import { import { helpPagePath } from '~/helpers/help_page_helper'; import Tracking from '~/tracking'; +import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin'; import { isTopLevelGroup } from '../utils'; import { @@ -59,7 +60,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [Tracking.mixin()], + mixins: [Tracking.mixin(), glAbilitiesMixin()], inject: [ 'mergeCommitsCsvExportPath', 'projectFrameworksCsvExportPath', @@ -110,6 +111,9 @@ export default { tabIndex() { return this.tabs.findIndex((tab) => tab.target === this.$route.name); }, + canAdminComplianceFramework() { + return this.glAbilities.adminComplianceFramework; + }, }, methods: { goTo(name) { @@ -159,6 +163,7 @@ export default { { violationsCsvExportPath: '/compliance_violation_reports.csv', adherencesCsvExportPath: '/compliance_standards_adherences.csv', frameworksCsvExportPath: '/compliance_frameworks_report.csv', + glAbilities: { + adminComplianceFramework: true, + }, }; const groupPath = 'top-level-group-path'; @@ -122,6 +125,19 @@ describe('ComplianceReportsApp component', () => { 'You can only create the compliance framework in top-level group Top Level Group', ); }); + + describe('when ability `adminComplianceFramework` is false', () => { + it('does not render the button', () => { + wrapper = createComponent( + mount, + {}, + { glAbilities: { adminComplianceFramework: false } }, + {}, + ); + + expect(findNewFrameworkButton().exists()).toBe(false); + }); + }); }); describe('violations report', () => { diff --git a/ee/spec/lib/ee/sidebars/projects/menus/security_compliance_menu_spec.rb b/ee/spec/lib/ee/sidebars/projects/menus/security_compliance_menu_spec.rb index 7562b581a9d7dc..f0b01b8583153b 100644 --- a/ee/spec/lib/ee/sidebars/projects/menus/security_compliance_menu_spec.rb +++ b/ee/spec/lib/ee/sidebars/projects/menus/security_compliance_menu_spec.rb @@ -10,8 +10,10 @@ let(:show_discover_project_security) { true } let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, show_promotions: show_promotions, show_discover_project_security: show_discover_project_security) } + subject(:items) { described_class.new(context).renderable_items.find { |i| i.item_id == item_id } } + describe '#link' do - subject { described_class.new(context) } + subject(:menu) { described_class.new(context) } let(:show_promotions) { false } let(:show_discover_project_security) { false } @@ -30,7 +32,7 @@ it 'returns the expected link' do stub_licensed_features(security_dashboard: security_dashboard_feature, audit_events: audit_events_feature, dependency_scanning: dependency_scanning_feature) - expect(subject.link).to include(expected_link) + expect(menu.link).to include(expected_link) end end @@ -38,8 +40,8 @@ let(:user) { nil } it 'returns nil', :aggregate_failures do - expect(subject.renderable_items).to be_empty - expect(subject.link).to be_nil + expect(menu.renderable_items).to be_empty + expect(menu.link).to be_nil end end end @@ -73,8 +75,6 @@ end end - subject { described_class.new(context).renderable_items.find { |i| i.item_id == item_id } } - describe 'Configuration' do let(:item_id) { :configuration } @@ -255,7 +255,7 @@ end end - describe 'Compliance' do + describe 'Compliance center' do let(:item_id) { :compliance } context 'when project_level_compliance_dashboard feature is enabled' do @@ -269,13 +269,53 @@ context 'when project is in group' do let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, :public, :in_group) } + let_it_be(:project) { create(:project, :private, :in_group) } + + context 'when user is an owner' do + before_all do + project.add_owner(user) + end - before_all do - project.add_owner(user) + it { is_expected.not_to be_nil } end - it { is_expected.not_to be_nil } + context 'when user is assigned a custom role with `read_compliance_dashboard` ability' do + let_it_be(:member_role) { create(:member_role, :guest, :read_compliance_dashboard, namespace: project.group) } + let_it_be(:member) { create(:project_member, :guest, user: user, source: project, member_role: member_role) } + + before do + stub_licensed_features(project_level_compliance_dashboard: true, custom_roles: true) + end + + it { is_expected.not_to be_nil } + + context 'when custom roles feature is disabled' do + before do + stub_licensed_features(project_level_compliance_dashboard: true, custom_roles: false) + end + + it { is_expected.to be_nil } + end + end + + context 'when user is assigned a custom role with `admin_compliance_framework` ability' do + let_it_be(:member_role) { create(:member_role, :guest, :admin_compliance_framework, namespace: project.group) } + let_it_be(:member) { create(:project_member, :guest, user: user, source: project, member_role: member_role) } + + before do + stub_licensed_features(project_level_compliance_dashboard: true, custom_roles: true) + end + + it { is_expected.not_to be_nil } + + context 'when custom roles feature is disabled' do + before do + stub_licensed_features(project_level_compliance_dashboard: true, custom_roles: false) + end + + it { is_expected.to be_nil } + end + end end end diff --git a/ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb b/ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb index fc6a4dc7a266c6..d72e59ead3454b 100644 --- a/ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb +++ b/ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb @@ -16,11 +16,11 @@ let(:menu) { described_class.new(context) } describe '#link' do - subject { menu.link } + subject(:link) { menu.link } context 'when menu has menu items' do it 'returns first visible menu item link' do - expect(subject).to eq menu.renderable_items.first.link + expect(link).to eq menu.renderable_items.first.link end end @@ -28,13 +28,13 @@ let(:user) { nil } it 'returns show group security page' do - expect(subject).to eq "/groups/#{group.full_path}/-/security/discover" + expect(link).to eq "/groups/#{group.full_path}/-/security/discover" end end end describe '#title' do - subject { menu.title } + subject(:title) { menu.title } specify do is_expected.to eq 'Security and compliance' @@ -50,7 +50,7 @@ end describe '#render?' do - subject { menu.render? } + subject(:render) { menu.render? } it 'returns true if there are menu items' do is_expected.to be true @@ -72,7 +72,7 @@ end describe 'Menu Items' do - subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } } + subject(:items) { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } } shared_examples 'menu access rights' do it { is_expected.not_to be_nil } @@ -116,7 +116,7 @@ end end - describe 'Compliance' do + describe 'Compliance center' do let(:item_id) { :compliance } context 'when group_level_compliance_dashboard feature is enabled' do @@ -125,6 +125,48 @@ end it_behaves_like 'menu access rights' + + context 'when user is assigned a custom role with `read_compliance_dashboard` ability' do + let_it_be(:user) { create(:user) } + + let_it_be(:member_role) { create(:member_role, :guest, :read_compliance_dashboard, namespace: group) } + let_it_be(:member) { create(:group_member, :guest, user: user, source: group, member_role: member_role) } + + before do + stub_licensed_features(group_level_compliance_dashboard: true, custom_roles: true) + end + + it_behaves_like 'menu access rights' + + context 'when custom roles feature is disabled' do + before do + stub_licensed_features(group_level_compliance_dashboard: true, custom_roles: false) + end + + it { is_expected.to be_nil } + end + end + + context 'when user is assigned a custom role with `admin_compliance_framework` ability' do + let_it_be(:user) { create(:user) } + + let_it_be(:member_role) { create(:member_role, :guest, :admin_compliance_framework, namespace: group) } + let_it_be(:member) { create(:group_member, :guest, user: user, source: group, member_role: member_role) } + + before do + stub_licensed_features(group_level_compliance_dashboard: true, custom_roles: true) + end + + it_behaves_like 'menu access rights' + + context 'when custom roles feature is disabled' do + before do + stub_licensed_features(group_level_compliance_dashboard: true, custom_roles: false) + end + + it { is_expected.to be_nil } + end + end end context 'when group_level_compliance_dashboard feature is not enabled' do diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 357fb500d0bc76..5850d6aafc7e34 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -3890,12 +3890,68 @@ def create_member_role(member, abilities = member_role_abilities) context 'for a member role with admin_compliance_framework true' do let(:member_role_abilities) { { admin_compliance_framework: true } } + + let(:allowed_abilities) do + [ + :admin_compliance_framework, + :admin_compliance_pipeline_configuration, + :read_compliance_dashboard, + :read_compliance_adherence_report, + :read_compliance_violations_report + ] + end + + context 'when compliance framework feature is available' do + let(:licensed_features) do + { + custom_compliance_frameworks: true, + evaluate_group_level_compliance_pipeline: true, + group_level_compliance_dashboard: true, + group_level_compliance_adherence_report: true, + group_level_compliance_violations_report: true + } + end + + it_behaves_like 'custom roles abilities' + end + + context 'when compliance framework features are unavailable' do + before do + create_member_role(group_member_guest) + + stub_licensed_features( + custom_roles: true, + custom_compliance_frameworks: false, + evaluate_group_level_compliance_pipeline: false, + group_level_compliance_dashboard: false, + group_level_compliance_adherence_report: false, + group_level_compliance_violations_report: false + ) + end + + it { is_expected.to be_disallowed(*allowed_abilities) } + end + end + + context 'for a member role with read_compliance_dashboard true' do + let(:member_role_abilities) { { read_compliance_dashboard: true } } + let(:allowed_abilities) do - [:admin_compliance_framework, :admin_compliance_pipeline_configuration, :read_compliance_dashboard] + [ + :read_compliance_dashboard, + :read_compliance_adherence_report, + :read_compliance_violations_report + ] end context 'when compliance framework feature is available' do - let(:licensed_features) { { custom_compliance_frameworks: true } } + let(:licensed_features) do + { + group_level_compliance_dashboard: true, + group_level_compliance_adherence_report: true, + group_level_compliance_violations_report: true + } + end it_behaves_like 'custom roles abilities' end @@ -3903,7 +3959,13 @@ def create_member_role(member, abilities = member_role_abilities) context 'when compliance framework feature is unavailable' do before do create_member_role(group_member_guest) - stub_licensed_features(custom_roles: true, custom_compliance_frameworks: false) + + stub_licensed_features( + custom_roles: true, + group_level_compliance_dashboard: false, + group_level_compliance_adherence_report: false, + group_level_compliance_violations_report: false + ) end it { is_expected.to be_disallowed(*allowed_abilities) } diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 62769939784dbf..da27a8f219aff9 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -3001,10 +3001,48 @@ def create_member_role(member, abilities = member_role_abilities) end end - context 'for a member role with `custom_compliance_frameworks` true' do - let(:licensed_features) { { compliance_framework: true } } + context 'for a custom role with the `admin_compliance_framework` ability' do + let(:licensed_features) do + { + compliance_framework: true, + project_level_compliance_dashboard: true, + project_level_compliance_adherence_report: true, + project_level_compliance_violations_report: true + } + end + let(:member_role_abilities) { { admin_compliance_framework: true } } - let(:allowed_abilities) { [:admin_compliance_framework] } + + let(:allowed_abilities) do + [ + :admin_compliance_framework, + :read_compliance_dashboard, + :read_compliance_adherence_report, + :read_compliance_violations_report + ] + end + + it_behaves_like 'custom roles abilities' + end + + context 'for a custom role with the `read_compliance_dashboard` ability' do + let(:licensed_features) do + { + project_level_compliance_dashboard: true, + project_level_compliance_adherence_report: true, + project_level_compliance_violations_report: true + } + end + + let(:member_role_abilities) { { read_compliance_dashboard: true } } + + let(:allowed_abilities) do + [ + :read_compliance_dashboard, + :read_compliance_adherence_report, + :read_compliance_violations_report + ] + end it_behaves_like 'custom roles abilities' end diff --git a/ee/spec/requests/custom_roles/admin_compliance_framework/request_spec.rb b/ee/spec/requests/custom_roles/admin_compliance_framework/request_spec.rb index 66f8592a07146b..f11baabe1b79b6 100644 --- a/ee/spec/requests/custom_roles/admin_compliance_framework/request_spec.rb +++ b/ee/spec/requests/custom_roles/admin_compliance_framework/request_spec.rb @@ -9,10 +9,6 @@ let_it_be(:role) { create(:member_role, :guest, :admin_compliance_framework, namespace: group) } let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: current_user, group: group) } let_it_be(:framework) { create(:compliance_framework, namespace: group) } - let(:compliance_features) do - { custom_compliance_frameworks: true, evaluate_group_level_compliance_pipeline: true, - group_level_compliance_dashboard: true, compliance_framework: true } - end before do stub_licensed_features(compliance_features.merge(custom_roles: true)) @@ -21,6 +17,16 @@ end describe GroupsController do + let(:compliance_features) do + { + custom_compliance_frameworks: true, + evaluate_group_level_compliance_pipeline: true, + group_level_compliance_dashboard: true, + group_level_compliance_adherence_report: true, + group_level_compliance_violations_report: true + } + end + it 'user can see edit a group page via a custom role' do get edit_group_path(group) @@ -38,6 +44,15 @@ end describe ProjectsController do + let(:compliance_features) do + { + compliance_framework: true, + project_level_compliance_dashboard: true, + project_level_compliance_adherence_report: true, + project_level_compliance_violations_report: true + } + end + it 'user can see edit a project page via a custom role' do get edit_project_path(project) diff --git a/ee/spec/requests/custom_roles/read_compliance_dashboard/request_spec.rb b/ee/spec/requests/custom_roles/read_compliance_dashboard/request_spec.rb new file mode 100644 index 00000000000000..6f8d10e12578e8 --- /dev/null +++ b/ee/spec/requests/custom_roles/read_compliance_dashboard/request_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User with read_compliance_dashboard custom role', feature_category: :compliance_management do + let_it_be(:current_user) { create(:user) } + let_it_be(:group, reload: true) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + let_it_be(:role) { create(:member_role, :guest, :read_compliance_dashboard, namespace: group) } + let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: current_user, group: group) } + + let_it_be(:framework) { create(:compliance_framework, namespace: group) } + + before do + stub_licensed_features(compliance_features.merge(custom_roles: true)) + + sign_in(current_user) + end + + describe Groups::Security::ComplianceDashboardsController do + let(:compliance_features) do + { + custom_compliance_frameworks: true, + evaluate_group_level_compliance_pipeline: true, + group_level_compliance_dashboard: true, + group_level_compliance_adherence_report: true, + group_level_compliance_violations_report: true + } + end + + describe "#show" do + it 'user can see compliance dashboard' do + get group_security_compliance_dashboard_path(group) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + end + + describe Projects::Security::ComplianceDashboardsController do + let(:compliance_features) do + { + compliance_framework: true, + project_level_compliance_dashboard: true, + project_level_compliance_adherence_report: true, + project_level_compliance_violations_report: true + } + end + + describe "#show" do + it 'user can see compliance dashboard' do + get project_security_compliance_dashboard_path(project) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:show) + end + end + end +end diff --git a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb index 01e73e1c5af922..3e1836774c7c58 100644 --- a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb @@ -24,8 +24,6 @@ let_it_be(:project) { create(:project, :security_and_compliance_disabled) } before do - allow(Ability).to receive(:allowed?).with(user, :access_security_and_compliance, project).and_return(false) - allow(Ability).to receive(:allowed?).with(user, :read_security_resource, project).and_return(false) allow(project).to receive(:security_and_compliance_enabled?).and_return(false) end -- GitLab From e0c853ea4608a37d8a3f4c096def9c5bfb0fa629 Mon Sep 17 00:00:00 2001 From: Hinam Mehra Date: Mon, 16 Dec 2024 17:14:38 +1100 Subject: [PATCH 2/3] Resolve pipeline failures --- ee/config/custom_abilities/admin_compliance_framework.yml | 3 +-- ee/config/custom_abilities/read_compliance_dashboard.yml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ee/config/custom_abilities/admin_compliance_framework.yml b/ee/config/custom_abilities/admin_compliance_framework.yml index 1f4521db151a8b..3468291d733e92 100644 --- a/ee/config/custom_abilities/admin_compliance_framework.yml +++ b/ee/config/custom_abilities/admin_compliance_framework.yml @@ -8,5 +8,4 @@ feature_category: compliance_management milestone: '17.0' group_ability: true project_ability: true -requirements: - - read_compliance_dashboard +requirements: [] diff --git a/ee/config/custom_abilities/read_compliance_dashboard.yml b/ee/config/custom_abilities/read_compliance_dashboard.yml index bd297be287530b..55bdf42d1a6454 100644 --- a/ee/config/custom_abilities/read_compliance_dashboard.yml +++ b/ee/config/custom_abilities/read_compliance_dashboard.yml @@ -10,4 +10,3 @@ milestone: '17.7' group_ability: true project_ability: true requirements: [] -available_from_access_level: 10 -- GitLab From 06ac4ea37074b43bf32e799dadc38dbc5651f973 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 18 Dec 2024 23:25:38 -0800 Subject: [PATCH 3/3] Regenerate GraphQL docs `bundle exec rake gitlab:graphql:compile_docs` adds the `READ_COMPLIANCE_DASHBOARD` permission for standard permissions. --- doc/api/graphql/reference/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f056d18bb7c30e..8130414d10f037 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -39882,6 +39882,7 @@ Member role standard permission. | `MANAGE_PROJECT_ACCESS_TOKENS` | Create, read, update, and delete project access tokens. When creating a token, users with this custom permission must select a role for that token that has the same or fewer permissions as the default role used as the base for the custom role. | | `MANAGE_SECURITY_POLICY_LINK` | Allows linking security policy projects. | | `READ_CODE` | Allows read-only access to the source code in the user interface. Does not allow users to edit or download repository archives, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. You can download individual files because read-only access inherently grants the ability to make a local copy of the file. | +| `READ_COMPLIANCE_DASHBOARD` | Read compliance capabilities including adherence, violations, and frameworks for groups and projects. | | `READ_CRM_CONTACT` | Read CRM contact. | | `READ_DEPENDENCY` | Allows read-only access to the dependencies and licenses. | | `READ_RUNNERS` | Allows read-only access to group or project runners, including the runner fleet dashboard. | -- GitLab