blob: 5ba7f067c094b58d019830598a42b1647efa1fb2 [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 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:
29 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:
33 The value of the field.
34 """
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:
57 fields (str): The fields being checked. Can be . separated nested
58 fields.
59 """
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(
72 "%s path does not exist: %s" % (field, value)
73 )
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
Alex Kleinbebccd52021-01-22 13:37:35 -070082def is_in(field: str, values: Iterable):
Alex Klein1699fab2022-09-08 08:46:06 -060083 """Validate |field| is an element of |values|.
Alex Klein231d2da2019-07-22 16:44:45 -060084
Alex Klein1699fab2022-09-08 08:46:06 -060085 Args:
86 field: The field being checked. May be . separated nested fields.
87 values: The possible values field may take.
88 """
89 assert field
90 assert values
Alex Klein231d2da2019-07-22 16:44:45 -060091
Alex Klein1699fab2022-09-08 08:46:06 -060092 def decorator(func):
93 @functools.wraps(func)
94 def _is_in(input_proto, output_proto, config, *args, **kwargs):
95 if config.do_validation:
96 logging.debug("Validating %s is in %r", field, values)
97 value = _value(field, input_proto)
Alex Klein231d2da2019-07-22 16:44:45 -060098
Alex Klein1699fab2022-09-08 08:46:06 -060099 if value not in values:
100 cros_build_lib.Die(
101 "%s (%r) must be in %r", field, value, values
102 )
Alex Klein231d2da2019-07-22 16:44:45 -0600103
Alex Klein1699fab2022-09-08 08:46:06 -0600104 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein231d2da2019-07-22 16:44:45 -0600105
Alex Klein1699fab2022-09-08 08:46:06 -0600106 return _is_in
Alex Klein231d2da2019-07-22 16:44:45 -0600107
Alex Klein1699fab2022-09-08 08:46:06 -0600108 return decorator
Alex Klein231d2da2019-07-22 16:44:45 -0600109
110
Alex Klein1699fab2022-09-08 08:46:06 -0600111def each_in(
112 field: str,
113 subfield: Optional[str],
114 values: Iterable,
115 optional: bool = False,
116):
117 """Validate each |subfield| of the repeated |field| is in |values|.
Alex Kleinbdace302020-12-03 14:40:23 -0700118
Alex Klein1699fab2022-09-08 08:46:06 -0600119 Args:
120 field: The field being checked. May be . separated nested fields.
121 subfield: The field in the repeated |field| to validate, or None
122 when |field| is not a repeated message, e.g. enum, scalars.
123 values: The possible values field may take.
124 optional: Also allow the field to be empty when True.
125 """
126 assert field
127 assert values
Alex Kleinbdace302020-12-03 14:40:23 -0700128
Alex Klein1699fab2022-09-08 08:46:06 -0600129 def decorator(func):
130 @functools.wraps(func)
131 def _is_in(input_proto, output_proto, config, *args, **kwargs):
132 if config.do_validation:
133 members = _value(field, input_proto) or []
134 if not optional and not members:
135 cros_build_lib.Die("The %s field is empty.", field)
136 for member in members:
137 logging.debug(
138 "Validating %s.[each].%s is in %r.",
139 field,
140 subfield,
141 values,
142 )
143 value = _value(subfield, member)
144 if value not in values:
145 cros_build_lib.Die(
146 "%s.[each].%s (%r) must be in %r is required.",
147 field,
148 subfield,
149 value,
150 values,
151 )
Alex Kleinbdace302020-12-03 14:40:23 -0700152
Alex Klein1699fab2022-09-08 08:46:06 -0600153 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Kleinbdace302020-12-03 14:40:23 -0700154
Alex Klein1699fab2022-09-08 08:46:06 -0600155 return _is_in
Alex Kleinbdace302020-12-03 14:40:23 -0700156
Alex Klein1699fab2022-09-08 08:46:06 -0600157 return decorator
Alex Kleinbdace302020-12-03 14:40:23 -0700158
159
Sean McAllister17eed8d2021-09-21 10:41:16 -0600160def constraint(description):
Alex Klein1699fab2022-09-08 08:46:06 -0600161 """Define a function to be used as a constraint check.
Sean McAllister17eed8d2021-09-21 10:41:16 -0600162
Alex Klein1699fab2022-09-08 08:46:06 -0600163 A constraint is a function that checks the value of a field and either
164 does nothing (returns None) or returns a string indicating why the value
165 isn't valid.
Sean McAllister17eed8d2021-09-21 10:41:16 -0600166
Alex Klein1699fab2022-09-08 08:46:06 -0600167 We bind a human readable description to the constraint for error reporting
168 and logging.
Sean McAllister17eed8d2021-09-21 10:41:16 -0600169
Alex Klein1699fab2022-09-08 08:46:06 -0600170 Args:
171 description: Human readable description of the constraint
172 """
Sean McAllister17eed8d2021-09-21 10:41:16 -0600173
Alex Klein1699fab2022-09-08 08:46:06 -0600174 def decorator(func):
175 @functools.wraps(func)
176 def _func(*args, **kwargs):
177 func(*args, **kwargs)
Sean McAllister17eed8d2021-09-21 10:41:16 -0600178
Alex Klein1699fab2022-09-08 08:46:06 -0600179 setattr(_func, "__constraint_description__", description)
180 return _func
Sean McAllister17eed8d2021-09-21 10:41:16 -0600181
Alex Klein1699fab2022-09-08 08:46:06 -0600182 return decorator
Sean McAllister17eed8d2021-09-21 10:41:16 -0600183
184
185def check_constraint(field: str, checkfunc: Callable):
Alex Klein1699fab2022-09-08 08:46:06 -0600186 """Validate all values of |field| pass a constraint.
Sean McAllister17eed8d2021-09-21 10:41:16 -0600187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 Args:
189 field: The field being checked. May be . separated nested fields.
190 checkfunc: A constraint function to check on each value
191 """
192 assert field
193 assert constraint
Sean McAllister17eed8d2021-09-21 10:41:16 -0600194
Alex Klein1699fab2022-09-08 08:46:06 -0600195 # Get description for the constraint if it's set
196 constraint_description = getattr(
197 checkfunc,
198 "__constraint_description__",
199 checkfunc.__name__,
200 )
Sean McAllister17eed8d2021-09-21 10:41:16 -0600201
Alex Klein1699fab2022-09-08 08:46:06 -0600202 def decorator(func):
203 @functools.wraps(func)
204 def _check_constraint(
205 input_proto, output_proto, config, *args, **kwargs
206 ):
207 if config.do_validation:
208 values = _value(field, input_proto) or []
Sean McAllister17eed8d2021-09-21 10:41:16 -0600209
Alex Klein1699fab2022-09-08 08:46:06 -0600210 failed = []
211 for val in values:
212 msg = checkfunc(val)
213 if msg is not None:
214 failed.append((val, msg))
Sean McAllister17eed8d2021-09-21 10:41:16 -0600215
Alex Klein1699fab2022-09-08 08:46:06 -0600216 if failed:
217 msg = (
218 f"{field}.[all] one or more values failed check "
219 f'"{constraint_description}"\n'
220 )
Sean McAllister17eed8d2021-09-21 10:41:16 -0600221
Alex Klein1699fab2022-09-08 08:46:06 -0600222 for value, msg in failed:
223 msg += " %s: %s\n" % (value, msg)
224 cros_build_lib.Die(msg)
Sean McAllister17eed8d2021-09-21 10:41:16 -0600225
Alex Klein1699fab2022-09-08 08:46:06 -0600226 return func(input_proto, output_proto, config, *args, **kwargs)
Sean McAllister17eed8d2021-09-21 10:41:16 -0600227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 return _check_constraint
Sean McAllister17eed8d2021-09-21 10:41:16 -0600229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 return decorator
Sean McAllister17eed8d2021-09-21 10:41:16 -0600231
232
Mike Frysinger88e02c12019-10-01 15:05:36 -0400233# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700234def require(*fields: str):
Alex Klein1699fab2022-09-08 08:46:06 -0600235 """Verify |fields| have all been set to truthy values.
Alex Klein2b236722019-06-19 15:44:26 -0600236
Alex Klein1699fab2022-09-08 08:46:06 -0600237 Args:
238 fields: The fields being checked. May be . separated nested fields.
239 """
240 assert fields
Alex Klein2b236722019-06-19 15:44:26 -0600241
Alex Klein1699fab2022-09-08 08:46:06 -0600242 def decorator(func):
243 @functools.wraps(func)
244 def _require(input_proto, output_proto, config, *args, **kwargs):
245 if config.do_validation:
246 for field in fields:
247 logging.debug("Validating %s is set.", field)
Alex Klein2b236722019-06-19 15:44:26 -0600248
Alex Klein1699fab2022-09-08 08:46:06 -0600249 value = _value(field, input_proto)
250 if not value:
251 cros_build_lib.Die("%s is required.", field)
Alex Klein2b236722019-06-19 15:44:26 -0600252
Alex Klein1699fab2022-09-08 08:46:06 -0600253 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein2b236722019-06-19 15:44:26 -0600254
Alex Klein1699fab2022-09-08 08:46:06 -0600255 return _require
Alex Klein2b236722019-06-19 15:44:26 -0600256
Alex Klein1699fab2022-09-08 08:46:06 -0600257 return decorator
Alex Klein69339cc2019-07-22 14:08:35 -0600258
259
Alex Klein60c80522020-10-13 18:05:38 -0600260# pylint: disable=docstring-misnamed-args
Alex Kleinbebccd52021-01-22 13:37:35 -0700261def require_any(*fields: str):
Alex Klein1699fab2022-09-08 08:46:06 -0600262 """Verify at least one of |fields| have been set.
Alex Klein60c80522020-10-13 18:05:38 -0600263
Alex Klein1699fab2022-09-08 08:46:06 -0600264 Args:
265 fields: The fields being checked. May be . separated nested fields.
266 """
267 assert fields
Alex Klein60c80522020-10-13 18:05:38 -0600268
Alex Klein1699fab2022-09-08 08:46:06 -0600269 def decorator(func):
270 @functools.wraps(func)
271 def _require(input_proto, output_proto, config, *args, **kwargs):
272 if config.do_validation:
273 for field in fields:
274 logging.debug("Validating %s is set.", field)
275 value = _value(field, input_proto)
276 if value:
277 break
278 else:
279 cros_build_lib.Die(
280 "At least one of the following must be set: %s",
281 ", ".join(fields),
282 )
Alex Klein60c80522020-10-13 18:05:38 -0600283
Alex Klein1699fab2022-09-08 08:46:06 -0600284 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein60c80522020-10-13 18:05:38 -0600285
Alex Klein1699fab2022-09-08 08:46:06 -0600286 return _require
Alex Klein60c80522020-10-13 18:05:38 -0600287
Alex Klein1699fab2022-09-08 08:46:06 -0600288 return decorator
Alex Klein60c80522020-10-13 18:05:38 -0600289
290
Alex Klein1699fab2022-09-08 08:46:06 -0600291def require_each(
292 field: str, subfields: Iterable[str], allow_empty: bool = True
293):
294 """Verify |field| each have all of the |subfields| set.
Alex Klein86242bf2020-09-22 15:23:23 -0600295
Alex Klein1699fab2022-09-08 08:46:06 -0600296 When |allow_empty| is True, |field| may be empty, and |subfields| are only
297 validated when it is not empty. When |allow_empty| is False, |field| must
298 also have at least one entry.
Alex Klein86242bf2020-09-22 15:23:23 -0600299
Alex Klein1699fab2022-09-08 08:46:06 -0600300 Args:
301 field: The repeated field being checked. May be . separated nested
302 fields.
303 subfields: The fields of the repeated message to validate.
304 allow_empty: Also require at least one entry in the repeated field.
305 """
306 assert field
307 assert subfields
308 assert not isinstance(subfields, str)
Alex Klein86242bf2020-09-22 15:23:23 -0600309
Alex Klein1699fab2022-09-08 08:46:06 -0600310 def decorator(func):
311 @functools.wraps(func)
312 def _require_each(input_proto, output_proto, config, *args, **kwargs):
313 if config.do_validation:
314 members = _value(field, input_proto) or []
315 if not allow_empty and not members:
316 cros_build_lib.Die("The %s field is empty.", field)
317 for member in members:
318 for subfield in subfields:
319 logging.debug(
320 "Validating %s.[each].%s is set.", field, subfield
321 )
322 value = _value(subfield, member)
323 if not value:
324 cros_build_lib.Die("%s is required.", field)
Alex Klein86242bf2020-09-22 15:23:23 -0600325
Alex Klein1699fab2022-09-08 08:46:06 -0600326 return func(input_proto, output_proto, config, *args, **kwargs)
Alex Klein86242bf2020-09-22 15:23:23 -0600327
Alex Klein1699fab2022-09-08 08:46:06 -0600328 return _require_each
Alex Klein86242bf2020-09-22 15:23:23 -0600329
Alex Klein1699fab2022-09-08 08:46:06 -0600330 return decorator
Alex Klein86242bf2020-09-22 15:23:23 -0600331
332
Alex Kleinbebccd52021-01-22 13:37:35 -0700333def validation_complete(func: Callable):
Alex Klein1699fab2022-09-08 08:46:06 -0600334 """Automatically skip the endpoint when called after all other validators.
Alex Klein69339cc2019-07-22 14:08:35 -0600335
Alex Klein1699fab2022-09-08 08:46:06 -0600336 This decorator MUST be applied after all other validate decorators.
337 The config can be checked manually if there is non-decorator validation, but
338 this is much cleaner if it is all done in decorators.
339 """
Alex Klein4de25e82019-08-05 15:58:39 -0600340
Alex Klein1699fab2022-09-08 08:46:06 -0600341 @functools.wraps(func)
342 def _validate_only(request, response, configs, *args, **kwargs):
343 if configs.validate_only:
344 # Avoid calling the endpoint.
345 return 0
346 else:
347 return func(request, response, configs, *args, **kwargs)
Alex Klein69339cc2019-07-22 14:08:35 -0600348
Alex Klein1699fab2022-09-08 08:46:06 -0600349 return _validate_only