-
Notifications
You must be signed in to change notification settings - Fork 361
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
Service Broker Create Service Instance Schema Validation #847
Changes from all commits
5081de9
aa84b5f
1c5dd78
842a941
b925b27
59b06bd
ff76cfe
d25b18c
afd4823
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,13 @@ | ||
require 'json-schema' | ||
|
||
module VCAP::Services::ServiceBrokers::V2 | ||
MAX_SCHEMA_SIZE = 65_536 | ||
class CatalogSchemas | ||
attr_reader :errors, :create_instance | ||
|
||
def initialize(attrs) | ||
def initialize(schema) | ||
@errors = VCAP::Services::ValidationErrors.new | ||
validate_and_populate_create_instance(attrs) | ||
validate_and_populate_create_instance(schema) | ||
end | ||
|
||
def valid? | ||
|
@@ -13,26 +16,94 @@ def valid? | |
|
||
private | ||
|
||
def validate_and_populate_create_instance(attrs) | ||
return unless attrs | ||
unless attrs.is_a? Hash | ||
errors.add("Schemas must be a hash, but has value #{attrs.inspect}") | ||
def validate_and_populate_create_instance(schemas) | ||
return unless schemas | ||
unless schemas.is_a? Hash | ||
errors.add("Schemas must be a hash, but has value #{schemas.inspect}") | ||
return | ||
end | ||
|
||
path = [] | ||
['service_instance', 'create', 'parameters'].each do |key| | ||
path += [key] | ||
attrs = attrs[key] | ||
return nil unless attrs | ||
schemas = schemas[key] | ||
return nil unless schemas | ||
|
||
unless attrs.is_a? Hash | ||
errors.add("Schemas #{path.join('.')} must be a hash, but has value #{attrs.inspect}") | ||
unless schemas.is_a? Hash | ||
errors.add("Schemas #{path.join('.')} must be a hash, but has value #{schemas.inspect}") | ||
return nil | ||
end | ||
end | ||
|
||
@create_instance = attrs | ||
create_instance_schema = schemas | ||
create_instance_path = path.join('.') | ||
|
||
validate_schema(create_instance_path, create_instance_schema) | ||
return unless errors.empty? | ||
|
||
@create_instance = create_instance_schema | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This comment doesn't necessarily reflect changes form this specific PR, but the set of PRs) We were also surprised to see the object being created conditionally through the validations. We'd prefer that the object's validity rely on |
||
end | ||
|
||
def validate_schema(path, schema) | ||
schema_validations.each do |validation| | ||
break if errors.present? | ||
send(validation, path, schema) | ||
end | ||
end | ||
|
||
def schema_validations | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is a prime example of being close to how ActiveModel validations are represented. See ActiveModel::Validations Custom Methods |
||
[ | ||
:validate_schema_size, | ||
:validate_metaschema, | ||
:validate_no_external_references, | ||
:validate_schema_type | ||
] | ||
end | ||
|
||
def validate_schema_type(path, schema) | ||
add_schema_error_msg(path, 'must have field "type", with value "object"') if schema['type'] != 'object' | ||
end | ||
|
||
def validate_schema_size(path, schema) | ||
errors.add("Schema #{path} is larger than 64KB") if schema.to_json.length > MAX_SCHEMA_SIZE | ||
end | ||
|
||
def validate_metaschema(path, schema) | ||
JSON::Validator.schema_reader = JSON::Schema::Reader.new(accept_uri: false, accept_file: false) | ||
file = File.read(JSON::Validator.validator_for_name('draft4').metaschema) | ||
|
||
metaschema = JSON.parse(file) | ||
|
||
begin | ||
errors = JSON::Validator.fully_validate(metaschema, schema) | ||
rescue => e | ||
add_schema_error_msg(path, e) | ||
return nil | ||
end | ||
|
||
errors.each do |error| | ||
add_schema_error_msg(path, "Must conform to JSON Schema Draft 04: #{error}") | ||
end | ||
end | ||
|
||
def validate_no_external_references(path, schema) | ||
JSON::Validator.schema_reader = JSON::Schema::Reader.new(accept_uri: false, accept_file: false) | ||
|
||
begin | ||
JSON::Validator.validate!(schema, {}) | ||
rescue JSON::Schema::SchemaError => e | ||
add_schema_error_msg(path, "Custom meta schemas are not supported: #{e}") | ||
rescue JSON::Schema::ReadRefused => e | ||
add_schema_error_msg(path, "No external references are allowed: #{e}") | ||
rescue JSON::Schema::ValidationError | ||
# We don't care if our input fails validation on broker schema | ||
rescue => e | ||
add_schema_error_msg(path, e) | ||
end | ||
end | ||
|
||
def add_schema_error_msg(path, err) | ||
errors.add("Schema #{path} is not valid. #{err}") | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
require 'spec_helper' | ||
|
||
RSpec.describe 'ServiceBrokers' do | ||
describe 'POST /v2/service_brokers' do | ||
service_name = 'myservice' | ||
plan_name = 'myplan' | ||
|
||
before do | ||
allow(VCAP::Services::ServiceBrokers::V2::Client).to receive(:new) do |*args, **kwargs, &block| | ||
fb = FakeServiceBrokerV2Client.new(*args, **kwargs, &block) | ||
fb.service_name = service_name | ||
fb.plan_name = plan_name | ||
fb | ||
end | ||
end | ||
|
||
it 'should register the service broker' do | ||
req_body = { | ||
name: 'service-broker-name', | ||
broker_url: 'https://broker.example.com', | ||
auth_username: 'admin', | ||
auth_password: 'secretpassw0rd' | ||
} | ||
|
||
post '/v2/service_brokers', req_body.to_json, admin_headers | ||
expect(last_response.status).to eq(201) | ||
|
||
broker = VCAP::CloudController::ServiceBroker.last | ||
expect(broker.name).to eq(req_body[:name]) | ||
expect(broker.broker_url).to eq(req_body[:broker_url]) | ||
expect(broker.auth_username).to eq(req_body[:auth_username]) | ||
expect(broker.auth_password).to eq(req_body[:auth_password]) | ||
|
||
service = VCAP::CloudController::Service.last | ||
expect(service.label).to eq(service_name) | ||
|
||
plan = VCAP::CloudController::ServicePlan.last | ||
expect(plan.name).to eq(plan_name) | ||
end | ||
|
||
context 'for brokers with schemas' do | ||
big_string = 'x' * 65 * 1024 | ||
|
||
schemas = { | ||
'service_instance' => { | ||
'create' => { | ||
'parameters' => { | ||
'type' => 'object', | ||
'foo' => big_string | ||
} | ||
} | ||
} | ||
} | ||
|
||
before do | ||
allow(VCAP::Services::ServiceBrokers::V2::Client).to receive(:new) do |*args, **kwargs, &block| | ||
fb = FakeServiceBrokerV2Client.new(*args, **kwargs, &block) | ||
fb.plan_schemas = schemas | ||
fb | ||
end | ||
end | ||
|
||
it 'should not allow schema bigger than 64KB' do | ||
req_body = { | ||
name: 'service-broker-name', | ||
broker_url: 'https://broker.example.com', | ||
auth_username: 'admin', | ||
auth_password: 'secretpassw0rd' | ||
} | ||
|
||
post '/v2/service_brokers', req_body.to_json, admin_headers | ||
expect(last_response.status).to eq(502) | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(This comment doesn't necessarily reflect changes form this specific PR, but the set of PRs)
We were surprised to see validations happening in the constructor. We are more familiar with the ActiveModel pattern of not explicitly implementing
valid?
, but rather includingvalidate
statements.With this approach, we'd be able to convert the basic validations (data types, presence checks, length) with the validations provided by ActiveModel, and separate those from the content validations.
We're asking for this change to make the code consistent with recent V3 code where validations are important. Examples are in the
messages/
folder