Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add release property validation #2602

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ PATH
bosh_cpi
cf-uaa-lib
json
json_schemer
logging
membrane
nats-pure
Expand Down Expand Up @@ -165,6 +166,7 @@ GEM
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
hana (1.3.7)
hashdiff (1.1.2)
httpclient (2.8.3)
i18n (1.14.7)
Expand All @@ -173,6 +175,11 @@ GEM
io-event (1.7.5)
io-stream (0.6.1)
json (2.9.1)
json_schemer (2.4.0)
bigdecimal
hana (~> 1.3)
regexp_parser (~> 2.0)
simpleidn (~> 0.2)
language_server-protocol (3.17.0.4)
little-plugger (1.1.4)
logger (1.6.5)
Expand Down Expand Up @@ -284,6 +291,7 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
simpleidn (0.2.3)
sinatra (4.1.1)
logger (>= 1.6.0)
mustermann (~> 3.0)
Expand Down
1 change: 1 addition & 0 deletions src/bosh-director/bosh-director.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'bosh_cpi'
spec.add_dependency 'cf-uaa-lib'
spec.add_dependency 'json'
spec.add_dependency 'json_schemer'
spec.add_dependency 'logging'
spec.add_dependency 'membrane'
spec.add_dependency 'nats-pure'
Expand Down
1 change: 1 addition & 0 deletions src/bosh-director/lib/bosh/director.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ module Director

require 'bosh/director/job_renderer'
require 'bosh/director/rendered_templates_persister'
require 'bosh/director/core/templates/job_schema_validator'

require 'bosh/director/audit_logger'
require 'bosh/director/cycle_helper'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ def render(spec_object)
RenderedJobInstance.new(rendered_templates)
end

def validate_properties!(spec_object)
@instance_jobs.each do |instance_job|
job_template_renderer = job_template_renderers[instance_job.name]
if job_template_renderer.properties_schema
JobSchemaValidator.validate(job_name: instance_job.name, schema: job_template_renderer.properties_schema, properties: spec_object['properties'][instance_job.name])
end
end
end

private

def job_template_renderers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'json_schemer'

module Bosh::Director::Core::Templates

class CustomType < JSONSchemer::Draft202012::Vocab::Validation::Type
def error(formatted_instance_location:, **)
case value
when 'certificate'
"value at #{formatted_instance_location} is not a certificate"
else
super
end
end

def valid_type(type, instance)
case type
when 'certificate'
return false unless instance.is_a?(String)
return true if instance == ""
begin
OpenSSL::X509::Certificate.load(instance)
rescue OpenSSL::X509::CertificateError
false
end
else
super
end
end
end

JSONSchemer::Draft202012::Vocab::VALIDATION['type'] = CustomType

class JobSchemaValidator
def self.validate(job_name:, schema:, properties:)
raise "You must declare your $schema draft version" if schema['$schema'].blank?
json_schemer_schema = JSONSchemer.schema(schema)
raise "Only https://json-schema.org/draft/2020-12/schema schema is currently supported" unless json_schemer_schema.meta_schema.base_uri == JSONSchemer::Draft202012::BASE_URI
errors = json_schemer_schema.validate(properties).map do |error|
error['error']
end
return true if errors.empty?
errors.unshift("Error validating properties for #{job_name}")
raise errors.join("\n")
end
end
end

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def process(instance_job)
monit_erb_file = File.read(File.join(template_dir, 'monit'))
monit_source_erb = SourceErb.new('monit', 'monit', monit_erb_file, instance_job.name)

if File.exist?(File.join(template_dir, 'properties_schema.json'))
properties_schema = JSON.load_file(File.join(template_dir, 'properties_schema.json'))
else
Comment on lines +42 to +44
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to start a trend of moving magic strings like 'properties_schema.json' into constants.

properties_schema = nil
end

source_erbs = []

instance_job.model.spec.fetch('templates', {}).each_pair do |src_filepath, dest_filepath|
Expand All @@ -49,6 +55,7 @@ def process(instance_job)
JobTemplateRenderer.new(instance_job: instance_job,
monit_erb: monit_source_erb,
source_erbs: source_erbs,
properties_schema: properties_schema,
logger: @logger,
link_provider_intents: @link_provider_intents,
dns_encoder: @dns_encoder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@
module Bosh::Director::Core::Templates
class JobTemplateRenderer

attr_reader :monit_erb, :source_erbs
attr_reader :monit_erb, :source_erbs, :properties_schema

def initialize(instance_job:,
monit_erb:,
source_erbs:,
properties_schema:,
logger:,
link_provider_intents:,
dns_encoder: nil)
@links_provided = instance_job.model.provides
@job_name = instance_job.name
@release = instance_job.release
@monit_erb = monit_erb
@properties_schema = properties_schema
@source_erbs = source_erbs
@logger = logger
@link_provider_intents = link_provider_intents
Expand Down
4 changes: 3 additions & 1 deletion src/bosh-director/lib/bosh/director/job_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def self.render_job_instance(instance_plan, loader, logger)
logger.debug("Rendering templates for instance #{instance}")

instance_renderer = Core::Templates::JobInstanceRenderer.new(instance_jobs, loader)
rendered_job_instance = instance_renderer.render(get_templates_spec(instance_plan))
templates_spec = get_templates_spec(instance_plan)
instance_renderer.validate_properties!(templates_spec)
rendered_job_instance = instance_renderer.render(templates_spec)

instance_plan.rendered_templates = rendered_job_instance

Expand Down
1 change: 1 addition & 0 deletions src/bosh-director/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def database_logger

RSpec.configure do |config|
config.include(FactoryBot::Syntax::Methods)
config.include(CertificateHelpers)

config.around(:each) do |example|
SpecHelper.reset_database(SpecHelper.database, example)
Expand Down
44 changes: 44 additions & 0 deletions src/bosh-director/spec/support/certificate_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module CertificateHelpers
class KeyCache
@key = nil
class << self
attr_accessor :key
end
end

def generate_rsa_certificate(sans: [])
rsa_private_key = KeyCache.key
if !rsa_private_key
rsa_private_key = OpenSSL::PKey::RSA.generate(512) #Smallest key OpenSSL will currently allow to not waste time on cpu cycles
KeyCache.key = rsa_private_key
end
rsa_cert = generate_cert(rsa_private_key, sans: sans)
private_key_cipher = OpenSSL::Cipher.new 'aes-256-cbc'
{
:cert_pem => rsa_cert.to_pem,
:public_key_pem => rsa_private_key.public_to_pem,
:private_key_pem => rsa_private_key.to_pem,
}
end

def generate_cert(key, sans: [])
cert = OpenSSL::X509::Certificate.new
cert.version = 2 # cf. RFC 5280 - to make it a "v3" certificate
cert.serial = 1 # Not secure, but this is a test certificate
cert.subject = OpenSSL::X509::Name.parse "/CN=Test CA"
cert.issuer = cert.subject # root CA's are "self-signed"
cert.public_key = key
cert.not_before = 5.minutes.ago
cert.not_after = cert.not_before + (2 * 7 * 24 * 60 * 60) # 2 weeks validity
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = cert
ef.issuer_certificate = cert
cert.add_extension(ef.create_extension("basicConstraints","CA:TRUE",true))
cert.add_extension(ef.create_extension("keyUsage","keyCertSign, cRLSign", true))
cert.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false))
cert.add_extension(ef.create_extension("authorityKeyIdentifier","keyid:always",false))
cert.add_extension(ef.create_extension('subjectAltName', sans.join(','))) unless sans.empty?
cert.sign(key, OpenSSL::Digest.new('SHA256'))
cert
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,28 @@

module Bosh::Director::Core::Templates
describe JobInstanceRenderer do
describe '#render' do
subject(:job_instance_renderer) { JobInstanceRenderer.new(templates, job_template_loader) }
subject(:job_instance_renderer) { JobInstanceRenderer.new(jobs, job_template_loader) }
let(:job_template_loader) { instance_double('Bosh::Director::Core::Templates::JobTemplateLoader') }
let(:job_template_renderer) { instance_double('Bosh::Director::Core::Templates::JobTemplateRenderer') }
let(:instance_group_name) { 'fake-instance-group-name' }
let(:properties) { {} }
let(:spec) do
{
'name' => instance_group_name,
'job' => { # <- here 'job' is the Bosh v1 term for 'instance group'
'name' => instance_group_name
},
'properties' => properties
}
end

let(:spec) do
{
'name' => 'fake-instance-group-name',
'job' => { # <- here 'job' is the Bosh v1 term for 'instance group'
'name' => 'fake-instance-group-name'
}
}
end
let(:job_template_loader) { instance_double('Bosh::Director::Core::Templates::JobTemplateLoader') }
let(:job_template_renderer) { instance_double('Bosh::Director::Core::Templates::JobTemplateRenderer') }
describe '#render' do
let(:expected_rendered_job_instance) { instance_double('Bosh::Director::Core::Templates::RenderedJobInstance') }

before { allow(RenderedJobInstance).to receive(:new).and_return(expected_rendered_job_instance) }
let(:expected_rendered_job_instance) { instance_double('Bosh::Director::Core::Templates::RenderedJobInstance') }

context 'when job has no templates' do
let(:templates) { [] }
let(:jobs) { [] }

it 'returns empty array' do

Expand All @@ -34,23 +37,23 @@ module Bosh::Director::Core::Templates
end

context 'when job has one job_template' do
let(:templates) { [double('template', name: 'a')] }
let(:jobs) { [double('template', name: 'a')] }
let(:expected_rendered_templates) { [double('rendered template')] }

before do
allow(job_template_renderer).to receive(:render).with(spec).and_return(expected_rendered_templates[0])
end

it 'returns the rendered template for the given instance' do
allow(job_template_loader).to receive(:process).with(templates[0]).and_return(job_template_renderer)
allow(job_template_loader).to receive(:process).with(jobs[0]).and_return(job_template_renderer)

job_instance_renderer.render(spec)
expect(RenderedJobInstance).to have_received(:new).with(expected_rendered_templates)
end

context 'when called for multiple instances' do
it 'only processes the source job templates once' do
expect(job_template_loader).to receive(:process).with(templates[0]).and_return(job_template_renderer)
expect(job_template_loader).to receive(:process).with(jobs[0]).and_return(job_template_renderer)

job_instance_renderer.render(spec)
job_instance_renderer.render(spec)
Expand All @@ -59,7 +62,7 @@ module Bosh::Director::Core::Templates
end

context 'when job has multiple job_templates' do
let(:templates) { [double('template1', name: 'b'), double('template2', name: 'a')] }
let(:jobs) { [double('template1', name: 'b'), double('template2', name: 'a')] }
let(:expected_rendered_templates) do
[
double('rendered job template1'),
Expand All @@ -69,8 +72,8 @@ module Bosh::Director::Core::Templates
let(:job_template_renderer2) { instance_double('Bosh::Director::Core::Templates::JobTemplateRenderer') }

before do
allow(job_template_loader).to receive(:process).with(templates[0]).and_return(job_template_renderer)
allow(job_template_loader).to receive(:process).with(templates[1]).and_return(job_template_renderer2)
allow(job_template_loader).to receive(:process).with(jobs[0]).and_return(job_template_renderer)
allow(job_template_loader).to receive(:process).with(jobs[1]).and_return(job_template_renderer2)

allow(job_template_renderer).to receive(:render).with(spec).and_return(expected_rendered_templates[0])
allow(job_template_renderer2).to receive(:render).with(spec).and_return(expected_rendered_templates[1])
Expand Down Expand Up @@ -123,5 +126,40 @@ module Bosh::Director::Core::Templates
end
end
end

describe 'validate_properties!' do
let(:job_name) { 'fake-job-name' }
let(:job) { instance_double('Bosh::Director::DeploymentPlan::Job', name: job_name) }
let(:jobs) { [job] }
let(:properties) do
{
job_name => {
'a_property' => 'a_value'
}
}
end

before do
allow(job_template_loader).to receive(:process).with(job).and_return(job_template_renderer)
allow(job_template_renderer).to receive(:properties_schema).and_return(properties_schema)
end

context 'when the job has a json schema' do
let(:properties_schema) { { "schema": true} }

it 'calls the schema verifier' do
expect(JobSchemaValidator).to receive(:validate).with(job_name: job_name, schema: properties_schema, properties: properties[job_name])
job_instance_renderer.validate_properties!(spec)
end
end

context 'when the job does not have a json schema' do
let(:properties_schema) { nil }

it 'does not error' do
expect { job_instance_renderer.validate_properties!(spec) }.not_to raise_error
end
end
end
end
end
Loading
Loading