Commit e79b6cc1 authored by Max Woolf's avatar Max Woolf

Add compliance frameworks to namespace and project in GraphQL API

Adds the ability to traverse to a namespace's
compliance frameworks using the GraphQL API.

Additionally adds other attributes to the
compliance framework type, namely:
id, description and color.
parent 8ee3a28e
......@@ -3139,6 +3139,21 @@ enum CommitEncoding {
Represents a ComplianceFramework associated with a Project
"""
type ComplianceFramework {
"""
Hexadecimal representation of compliance framework's label color
"""
color: String!
"""
Description of the compliance framework
"""
description: String!
"""
Compliance framework ID
"""
id: ID!
"""
Name of the compliance framework
"""
......@@ -8818,6 +8833,32 @@ type Group {
startDate: Date!
): CodeCoverageActivityConnection
"""
Compliance frameworks available to projects in this namespace. Available only
when feature flag `ff_custom_compliance_frameworks` is enabled
"""
complianceFrameworks(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ComplianceFrameworkConnection
"""
Container repositories of the group
"""
......@@ -14182,6 +14223,32 @@ type Namespace {
"""
additionalPurchasedStorageSize: Float
"""
Compliance frameworks available to projects in this namespace. Available only
when feature flag `ff_custom_compliance_frameworks` is enabled
"""
complianceFrameworks(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ComplianceFrameworkConnection
"""
Includes at least one project where the repository size exceeds the limit
"""
......
......@@ -8529,6 +8529,60 @@
"name": "ComplianceFramework",
"description": "Represents a ComplianceFramework associated with a Project",
"fields": [
{
"name": "color",
"description": "Hexadecimal representation of compliance framework's label color",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Description of the compliance framework",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "Compliance framework ID",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the compliance framework",
......@@ -24512,6 +24566,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "complianceFrameworks",
"description": "Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFrameworkConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "containerRepositories",
"description": "Container repositories of the group",
......@@ -42200,6 +42307,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "complianceFrameworks",
"description": "Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFrameworkConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "containsLockedProjects",
"description": "Includes at least one project where the repository size exceeds the limit",
......@@ -507,6 +507,9 @@ Represents a ComplianceFramework associated with a Project.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `color` | String! | Hexadecimal representation of compliance framework's label color |
| `description` | String! | Description of the compliance framework |
| `id` | ID! | Compliance framework ID |
| `name` | String! | Name of the compliance framework |
### ConfigureSastPayload
......@@ -1459,6 +1462,7 @@ Autogenerated return type of EpicTreeReorder.
| `board` | Board | A single board of the group |
| `boards` | BoardConnection | Boards of the group |
| `codeCoverageActivities` | CodeCoverageActivityConnection | Represents the code coverage activity for this group |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled |
| `containerRepositories` | ContainerRepositoryConnection | Container repositories of the group |
| `containerRepositoriesCount` | Int! | Number of container repositories in the group |
| `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit |
......@@ -2183,6 +2187,7 @@ Contains statistics about a milestone.
| ----- | ---- | ----------- |
| `actualRepositorySizeLimit` | Float | Size limit for repositories in the namespace in bytes |
| `additionalPurchasedStorageSize` | Float | Additional storage purchased for the root namespace in bytes |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled |
| `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit |
| `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
......
......@@ -54,6 +54,12 @@ module EE
null: true,
description: 'Date until the temporary storage increase is active'
field :compliance_frameworks,
::Types::ComplianceManagement::ComplianceFrameworkType.connection_type,
null: true,
description: 'Compliance frameworks available to projects in this namespace',
feature_flag: :ff_custom_compliance_frameworks
def additional_purchased_storage_size
object.additional_purchased_storage_size.megabytes
end
......@@ -61,6 +67,16 @@ module EE
def storage_size_limit
object.root_storage_size.limit
end
def compliance_frameworks
BatchLoader::GraphQL.for(object.id).batch(default_value: []) do |namespace_ids, loader|
results = ::ComplianceManagement::Framework.with_namespaces(namespace_ids)
results.each do |framework|
loader.call(framework.namespace.id) { |xs| xs << framework }
end
end
end
end
end
end
......
......@@ -55,7 +55,6 @@ module EE
field :compliance_frameworks, ::Types::ComplianceManagement::ComplianceFrameworkType.connection_type,
description: 'Compliance frameworks associated with the project',
resolver: ::Resolvers::ComplianceFrameworksResolver,
null: true
field :security_dashboard_path, GraphQL::STRING_TYPE,
......@@ -141,6 +140,18 @@ module EE
def security_dashboard_path
Rails.application.routes.url_helpers.project_security_dashboard_index_path(object)
end
def compliance_frameworks
BatchLoader::GraphQL.for(object.id).batch(default_value: []) do |project_ids, loader|
results = ::ComplianceManagement::Framework.with_projects(project_ids)
results.each do |framework|
framework.project_ids.each do |project_id|
loader.call(project_id) { |xs| xs << framework }
end
end
end
end
end
end
end
......
# frozen_string_literal: true
module Resolvers
class ComplianceFrameworksResolver < BaseResolver
type Types::ComplianceManagement::ComplianceFrameworkType, null: true
alias_method :project, :object
def resolve(**args)
Array.wrap(project.compliance_framework_setting)
end
end
end
......@@ -7,13 +7,21 @@ module Types
graphql_name 'ComplianceFramework'
description 'Represents a ComplianceFramework associated with a Project'
field :id, GraphQL::ID_TYPE,
null: false,
description: 'Compliance framework ID'
field :name, GraphQL::STRING_TYPE,
null: false,
description: 'Name of the compliance framework'
def name
object.compliance_management_framework.name
end
field :description, GraphQL::STRING_TYPE,
null: false,
description: 'Description of the compliance framework'
field :color, GraphQL::STRING_TYPE,
null: false,
description: 'Hexadecimal representation of compliance framework\'s label color'
end
end
end
......@@ -59,6 +59,8 @@ module ComplianceManagement
strip_attributes :name, :color
belongs_to :namespace
has_many :project_settings, class_name: 'ComplianceManagement::ComplianceFramework::ProjectSettings'
has_many :projects, through: :project_settings
validates :namespace, presence: true
validates :name, presence: true, length: { maximum: 255 }
......@@ -67,6 +69,9 @@ module ComplianceManagement
validates :regulated, presence: true
validates :namespace_id, uniqueness: { scope: :name }
scope :with_projects, ->(project_ids) { includes(:projects).where(projects: { id: project_ids }) }
scope :with_namespaces, ->(namespace_ids) { includes(:namespace).where(namespaces: { id: namespace_ids })}
def default_framework_definition
strong_memoize(:default_framework_definition) do
DEFAULT_FRAMEWORKS.find { |framework| framework.name.eql?(name) }
......
......@@ -43,6 +43,7 @@ module EE
has_one :status_page_setting, inverse_of: :project, class_name: 'StatusPage::ProjectSetting'
has_one :compliance_framework_setting, class_name: 'ComplianceManagement::ComplianceFramework::ProjectSettings', inverse_of: :project
has_many :compliance_management_frameworks, through: :compliance_framework_setting, source: 'compliance_management_framework'
has_one :security_setting, class_name: 'ProjectSecuritySetting'
has_one :vulnerability_statistic, class_name: 'Vulnerabilities::Statistic'
......
---
title: Add compliance frameworks to namespaces in GraphQL API
merge_request: 47779
author:
type: added
---
name: ff_custom_compliance_frameworks
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47779
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/287779
milestone: '13.7'
type: development
group: group::compliance
default_enabled: false
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::ComplianceFrameworksResolver do
include GraphqlHelpers
let(:project) { create(:project) }
describe '#resolve' do
subject { resolve_compliance_frameworks(project) }
context 'when a project has a compliance framework set' do
before do
project.update!(compliance_framework_setting: create(:compliance_framework_project_setting, :sox))
end
it 'includes the name of the compliance frameworks' do
expect(subject.size).to eq(1)
framework = subject.first.compliance_management_framework
expect(framework.name).to eq('SOX')
end
end
context 'when a project has no compliance framework set' do
it 'is an empty array' do
expect(subject).to be_empty
end
end
end
def resolve_compliance_frameworks(project)
resolve(described_class, obj: project)
end
end
......@@ -3,5 +3,16 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ComplianceFramework'] do
it { expect(described_class).to have_graphql_field(:name) }
subject { described_class }
fields = %w[
id
name
description
color
]
it 'has the correct fields' do
is_expected.to have_graphql_fields(fields)
end
end
......@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['Namespace'] do
storage_size_limit
is_temporary_storage_increase_enabled
temporary_storage_increase_ends_on
compliance_frameworks
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Project'] do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:vulnerability) { create(:vulnerability, project: project, severity: :high) }
......@@ -356,4 +358,29 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::Ci::CodeCoverageSummaryType) }
end
describe 'compliance_frameworks' do
it 'queries in batches' do
projects = create_list(:project, 2, :with_compliance_framework)
projects.each { |p| p.add_maintainer(user) }
results = batch_sync(max_queries: 1) do
projects.flat_map do |p|
resolve_field(:compliance_frameworks, p)
end
end
frameworks = results.flat_map(&:items)
expect(frameworks).to match_array(projects.flat_map(&:compliance_management_frameworks))
end
end
private
def query_for_project(project)
graphql_query_for(
:projects, { ids: [global_id_of(project)] }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting a list of compliance frameworks for a root namespace' do
include GraphqlHelpers
let_it_be(:namespace) { create(:namespace) }
let_it_be(:compliance_framework_1) { create(:compliance_framework, namespace: namespace, name: 'Test1') }
let_it_be(:compliance_framework_2) { create(:compliance_framework, namespace: namespace, name: 'Test2') }
let(:path) { %i[namespace compliance_frameworks nodes] }
let!(:query) do
graphql_query_for(
:namespace, { full_path: namespace.full_path }, query_nodes(:compliance_frameworks)
)
end
context 'when authenticated as the namespace owner' do
let(:current_user) { namespace.owner }
it 'returns the groups compliance frameworks' do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(*path)).to contain_exactly(
a_hash_including('id' => global_id_of(compliance_framework_1)),
a_hash_including('id' => global_id_of(compliance_framework_2))
)
end
context 'when querying multiple namespaces' do
let(:group) { create(:group) }
let(:multiple_namespace_query) do
<<~QUERY
query {
a: namespace(fullPath: "#{namespace.full_path}") {
complianceFrameworks { nodes { id name } }
}
b: namespace(fullPath: "#{group.full_path}") {
complianceFrameworks { nodes { id name } }
}
}
QUERY
end
before do
create(:compliance_framework, namespace: group)
group.add_owner(current_user)
end
it 'avoids N+1 queries' do
query_count = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }.count
expect { post_graphql(multiple_namespace_query, current_user: current_user) }.not_to exceed_query_limit(query_count + 4)
end
it 'responds with the expected list of compliance frameworks' do
post_graphql(multiple_namespace_query, current_user: current_user)
expect(graphql_data_at(:a, :complianceFrameworks, :nodes).map { |f| f['name'] }).to contain_exactly('Test1', 'Test2')
expect(graphql_data_at(:b, :complianceFrameworks, :nodes).map { |f| f['name'] }).to contain_exactly('GDPR')
end
end
context 'feature is disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
end
it 'responds with error when querying a compliance framework' do
post_graphql(query, current_user: current_user)
expect(graphql_errors).to contain_exactly(include('message' => "Field 'complianceFrameworks' doesn't exist on type 'Namespace'"))
end
end
end
context 'when authenticated as a different user' do
let(:current_user) { build(:user) }
it "does not return the namespaces compliance frameworks" do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(*path)).to be_nil
end
end
context 'when not authenticated' do
it "does not return the namespace's compliance frameworks" do
post_graphql(query)
expect(graphql_data_at(*path)).to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting compliance frameworks for a collection of projects' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project_members) { create_list(:project_member, 2, :maintainer, user: current_user) }
let_it_be(:project_ids) { project_members.map { |p| global_id_of(p.source) } }
let(:query) do
graphql_query_for(
:projects, { ids: project_ids }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
before_all do
project_members.map(&:project).each do |project|
project.compliance_framework_setting = create(:compliance_framework_project_setting)
end
end
context 'querying a single project' do
let(:single_project_query) do
graphql_query_for(
:projects, { ids: [project_ids.first] }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
it 'avoids N+1 queries', :use_sql_query_cache do
query_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, current_user: current_user) }.count
expect { post_graphql(single_project_query, current_user: current_user) }.not_to exceed_all_query_limit(query_count)
end
it 'contains the expected compliance framework' do
post_graphql(single_project_query, current_user: current_user)
expect(graphql_data_at(:projects, :nodes, 0, :complianceFrameworks, :nodes, 0, :name)).to eq 'GDPR'
end
end
context 'projects can have a compliance framework' do
let_it_be(:compliance_projects) { create_list(:project, 2, :with_compliance_framework) }
let_it_be(:non_compliance_project) { create(:project) }
let(:projects) { compliance_projects + [non_compliance_project] }
let(:project_ids) { projects.map { |p| global_id_of(p) } }
let(:query) do
graphql_query_for(
:projects, { ids: project_ids }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
before do
projects.each { |p| create(:project_member, :maintainer, source: p, user: current_user)}
post_graphql(query, current_user: current_user)
end
subject { graphql_data_at(:projects, :nodes).map { |p| p.dig('complianceFrameworks', 'nodes') } }
it 'contains the correct number of compliance frameworks' do
expect(subject[0].size).to eq 0
expect(subject[1].size).to eq 1
expect(subject[2].size).to eq 1
end
end
context 'projects that share the same compliance framework' do
let_it_be(:framework) { create(:compliance_framework) }
let_it_be(:project_1) { create(:project, compliance_framework_setting: create(:compliance_framework_project_setting, compliance_management_framework: framework )) }
let_it_be(:project_2) { create(:project, compliance_framework_setting: create(:compliance_framework_project_setting, compliance_management_framework: framework )) }
let(:projects) { [project_1, project_2] }
let(:project_ids) { projects.map { |p| global_id_of(p) } }
let(:query) do
graphql_query_for(
:projects, { ids: project_ids }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
before do
projects.each { |p| create(:project_member, :maintainer, source: p, user: current_user)}
post_graphql(query, current_user: current_user)
end
subject { graphql_data_at(:projects, :nodes).map { |p| p.dig('complianceFrameworks', 'nodes', 0, 'id') } }
it 'shares the same compliance framework id' do
expect(subject[0]).to eq(subject[1])
end
end
end
......@@ -542,6 +542,7 @@ project:
- daily_build_group_report_results
- jira_imports
- compliance_framework_setting
- compliance_management_frameworks
- metrics_users_starred_dashboards
- alert_management_alerts
- repository_storage_moves
......
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