blob: f8515548fc84f8eaa13dc98b0a005fc382459efd [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2019 The ChromiumOS Authors
Alex Klein2b236722019-06-19 15:44:26 -06002# 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
Greg Edelston53d34a12023-03-27 15:43:53 -060016from typing import Any, 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 Klein1699fab2022-09-08 08:46:06 -060026 """Helper function to fetch the value of the field.
Alex Klein2b236722019-06-19 15:44:26 -060027
Alex Klein1699fab2022-09-08 08:46:06 -060028 Args:
Alex Kleina0442682022-10-10 13:47:38 -060029 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
Alex Klein1699fab2022-09-08 08:46:06 -060032 Returns:
Alex Kleina0442682022-10-10 13:47:38 -060033 The value of the field.
Alex Klein1699fab2022-09-08 08:46:06 -060034 """
35 if not field:
36 return message
Alex Kleinbdace302020-12-03 14:40:23 -070037
Alex Klein1699fab2022-09-08 08:46:06 -060038 value = message
39 for part in field.split("."):
40 if not isinstance(value, protobuf_message.Message):
41 value = None
42 break
Alex Klein2b236722019-06-19 15:44:26 -060043
Alex Klein1699fab2022-09-08 08:46:06 -060044 try:
45 value = getattr(value, part)
46 except AttributeError as e:
47 cros_build_lib.Die("Invalid field: %s", e)
Alex Klein2b236722019-06-19 15:44:26 -060048
Alex Klein1699fab2022-09-08 08:46:06 -060049 return value
Alex Klein2b236722019-06-19 15:44:26 -060050
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 Klein1699fab2022-09-08 08:46:06 -060054 """Validate that the paths in |fields| exist.
Alex Klein2b236722019-06-19 15:44:26 -060055
Alex Klein1699fab2022-09-08 08:46:06 -060056 Args:
Alex Kleina0442682022-10-10 13:47:38 -060057 fields (str): The fields being checked. Can be . separated nested
58 fields.
Alex Klein1699fab2022-09-08 08:46:06 -060059 """
60 assert fields
Alex Klein2b236722019-06-19 15:44:26 -060061
Alex Klein1699fab2022-09-08 08:46:06 -060062 def decorator(func):
63 @functools.wraps(func)
64 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 Klein1699fab2022-09-08 08:46:06 -060069 value = _value(field, input_proto)
70 if not value or not os.path.exists(value):
71 cros_build_lib.Die(
Alex Kleindf8ee502022-10-18 09:48:15 -060072 "%s path does not exist: %s", field, value
Alex Klein1699fab2022-09-08 08:46:06 -060073 )
Alex Klein2b236722019-06-19 15:44:26 -060074
Alex Klein1699fab2022-09-08 08:46:06 -060075 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -060076
Alex Klein1699fab2022-09-08 08:46:06 -060077 return _exists
Alex Klein2b236722019-06-19 15:44:26 -060078
Alex Klein1699fab2022-09-08 08:46:06 -060079 return decorator
Alex Klein2b236722019-06-19 15:44:26 -060080
81
Greg Edelston53d34a12023-03-27 15:43:53 -060082def eq(field: str, expected_value: Any):
83 """Validate |field| is set to |expected_value|.
84
85 Args:
86 field: The field being checked. May be a `.` separated field.
87 expected_value: The value to which the field must be equal.
88 """
89 assert field
90
91 def decorator(func):
92 @functools.wraps(func)
93 def _eq(input_proto, output_proto, config, *args, **kwargs):
94 if config.do_validation:
95 logging.debug(
96 "Validating %s is equal to %r", field, expected_value
97 )
98 actual_value = _value(field, input_proto)
99
100 if actual_value != expected_value:
101 cros_build_lib.Die(
102 "%s (%r) must be equal to %r",
103 field,
104 actual_value,
105 expected_value,
106 )
107
108 return func(input_proto, output_proto, config, *args, **kwargs)
109
110 return _eq
111
112 return decorator
113
114
Alex Kleinbebccd52021-01-22 13:37:35 -0700115def is_in(field: str, values: Iterable):
Alex Klein1699fab2022-09-08 08:46:06 -0600116 """Validate |field| is an element of |values|.
Alex Klein231d2da2019-07-22 16:44:45 -0600117
Alex Klein1699fab2022-09-08 08:46:06 -0600118 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600119 field: The field being checked. May be . separated nested fields.
120 values: The possible values field may take.
Alex Klein1699fab2022-09-08 08:46:06 -0600121 """
122 assert field
123 assert values
Alex Klein231d2da2019-07-22 16:44:45 -0600124
Alex Klein1699fab2022-09-08 08:46:06 -0600125 def decorator(func):
126 @functools.wraps(func)
127 def _is_in(input_proto, output_proto, config, *args, **kwargs):
128 if config.do_validation:
129 logging.debug("Validating %s is in %r", field, values)
130 value = _value(field, input_proto)
Alex Klein231d2da2019-07-22 16:44:45 -0600131
Alex Klein1699fab2022-09-08 08:46:06 -0600132 if value not in values:
133 cros_build_lib.Die(
134 "%s (%r) must be in %r", field, value, values
135 )
Alex Klein231d2da2019-07-22 16:44:45 -0600136
Alex Klein1699fab2022-09-08 08:46:06 -0600137 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein231d2da2019-07-22 16:44:45 -0600138
Alex Klein1699fab2022-09-08 08:46:06 -0600139 return _is_in
Alex Klein231d2da2019-07-22 16:44:45 -0600140
Alex Klein1699fab2022-09-08 08:46:06 -0600141 return decorator
Alex Klein231d2da2019-07-22 16:44:45 -0600142
143
Alex Klein1699fab2022-09-08 08:46:06 -0600144def each_in(
145 field: str,
146 subfield: Optional[str],
147 values: Iterable,
148 optional: bool = False,
149):
150 """Validate each |subfield| of the repeated |field| is in |values|.
Alex Kleinbdace302020-12-03 14:40:23 -0700151
Alex Klein1699fab2022-09-08 08:46:06 -0600152 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600153 field: The field being checked. May be . separated nested fields.
154 subfield: The field in the repeated |field| to validate, or None
155 when |field| is not a repeated message, e.g. enum, scalars.
156 values: The possible values field may take.
157 optional: Also allow the field to be empty when True.
Alex Klein1699fab2022-09-08 08:46:06 -0600158 """
159 assert field
160 assert values
Alex Kleinbdace302020-12-03 14:40:23 -0700161
Alex Klein1699fab2022-09-08 08:46:06 -0600162 def decorator(func):
163 @functools.wraps(func)
164 def _is_in(input_proto, output_proto, config, *args, **kwargs):
165 if config.do_validation:
166 members = _value(field, input_proto) or []
167 if not optional and not members:
168 cros_build_lib.Die("The %s field is empty.", field)
169 for member in members:
170 logging.debug(
171 "Validating %s.[each].%s is in %r.",
172 field,
173 subfield,
174 values,
175 )
176 value = _value(subfield, member)
177 if value not in values:
178 cros_build_lib.Die(
179 "%s.[each].%s (%r) must be in %r is required.",
180 field,
181 subfield,
182 value,
183 values,
184 )
Alex Kleinbdace302020-12-03 14:40:23 -0700185
Alex Klein1699fab2022-09-08 08:46:06 -0600186 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Kleinbdace302020-12-03 14:40:23 -0700187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 return _is_in
Alex Kleinbdace302020-12-03 14:40:23 -0700189
Alex Klein1699fab2022-09-08 08:46:06 -0600190 return decorator
Alex Kleinbdace302020-12-03 14:40:23 -0700191
192
Sean McAllister17eed8d2021-09-21 10:41:16 -0600193def constraint(description):
Alex Klein1699fab2022-09-08 08:46:06 -0600194 """Define a function to be used as a constraint check.
Sean McAllister17eed8d2021-09-21 10:41:16 -0600195
Alex Klein1699fab2022-09-08 08:46:06 -0600196 A constraint is a function that checks the value of a field and either
197 does nothing (returns None) or returns a string indicating why the value
198 isn't valid.
Sean McAllister17eed8d2021-09-21 10:41:16 -0600199
Alex Klein1699fab2022-09-08 08:46:06 -0600200 We bind a human readable description to the constraint for error reporting
201 and logging.
Sean McAllister17eed8d2021-09-21 10:41:16 -0600202
Alex Klein1699fab2022-09-08 08:46:06 -0600203 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600204 description: Human readable description of the constraint
Alex Klein1699fab2022-09-08 08:46:06 -0600205 """
Sean McAllister17eed8d2021-09-21 10:41:16 -0600206
Alex Klein1699fab2022-09-08 08:46:06 -0600207 def decorator(func):
208 @functools.wraps(func)
209 def _func(*args, **kwargs):
210 func(*args, **kwargs)
Sean McAllister17eed8d2021-09-21 10:41:16 -0600211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 setattr(_func, "__constraint_description__", description)
213 return _func
Sean McAllister17eed8d2021-09-21 10:41:16 -0600214
Alex Klein1699fab2022-09-08 08:46:06 -0600215 return decorator
Sean McAllister17eed8d2021-09-21 10:41:16 -0600216
217
218def check_constraint(field: str, checkfunc: Callable):
Alex Klein1699fab2022-09-08 08:46:06 -0600219 """Validate all values of |field| pass a constraint.
Sean McAllister17eed8d2021-09-21 10:41:16 -0600220
Alex Klein1699fab2022-09-08 08:46:06 -0600221 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600222 field: The field being checked. May be . separated nested fields.
223 checkfunc: A constraint function to check on each value
Alex Klein1699fab2022-09-08 08:46:06 -0600224 """
225 assert field
226 assert constraint
Sean McAllister17eed8d2021-09-21 10:41:16 -0600227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 # Get description for the constraint if it's set
229 constraint_description = getattr(
230 checkfunc,
231 "__constraint_description__",
232 checkfunc.__name__,
233 )
Sean McAllister17eed8d2021-09-21 10:41:16 -0600234
Alex Klein1699fab2022-09-08 08:46:06 -0600235 def decorator(func):
236 @functools.wraps(func)
237 def _check_constraint(
238 input_proto, output_proto, config, *args, **kwargs
239 ):
240 if config.do_validation:
241 values = _value(field, input_proto) or []
Sean McAllister17eed8d2021-09-21 10:41:16 -0600242
Alex Klein1699fab2022-09-08 08:46:06 -0600243 failed = []
244 for val in values:
245 msg = checkfunc(val)
246 if msg is not None:
247 failed.append((val, msg))
Sean McAllister17eed8d2021-09-21 10:41:16 -0600248
Alex Klein1699fab2022-09-08 08:46:06 -0600249 if failed:
250 msg = (
251 f"{field}.[all] one or more values failed check "
252 f'"{constraint_description}"\n'
253 )
Sean McAllister17eed8d2021-09-21 10:41:16 -0600254
Alex Klein1699fab2022-09-08 08:46:06 -0600255 for value, msg in failed:
256 msg += " %s: %s\n" % (value, msg)
257 cros_build_lib.Die(msg)
Sean McAllister17eed8d2021-09-21 10:41:16 -0600258
Alex Klein1699fab2022-09-08 08:46:06 -0600259 return func(input_proto, output_proto, config, *args, **kwargs)
Sean McAllister17eed8d2021-09-21 10:41:16 -0600260
Alex Klein1699fab2022-09-08 08:46:06 -0600261 return _check_constraint
Sean McAllister17eed8d2021-09-21 10:41:16 -0600262
Alex Klein1699fab2022-09-08 08:46:06 -0600263 return decorator
Sean McAllister17eed8d2021-09-21 10:41:16 -0600264
265
Mike Frysinger88e02c12019-10-01 15:05:36 -0400266# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700267def require(*fields: str):
Alex Klein1699fab2022-09-08 08:46:06 -0600268 """Verify |fields| have all been set to truthy values.
Alex Klein2b236722019-06-19 15:44:26 -0600269
Alex Klein1699fab2022-09-08 08:46:06 -0600270 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600271 fields: The fields being checked. May be . separated nested fields.
Alex Klein1699fab2022-09-08 08:46:06 -0600272 """
273 assert fields
Alex Klein2b236722019-06-19 15:44:26 -0600274
Alex Klein1699fab2022-09-08 08:46:06 -0600275 def decorator(func):
276 @functools.wraps(func)
277 def _require(input_proto, output_proto, config, *args, **kwargs):
278 if config.do_validation:
279 for field in fields:
280 logging.debug("Validating %s is set.", field)
Alex Klein2b236722019-06-19 15:44:26 -0600281
Alex Klein1699fab2022-09-08 08:46:06 -0600282 value = _value(field, input_proto)
283 if not value:
284 cros_build_lib.Die("%s is required.", field)
Alex Klein2b236722019-06-19 15:44:26 -0600285
Alex Klein1699fab2022-09-08 08:46:06 -0600286 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -0600287
Alex Klein1699fab2022-09-08 08:46:06 -0600288 return _require
Alex Klein2b236722019-06-19 15:44:26 -0600289
Alex Klein1699fab2022-09-08 08:46:06 -0600290 return decorator
Alex Klein69339cc2019-07-22 14:08:35 -0600291
292
Alex Klein60c80522020-10-13 18:05:38 -0600293# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700294def require_any(*fields: str):
Alex Klein1699fab2022-09-08 08:46:06 -0600295 """Verify at least one of |fields| have been set.
Alex Klein60c80522020-10-13 18:05:38 -0600296
Alex Klein1699fab2022-09-08 08:46:06 -0600297 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600298 fields: The fields being checked. May be . separated nested fields.
Alex Klein1699fab2022-09-08 08:46:06 -0600299 """
300 assert fields
Alex Klein60c80522020-10-13 18:05:38 -0600301
Alex Klein1699fab2022-09-08 08:46:06 -0600302 def decorator(func):
303 @functools.wraps(func)
304 def _require(input_proto, output_proto, config, *args, **kwargs):
305 if config.do_validation:
306 for field in fields:
307 logging.debug("Validating %s is set.", field)
308 value = _value(field, input_proto)
309 if value:
310 break
311 else:
312 cros_build_lib.Die(
313 "At least one of the following must be set: %s",
314 ", ".join(fields),
315 )
Alex Klein60c80522020-10-13 18:05:38 -0600316
Alex Klein1699fab2022-09-08 08:46:06 -0600317 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein60c80522020-10-13 18:05:38 -0600318
Alex Klein1699fab2022-09-08 08:46:06 -0600319 return _require
Alex Klein60c80522020-10-13 18:05:38 -0600320
Alex Klein1699fab2022-09-08 08:46:06 -0600321 return decorator
Alex Klein60c80522020-10-13 18:05:38 -0600322
323
Alex Klein1699fab2022-09-08 08:46:06 -0600324def require_each(
325 field: str, subfields: Iterable[str], allow_empty: bool = True
326):
327 """Verify |field| each have all of the |subfields| set.
Alex Klein86242bf2020-09-22 15:23:23 -0600328
Alex Klein1699fab2022-09-08 08:46:06 -0600329 When |allow_empty| is True, |field| may be empty, and |subfields| are only
330 validated when it is not empty. When |allow_empty| is False, |field| must
331 also have at least one entry.
Alex Klein86242bf2020-09-22 15:23:23 -0600332
Alex Klein1699fab2022-09-08 08:46:06 -0600333 Args:
Alex Kleina0442682022-10-10 13:47:38 -0600334 field: The repeated field being checked. May be . separated nested
335 fields.
336 subfields: The fields of the repeated message to validate.
337 allow_empty: Also require at least one entry in the repeated field.
Alex Klein1699fab2022-09-08 08:46:06 -0600338 """
339 assert field
340 assert subfields
341 assert not isinstance(subfields, str)
Alex Klein86242bf2020-09-22 15:23:23 -0600342
Alex Klein1699fab2022-09-08 08:46:06 -0600343 def decorator(func):
344 @functools.wraps(func)
345 def _require_each(input_proto, output_proto, config, *args, **kwargs):
346 if config.do_validation:
347 members = _value(field, input_proto) or []
348 if not allow_empty and not members:
349 cros_build_lib.Die("The %s field is empty.", field)
350 for member in members:
351 for subfield in subfields:
352 logging.debug(
353 "Validating %s.[each].%s is set.", field, subfield
354 )
355 value = _value(subfield, member)
356 if not value:
357 cros_build_lib.Die("%s is required.", field)
Alex Klein86242bf2020-09-22 15:23:23 -0600358
Alex Klein1699fab2022-09-08 08:46:06 -0600359 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein86242bf2020-09-22 15:23:23 -0600360
Alex Klein1699fab2022-09-08 08:46:06 -0600361 return _require_each
Alex Klein86242bf2020-09-22 15:23:23 -0600362
Alex Klein1699fab2022-09-08 08:46:06 -0600363 return decorator
Alex Klein86242bf2020-09-22 15:23:23 -0600364
365
Alex Kleinbebccd52021-01-22 13:37:35 -0700366def validation_complete(func: Callable):
Alex Klein1699fab2022-09-08 08:46:06 -0600367 """Automatically skip the endpoint when called after all other validators.
Alex Klein69339cc2019-07-22 14:08:35 -0600368
Alex Klein1699fab2022-09-08 08:46:06 -0600369 This decorator MUST be applied after all other validate decorators.
370 The config can be checked manually if there is non-decorator validation, but
371 this is much cleaner if it is all done in decorators.
372 """
Alex Klein4de25e82019-08-05 15:58:39 -0600373
Alex Klein1699fab2022-09-08 08:46:06 -0600374 @functools.wraps(func)
375 def _validate_only(request, response, configs, *args, **kwargs):
376 if configs.validate_only:
377 # Avoid calling the endpoint.
378 return 0
379 else:
380 return func(request, response, configs, *args, **kwargs)
Alex Klein69339cc2019-07-22 14:08:35 -0600381
Alex Klein1699fab2022-09-08 08:46:06 -0600382 return _validate_only