1 | # Licensed to the StackStorm, Inc ('StackStorm') under one or more |
||
2 | # contributor license agreements. See the NOTICE file distributed with |
||
3 | # this work for additional information regarding copyright ownership. |
||
4 | # The ASF licenses this file to You under the Apache License, Version 2.0 |
||
5 | # (the "License"); you may not use this file except in compliance with |
||
6 | # the License. You may obtain a copy of the License at |
||
7 | # |
||
8 | # http://www.apache.org/licenses/LICENSE-2.0 |
||
9 | # |
||
10 | # Unless required by applicable law or agreed to in writing, software |
||
11 | # distributed under the License is distributed on an "AS IS" BASIS, |
||
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||
13 | # See the License for the specific language governing permissions and |
||
14 | # limitations under the License. |
||
15 | |||
16 | from __future__ import absolute_import |
||
17 | import os |
||
18 | import copy |
||
19 | |||
20 | import six |
||
21 | import jsonschema |
||
22 | from jsonschema import _validators |
||
23 | from jsonschema.validators import create |
||
24 | |||
25 | from st2common.exceptions.action import InvalidActionParameterException |
||
26 | from st2common.util import jsonify |
||
27 | from st2common.util.misc import deep_update |
||
28 | |||
29 | __all__ = [ |
||
30 | 'get_validator', |
||
31 | 'get_draft_schema', |
||
32 | 'get_action_parameters_schema', |
||
33 | 'get_schema_for_action_parameters', |
||
34 | 'get_schema_for_resource_parameters', |
||
35 | 'is_property_type_single', |
||
36 | 'is_property_type_list', |
||
37 | 'is_property_type_anyof', |
||
38 | 'is_property_type_oneof', |
||
39 | 'is_property_nullable', |
||
40 | 'is_attribute_type_array', |
||
41 | 'is_attribute_type_object', |
||
42 | 'validate' |
||
43 | ] |
||
44 | |||
45 | # https://github.com/json-schema/json-schema/blob/master/draft-04/schema |
||
46 | # The source material is licensed under the AFL or BSD license. |
||
47 | # Both draft 4 and custom schema has additionalProperties set to false by default. |
||
48 | # The custom schema differs from draft 4 with the extension of position, immutable, |
||
49 | # and draft 3 version of required. |
||
50 | PATH = os.path.join(os.path.dirname(os.path.realpath(__file__))) |
||
51 | SCHEMAS = { |
||
52 | 'draft4': jsonify.load_file(os.path.join(PATH, 'draft4.json')), |
||
53 | 'custom': jsonify.load_file(os.path.join(PATH, 'custom.json')), |
||
54 | |||
55 | # Custom schema for action params which doesn't allow parameter "type" attribute to be array |
||
56 | 'action_params': jsonify.load_file(os.path.join(PATH, 'action_params.json')) |
||
57 | } |
||
58 | |||
59 | SCHEMA_ANY_TYPE = { |
||
60 | "anyOf": [ |
||
61 | {"type": "array"}, |
||
62 | {"type": "boolean"}, |
||
63 | {"type": "integer"}, |
||
64 | {"type": "number"}, |
||
65 | {"type": "object"}, |
||
66 | {"type": "string"} |
||
67 | ] |
||
68 | } |
||
69 | |||
70 | RUNNER_PARAM_OVERRIDABLE_ATTRS = [ |
||
71 | 'default', |
||
72 | 'description', |
||
73 | 'enum', |
||
74 | 'immutable', |
||
75 | 'required' |
||
76 | ] |
||
77 | |||
78 | |||
79 | def get_draft_schema(version='custom', additional_properties=False): |
||
80 | schema = copy.deepcopy(SCHEMAS[version]) |
||
81 | if additional_properties and 'additionalProperties' in schema: |
||
82 | del schema['additionalProperties'] |
||
83 | return schema |
||
84 | |||
85 | |||
86 | def get_action_parameters_schema(additional_properties=False): |
||
87 | """ |
||
88 | Return a generic schema which is used for validating action parameters definition. |
||
89 | """ |
||
90 | return get_draft_schema(version='action_params', additional_properties=additional_properties) |
||
91 | |||
92 | |||
93 | CustomValidator = create( |
||
94 | meta_schema=get_draft_schema(version='custom', additional_properties=True), |
||
95 | validators={ |
||
96 | u"$ref": _validators.ref, |
||
97 | u"additionalItems": _validators.additionalItems, |
||
98 | u"additionalProperties": _validators.additionalProperties, |
||
99 | u"allOf": _validators.allOf_draft4, |
||
100 | u"anyOf": _validators.anyOf_draft4, |
||
101 | u"dependencies": _validators.dependencies, |
||
102 | u"enum": _validators.enum, |
||
103 | u"format": _validators.format, |
||
104 | u"items": _validators.items, |
||
105 | u"maxItems": _validators.maxItems, |
||
106 | u"maxLength": _validators.maxLength, |
||
107 | u"maxProperties": _validators.maxProperties_draft4, |
||
108 | u"maximum": _validators.maximum, |
||
109 | u"minItems": _validators.minItems, |
||
110 | u"minLength": _validators.minLength, |
||
111 | u"minProperties": _validators.minProperties_draft4, |
||
112 | u"minimum": _validators.minimum, |
||
113 | u"multipleOf": _validators.multipleOf, |
||
114 | u"not": _validators.not_draft4, |
||
115 | u"oneOf": _validators.oneOf_draft4, |
||
116 | u"pattern": _validators.pattern, |
||
117 | u"patternProperties": _validators.patternProperties, |
||
118 | u"properties": _validators.properties_draft3, |
||
119 | u"type": _validators.type_draft4, |
||
120 | u"uniqueItems": _validators.uniqueItems, |
||
121 | }, |
||
122 | version="custom_validator", |
||
123 | ) |
||
124 | |||
125 | |||
126 | def is_property_type_single(property_schema): |
||
127 | return (isinstance(property_schema, dict) and |
||
128 | 'anyOf' not in list(property_schema.keys()) and |
||
129 | 'oneOf' not in list(property_schema.keys()) and |
||
130 | not isinstance(property_schema.get('type', 'string'), list)) |
||
131 | |||
132 | |||
133 | def is_property_type_list(property_schema): |
||
134 | return (isinstance(property_schema, dict) and |
||
135 | isinstance(property_schema.get('type', 'string'), list)) |
||
136 | |||
137 | |||
138 | def is_property_type_anyof(property_schema): |
||
139 | return isinstance(property_schema, dict) and 'anyOf' in list(property_schema.keys()) |
||
140 | |||
141 | |||
142 | def is_property_type_oneof(property_schema): |
||
143 | return isinstance(property_schema, dict) and 'oneOf' in list(property_schema.keys()) |
||
144 | |||
145 | |||
146 | def is_property_nullable(property_type_schema): |
||
147 | # For anyOf and oneOf, the property_schema is a list of types. |
||
148 | if isinstance(property_type_schema, list): |
||
149 | return len([t for t in property_type_schema |
||
150 | if ((isinstance(t, six.string_types) and t == 'null') or |
||
151 | (isinstance(t, dict) and t.get('type', 'string') == 'null'))]) > 0 |
||
152 | |||
153 | return (isinstance(property_type_schema, dict) and |
||
154 | property_type_schema.get('type', 'string') == 'null') |
||
155 | |||
156 | |||
157 | def is_attribute_type_array(attribute_type): |
||
158 | return (attribute_type == 'array' or |
||
159 | (isinstance(attribute_type, list) and 'array' in attribute_type)) |
||
160 | |||
161 | |||
162 | def is_attribute_type_object(attribute_type): |
||
163 | return (attribute_type == 'object' or |
||
164 | (isinstance(attribute_type, list) and 'object' in attribute_type)) |
||
165 | |||
166 | |||
167 | def assign_default_values(instance, schema): |
||
168 | """ |
||
169 | Assign default values on the provided instance based on the schema default specification. |
||
170 | """ |
||
171 | instance = copy.deepcopy(instance) |
||
172 | instance_is_dict = isinstance(instance, dict) |
||
173 | instance_is_array = isinstance(instance, list) |
||
174 | |||
175 | if not instance_is_dict and not instance_is_array: |
||
176 | return instance |
||
177 | |||
178 | properties = schema.get('properties', {}) |
||
179 | |||
180 | for property_name, property_data in six.iteritems(properties): |
||
181 | has_default_value = 'default' in property_data |
||
182 | default_value = property_data.get('default', None) |
||
183 | |||
184 | # Assign default value on the instance so the validation doesn't fail if requires is true |
||
185 | # but the value is not provided |
||
186 | if has_default_value: |
||
187 | if instance_is_dict and instance.get(property_name, None) is None: |
||
188 | instance[property_name] = default_value |
||
189 | elif instance_is_array: |
||
190 | for index, _ in enumerate(instance): |
||
191 | if instance[index].get(property_name, None) is None: |
||
192 | instance[index][property_name] = default_value |
||
193 | |||
194 | # Support for nested properties (array and object) |
||
195 | attribute_type = property_data.get('type', None) |
||
196 | schema_items = property_data.get('items', {}) |
||
197 | |||
198 | # Array |
||
199 | if (is_attribute_type_array(attribute_type) and |
||
200 | schema_items and schema_items.get('properties', {})): |
||
201 | array_instance = instance.get(property_name, None) |
||
202 | array_schema = schema['properties'][property_name]['items'] |
||
203 | |||
204 | if array_instance is not None: |
||
205 | # Note: We don't perform subschema assignment if no value is provided |
||
206 | instance[property_name] = assign_default_values(instance=array_instance, |
||
207 | schema=array_schema) |
||
208 | |||
209 | # Object |
||
210 | if is_attribute_type_object(attribute_type) and property_data.get('properties', {}): |
||
211 | object_instance = instance.get(property_name, None) |
||
212 | object_schema = schema['properties'][property_name] |
||
213 | |||
214 | if object_instance is not None: |
||
215 | # Note: We don't perform subschema assignment if no value is provided |
||
216 | instance[property_name] = assign_default_values(instance=object_instance, |
||
217 | schema=object_schema) |
||
218 | |||
219 | return instance |
||
220 | |||
221 | |||
222 | def modify_schema_allow_default_none(schema): |
||
223 | """ |
||
224 | Manipulate the provided schema so None is also an allowed value for each attribute which |
||
225 | defines a default value of None. |
||
226 | """ |
||
227 | schema = copy.deepcopy(schema) |
||
228 | properties = schema.get('properties', {}) |
||
229 | |||
230 | for property_name, property_data in six.iteritems(properties): |
||
231 | is_optional = not property_data.get('required', False) |
||
232 | has_default_value = 'default' in property_data |
||
233 | default_value = property_data.get('default', None) |
||
234 | property_schema = schema['properties'][property_name] |
||
235 | |||
236 | if (has_default_value or is_optional) and default_value is None: |
||
237 | # If property is anyOf and oneOf then it has to be process differently. |
||
238 | if (is_property_type_anyof(property_schema) and |
||
239 | not is_property_nullable(property_schema['anyOf'])): |
||
240 | property_schema['anyOf'].append({'type': 'null'}) |
||
241 | elif (is_property_type_oneof(property_schema) and |
||
242 | not is_property_nullable(property_schema['oneOf'])): |
||
243 | property_schema['oneOf'].append({'type': 'null'}) |
||
244 | elif (is_property_type_list(property_schema) and |
||
245 | not is_property_nullable(property_schema.get('type'))): |
||
246 | property_schema['type'].append('null') |
||
247 | elif (is_property_type_single(property_schema) and |
||
248 | not is_property_nullable(property_schema.get('type'))): |
||
249 | property_schema['type'] = [property_schema.get('type', 'string'), 'null'] |
||
250 | |||
251 | # Support for nested properties (array and object) |
||
252 | attribute_type = property_data.get('type', None) |
||
253 | schema_items = property_data.get('items', {}) |
||
254 | |||
255 | # Array |
||
256 | if (is_attribute_type_array(attribute_type) and |
||
257 | schema_items and schema_items.get('properties', {})): |
||
258 | array_schema = schema_items |
||
259 | array_schema = modify_schema_allow_default_none(schema=array_schema) |
||
260 | schema['properties'][property_name]['items'] = array_schema |
||
261 | |||
262 | # Object |
||
263 | if is_attribute_type_object(attribute_type) and property_data.get('properties', {}): |
||
264 | object_schema = property_data |
||
265 | object_schema = modify_schema_allow_default_none(schema=object_schema) |
||
266 | schema['properties'][property_name] = object_schema |
||
267 | |||
268 | return schema |
||
269 | |||
270 | |||
271 | def validate(instance, schema, cls=None, use_default=True, allow_default_none=False, *args, |
||
272 | **kwargs): |
||
273 | """ |
||
274 | Custom validate function which supports default arguments combined with the "required" |
||
275 | property. |
||
276 | |||
277 | Note: This function returns cleaned instance with default values assigned. |
||
278 | |||
279 | :param use_default: True to support the use of the optional "default" property. |
||
280 | :type use_default: ``bool`` |
||
281 | """ |
||
282 | |||
283 | instance = copy.deepcopy(instance) |
||
284 | schema_type = schema.get('type', None) |
||
285 | instance_is_dict = isinstance(instance, dict) |
||
286 | |||
287 | if use_default and allow_default_none: |
||
288 | schema = modify_schema_allow_default_none(schema=schema) |
||
289 | |||
290 | if use_default and schema_type == 'object' and instance_is_dict: |
||
291 | instance = assign_default_values(instance=instance, schema=schema) |
||
292 | |||
293 | # pylint: disable=assignment-from-no-return |
||
294 | jsonschema.validate(instance=instance, schema=schema, cls=cls, *args, **kwargs) |
||
295 | |||
296 | return instance |
||
297 | |||
298 | |||
299 | VALIDATORS = { |
||
300 | 'draft4': jsonschema.Draft4Validator, |
||
301 | 'custom': CustomValidator |
||
302 | } |
||
303 | |||
304 | |||
305 | def get_validator(version='custom'): |
||
306 | validator = VALIDATORS[version] |
||
307 | return validator |
||
308 | |||
309 | |||
310 | def validate_runner_parameter_attribute_override(action_ref, param_name, attr_name, |
||
311 | runner_param_attr_value, action_param_attr_value): |
||
312 | """ |
||
313 | Validate that the provided parameter from the action schema can override the |
||
314 | runner parameter. |
||
315 | """ |
||
316 | param_values_are_the_same = action_param_attr_value == runner_param_attr_value |
||
317 | if (attr_name not in RUNNER_PARAM_OVERRIDABLE_ATTRS and not param_values_are_the_same): |
||
0 ignored issues
–
show
Unused Code
Coding Style
introduced
by
Loading history...
|
|||
318 | raise InvalidActionParameterException( |
||
319 | 'The attribute "%s" for the runner parameter "%s" in action "%s" ' |
||
320 | 'cannot be overridden.' % (attr_name, param_name, action_ref)) |
||
321 | |||
322 | return True |
||
323 | |||
324 | |||
325 | def get_schema_for_action_parameters(action_db): |
||
326 | """ |
||
327 | Dynamically construct JSON schema for the provided action from the parameters metadata. |
||
328 | |||
329 | Note: This schema is used to validate parameters which are passed to the action. |
||
330 | """ |
||
331 | from st2common.util.action_db import get_runnertype_by_name |
||
332 | runner_type = get_runnertype_by_name(action_db.runner_type['name']) |
||
333 | |||
334 | # Note: We need to perform a deep merge because user can only specify a single parameter |
||
335 | # attribute when overriding it in an action metadata. |
||
336 | parameters_schema = {} |
||
337 | deep_update(parameters_schema, runner_type.runner_parameters) |
||
338 | deep_update(parameters_schema, action_db.parameters) |
||
339 | |||
340 | # Perform validation, make sure user is not providing parameters which can't |
||
341 | # be overriden |
||
342 | runner_parameter_names = list(runner_type.runner_parameters.keys()) |
||
343 | |||
344 | for name, schema in six.iteritems(action_db.parameters): |
||
345 | if name not in runner_parameter_names: |
||
346 | continue |
||
347 | |||
348 | for attribute, value in six.iteritems(schema): |
||
349 | runner_param_value = runner_type.runner_parameters[name].get(attribute) |
||
350 | validate_runner_parameter_attribute_override(action_ref=action_db.ref, |
||
351 | param_name=name, |
||
352 | attr_name=attribute, |
||
353 | runner_param_attr_value=runner_param_value, |
||
354 | action_param_attr_value=value) |
||
355 | |||
356 | schema = get_schema_for_resource_parameters(parameters_schema=parameters_schema) |
||
357 | |||
358 | if parameters_schema: |
||
359 | schema['title'] = action_db.name |
||
360 | if action_db.description: |
||
361 | schema['description'] = action_db.description |
||
362 | |||
363 | return schema |
||
364 | |||
365 | |||
366 | def get_schema_for_resource_parameters(parameters_schema, allow_additional_properties=False): |
||
367 | """ |
||
368 | Dynamically construct JSON schema for the provided resource from the parameters metadata. |
||
369 | """ |
||
370 | def normalize(x): |
||
371 | return {k: v if v else SCHEMA_ANY_TYPE for k, v in six.iteritems(x)} |
||
372 | |||
373 | schema = {} |
||
374 | properties = {} |
||
375 | properties.update(normalize(parameters_schema)) |
||
376 | if properties: |
||
377 | schema['type'] = 'object' |
||
378 | schema['properties'] = properties |
||
379 | schema['additionalProperties'] = allow_additional_properties |
||
380 | |||
381 | return schema |
||
382 |