blob: 7501c0eea3d9b7ba581d67b891d506d578ef3850 [file] [log] [blame]
Alex Klein2b236722019-06-19 15:44:26 -06001# Copyright 2019 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Alex Klein2008aee2019-08-20 16:25:27 -06005"""Validation helpers for simple input validation in the API.
6
7Note: Every validator MUST respect config.do_validation. This is an internally
8set config option that allows the mock call decorators to be placed before or
9after the validation decorators, rather than forcing an ordering that could then
10produce incorrect outputs if missed.
11"""
Alex Klein2b236722019-06-19 15:44:26 -060012
Alex Klein4de25e82019-08-05 15:58:39 -060013import functools
Chris McDonald1672ddb2021-07-21 11:48:23 -060014import logging
Alex Klein2b236722019-06-19 15:44:26 -060015import os
Alex Kleinbebccd52021-01-22 13:37:35 -070016from typing import Callable, Iterable, List, Optional, Union
Alex Klein2b236722019-06-19 15:44:26 -060017
Mike Frysinger2c024062021-05-22 15:43:22 -040018from chromite.third_party.google.protobuf import message as protobuf_message
Mike Frysinger849d6402019-10-17 00:14:16 -040019
Alex Klein2b236722019-06-19 15:44:26 -060020from chromite.lib import cros_build_lib
Alex Klein2b236722019-06-19 15:44:26 -060021
Mike Frysingeref94e4c2020-02-10 23:59:54 -050022
Alex Kleinbebccd52021-01-22 13:37:35 -070023def _value(
24 field: str, message: protobuf_message.Message
25) -> Union[bool, int, str, None, List, protobuf_message.Message]:
Alex Klein2b236722019-06-19 15:44:26 -060026 """Helper function to fetch the value of the field.
27
28 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -070029 field: The field name. Can be nested via . separation.
30 message: The protobuf message it is being fetched from.
Alex Klein2b236722019-06-19 15:44:26 -060031
32 Returns:
Alex Kleinbebccd52021-01-22 13:37:35 -070033 The value of the field.
Alex Klein2b236722019-06-19 15:44:26 -060034 """
Alex Kleinbdace302020-12-03 14:40:23 -070035 if not field:
36 return message
37
Alex Klein2b236722019-06-19 15:44:26 -060038 value = message
39 for part in field.split('.'):
40 if not isinstance(value, protobuf_message.Message):
41 value = None
42 break
43
44 try:
45 value = getattr(value, part)
46 except AttributeError as e:
Mike Frysinger6b5c3cd2019-08-27 16:51:00 -040047 cros_build_lib.Die('Invalid field: %s', e)
Alex Klein2b236722019-06-19 15:44:26 -060048
49 return value
50
Alex Klein69339cc2019-07-22 14:08:35 -060051
Mike Frysinger88e02c12019-10-01 15:05:36 -040052# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -070053def exists(*fields: str):
Alex Klein2b236722019-06-19 15:44:26 -060054 """Validate that the paths in |fields| exist.
55
56 Args:
57 fields (str): The fields being checked. Can be . separated nested
58 fields.
59 """
60 assert fields
61
62 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060063 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060064 def _exists(input_proto, output_proto, config, *args, **kwargs):
65 if config.do_validation:
66 for field in fields:
67 logging.debug('Validating %s exists.', field)
Alex Klein2b236722019-06-19 15:44:26 -060068
Alex Klein2008aee2019-08-20 16:25:27 -060069 value = _value(field, input_proto)
70 if not value or not os.path.exists(value):
71 cros_build_lib.Die('%s path does not exist: %s' % (field, value))
Alex Klein2b236722019-06-19 15:44:26 -060072
Alex Klein2008aee2019-08-20 16:25:27 -060073 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -060074
75 return _exists
76
77 return decorator
78
79
Alex Kleinbebccd52021-01-22 13:37:35 -070080def is_in(field: str, values: Iterable):
Alex Kleinbdace302020-12-03 14:40:23 -070081 """Validate |field| is an element of |values|.
Alex Klein231d2da2019-07-22 16:44:45 -060082
83 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -070084 field: The field being checked. May be . separated nested fields.
85 values: The possible values field may take.
Alex Klein231d2da2019-07-22 16:44:45 -060086 """
87 assert field
88 assert values
89
90 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -060091 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -060092 def _is_in(input_proto, output_proto, config, *args, **kwargs):
93 if config.do_validation:
94 logging.debug('Validating %s is in %r', field, values)
95 value = _value(field, input_proto)
Alex Klein231d2da2019-07-22 16:44:45 -060096
Alex Klein2008aee2019-08-20 16:25:27 -060097 if value not in values:
98 cros_build_lib.Die('%s (%r) must be in %r', field, value, values)
Alex Klein231d2da2019-07-22 16:44:45 -060099
Alex Klein2008aee2019-08-20 16:25:27 -0600100 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein231d2da2019-07-22 16:44:45 -0600101
102 return _is_in
103
104 return decorator
105
106
Alex Kleinbebccd52021-01-22 13:37:35 -0700107def each_in(field: str,
108 subfield: Optional[str],
109 values: Iterable,
110 optional: bool = False):
Alex Kleinbdace302020-12-03 14:40:23 -0700111 """Validate each |subfield| of the repeated |field| is in |values|.
112
113 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700114 field: The field being checked. May be . separated nested fields.
115 subfield: The field in the repeated |field| to validate, or None
Alex Kleinbdace302020-12-03 14:40:23 -0700116 when |field| is not a repeated message, e.g. enum, scalars.
Alex Kleinbebccd52021-01-22 13:37:35 -0700117 values: The possible values field may take.
118 optional: Also allow the field to be empty when True.
Alex Kleinbdace302020-12-03 14:40:23 -0700119 """
120 assert field
121 assert values
122
123 def decorator(func):
124 @functools.wraps(func)
125 def _is_in(input_proto, output_proto, config, *args, **kwargs):
126 if config.do_validation:
127 members = _value(field, input_proto) or []
128 if not optional and not members:
129 cros_build_lib.Die('The %s field is empty.', field)
130 for member in members:
131 logging.debug('Validating %s.[each].%s is in %r.', field, subfield,
132 values)
133 value = _value(subfield, member)
134 if value not in values:
135 cros_build_lib.Die('%s.[each].%s (%r) must be in %r is required.',
136 field, subfield, value, values)
137
138 return func(input_proto, output_proto, config, *args, **kwargs)
139
140 return _is_in
141
142 return decorator
143
144
Sean McAllister17eed8d2021-09-21 10:41:16 -0600145def constraint(description):
146 """Define a function to be used as a constraint check.
147
148 A constraint is a function that checks the value of a field and either
149 does nothing (returns None) or returns a string indicating why the value
150 isn't valid.
151
152 We bind a human readable description to the constraint for error reporting
153 and logging.
154
155 Args:
156 description: Human readable description of the constraint
157 """
158
159 def decorator(func):
160 @functools.wraps(func)
161 def _func(*args, **kwargs):
162 func(*args, **kwargs)
163
164 setattr(_func, '__constraint_description__', description)
165 return _func
166
167 return decorator
168
169
170def check_constraint(field: str, checkfunc: Callable):
171 """Validate all values of |field| pass a constraint.
172
173 Args:
174 field: The field being checked. May be . separated nested fields.
175 checkfunc: A constraint function to check on each value
176 """
177 assert field
178 assert constraint
179
180 # Get description for the constraint if it's set
181 constraint_description = getattr(
182 checkfunc,
183 '__constraint_description__',
184 checkfunc.__name__,
185 )
186
187 def decorator(func):
188 @functools.wraps(func)
189 def _check_constraint(input_proto, output_proto, config, *args, **kwargs):
190 if config.do_validation:
191 values = _value(field, input_proto) or []
192
193 failed = []
194 for val in values:
195 msg = checkfunc(val)
196 if msg is not None:
197 failed.append((val, msg))
198
199 if failed:
Lizzy Preslandb2d60642021-11-12 01:41:58 +0000200 msg = f'{field}.[all] one or more values failed check ' \
201 f'"{constraint_description}"\n'
Sean McAllister17eed8d2021-09-21 10:41:16 -0600202
203 for value, msg in failed:
Lizzy Preslandb2d60642021-11-12 01:41:58 +0000204 msg += ' %s: %s\n' % (value, msg)
Sean McAllister17eed8d2021-09-21 10:41:16 -0600205 cros_build_lib.Die(msg)
206
207 return func(input_proto, output_proto, config, *args, **kwargs)
208
209 return _check_constraint
210
211 return decorator
212
213
Mike Frysinger88e02c12019-10-01 15:05:36 -0400214# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700215def require(*fields: str):
Greg Edelstondc941072021-08-11 12:32:30 -0600216 """Verify |fields| have all been set to truthy values.
Alex Klein2b236722019-06-19 15:44:26 -0600217
218 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700219 fields: The fields being checked. May be . separated nested fields.
Alex Klein2b236722019-06-19 15:44:26 -0600220 """
221 assert fields
222
223 def decorator(func):
Alex Klein4de25e82019-08-05 15:58:39 -0600224 @functools.wraps(func)
Alex Klein2008aee2019-08-20 16:25:27 -0600225 def _require(input_proto, output_proto, config, *args, **kwargs):
226 if config.do_validation:
227 for field in fields:
228 logging.debug('Validating %s is set.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600229
Alex Klein2008aee2019-08-20 16:25:27 -0600230 value = _value(field, input_proto)
231 if not value:
232 cros_build_lib.Die('%s is required.', field)
Alex Klein2b236722019-06-19 15:44:26 -0600233
Alex Klein2008aee2019-08-20 16:25:27 -0600234 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -0600235
236 return _require
237
238 return decorator
Alex Klein69339cc2019-07-22 14:08:35 -0600239
240
Alex Klein60c80522020-10-13 18:05:38 -0600241# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700242def require_any(*fields: str):
Alex Klein60c80522020-10-13 18:05:38 -0600243 """Verify at least one of |fields| have been set.
244
245 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700246 fields: The fields being checked. May be . separated nested fields.
Alex Klein60c80522020-10-13 18:05:38 -0600247 """
248 assert fields
249
250 def decorator(func):
251 @functools.wraps(func)
252 def _require(input_proto, output_proto, config, *args, **kwargs):
253 if config.do_validation:
254 for field in fields:
255 logging.debug('Validating %s is set.', field)
256 value = _value(field, input_proto)
257 if value:
258 break
259 else:
260 cros_build_lib.Die('At least one of the following must be set: %s',
261 ', '.join(fields))
262
263 return func(input_proto, output_proto, config, *args, **kwargs)
264
265 return _require
266
267 return decorator
268
269
Alex Kleinbebccd52021-01-22 13:37:35 -0700270def require_each(field: str,
271 subfields: Iterable[str],
272 allow_empty: bool = True):
Alex Klein86242bf2020-09-22 15:23:23 -0600273 """Verify |field| each have all of the |subfields| set.
274
275 When |allow_empty| is True, |field| may be empty, and |subfields| are only
276 validated when it is not empty. When |allow_empty| is False, |field| must
277 also have at least one entry.
278
279 Args:
Alex Kleinbebccd52021-01-22 13:37:35 -0700280 field: The repeated field being checked. May be . separated nested
Alex Klein86242bf2020-09-22 15:23:23 -0600281 fields.
Alex Kleinbebccd52021-01-22 13:37:35 -0700282 subfields: The fields of the repeated message to validate.
283 allow_empty: Also require at least one entry in the repeated field.
Alex Klein86242bf2020-09-22 15:23:23 -0600284 """
285 assert field
286 assert subfields
287 assert not isinstance(subfields, str)
288
289 def decorator(func):
290 @functools.wraps(func)
291 def _require_each(input_proto, output_proto, config, *args, **kwargs):
292 if config.do_validation:
293 members = _value(field, input_proto) or []
294 if not allow_empty and not members:
295 cros_build_lib.Die('The %s field is empty.', field)
296 for member in members:
297 for subfield in subfields:
298 logging.debug('Validating %s.[each].%s is set.', field, subfield)
299 value = _value(subfield, member)
300 if not value:
301 cros_build_lib.Die('%s is required.', field)
302
303 return func(input_proto, output_proto, config, *args, **kwargs)
304
305 return _require_each
306
307 return decorator
308
309
Alex Kleinbebccd52021-01-22 13:37:35 -0700310def validation_complete(func: Callable):
Alex Klein69339cc2019-07-22 14:08:35 -0600311 """Automatically skip the endpoint when called after all other validators.
312
313 This decorator MUST be applied after all other validate decorators.
314 The config can be checked manually if there is non-decorator validation, but
315 this is much cleaner if it is all done in decorators.
316 """
Alex Klein4de25e82019-08-05 15:58:39 -0600317
318 @functools.wraps(func)
Alex Klein69339cc2019-07-22 14:08:35 -0600319 def _validate_only(request, response, configs, *args, **kwargs):
320 if configs.validate_only:
321 # Avoid calling the endpoint.
322 return 0
323 else:
324 return func(request, response, configs, *args, **kwargs)
325
326 return _validate_only