1 /*
2  * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License, version 2.0,
6  * as published by the Free Software Foundation.
7  *
8  * This program is also distributed with certain software (including
9  * but not limited to OpenSSL) that is licensed under separate terms,
10  * as designated in a particular file or component or in included license
11  * documentation.  The authors of MySQL hereby grant you an additional
12  * permission to link the program and your derivative works with the
13  * separately licensed software that they have included with MySQL.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License, version 2.0, for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
23  */
24 
25 #include "plugin/x/src/meta_schema_validator.h"
26 
27 #include <rapidjson/error/en.h>
28 #include <rapidjson/stringbuffer.h>
29 #include <rapidjson/writer.h>
30 
31 #include "plugin/x/src/xpl_log.h"
32 
33 namespace xpl {
34 
35 namespace {
36 
37 static const char *const k_invalid_document =
38     "Validation schema is not a valid JSON document";
39 
get_pointer_string(const rapidjson::Pointer & pointer)40 inline std::string get_pointer_string(const rapidjson::Pointer &pointer) {
41   rapidjson::StringBuffer buff;
42   pointer.StringifyUriFragment(buff);
43   return {buff.GetString(), buff.GetSize()};
44 }
45 
46 using Any = Mysqlx::Datatypes::Any;
47 using Array = Mysqlx::Datatypes::Array;
48 using Scalar = Mysqlx::Datatypes::Scalar;
49 using Object = Mysqlx::Datatypes::Object;
50 
scalar_to_json(const Scalar & scalar,rapidjson::Value * value)51 bool scalar_to_json(const Scalar &scalar, rapidjson::Value *value) {
52   switch (scalar.type()) {
53     case Scalar::V_SINT:
54       if (!scalar.has_v_signed_int()) return false;
55       value->SetInt64(scalar.v_signed_int());
56       break;
57 
58     case Scalar::V_UINT:
59       if (!scalar.has_v_unsigned_int()) return false;
60       value->SetUint64(scalar.v_unsigned_int());
61       break;
62 
63     case Scalar::V_NULL:
64       value->SetNull();
65       break;
66 
67     case Scalar::V_OCTETS:
68       if (!scalar.has_v_octets() || !scalar.v_octets().has_value())
69         return false;
70       value->SetString(scalar.v_octets().value().c_str(),
71                        scalar.v_octets().value().length());
72       break;
73 
74     case Scalar::V_DOUBLE:
75       if (!scalar.has_v_double()) return false;
76       value->SetDouble(scalar.v_double());
77       break;
78 
79     case Scalar::V_FLOAT:
80       if (!scalar.has_v_float()) return false;
81       value->SetFloat(scalar.v_float());
82       break;
83 
84     case Scalar::V_BOOL:
85       if (!scalar.has_v_bool()) return false;
86       value->SetBool(scalar.v_bool());
87       break;
88 
89     case Scalar::V_STRING:
90       if (!scalar.has_v_string() || !scalar.v_string().has_value())
91         return false;
92       value->SetString(scalar.v_string().value().c_str(),
93                        scalar.v_string().value().length());
94       break;
95   }
96   return true;
97 }
98 
99 bool any_to_json(const Any &any, rapidjson::Value *value,
100                  rapidjson::Document::AllocatorType *alloc);
101 
array_to_json(const Array & array,rapidjson::Value * value,rapidjson::Document::AllocatorType * alloc)102 bool array_to_json(const Array &array, rapidjson::Value *value,
103                    rapidjson::Document::AllocatorType *alloc) {
104   value->SetArray();
105   for (const auto &a : array.value()) {
106     rapidjson::Value tmp;
107     if (!any_to_json(a, &tmp, alloc)) return false;
108     value->PushBack(tmp, *alloc);
109   }
110   return true;
111 }
112 
object_to_json(const Object & object,rapidjson::Value * value,rapidjson::Document::AllocatorType * alloc)113 bool object_to_json(const Object &object, rapidjson::Value *value,
114                     rapidjson::Document::AllocatorType *alloc) {
115   value->SetObject();
116   for (const auto &o : object.fld()) {
117     rapidjson::Value tmp_value;
118     if (!any_to_json(o.value(), &tmp_value, alloc)) return false;
119     rapidjson::Value name;
120     name.SetString(o.key().c_str(), o.key().length());
121     value->AddMember(name, tmp_value, *alloc);
122   }
123   return true;
124 }
125 
any_to_json(const Any & any,rapidjson::Value * value,rapidjson::Document::AllocatorType * alloc)126 bool any_to_json(const Any &any, rapidjson::Value *value,
127                  rapidjson::Document::AllocatorType *alloc) {
128   switch (any.type()) {
129     case Any::SCALAR:
130       return scalar_to_json(any.scalar(), value);
131 
132     case Any::OBJECT:
133       return object_to_json(any.obj(), value, alloc);
134 
135     case Any::ARRAY:
136       return array_to_json(any.array(), value, alloc);
137   }
138   return false;
139 }
140 
json_to_string(const rapidjson::Document & document,std::string * json_string)141 inline void json_to_string(const rapidjson::Document &document,
142                            std::string *json_string) {
143   rapidjson::StringBuffer buff;
144   rapidjson::Writer<rapidjson::StringBuffer> w(buff);
145   document.Accept(w);
146   json_string->assign(buff.GetString(), buff.GetSize());
147 }
148 
149 }  // namespace
150 
validate(const std::string & schema) const151 ngs::Error_code Meta_schema_validator::validate(
152     const std::string &schema) const {
153   rapidjson::Document input_document;
154   const rapidjson::ParseResult ok = input_document.Parse(schema.c_str());
155   if (!ok) {
156     log_debug("JSON schema parse error: %s",
157               rapidjson::GetParseError_En(ok.Code()));
158     return ngs::Error(ER_X_INVALID_VALIDATION_SCHEMA, k_invalid_document);
159   }
160   return validate_impl(&input_document);
161 }
162 
validate(const Any & schema,std::string * schema_string) const163 ngs::Error_code Meta_schema_validator::validate(
164     const Any &schema, std::string *schema_string) const {
165   rapidjson::Document input_document;
166 
167   switch (schema.type()) {
168     case Any::OBJECT: {
169       if (!object_to_json(schema.obj(), &input_document,
170                           &input_document.GetAllocator()))
171         return ngs::Error(ER_X_INVALID_VALIDATION_SCHEMA, k_invalid_document);
172     } break;
173 
174     case Any::SCALAR: {
175       if (!schema.scalar().has_v_string() ||
176           !schema.scalar().v_string().has_value())
177         return ngs::Error(ER_X_INVALID_VALIDATION_SCHEMA, k_invalid_document);
178       if (schema.scalar().v_string().value().empty()) {
179         *schema_string = "";
180         return ngs::Success();
181       }
182       if (input_document.Parse(schema.scalar().v_string().value().c_str())
183               .HasParseError())
184         return ngs::Error(ER_X_INVALID_VALIDATION_SCHEMA, k_invalid_document);
185     } break;
186 
187     default: {
188       return ngs::Error(ER_X_INVALID_VALIDATION_SCHEMA, k_invalid_document);
189     }
190   }
191 
192   json_to_string(input_document, schema_string);
193   return validate_impl(&input_document);
194 }
195 
validate_impl(rapidjson::Document * input_document) const196 ngs::Error_code Meta_schema_validator::validate_impl(
197     rapidjson::Document *input_document) const {
198   const auto e = pre_validate(input_document);
199   if (e) return e;
200 
201   rapidjson::SchemaValidator validator{get_meta_schema()};
202   if (input_document->Accept(validator)) return ngs::Success();
203 
204   return ngs::Error(
205       ER_X_INVALID_VALIDATION_SCHEMA,
206       "JSON validation schema location %s failed requirement: "
207       "'%s' at meta schema location '%s'",
208       get_pointer_string(validator.GetInvalidDocumentPointer()).c_str(),
209       validator.GetInvalidSchemaKeyword(),
210       get_pointer_string(validator.GetInvalidSchemaPointer()).c_str());
211 }
212 
pre_validate(rapidjson::Document * document) const213 ngs::Error_code Meta_schema_validator::pre_validate(
214     rapidjson::Document *document) const {
215   if (!document->IsObject())
216     return ngs::Error(ER_X_INVALID_VALIDATION_SCHEMA,
217                       "Validation schema is not a valid JSON object");
218   return pre_validate_impl(0, rapidjson::Pointer(), document, document);
219 }
220 
221 namespace {
222 
is_reference_valid(const rapidjson::Document & document,const rapidjson::Value & reference)223 bool is_reference_valid(const rapidjson::Document &document,
224                         const rapidjson::Value &reference) {
225   if (!reference.IsString()) return false;
226   const auto pointer =
227       rapidjson::Pointer(reference.GetString(), reference.GetStringLength());
228   if (!pointer.IsValid()) return false;
229   return pointer.Get(document);
230 }
231 
232 }  // namespace
233 
pre_validate_impl(const uint32_t level,const rapidjson::Pointer & pointer,rapidjson::Document * document,rapidjson::Value * value) const234 ngs::Error_code Meta_schema_validator::pre_validate_impl(
235     const uint32_t level, const rapidjson::Pointer &pointer,
236     rapidjson::Document *document, rapidjson::Value *value) const {
237   if (level > m_max_depth)
238     return ngs::Error(ER_X_INVALID_VALIDATION_SCHEMA,
239                       "Validation schema exceeds the maximum depth on %s",
240                       get_pointer_string(pointer).c_str());
241 
242   if (value->IsArray()) {
243     for (rapidjson::SizeType i = 0; i < value->Size(); i++) {
244       const auto e = pre_validate_impl(level + 1, pointer.Append(i), document,
245                                        &(*value)[i]);
246       if (e) return e;
247     }
248     return ngs::Success();
249   }
250 
251   if (!value->IsObject()) return ngs::Success();
252 
253   using It = rapidjson::Value::MemberIterator;
254   static const rapidjson::Value k_ref("$ref");
255 
256   // 'json schema reference' is an object with one member named '$ref'
257   It ref = value->FindMember(k_ref);
258   // if not '$ref' member - proceed as regular object
259   if (ref == value->MemberEnd()) {
260     for (It i = value->MemberBegin(); i != value->MemberEnd(); ++i) {
261       const auto e = pre_validate_impl(level + 1, pointer.Append(i->name),
262                                        document, &(i->value));
263       if (e) return e;
264     }
265     return ngs::Success();
266   }
267 
268   // this is reference object - check reference string
269   if (!is_reference_valid(*document, ref->value))
270     return ngs::Error(ER_X_INVALID_VALIDATION_SCHEMA,
271                       "Validation schema reference '%s' is not valid",
272                       get_pointer_string(pointer).c_str());
273 
274   return ngs::Success();
275 }
276 
get_meta_schema() const277 const rapidjson::SchemaDocument &Meta_schema_validator::get_meta_schema()
278     const {
279   static const rapidjson::SchemaDocument meta_schema{get_schema_document()};
280   return meta_schema;
281 }
282 
get_schema_document() const283 const rapidjson::Document &Meta_schema_validator::get_schema_document() const {
284   static rapidjson::Document document;
285   document.Parse(k_reference_schema);
286   // additional requirement to raw meta-schema:
287   // - not allow additional properties (to avoid typos).
288   document.AddMember("additionalProperties", false, document.GetAllocator());
289   // - allow '$ref' property as internal json reference
290   rapidjson::Document::ValueType reference;
291   reference.SetObject();
292   reference.AddMember("type", "string", document.GetAllocator());
293   reference.AddMember("pattern", "^#(/(.+))*$", document.GetAllocator());
294   const auto properties = document.FindMember("properties");
295   properties->value.AddMember("$ref", reference, document.GetAllocator());
296   return document;
297 }
298 
299 // Meta-schema used for validation of a user provided schemas,
300 // taken from http://json-schema.org/draft-04/schema
301 const char *const Meta_schema_validator::k_reference_schema = R"({
302   "id": "http://json-schema.org/draft-04/schema#",
303   "$schema": "http://json-schema.org/draft-04/schema#",
304   "description": "Core schema meta-schema",
305   "definitions": {
306     "schemaArray": {
307       "type": "array",
308       "minItems": 1,
309       "items": { "$ref": "#" }
310     },
311     "positiveInteger": {
312       "type": "integer",
313       "minimum": 0
314     },
315     "positiveIntegerDefault0": {
316       "allOf": [
317         { "$ref": "#/definitions/positiveInteger" },
318         { "default": 0 }
319       ]
320     },
321     "simpleTypes": {
322       "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
323     },
324     "stringArray": {
325       "type": "array",
326       "items": { "type": "string" },
327       "minItems": 1,
328       "uniqueItems": true
329     }
330   },
331   "type": "object",
332   "properties": {
333     "id": { "type": "string" },
334     "$schema": { "type": "string" },
335     "title": { "type": "string" },
336     "description": { "type": "string" },
337     "default": {},
338     "multipleOf": {
339       "type": "number",
340       "minimum": 0,
341       "exclusiveMinimum": true
342     },
343     "maximum": { "type": "number" },
344     "exclusiveMaximum": {
345       "type": "boolean",
346       "default": false
347     },
348     "minimum": { "type": "number" },
349     "exclusiveMinimum": {
350       "type": "boolean",
351       "default": false
352     },
353     "maxLength": { "$ref": "#/definitions/positiveInteger" },
354     "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
355     "pattern": {
356       "type": "string",
357       "format": "regex"
358     },
359     "additionalItems": {
360       "anyOf": [
361         { "type": "boolean" },
362         { "$ref": "#" }
363       ],
364       "default": {}
365     },
366     "items": {
367       "anyOf": [
368         { "$ref": "#" },
369         { "$ref": "#/definitions/schemaArray" }
370       ],
371       "default": {}
372     },
373     "maxItems": { "$ref": "#/definitions/positiveInteger" },
374     "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
375     "uniqueItems": {
376       "type": "boolean",
377       "default": false
378     },
379     "maxProperties": { "$ref": "#/definitions/positiveInteger" },
380     "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
381     "required": { "$ref": "#/definitions/stringArray" },
382     "additionalProperties": {
383       "anyOf": [
384         { "type": "boolean" },
385         { "$ref": "#" }
386       ],
387       "default": {}
388     },
389     "definitions": {
390       "type": "object",
391       "additionalProperties": { "$ref": "#" },
392       "default": {}
393     },
394     "properties": {
395       "type": "object",
396       "additionalProperties": { "$ref": "#" },
397       "default": {}
398     },
399     "patternProperties": {
400       "type": "object",
401       "additionalProperties": { "$ref": "#" },
402       "default": {}
403     },
404     "dependencies": {
405       "type": "object",
406       "additionalProperties": {
407         "anyOf": [
408           { "$ref": "#" },
409           { "$ref": "#/definitions/stringArray" }
410         ]
411       }
412     },
413     "enum": {
414       "type": "array",
415       "minItems": 1,
416       "uniqueItems": true
417     },
418     "type": {
419       "anyOf": [
420         { "$ref": "#/definitions/simpleTypes" },
421         {
422           "type": "array",
423           "items": { "$ref": "#/definitions/simpleTypes" },
424           "minItems": 1,
425           "uniqueItems": true
426         }
427       ]
428     },
429     "format": { "type": "string" },
430     "allOf": { "$ref": "#/definitions/schemaArray" },
431     "anyOf": { "$ref": "#/definitions/schemaArray" },
432     "oneOf": { "$ref": "#/definitions/schemaArray" },
433     "not": { "$ref": "#" }
434   },
435   "dependencies": {
436     "exclusiveMaximum": [ "maximum" ],
437     "exclusiveMinimum": [ "minimum" ]
438   },
439   "default": {}
440 })";
441 
442 }  // namespace xpl
443