diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d6fe56bea735d4a2bc14395768ce6500d3efaf6b..da2133e1f37b4c5721321b67efbdc7c942144419 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -8565,6 +8565,7 @@ Represents a DAST Profile. | Name | Type | Description | | ---- | ---- | ----------- | | `branch` | [`DastProfileBranch`](#dastprofilebranch) | The associated branch. | +| `dastProfileSchedule` | [`DastProfileSchedule`](#dastprofileschedule) | Associated profile schedule. Will always return `null` if `dast_on_demand_scans_scheduler` feature flag is disabled. | | `dastScannerProfile` | [`DastScannerProfile`](#dastscannerprofile) | The associated scanner profile. | | `dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | The associated site profile. | | `description` | [`String`](#string) | The description of the scan. | @@ -8583,6 +8584,32 @@ Represents a DAST Profile Branch. | `exists` | [`Boolean`](#boolean) | Indicates whether or not the branch exists. | | `name` | [`String`](#string) | The name of the branch. | +### `DastProfileCadence` + +Represents DAST Profile Cadence. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `duration` | [`Int`](#int) | Duration of the DAST profile cadence. | +| `unit` | [`DastProfileCadenceUnit`](#dastprofilecadenceunit) | Unit for the duration of DAST profile cadence. | + +### `DastProfileSchedule` + +Represents a DAST profile schedule. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `active` | [`Boolean`](#boolean) | Status of the DAST profile schedule. | +| `cadence` | [`DastProfileCadence`](#dastprofilecadence) | Cadence of the DAST profile schedule. | +| `id` | [`DastProfileScheduleID!`](#dastprofilescheduleid) | ID of the DAST profile schedule. | +| `nextRunAt` | [`Time`](#time) | Next run time of the DAST profile schedule in the given timezone. | +| `startsAt` | [`Time`](#time) | Start time of the DAST profile schedule in the given timezone. | +| `timezone` | [`String`](#string) | Time zone of the start time of the DAST profile schedule. | + ### `DastScannerProfile` Represents a DAST scanner profile. @@ -16443,6 +16470,12 @@ A `DastProfileID` is a global ID. It is encoded as a string. An example `DastProfileID` is: `"gid://gitlab/Dast::Profile/1"`. +### `DastProfileScheduleID` + +A `DastProfileScheduleID` is a global ID. It is encoded as a string. + +An example `DastProfileScheduleID` is: `"gid://gitlab/Dast::ProfileSchedule/1"`. + ### `DastScannerProfileID` A `DastScannerProfileID` is a global ID. It is encoded as a string. diff --git a/ee/app/graphql/resolvers/app_sec/dast/profile_resolver.rb b/ee/app/graphql/resolvers/app_sec/dast/profile_resolver.rb index 01b78b298e25cba071ea35fb254212fb0b552160..654a156fa636d19645a83fc9739fdde9cadb882e 100644 --- a/ee/app/graphql/resolvers/app_sec/dast/profile_resolver.rb +++ b/ee/app/graphql/resolvers/app_sec/dast/profile_resolver.rb @@ -25,7 +25,8 @@ def resolve_with_lookahead(**args) def preloads { dast_site_profile: [{ dast_site_profile: [:dast_site, :secret_variables] }], - dast_scanner_profile: [:dast_scanner_profile] + dast_scanner_profile: [:dast_scanner_profile], + dast_profile_schedule: [:dast_profile_schedule] } end diff --git a/ee/app/graphql/types/dast/profile_cadence_type.rb b/ee/app/graphql/types/dast/profile_cadence_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..0097ff0faa38f7f536d92caa415758fa4cb0e733 --- /dev/null +++ b/ee/app/graphql/types/dast/profile_cadence_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Disabling this cop as the auth check is happening in ProfileScheduleType. +# ProfileCadenceType is a dependent entity on ProfileScheduleType and does not exist without it. +# rubocop:disable Graphql/AuthorizeTypes + +module Types + module Dast + class ProfileCadenceType < BaseObject + graphql_name 'DastProfileCadence' + description 'Represents DAST Profile Cadence.' + + field :unit, ::Types::Dast::ProfileCadenceUnitEnum, + null: true, + description: 'Unit for the duration of DAST profile cadence.' + + field :duration, GraphQL::Types::Int, + null: true, + description: 'Duration of the DAST profile cadence.' + end + end +end diff --git a/ee/app/graphql/types/dast/profile_schedule_type.rb b/ee/app/graphql/types/dast/profile_schedule_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..6b30f634a8016fff79e7e4d0f060af8135000b17 --- /dev/null +++ b/ee/app/graphql/types/dast/profile_schedule_type.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Types + module Dast + class ProfileScheduleType < BaseObject + graphql_name 'DastProfileSchedule' + description 'Represents a DAST profile schedule.' + + authorize :read_on_demand_scans + + field :id, ::Types::GlobalIDType[::Dast::ProfileSchedule], null: false, + description: 'ID of the DAST profile schedule.' + + field :active, GraphQL::Types::Boolean, null: true, + description: 'Status of the DAST profile schedule.' + + field :starts_at, Types::TimeType, null: true, + description: 'Start time of the DAST profile schedule in the given timezone.' + + field :timezone, GraphQL::Types::String, null: true, + description: 'Time zone of the start time of the DAST profile schedule.' + + field :cadence, Types::Dast::ProfileCadenceType, null: true, + description: 'Cadence of the DAST profile schedule.' + + field :next_run_at, Types::TimeType, null: true, + description: 'Next run time of the DAST profile schedule in the given timezone.' + + def starts_at + return unless object.starts_at && object.timezone + + object.starts_at.in_time_zone(object.timezone) + end + + def next_run_at + return unless object.next_run_at && object.timezone + + object.next_run_at.in_time_zone(object.timezone) + end + end + end +end diff --git a/ee/app/graphql/types/dast/profile_type.rb b/ee/app/graphql/types/dast/profile_type.rb index e57e7af15ef7640cbbf6dcb069aaa388873ec45f..9a8437cfa9faaa13a9a436f2dcbadf90a7122e91 100644 --- a/ee/app/graphql/types/dast/profile_type.rb +++ b/ee/app/graphql/types/dast/profile_type.rb @@ -23,6 +23,10 @@ class ProfileType < BaseObject field :dast_scanner_profile, DastScannerProfileType, null: true, description: 'The associated scanner profile.' + field :dast_profile_schedule, ::Types::Dast::ProfileScheduleType, null: true, + description: 'Associated profile schedule. Will always return `null` ' \ + 'if `dast_on_demand_scans_scheduler` feature flag is disabled.' + field :branch, Dast::ProfileBranchType, null: true, description: 'The associated branch.', calls_gitaly: true @@ -33,6 +37,12 @@ class ProfileType < BaseObject def edit_path Gitlab::Routing.url_helpers.edit_project_on_demand_scan_path(object.project, object) end + + def dast_profile_schedule + return unless Feature.enabled?(:dast_on_demand_scans_scheduler, object.project, default_enabled: :yaml) + + object.dast_profile_schedule + end end end end diff --git a/ee/app/policies/dast/profile_schedule_policy.rb b/ee/app/policies/dast/profile_schedule_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..3b7064515509dc3122ea4d866d557d37b4a18fa4 --- /dev/null +++ b/ee/app/policies/dast/profile_schedule_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Dast + class ProfileSchedulePolicy < BasePolicy + delegate { @subject.project } + end +end diff --git a/ee/spec/graphql/types/dast/profile_cadence_type_spec.rb b/ee/spec/graphql/types/dast/profile_cadence_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..85cca34f3481ca806daa554e4e23a4dc7e887bd6 --- /dev/null +++ b/ee/spec/graphql/types/dast/profile_cadence_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['DastProfileCadence'] do + include GraphqlHelpers + + let_it_be(:fields) { %i[unit duration] } + + specify { expect(described_class.graphql_name).to eq('DastProfileCadence') } + + it { expect(described_class).to have_graphql_fields(fields) } +end diff --git a/ee/spec/graphql/types/dast/profile_schedule_type_spec.rb b/ee/spec/graphql/types/dast/profile_schedule_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5ef9fefaeedc3337721e9f9329e2452f90ac8161 --- /dev/null +++ b/ee/spec/graphql/types/dast/profile_schedule_type_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['DastProfileSchedule'] do + include GraphqlHelpers + + let_it_be(:fields) { %i[id active startsAt timezone nextRunAt cadence] } + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user, developer_projects: [project]) } + let_it_be(:object) { create(:dast_profile_schedule, project: project, owner: user) } + + specify { expect(described_class.graphql_name).to eq('DastProfileSchedule') } + + it { expect(described_class).to have_graphql_fields(fields) } + + before do + stub_licensed_features(security_on_demand_scans: true) + end + + describe 'startsAt field' do + it 'converts the startsAt to the timezone' do + expect(resolve_field(:starts_at, object, current_user: user)).to eq(object.starts_at.in_time_zone(object.timezone)) + end + end + + describe 'nextRunAt field' do + it 'converts the nextRunAt to the timezone' do + expect(resolve_field(:next_run_at, object, current_user: user)).to eq(object.next_run_at.in_time_zone(object.timezone)) + end + end +end diff --git a/ee/spec/graphql/types/dast/profile_type_spec.rb b/ee/spec/graphql/types/dast/profile_type_spec.rb index 3f7c85e45f99af39f49160fb9123c009846207e6..502d037bbca1d78f0c090805bb4a97e847c6a867 100644 --- a/ee/spec/graphql/types/dast/profile_type_spec.rb +++ b/ee/spec/graphql/types/dast/profile_type_spec.rb @@ -8,7 +8,7 @@ let_it_be(:project) { create(:project, :repository) } let_it_be(:object) { create(:dast_profile, project: project) } let_it_be(:user) { create(:user, developer_projects: [project]) } - let_it_be(:fields) { %i[id name description dastSiteProfile dastScannerProfile branch editPath] } + let_it_be(:fields) { %i[id name description dastSiteProfile dastScannerProfile dastProfileSchedule branch editPath] } specify { expect(described_class.graphql_name).to eq('DastProfile') } specify { expect(described_class).to require_graphql_authorizations(:read_on_demand_scans) } @@ -35,4 +35,22 @@ expect(resolve_field(:edit_path, object, current_user: user)).to eq(expected_result) end end + + describe 'dastProfileSchedule field' do + context 'when the feature flag is enabled' do + it 'correctly resolves the field' do + expect(resolve_field(:dast_profile_schedule, object, current_user: user)).to eq(object.dast_profile_schedule) + end + end + + context 'when the feature flag is not enabled' do + before do + stub_feature_flags(dast_on_demand_scans_scheduler: false) + end + + it 'is nil' do + expect(resolve_field(:dast_profile_schedule, object, current_user: user)).to be_nil + end + end + end end diff --git a/ee/spec/policies/dast/profile_schedule_policy_spec.rb b/ee/spec/policies/dast/profile_schedule_policy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7190ee60b980458491d5461dfd128676221dd3a1 --- /dev/null +++ b/ee/spec/policies/dast/profile_schedule_policy_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Dast::ProfileSchedulePolicy do + it_behaves_like 'a dast on-demand scan policy' do + let_it_be(:record) { create(:dast_profile_schedule, project: project) } + end +end