Commit adb0044a authored by Philip Cunningham's avatar Philip Cunningham Committed by Luke Duncalfe

Return additional site profile config from GraphQL

- Extend auth type
- Extend profile type
- Rework specs
- Add new specs
- Add model methods

https://gitlab.com/gitlab-org/gitlab/-/issues/277353
https://gitlab.com/gitlab-org/gitlab/-/issues/323449
parent fe41d72c
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Resolvers module Resolvers
class DastSiteProfileResolver < BaseResolver class DastSiteProfileResolver < BaseResolver
include LooksAhead
alias_method :project, :object alias_method :project, :object
type Types::DastSiteProfileType.connection_type, null: true type Types::DastSiteProfileType.connection_type, null: true
...@@ -12,7 +14,20 @@ module Resolvers ...@@ -12,7 +14,20 @@ module Resolvers
description: "ID of the site profile." description: "ID of the site profile."
end end
def resolve(**args) def resolve_with_lookahead(**args)
apply_lookahead(find_dast_site_profiles(args))
end
private
def preloads
{
request_headers: [:secret_variables],
auth: [:secret_variables]
}
end
def find_dast_site_profiles(args)
if args[:id] if args[:id]
# TODO: remove this coercion when the compatibility layer is removed # TODO: remove this coercion when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Types module Types
module Dast module Dast
class SiteProfileAuthType < BaseObject class SiteProfileAuthType < BaseObject
REDACTED_PASSWORD = '••••••••'
graphql_name 'DastSiteProfileAuth' graphql_name 'DastSiteProfileAuth'
description 'Input type for DastSiteProfile authentication' description 'Input type for DastSiteProfile authentication'
...@@ -39,7 +41,9 @@ module Types ...@@ -39,7 +41,9 @@ module Types
description: 'Redacted password to authenticate with on the target website.' description: 'Redacted password to authenticate with on the target website.'
def password def password
nil return unless object.secret_variables.any? { |variable| variable.key == ::Dast::SiteProfileSecretVariable::PASSWORD }
REDACTED_PASSWORD
end end
end end
end end
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Types module Types
class DastSiteProfileType < BaseObject class DastSiteProfileType < BaseObject
REDACTED_REQUEST_HEADERS = '[Redacted]'
graphql_name 'DastSiteProfile' graphql_name 'DastSiteProfile'
description 'Represents a DAST Site Profile' description 'Represents a DAST Site Profile'
...@@ -68,7 +70,10 @@ module Types ...@@ -68,7 +70,10 @@ module Types
end end
def request_headers def request_headers
nil return unless Feature.enabled?(:security_dast_site_profiles_additional_fields, object.project, default_enabled: :yaml)
return unless object.secret_variables.any? { |variable| variable.key == ::Dast::SiteProfileSecretVariable::REQUEST_HEADERS }
REDACTED_REQUEST_HEADERS
end end
def normalized_target_url def normalized_target_url
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
module Dast module Dast
class SiteProfileSecretVariable < ApplicationRecord class SiteProfileSecretVariable < ApplicationRecord
REQUEST_HEADERS = 'DAST_REQUEST_HEADERS_BASE64'
PASSWORD = 'DAST_PASSWORD_BASE64'
include Ci::HasVariable include Ci::HasVariable
include Ci::Maskable include Ci::Maskable
......
...@@ -2,9 +2,66 @@ ...@@ -2,9 +2,66 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Types::Dast::SiteProfileAuthType do RSpec.describe GitlabSchema.types['DastSiteProfileAuth'] do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
let_it_be(:object, reload: true) { create(:dast_site_profile, project: project) }
let_it_be(:fields) { %i[enabled url usernameField passwordField username password] }
before do
stub_licensed_features(security_on_demand_scans: true)
end
specify { expect(described_class.graphql_name).to eq('DastSiteProfileAuth') } specify { expect(described_class.graphql_name).to eq('DastSiteProfileAuth') }
specify { expect(described_class).to require_graphql_authorizations(:read_on_demand_scans) } specify { expect(described_class).to require_graphql_authorizations(:read_on_demand_scans) }
it { expect(described_class).to have_graphql_fields(%w[enabled url usernameField passwordField username password]) } it { expect(described_class).to have_graphql_fields(fields) }
describe 'enabled field' do
it 'is auth_enabled' do
expect(resolve_field(:enabled, object, current_user: user)).to eq(object.auth_enabled)
end
end
describe 'url field' do
it 'is auth_url' do
expect(resolve_field(:url, object, current_user: user)).to eq(object.auth_url)
end
end
describe 'usernameField field' do
it 'is auth_username_field' do
expect(resolve_field(:username_field, object, current_user: user)).to eq(object.auth_username_field)
end
end
describe 'passwordField field' do
it 'is auth_password_field' do
expect(resolve_field(:password_field, object, current_user: user)).to eq(object.auth_password_field)
end
end
describe 'username field' do
it 'is auth_username' do
expect(resolve_field(:username, object, current_user: user)).to eq(object.auth_username)
end
end
describe 'password field' do
context 'when there is no associated secret variable' do
it 'is nil' do
expect(resolve_field(:password, object, current_user: user)).to be_nil
end
end
context 'when there an associated secret variable' do
it 'is redacted' do
create(:dast_site_profile_secret_variable, dast_site_profile: object, key: Dast::SiteProfileSecretVariable::PASSWORD)
expect(resolve_field(:password, object, current_user: user)).to eq('••••••••')
end
end
end
end end
...@@ -5,23 +5,11 @@ require 'spec_helper' ...@@ -5,23 +5,11 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['DastSiteProfile'] do RSpec.describe GitlabSchema.types['DastSiteProfile'] do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:dast_site_profile) { create(:dast_site_profile) } let_it_be(:project) { create(:project) }
let_it_be(:project) { dast_site_profile.project } let_it_be(:user) { create(:user, developer_projects: [project]) }
let_it_be(:user) { create(:user) } let_it_be(:object, reload: true) { create(:dast_site_profile, project: project) }
let_it_be(:fields) { %i[id profileName targetUrl editPath excludedUrls requestHeaders validationStatus userPermissions normalizedTargetUrl auth referencedInSecurityPolicies] } let_it_be(:fields) { %i[id profileName targetUrl editPath excludedUrls requestHeaders validationStatus userPermissions normalizedTargetUrl auth referencedInSecurityPolicies] }
subject do
GitlabSchema.execute(
query,
context: {
current_user: user
},
variables: {
fullPath: project.full_path
}
).as_json
end
before do before do
stub_licensed_features(security_on_demand_scans: true) stub_licensed_features(security_on_demand_scans: true)
end end
...@@ -31,51 +19,47 @@ RSpec.describe GitlabSchema.types['DastSiteProfile'] do ...@@ -31,51 +19,47 @@ RSpec.describe GitlabSchema.types['DastSiteProfile'] do
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::DastSiteProfile) } specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::DastSiteProfile) }
it { expect(described_class).to have_graphql_fields(fields) } it { expect(described_class).to have_graphql_fields(fields) }
it { expect(described_class).to have_graphql_field(:referenced_in_security_policies, calls_gitaly?: true, complexity: 10) }
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 { #{all_graphql_fields_for('DastSiteProfile')} }
}
}
}
)
end
let(:first_dast_site_profile) do
subject.dig('data', 'project', 'dastSiteProfiles', 'nodes', 0)
end
describe 'id field' do describe 'id field' do
it 'is a global id' do it 'is the global id' do
expect(first_dast_site_profile['id']).to eq(dast_site_profile.to_global_id.to_s) expect(resolve_field(:id, object, current_user: user)).to eq(object.to_global_id)
end end
end end
describe 'profile_name field' do describe 'profileName field' do
it 'is the name' do it 'is the name' do
expect(first_dast_site_profile['profileName']).to eq(dast_site_profile.name) expect(resolve_field(:profile_name, object, current_user: user)).to eq(object.name)
end end
end end
describe 'target_url field' do describe 'targetUrl field' do
it 'is the url of the associated dast_site' 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) expect(resolve_field(:target_url, object, current_user: user)).to eq(object.dast_site.url)
end end
end end
describe 'edit_path field' do describe 'editPath field' do
it 'is the relative path to edit the dast_site_profile' do it 'is the relative path to edit the dast_site_profile' do
path = "/#{project.full_path}/-/security/configuration/dast_scans/dast_site_profiles/#{dast_site_profile.id}/edit" path = "/#{project.full_path}/-/security/configuration/dast_scans/dast_site_profiles/#{object.id}/edit"
expect(first_dast_site_profile['editPath']).to eq(path) expect(resolve_field(:edit_path, object, current_user: user)).to eq(path)
end
end
describe 'auth field' do
context 'when the feature flag is disabled' do
it 'is nil' do
stub_feature_flags(security_dast_site_profiles_additional_fields: false)
expect(resolve_field(:auth, object, current_user: user)).to be_nil
end
end
context 'when the feature flag is enabled' do
it 'is the dast_site_profile' do
expect(resolve_field(:auth, object, current_user: user)).to eq(object)
end
end end
end end
...@@ -84,64 +68,60 @@ RSpec.describe GitlabSchema.types['DastSiteProfile'] do ...@@ -84,64 +68,60 @@ RSpec.describe GitlabSchema.types['DastSiteProfile'] do
it 'is nil' do it 'is nil' do
stub_feature_flags(security_dast_site_profiles_additional_fields: false) stub_feature_flags(security_dast_site_profiles_additional_fields: false)
expect(first_dast_site_profile['excludedUrls']).to eq(nil) expect(resolve_field(:excluded_urls, object, current_user: user)).to be_nil
end end
end end
context 'when the feature flag is enabled' do context 'when the feature flag is enabled' do
it 'is the excluded urls' do it 'is the excluded urls' do
expect(first_dast_site_profile['excludedUrls']).to eq(dast_site_profile.excluded_urls) expect(resolve_field(:excluded_urls, object, current_user: user)).to eq(object.excluded_urls)
end end
end end
end end
describe 'auth field' do describe 'requestHeaders field' do
context 'when the feature flag is disabled' do context 'when the feature flag is disabled' do
it 'is nil' do it 'is nil' do
stub_feature_flags(security_dast_site_profiles_additional_fields: false) stub_feature_flags(security_dast_site_profiles_additional_fields: false)
expect(first_dast_site_profile['auth']).to eq(nil) expect(resolve_field(:request_headers, object, current_user: user)).to be_nil
end end
end end
context 'when the feature flag is enabled' do context 'when the feature flag is enabled' do
it 'includes the correct values' do context 'when there is no associated secret variable' do
auth = first_dast_site_profile['auth'] it 'is nil' do
expect(resolve_field(:request_headers, object, current_user: user)).to be_nil
end
end
expect(auth).to include( context 'when there an associated secret variable' do
'enabled' => false, it 'is redacted' do
'url' => dast_site_profile.auth_url, create(:dast_site_profile_secret_variable, dast_site_profile: object, key: Dast::SiteProfileSecretVariable::REQUEST_HEADERS)
'usernameField' => dast_site_profile.auth_username_field,
'passwordField' => dast_site_profile.auth_password_field, expect(resolve_field(:request_headers, object, current_user: user)).to eq('[Redacted]')
'username' => dast_site_profile.auth_username, end
'password' => nil
)
end end
end end
end end
describe 'validation_status field' do describe 'validation_status field' do
it 'is the validation status' do it 'is the validation status' do
expect(first_dast_site_profile['validationStatus']).to eq('NONE') expect(resolve_field(:validation_status, object, current_user: user)).to eq('none')
end end
end end
describe 'normalized_target_url field' do describe 'normalizedTargetUrl field' do
it 'is the normalized url of the associated dast_site' do it 'is the normalized url of the associated dast_site' do
normalized_url = DastSiteValidation.get_normalized_url_base(dast_site_profile.dast_site.url) normalized_url = DastSiteValidation.get_normalized_url_base(object.dast_site.url)
expect(first_dast_site_profile['normalizedTargetUrl']).to eq(normalized_url) expect(resolve_field(:normalized_target_url, object, current_user: user)).to eq(normalized_url)
end end
end end
context 'when there are no dast_site_profiles' do describe 'referencedInSecurityPolicies field' do
let(:project) { create(:project) } it 'is the policies' do
expect(resolve_field(:referenced_in_security_policies, object, current_user: user)).to eq(object.referenced_in_security_policies)
it 'has no nodes' do
nodes = subject.dig('data', 'project', 'dastSiteProfiles', 'nodes')
expect(nodes).to be_empty
end
end end
end end
end end
...@@ -122,6 +122,7 @@ RSpec.describe DastSiteProfile, type: :model do ...@@ -122,6 +122,7 @@ RSpec.describe DastSiteProfile, type: :model do
end end
end end
describe 'instance methods' do
describe '#destroy!' do describe '#destroy!' do
context 'when the associated dast_site has no dast_site_profiles' do context 'when the associated dast_site has no dast_site_profiles' do
it 'is also destroyed' do it 'is also destroyed' do
...@@ -184,4 +185,5 @@ RSpec.describe DastSiteProfile, type: :model do ...@@ -184,4 +185,5 @@ RSpec.describe DastSiteProfile, type: :model do
end end
end end
end end
end
end end
...@@ -70,7 +70,7 @@ RSpec.describe 'Query.project(fullPath).dastSiteProfiles' do ...@@ -70,7 +70,7 @@ RSpec.describe 'Query.project(fullPath).dastSiteProfiles' do
expect(first_dast_site_profile_response['id']).to eq(dast_site_profile.to_global_id.to_s) expect(first_dast_site_profile_response['id']).to eq(dast_site_profile.to_global_id.to_s)
end end
it 'eager loads the dast site and dast site validation' do it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new do control = ActiveRecord::QueryRecorder.new do
post_graphql( post_graphql(
query, query,
......
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