diff --git a/app/validators/json_schemas/member_role_permissions.json b/app/validators/json_schemas/member_role_permissions.json
index 6a4095908b339e4660de8aca84213824abca98c1..cf42ece8ebffb896475c70e34fce3edb80d08c03 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 ef49c43e85c6b55ffe494d6f0a5ca00330172d51..8130414d10f0373d96211f5da92344f88bc77893 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. |
@@ -39881,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. |
diff --git a/doc/user/custom_roles/abilities.md b/doc/user/custom_roles/abilities.md
index c32f90bb9b6ab8b8aab17d98c605ba524ffdfbdc..29130e803d18fe35bff4aa17f7c6869fb4790fdb 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 d087db48362913c39a04bba0e1228f464ae61a1a..7839ee582f5fcb0bfe836ffcf13c3e91e5e5eb31 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 7562b581a9d7dcb52dfa81bf1f9046240bea87ea..f0b01b8583153bee1fc1296763964ed761bb340f 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 fc6a4dc7a266c6cfc6887b4e97857745c105b3c2..d72e59ead3454b42b15443ee84c3ab2da5e01310 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 357fb500d0bc7663f3d60942ae5de315af2e42b6..5850d6aafc7e3428667412dbe74060fa64a1b9a6 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 62769939784dbfee14abfeaa906181a232c2ce04..da27a8f219aff979beeea8d7504dd35124c50e50 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 66f8592a07146be80f1d387facb98d764a4900a0..f11baabe1b79b603f852c3782ada83130ed8dbb2 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 0000000000000000000000000000000000000000..6f8d10e12578e820d4b1b07978b05a891ce415cb
--- /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 01e73e1c5af9224499b7d56756576ceab1cc4d47..3e1836774c7c588668839452844b60536883e4e0 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