Commit efd89ac2 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 779d1caa cdd241d2
......@@ -19,9 +19,14 @@ module Types
field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.'
field :tags_count, GraphQL::INT_TYPE, null: false, description: 'Number of tags associated with this image.'
field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete the container repository.'
field :project, Types::ProjectType, null: false, description: 'Project of the container registry'
def can_delete
Ability.allowed?(current_user, :update_container_image, object)
end
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
end
end
---
title: Add Project to ContainerRepository GraphQL type
merge_request: 49019
author:
type: added
......@@ -3510,6 +3510,11 @@ type ContainerRepository {
"""
path: String!
"""
Project of the container registry
"""
project: Project!
"""
Status of the container repository.
"""
......@@ -3615,6 +3620,11 @@ type ContainerRepositoryDetails {
"""
path: String!
"""
Project of the container registry
"""
project: Project!
"""
Status of the container repository.
"""
......@@ -5342,6 +5352,11 @@ enum DastSiteProfileValidationStatusEnum {
"""
INPROGRESS_VALIDATION
"""
No site validation exists
"""
NONE
"""
Site validation process finished successfully
"""
......@@ -5423,6 +5438,26 @@ type DastSiteValidation {
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
"""
......@@ -5478,6 +5513,21 @@ type DastSiteValidationCreatePayload {
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
"""
......@@ -15951,15 +16001,52 @@ type Project {
): 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(
"""
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!
): 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
"""
......
......@@ -9598,6 +9598,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "project",
"description": "Project of the container registry",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Project",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "status",
"description": "Status of the container repository.",
......@@ -9899,6 +9917,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "project",
"description": "Project of the container registry",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Project",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "status",
"description": "Status of the container repository.",
......@@ -14715,6 +14751,12 @@
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "NONE",
"description": "No site validation exists",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PENDING_VALIDATION",
"description": "Site validation process has not started",
......@@ -14941,6 +14983,73 @@
"enumValues": 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",
"name": "DastSiteValidationCreateInput",
......@@ -15095,6 +15204,51 @@
"enumValues": 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",
"name": "DastSiteValidationID",
......@@ -47218,11 +47372,29 @@
},
{
"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": [
{
"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",
"description": "target URL of the DAST Site Validation",
"description": "URL of the target to be scanned",
"type": {
"kind": "NON_NULL",
"name": null,
......@@ -47243,6 +47415,77 @@
"isDeprecated": false,
"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",
"description": "Short description of the project",
......@@ -592,6 +592,7 @@ A container repository.
| `location` | String! | URL of the container repository. |
| `name` | String! | Name of the container repository. |
| `path` | String! | Path of the container repository. |
| `project` | Project! | Project of the container registry |
| `status` | ContainerRepositoryStatus | Status of the container repository. |
| `tagsCount` | Int! | Number of tags associated with this image. |
| `updatedAt` | Time! | Timestamp when the container repository was updated. |
......@@ -610,6 +611,7 @@ Details of a container repository.
| `location` | String! | URL of the container repository. |
| `name` | String! | Name of the container repository. |
| `path` | String! | Path of the container repository. |
| `project` | Project! | Project of the container registry |
| `status` | ContainerRepositoryStatus | Status of the container repository. |
| `tags` | ContainerRepositoryTagConnection | Tags of the container repository |
| `tagsCount` | Int! | Number of tags associated with this image. |
......@@ -2454,7 +2456,8 @@ Autogenerated return type of PipelineRetry.
| `dastScannerProfiles` | DastScannerProfileConnection | The DAST scanner profiles associated with the project |
| `dastSiteProfile` | DastSiteProfile | DAST Site Profile 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 |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `environment` | Environment | A single environment of the project |
......@@ -4055,6 +4058,7 @@ Status of a container repository.
| ----- | ----------- |
| `FAILED_VALIDATION` | Site validation process finished but failed |
| `INPROGRESS_VALIDATION` | Site validation process is in progress |
| `NONE` | No site validation exists |
| `PASSED_VALIDATION` | Site validation process finished successfully |
| `PENDING_VALIDATION` | Site validation process has not started |
......
......@@ -80,10 +80,16 @@ module EE
field :dast_site_validation,
::Types::DastSiteValidationType,
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,
description: 'DAST Site Validation associated with the project' do
argument :target_url, GraphQL::STRING_TYPE, required: true, description: 'target URL of the DAST Site Validation'
end
description: 'DAST Site Validations associated with the project. Will always return no nodes ' \
'if `security_on_demand_scans_site_validation` is disabled'
field :cluster_agent,
::Types::Clusters::AgentType,
......
......@@ -6,13 +6,30 @@ module Resolvers
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)
unless ::Feature.enabled?(:security_on_demand_scans_site_validation, project)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled'
end
return DastSiteValidation.none unless allowed?
DastSiteValidationsFinder.new(project_id: project.id, url_base: url_base(args)).execute
end
private
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]
url_base = DastSiteValidation.get_normalized_url_base(args[:target_url])
DastSiteValidationsFinder.new(project_id: project.id, url_base: url_base).execute.first
args[:normalized_target_urls]
end
end
end
......@@ -2,6 +2,7 @@
module Types
class DastSiteProfileValidationStatusEnum < BaseEnum
value 'NONE', value: DastSiteValidation::NONE_STATE, description: 'No site validation exists'
value 'PENDING_VALIDATION', value: 'pending', description: 'Site validation process has not started'
value 'INPROGRESS_VALIDATION', value: 'inprogress', description: 'Site validation process is in progress'
value 'PASSED_VALIDATION', value: 'passed', description: 'Site validation process finished successfully'
......
......@@ -15,7 +15,7 @@ class DastSiteProfile < ApplicationRecord
delegate :dast_site_validation, to: :dast_site, allow_nil: true
def status
return DastSiteValidation::INITIAL_STATE unless dast_site_validation
return DastSiteValidation::NONE_STATE unless dast_site_validation
dast_site_validation.state
end
......
......@@ -27,6 +27,7 @@ class DastSiteValidation < ApplicationRecord
"#{url_base}/#{url_path}"
end
NONE_STATE = 'none'
INITIAL_STATE = 'pending'
state_machine :state, initial: INITIAL_STATE.to_sym do
......
---
title: Return NONE for GraphQL DastSiteValidation type status when there is no DAST site validation
merge_request: 48751
author:
type: changed
......@@ -8,10 +8,12 @@ RSpec.describe Resolvers::DastSiteValidationResolver do
let_it_be(:target_url) { generate(:url) }
let_it_be(:current_user) { create(:user) }
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_validation) { create(:dast_site_validation, dast_site_token: dast_site_token) }
subject { sync(resolve_dast_site_validations(target_url: target_url)) }
let_it_be(:dast_site_token1) { create(:dast_site_token, project: project, url: target_url) }
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)) }
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
project.add_maintainer(current_user)
......@@ -21,13 +23,42 @@ RSpec.describe Resolvers::DastSiteValidationResolver do
expect(described_class).to have_nullable_graphql_type(Types::DastSiteValidationType.connection_type)
end
it 'returns DAST site validation' do
is_expected.to eq(dast_site_validation)
subject { sync(resolver) }
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
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)
end
end
......@@ -74,12 +74,12 @@ RSpec.describe DastSiteProfile, type: :model do
describe '#status' do
context 'when dast_site_validation association does not exist' do
it 'is pending' do
it 'is none' do
subject.dast_site.update!(dast_site_validation_id: nil)
aggregate_failures do
expect(subject.dast_site_validation).to be_nil
expect(subject.status).to eq('pending')
expect(subject.status).to eq('none')
end
end
end
......
......@@ -93,10 +93,10 @@ RSpec.describe 'Query.project(fullPath).dastSiteProfile' do
end
context 'when there is no associated dast_site_validation' do
it 'returns a pending validation status' do
it 'returns a none validation status' do
dast_site_profile.dast_site_validation.destroy!
expect(dast_site_profile_response['validationStatus']).to eq('PENDING_VALIDATION')
expect(dast_site_profile_response['validationStatus']).to eq('NONE')
end
end
end
......
......@@ -65,9 +65,8 @@ RSpec.describe 'Query.project(fullPath).dastSiteValidation' do
stub_feature_flags(security_on_demand_scans_site_validation: false)
end
it 'returns populated edges array' do
subject
expect(graphql_errors.first['message']).to eq("Feature disabled")
it 'returns a null dast_site_validation' do
expect(dast_site_validation_response).to be_nil
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
{
"type": "object",
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus"],
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus", "project"],
"properties": {
"id": {
"type": "string"
......@@ -35,6 +35,9 @@
"expirationPolicyCleanupStatus": {
"type": "string",
"enum": ["UNSCHEDULED", "SCHEDULED", "UNFINISHED", "ONGOING"]
},
"project": {
"type": "object"
}
}
}
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status tags]
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status tags project]
it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') }
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepository'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status]
fields = %i[id name path location created_at updated_at expiration_policy_started_at status tags_count can_delete expiration_policy_cleanup_status project]
it { expect(described_class.graphql_name).to eq('ContainerRepository') }
......
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