Commit 2010806d authored by James Fargher's avatar James Fargher

Merge branch 'dast-site-profiles-graphql-query-231270' into 'master'

Add ability to query DastSiteProfiles via GraphQL

See merge request gitlab-org/gitlab!38014
parents 2a334447 c66cc7d6
......@@ -2314,6 +2314,56 @@ type DastScannerProfileCreatePayload {
id: ID
}
"""
Represents a DAST Site Profile.
"""
type DastSiteProfile {
"""
ID of the site profile
"""
id: ID!
"""
The name of the site profile
"""
profileName: String
"""
The URL of the target to be scanned
"""
targetUrl: String
"""
Permissions for the current user on the resource
"""
userPermissions: DastSiteProfilePermissions!
"""
The current validation status of the site profile
"""
validationStatus: DastSiteProfileValidationStatusEnum
}
"""
The connection type for DastSiteProfile.
"""
type DastSiteProfileConnection {
"""
A list of edges.
"""
edges: [DastSiteProfileEdge]
"""
A list of nodes.
"""
nodes: [DastSiteProfile]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
Autogenerated input type of DastSiteProfileCreate
"""
......@@ -2394,11 +2444,58 @@ type DastSiteProfileDeletePayload {
errors: [String!]!
}
"""
An edge in a connection.
"""
type DastSiteProfileEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: DastSiteProfile
}
"""
Identifier of DastSiteProfile
"""
scalar DastSiteProfileID
"""
Check permissions for the current user on site profile
"""
type DastSiteProfilePermissions {
"""
Indicates the user can perform `create_on_demand_dast_scan` on this resource
"""
createOnDemandDastScan: Boolean!
}
enum DastSiteProfileValidationStatusEnum {
"""
Site validation process finished but failed
"""
FAILED_VALIDATION
"""
Site validation process is in progress
"""
INPROGRESS_VALIDATION
"""
Site validation process finished successfully
"""
PASSED_VALIDATION
"""
Site validation process has not started
"""
PENDING_VALIDATION
}
"""
Autogenerated input type of DeleteAnnotation
"""
......@@ -9575,6 +9672,31 @@ type Project {
"""
createdAt: Time
"""
DAST Site Profiles associated with the project
"""
dastSiteProfiles(
"""
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
): DastSiteProfileConnection
"""
Short description of the project
"""
......
......@@ -6214,6 +6214,164 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastSiteProfile",
"description": "Represents a DAST Site Profile.",
"fields": [
{
"name": "id",
"description": "ID of the site profile",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "profileName",
"description": "The name of the site profile",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "targetUrl",
"description": "The URL of the target to be scanned",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "userPermissions",
"description": "Permissions for the current user on the resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DastSiteProfilePermissions",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "validationStatus",
"description": "The current validation status of the site profile",
"args": [
],
"type": {
"kind": "ENUM",
"name": "DastSiteProfileValidationStatusEnum",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastSiteProfileConnection",
"description": "The connection type for DastSiteProfile.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DastSiteProfileEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DastSiteProfile",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DastSiteProfileCreateInput",
......@@ -6442,6 +6600,51 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastSiteProfileEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DastSiteProfile",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "DastSiteProfileID",
......@@ -6452,6 +6655,72 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastSiteProfilePermissions",
"description": "Check permissions for the current user on site profile",
"fields": [
{
"name": "createOnDemandDastScan",
"description": "Indicates the user can perform `create_on_demand_dast_scan` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "DastSiteProfileValidationStatusEnum",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "PENDING_VALIDATION",
"description": "Site validation process has not started",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "INPROGRESS_VALIDATION",
"description": "Site validation process is in progress",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PASSED_VALIDATION",
"description": "Site validation process finished successfully",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "FAILED_VALIDATION",
"description": "Site validation process finished but failed",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DeleteAnnotationInput",
......@@ -28694,6 +28963,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dastSiteProfiles",
"description": "DAST Site Profiles associated with the project",
"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": "DastSiteProfileConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Short description of the project",
......@@ -402,6 +402,18 @@ Autogenerated return type of DastScannerProfileCreate
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `id` | ID | ID of the scanner profile. |
## DastSiteProfile
Represents a DAST Site Profile.
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | ID of the site profile |
| `profileName` | String | The name of the site profile |
| `targetUrl` | String | The URL of the target to be scanned |
| `userPermissions` | DastSiteProfilePermissions! | Permissions for the current user on the resource |
| `validationStatus` | DastSiteProfileValidationStatusEnum | The current validation status of the site profile |
## DastSiteProfileCreatePayload
Autogenerated return type of DastSiteProfileCreate
......@@ -421,6 +433,14 @@ Autogenerated return type of DastSiteProfileDelete
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## DastSiteProfilePermissions
Check permissions for the current user on site profile
| Name | Type | Description |
| --- | ---- | ---------- |
| `createOnDemandDastScan` | Boolean! | Indicates the user can perform `create_on_demand_dast_scan` on this resource |
## DeleteAnnotationPayload
Autogenerated return type of DeleteAnnotation
......
......@@ -60,6 +60,12 @@ module EE
description: 'Find iterations',
resolver: ::Resolvers::IterationsResolver
field :dast_site_profiles,
::Types::DastSiteProfileType.connection_type,
null: true,
description: 'DAST Site Profiles associated with the project',
resolve: -> (obj, _args, _ctx) { obj.dast_site_profiles.with_dast_site }
def self.requirements_available?(project, user)
::Feature.enabled?(:requirements_management, project, default_enabled: true) && Ability.allowed?(user, :read_requirement, project)
end
......
# frozen_string_literal: true
module Types
class DastSiteProfileType < BaseObject
graphql_name 'DastSiteProfile'
description 'Represents a DAST Site Profile.'
authorize :create_on_demand_dast_scan
expose_permissions Types::PermissionTypes::DastSiteProfile
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the site profile'
field :profile_name, GraphQL::STRING_TYPE, null: true,
description: 'The name of the site profile',
resolve: -> (obj, _args, _ctx) { obj.name }
field :target_url, GraphQL::STRING_TYPE, null: true,
description: 'The URL of the target to be scanned',
resolve: -> (obj, _args, _ctx) { obj.dast_site.url }
field :validation_status, Types::DastSiteProfileValidationStatusEnum, null: true,
description: 'The current validation status of the site profile',
resolve: -> (_obj, _args, _ctx) { Types::DastSiteProfileValidationStatusEnum.enum['pending_validation'] }
end
end
# frozen_string_literal: true
module Types
class DastSiteProfileValidationStatusEnum < BaseEnum
value 'PENDING_VALIDATION', description: 'Site validation process has not started'
value 'INPROGRESS_VALIDATION', description: 'Site validation process is in progress'
value 'PASSED_VALIDATION', description: 'Site validation process finished successfully'
value 'FAILED_VALIDATION', description: 'Site validation process finished but failed'
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class DastSiteProfile < BasePermissionType
graphql_name 'DastSiteProfilePermissions'
description 'Check permissions for the current user on site profile'
abilities :create_on_demand_dast_scan
end
end
end
# frozen_string_literal: true
class DastSiteProfilePolicy < BasePolicy
delegate { @subject.project }
end
......@@ -239,7 +239,10 @@ module EE
enable :read_vulnerability_scanner
end
rule { on_demand_scans_enabled & can?(:developer_access) }.enable :read_on_demand_scans
rule { on_demand_scans_enabled & can?(:developer_access) }.policy do
enable :read_on_demand_scans
enable :create_on_demand_dast_scan
end
rule { can?(:read_merge_request) & can?(:read_pipeline) }.enable :read_merge_train
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['DastSiteProfile'] do
let_it_be(:dast_site_profile) { create(:dast_site_profile) }
let_it_be(:project) { dast_site_profile.project }
let_it_be(:user) { create(:user) }
let_it_be(:fields) { %i[id profileName targetUrl validationStatus userPermissions] }
subject do
GitlabSchema.execute(
query,
context: {
current_user: user
},
variables: {
fullPath: project.full_path
}
).as_json
end
before do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class.graphql_name).to eq('DastSiteProfile') }
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::DastSiteProfile) }
it { expect(described_class).to have_graphql_fields(fields) }
describe 'dast_site_profiles' do
before do
project.add_developer(user)
end
let(:query) do
%(
query project($fullPath: ID!) {
project(fullPath: $fullPath) {
dastSiteProfiles(first: 1) {
nodes {
id
profileName
targetUrl
validationStatus
}
}
}
}
)
end
let(:first_dast_site_profile) do
subject.dig('data', 'project', 'dastSiteProfiles', 'nodes', 0)
end
describe 'id field' do
it 'is a global id' do
expect(first_dast_site_profile['id']).to eq(dast_site_profile.to_global_id.to_s)
end
end
describe 'profile_name field' do
it 'is the name' do
expect(first_dast_site_profile['profileName']).to eq(dast_site_profile.name)
end
end
describe 'target_url field' do
it 'is the url of the associated dast_site' do
expect(first_dast_site_profile['targetUrl']).to eq(dast_site_profile.dast_site.url)
end
end
describe 'validation_status field' do
it 'is a placeholder validation status' do
expect(first_dast_site_profile['validationStatus']).to eq('PENDING_VALIDATION')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DastSiteProfilePolicy do
describe 'create_on_demand_dast_scan' do
let(:dast_site_profile) { create(:dast_site_profile) }
let(:project) { dast_site_profile.project }
let(:user) { create(:user) }
subject { described_class.new(user, dast_site_profile) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when a user does not have access to the project' do
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user does not have access to dast_site_profiles' do
before do
project.add_guest(user)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when a user has access dast_site_profiles' do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:create_on_demand_dast_scan) }
context 'when on demand scan feature flag is disabled' do
before do
stub_feature_flags(security_on_demand_scans_feature_flag: false)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
context 'when on demand scan licensed feature is not available' do
before do
stub_licensed_features(security_on_demand_scans: false)
end
it { is_expected.to be_disallowed(:create_on_demand_dast_scan) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).dastSiteProfiles' do
include GraphqlHelpers
let_it_be(:dast_site_profile) { create(:dast_site_profile) }
let_it_be(:project) { dast_site_profile.project }
let_it_be(:current_user) { create(:user) }
let(:query) do
%(
query project($fullPath: ID!) {
project(fullPath: $fullPath) {
dastSiteProfiles(first: 3) {
pageInfo {
hasNextPage
}
nodes {
id
profileName
targetUrl
validationStatus
}
}
}
}
)
end
let(:project_response) { subject.dig('project') }
let(:dast_site_profiles_response) { project_response.dig('dastSiteProfiles') }
let(:first_dast_site_profile_response) { dast_site_profiles_response.dig('nodes', 0) }
subject do
post_graphql(
query,
current_user: current_user,
variables: {
fullPath: project.full_path
}
)
graphql_data
end
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when a user does not have access to the project' do
it 'returns a null project' do
expect(project_response).to be_nil
end
end
context 'when a user does not have access to dast_site_profiles' do
it 'returns an empty edges array' do
project.add_guest(current_user)
expect(dast_site_profiles_response['nodes']).to be_empty
end
end
context 'when a user has access dast_site_profiles' do
before do
project.add_developer(current_user)
end
it 'returns populated edges array' do
expect(dast_site_profiles_response['nodes']).not_to be_empty
end
it 'returns a populated edges array containing a dast_site_profile associated with the project' do
expect(first_dast_site_profile_response['id']).to eq(dast_site_profile.to_global_id.to_s)
end
it 'eager loads the dast site' do
control = ActiveRecord::QueryRecorder.new do
post_graphql(
query,
current_user: current_user,
variables: {
fullPath: project.full_path
}
)
end
create_list(:dast_site_profile, 2, project: project)
expect { subject }.not_to exceed_query_limit(control)
end
context 'when there are fewer dast_site_profiles than the page limit' do
it 'indicates there are no more pages available' do
expect(dast_site_profiles_response.dig('pageInfo', 'hasNextPage')).to be(false)
end
end
context 'when there are more dast_site_profiles than the page limit' do
it 'indicates there are more pages available' do
create_list(:dast_site_profile, 5, project: project)
expect(dast_site_profiles_response.dig('pageInfo', 'hasNextPage')).to be(true)
end
end
context 'when on demand scan feature flag is disabled' do
it 'returns an empty edges array' do
stub_feature_flags(security_on_demand_scans_feature_flag: false)
expect(dast_site_profiles_response['nodes']).to be_empty
end
end
context 'when on demand scan licensed feature is not available' do
it 'returns an empty edges array' do
stub_licensed_features(security_on_demand_scans: false)
expect(dast_site_profiles_response['nodes']).to be_empty
end
end
end
end
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