Commit 1014caf0 authored by Philip Cunningham's avatar Philip Cunningham Committed by Alex Kalderimis

Add new DAST internal API for site validations

Endpoint can only be called from CI with a job token. DAST site validation must
belong to the current project, and the transition must be valid.

- Add new Grape API defition
- Add specs
- Extract helper to allow it to be shared
- Add feature flag
parent 9b9ef4a3
---
name: dast_runner_site_validation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61649
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331082
milestone: '14.0'
type: development
group: group::dynamic analysis
default_enabled: false
---
title: Add dast_runner_site_validation feature flag
merge_request: 61649
author:
type: added
# frozen_string_literal: true
module API
module Internal
module AppSec
module Dast
class SiteValidations < ::API::Base
before do
authenticate!
validate_job_token_used!
end
feature_category :dynamic_application_security_testing
namespace :internal do
namespace :dast do
resource :site_validations do
desc 'Transitions a DAST site validation to a new state.' do
detail 'This feature is gated by the :dast_runner_site_validation feature flag.'
end
route_setting :authentication, job_token_allowed: true
params do
requires :event, type: Symbol, values: %i[start fail_op retry pass], desc: 'The transition event.'
end
post ':id/transition' do
validation = DastSiteValidation.find(params[:id])
unless Feature.enabled?(:dast_runner_site_validation, validation.project, default_enabled: :yaml)
render_api_error!('404 Feature flag disabled: :dast_runner_site_validation', 404)
end
authorize!(:create_on_demand_dast_scan, validation)
bad_request!('Project mismatch') unless current_authenticated_job.project == validation.project
success = case params[:event]
when :start
validation.start
when :fail_op
validation.fail_op
when :retry
validation.retry
when :pass
validation.pass
end
bad_request!('Could not update DAST site validation') unless success
status 200, { state: validation.state }
end
end
end
end
helpers do
def validate_job_token_used!
bad_request!('Must authenticate using job token') unless current_authenticated_job
end
end
end
end
end
end
end
...@@ -52,6 +52,8 @@ module EE ...@@ -52,6 +52,8 @@ module EE
mount ::API::ResourceIterationEvents mount ::API::ResourceIterationEvents
mount ::API::Iterations mount ::API::Iterations
mount ::API::GroupRepositoryStorageMoves mount ::API::GroupRepositoryStorageMoves
mount ::API::Internal::AppSec::Dast::SiteValidations
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Internal::AppSec::Dast::SiteValidations do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let_it_be(:site_validation) { create(:dast_site_validation, dast_site_token: create(:dast_site_token, project: project)) }
let_it_be(:job) { create(:ci_build, :running, project: project, user: developer) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
describe 'POST /internal/dast/site_validations/:id/transition' do
let(:url) { "/internal/dast/site_validations/#{site_validation.id}/transition" }
let(:event_param) { :pass }
let(:params) { { event: event_param } }
let(:headers) { {} }
subject do
post api(url), params: params, headers: headers
end
context 'when a job token header is not set' do
it 'returns 401' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
context 'when user token is set' do
it 'returns 400 and a contextual error message', :aggregate_failures do
post api(url, developer), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('message' => '400 Bad request - Must authenticate using job token')
end
end
end
context 'when a job token header is set' do
let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token } }
context 'when user does not have access to the site validation' do
let(:job) { create(:ci_build, :running, user: create(:user)) }
it 'returns 403' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when site validation does not exist' do
let(:site_validation) { build(:dast_site_validation, id: non_existing_record_id) }
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when site validation and job are associated with different projects' do
let_it_be(:job) { create(:ci_build, :running, user: developer) }
it 'returns 400 and a contextual error message', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('message' => '400 Bad request - Project mismatch')
end
end
context 'when site validation exists' do
context 'when the licensed feature is not available' do
before do
stub_licensed_features(security_on_demand_scans: false)
end
it 'returns 403' do
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(dast_runner_site_validation: false)
end
it 'returns 404 and a contextual error message', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response).to eq('message' => '404 Feature flag disabled: :dast_runner_site_validation')
end
end
context 'when user has access to the site validation' do
context 'when the state transition is unknown' do
let(:event_param) { :unknown_transition }
it 'returns 400 and a contextual error message', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('error' => 'event does not have a valid value')
end
end
context 'when the state transition is invalid' do
it 'returns 400 and a contextual error message', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq('message' => '400 Bad request - Could not update DAST site validation')
end
end
shared_examples 'it transitions' do |event|
let(:event_param) { event }
it "calls the underlying transition method: ##{event}", :aggregate_failures do
expect(DastSiteValidation).to receive(:find).with(String(site_validation.id)).and_return(site_validation)
expect(site_validation).to receive(event).and_call_original
subject
end
end
context 'when the state transition is valid' do
let(:event_param) { :start }
it 'updates the record' do
expect { subject }.to change { site_validation.reload.state }.from('pending').to('inprogress')
end
it_behaves_like 'it transitions', :start
it_behaves_like 'it transitions', :fail_op
it_behaves_like 'it transitions', :retry
it_behaves_like 'it transitions', :pass
end
end
end
end
end
end
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
module API module API
class Jobs < ::API::Base class Jobs < ::API::Base
include PaginationParams include PaginationParams
before { authenticate! } before { authenticate! }
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
......
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