Commit 54e44bdd authored by Philip Cunningham's avatar Philip Cunningham Committed by Luke Duncalfe

Filter DastSiteValidations by normalizedTargetUrl

Allow filtering in GraphQL.
parent 4f2c23a7
...@@ -5328,6 +5328,26 @@ type DastSiteValidation { ...@@ -5328,6 +5328,26 @@ type DastSiteValidation {
status: DastSiteProfileValidationStatusEnum! status: DastSiteProfileValidationStatusEnum!
} }
"""
The connection type for DastSiteValidation.
"""
type DastSiteValidationConnection {
"""
A list of edges.
"""
edges: [DastSiteValidationEdge]
"""
A list of nodes.
"""
nodes: [DastSiteValidation]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
""" """
Autogenerated input type of DastSiteValidationCreate Autogenerated input type of DastSiteValidationCreate
""" """
...@@ -5383,6 +5403,21 @@ type DastSiteValidationCreatePayload { ...@@ -5383,6 +5403,21 @@ type DastSiteValidationCreatePayload {
status: DastSiteProfileValidationStatusEnum status: DastSiteProfileValidationStatusEnum
} }
"""
An edge in a connection.
"""
type DastSiteValidationEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: DastSiteValidation
}
""" """
Identifier of DastSiteValidation Identifier of DastSiteValidation
""" """
...@@ -15856,15 +15891,52 @@ type Project { ...@@ -15856,15 +15891,52 @@ type Project {
): DastSiteProfileConnection ): DastSiteProfileConnection
""" """
DAST Site Validation associated with the project DAST Site Validation associated with the project. Will always return `null` if
`security_on_demand_scans_site_validation` is disabled
""" """
dastSiteValidation( dastSiteValidation(
""" """
target URL of the DAST Site Validation Normalized URL of the target to be scanned
"""
normalizedTargetUrls: [String!]
"""
URL of the target to be scanned
""" """
targetUrl: String! targetUrl: String!
): DastSiteValidation ): DastSiteValidation
"""
DAST Site Validations associated with the project. Will always return no nodes
if `security_on_demand_scans_site_validation` is disabled
"""
dastSiteValidations(
"""
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
"""
Normalized URL of the target to be scanned
"""
normalizedTargetUrls: [String!]
): DastSiteValidationConnection
""" """
Short description of the project Short description of the project
""" """
......
...@@ -14617,6 +14617,73 @@ ...@@ -14617,6 +14617,73 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "DastSiteValidationConnection",
"description": "The connection type for DastSiteValidation.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DastSiteValidationEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DastSiteValidation",
"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", "kind": "INPUT_OBJECT",
"name": "DastSiteValidationCreateInput", "name": "DastSiteValidationCreateInput",
...@@ -14771,6 +14838,51 @@ ...@@ -14771,6 +14838,51 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "DastSiteValidationEdge",
"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": "DastSiteValidation",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "SCALAR", "kind": "SCALAR",
"name": "DastSiteValidationID", "name": "DastSiteValidationID",
...@@ -46894,11 +47006,29 @@ ...@@ -46894,11 +47006,29 @@
}, },
{ {
"name": "dastSiteValidation", "name": "dastSiteValidation",
"description": "DAST Site Validation associated with the project", "description": "DAST Site Validation associated with the project. Will always return `null` if `security_on_demand_scans_site_validation` is disabled",
"args": [ "args": [
{
"name": "normalizedTargetUrls",
"description": "Normalized URL of the target to be scanned",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "targetUrl", "name": "targetUrl",
"description": "target URL of the DAST Site Validation", "description": "URL of the target to be scanned",
"type": { "type": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
...@@ -46919,6 +47049,77 @@ ...@@ -46919,6 +47049,77 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "dastSiteValidations",
"description": "DAST Site Validations associated with the project. Will always return no nodes if `security_on_demand_scans_site_validation` is disabled",
"args": [
{
"name": "normalizedTargetUrls",
"description": "Normalized URL of the target to be scanned",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"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": "DastSiteValidationConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "description", "name": "description",
"description": "Short description of the project", "description": "Short description of the project",
...@@ -2415,7 +2415,8 @@ Autogenerated return type of PipelineRetry. ...@@ -2415,7 +2415,8 @@ Autogenerated return type of PipelineRetry.
| `dastScannerProfiles` | DastScannerProfileConnection | The DAST scanner profiles associated with the project | | `dastScannerProfiles` | DastScannerProfileConnection | The DAST scanner profiles associated with the project |
| `dastSiteProfile` | DastSiteProfile | DAST Site Profile associated with the project | | `dastSiteProfile` | DastSiteProfile | DAST Site Profile associated with the project |
| `dastSiteProfiles` | DastSiteProfileConnection | DAST Site Profiles associated with the project | | `dastSiteProfiles` | DastSiteProfileConnection | DAST Site Profiles associated with the project |
| `dastSiteValidation` | DastSiteValidation | DAST Site Validation associated with the project | | `dastSiteValidation` | DastSiteValidation | DAST Site Validation associated with the project. Will always return `null` if `security_on_demand_scans_site_validation` is disabled |
| `dastSiteValidations` | DastSiteValidationConnection | DAST Site Validations associated with the project. Will always return no nodes if `security_on_demand_scans_site_validation` is disabled |
| `description` | String | Short description of the project | | `description` | String | Short description of the project |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `environment` | Environment | A single environment of the project | | `environment` | Environment | A single environment of the project |
......
...@@ -80,10 +80,16 @@ module EE ...@@ -80,10 +80,16 @@ module EE
field :dast_site_validation, field :dast_site_validation,
::Types::DastSiteValidationType, ::Types::DastSiteValidationType,
null: true, null: true,
resolver: ::Resolvers::DastSiteValidationResolver.single,
description: 'DAST Site Validation associated with the project. Will always return `null` ' \
'if `security_on_demand_scans_site_validation` is disabled'
field :dast_site_validations,
::Types::DastSiteValidationType.connection_type,
null: true,
resolver: ::Resolvers::DastSiteValidationResolver, resolver: ::Resolvers::DastSiteValidationResolver,
description: 'DAST Site Validation associated with the project' do description: 'DAST Site Validations associated with the project. Will always return no nodes ' \
argument :target_url, GraphQL::STRING_TYPE, required: true, description: 'target URL of the DAST Site Validation' 'if `security_on_demand_scans_site_validation` is disabled'
end
field :cluster_agent, field :cluster_agent,
::Types::Clusters::AgentType, ::Types::Clusters::AgentType,
......
...@@ -6,13 +6,30 @@ module Resolvers ...@@ -6,13 +6,30 @@ module Resolvers
type Types::DastSiteValidationType.connection_type, null: true type Types::DastSiteValidationType.connection_type, null: true
argument :normalized_target_urls, [GraphQL::STRING_TYPE], required: false,
description: 'Normalized URL of the target to be scanned'
when_single do
argument :target_url, GraphQL::STRING_TYPE, required: true,
description: 'URL of the target to be scanned'
end
def resolve(**args) def resolve(**args)
unless ::Feature.enabled?(:security_on_demand_scans_site_validation, project) return DastSiteValidation.none unless allowed?
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled'
DastSiteValidationsFinder.new(project_id: project.id, url_base: url_base(args)).execute
end end
url_base = DastSiteValidation.get_normalized_url_base(args[:target_url]) private
DastSiteValidationsFinder.new(project_id: project.id, url_base: url_base).execute.first
def allowed?
::Feature.enabled?(:security_on_demand_scans_site_validation, project)
end
def url_base(args)
return DastSiteValidation.get_normalized_url_base(args[:target_url]) if args[:target_url]
args[:normalized_target_urls]
end end
end end
end end
...@@ -8,10 +8,12 @@ RSpec.describe Resolvers::DastSiteValidationResolver do ...@@ -8,10 +8,12 @@ RSpec.describe Resolvers::DastSiteValidationResolver do
let_it_be(:target_url) { generate(:url) } let_it_be(:target_url) { generate(:url) }
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:dast_site_token) { create(:dast_site_token, project: project, url: target_url) } let_it_be(:dast_site_token1) { create(:dast_site_token, project: project, url: target_url) }
let_it_be(:dast_site_validation) { create(:dast_site_validation, dast_site_token: dast_site_token) } let_it_be(:dast_site_validation1) { create(:dast_site_validation, dast_site_token: dast_site_token1) }
let_it_be(:dast_site_token2) { create(:dast_site_token, project: project, url: generate(:url)) }
subject { sync(resolve_dast_site_validations(target_url: target_url)) } let_it_be(:dast_site_validation2) { create(:dast_site_validation, dast_site_token: dast_site_token2) }
let_it_be(:dast_site_token3) { create(:dast_site_token, project: project, url: generate(:url)) }
let_it_be(:dast_site_validation3) { create(:dast_site_validation, dast_site_token: dast_site_token3) }
before do before do
project.add_maintainer(current_user) project.add_maintainer(current_user)
...@@ -21,13 +23,42 @@ RSpec.describe Resolvers::DastSiteValidationResolver do ...@@ -21,13 +23,42 @@ RSpec.describe Resolvers::DastSiteValidationResolver do
expect(described_class).to have_nullable_graphql_type(Types::DastSiteValidationType.connection_type) expect(described_class).to have_nullable_graphql_type(Types::DastSiteValidationType.connection_type)
end end
it 'returns DAST site validation' do subject { sync(resolver) }
is_expected.to eq(dast_site_validation)
context 'when resolving a single DAST site validation' do
let(:resolver) { dast_site_validations(target_url: target_url) }
it { is_expected.to contain_exactly(dast_site_validation1) }
end
context 'when resolving multiple DAST site validations' do
let(:args) { {} }
let(:resolver) { dast_site_validations(args) }
it { is_expected.to contain_exactly(dast_site_validation3, dast_site_validation2, dast_site_validation1) }
context 'when multiple normalized_target_urls are specified' do
let(:args) { { normalized_target_urls: [dast_site_validation1.url_base, dast_site_validation3.url_base] } }
it { is_expected.to contain_exactly(dast_site_validation3, dast_site_validation1) }
end
context 'when one normalized_target_urls is specified' do
let(:args) { { normalized_target_urls: [dast_site_validation2.url_base] } }
it { is_expected.to contain_exactly(dast_site_validation2) }
end
context 'when an empty array is specified' do
let(:args) { { normalized_target_urls: [] } }
it { is_expected.to be_empty }
end
end end
private private
def resolve_dast_site_validations(args = {}, context = { current_user: current_user }) def dast_site_validations(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context) resolve(described_class, obj: project, args: args, ctx: context)
end end
end end
...@@ -65,9 +65,8 @@ RSpec.describe 'Query.project(fullPath).dastSiteValidation' do ...@@ -65,9 +65,8 @@ RSpec.describe 'Query.project(fullPath).dastSiteValidation' do
stub_feature_flags(security_on_demand_scans_site_validation: false) stub_feature_flags(security_on_demand_scans_site_validation: false)
end end
it 'returns populated edges array' do it 'returns a null dast_site_validation' do
subject expect(dast_site_validation_response).to be_nil
expect(graphql_errors.first['message']).to eq("Feature disabled")
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).dastSiteValidations' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:dast_site_token) { create(:dast_site_token, project: project, url: generate(:url)) }
let_it_be(:dast_site_validation1) { create(:dast_site_validation, dast_site_token: dast_site_token) }
let_it_be(:dast_site_validation2) { create(:dast_site_validation, dast_site_token: dast_site_token) }
let_it_be(:dast_site_validation3) { create(:dast_site_validation, dast_site_token: dast_site_token) }
let_it_be(:dast_site_validation4) { create(:dast_site_validation, dast_site_token: dast_site_token) }
let_it_be(:current_user) { create(:user) }
let(:query) do
fields = all_graphql_fields_for('DastSiteValidation')
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('dastSiteValidations', 'first: 3', "edges { node { #{fields} } }")
)
end
let(:project_response) { subject['project'] }
let(:dast_site_validations_response) { project_response&.[]('dastSiteValidations') }
let(:edges) { dast_site_validations_response&.[]('edges') }
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_validations' do
it 'returns an empty edges array' do
project.add_guest(current_user)
expect(edges).to be_empty
end
end
context 'when a user has access to dast_site_validations' do
before do
project.add_developer(current_user)
end
let(:expected_results) do
[
dast_site_validation4,
dast_site_validation3,
dast_site_validation2
].map { |validation| global_id_of(validation)}
end
it 'returns a populated edges array containing the correct dast_site_validations' do
results = edges.map { |edge| edge['node']['id'] }
expect(results).to eq(expected_results)
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