"Fossies" - the Fresh Open Source Software Archive

Member "barbican-12.0.0/barbican/common/validators.py" (14 Apr 2021, 38258 Bytes) of package /linux/misc/openstack/barbican-12.0.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "validators.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 11.0.0_vs_12.0.0.

    1 #  Licensed under the Apache License, Version 2.0 (the "License"); you may
    2 #  not use this file except in compliance with the License. You may obtain
    3 #  a copy of the License at
    4 #
    5 #       http://www.apache.org/licenses/LICENSE-2.0
    6 #
    7 #  Unless required by applicable law or agreed to in writing, software
    8 #  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    9 #  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   10 #  License for the specific language governing permissions and limitations
   11 #  under the License.
   12 """
   13 API JSON validators.
   14 """
   15 
   16 import abc
   17 import base64
   18 import re
   19 
   20 import jsonschema as schema
   21 from ldap3.core import exceptions as ldap_exceptions
   22 from ldap3.utils.dn import parse_dn
   23 from OpenSSL import crypto
   24 from oslo_utils import timeutils
   25 import six
   26 
   27 from barbican.api import controllers
   28 from barbican.common import config
   29 from barbican.common import exception
   30 from barbican.common import hrefs
   31 from barbican.common import utils
   32 from barbican import i18n as u
   33 from barbican.model import models
   34 from barbican.model import repositories as repo
   35 from barbican.plugin.interface import secret_store
   36 from barbican.plugin.util import mime_types
   37 
   38 
   39 DEFAULT_MAX_SECRET_BYTES = config.DEFAULT_MAX_SECRET_BYTES
   40 LOG = utils.getLogger(__name__)
   41 CONF = config.CONF
   42 
   43 MYSQL_SMALL_INT_MAX = 32767
   44 
   45 ACL_OPERATIONS = ['read', 'write', 'delete', 'list']
   46 
   47 
   48 def secret_too_big(data):
   49     if isinstance(data, six.text_type):
   50         return len(data.encode('UTF-8')) > CONF.max_allowed_secret_in_bytes
   51     else:
   52         return len(data) > CONF.max_allowed_secret_in_bytes
   53 
   54 
   55 def get_invalid_property(validation_error):
   56     # we are interested in the second item which is the failed propertyName.
   57     if validation_error.schema_path and len(validation_error.schema_path) > 1:
   58         return validation_error.schema_path[1]
   59 
   60 
   61 def validate_stored_key_rsa_container(project_id, container_ref, req):
   62     try:
   63         container_id = hrefs.get_container_id_from_ref(container_ref)
   64     except Exception:
   65         reason = u._("Bad Container Reference {ref}").format(
   66             ref=container_ref
   67         )
   68         raise exception.InvalidContainer(reason=reason)
   69 
   70     container_repo = repo.get_container_repository()
   71 
   72     container = container_repo.get_container_by_id(entity_id=container_id,
   73                                                    suppress_exception=True)
   74     if not container:
   75         reason = u._("Container Not Found")
   76         raise exception.InvalidContainer(reason=reason)
   77 
   78     if container.type != 'rsa':
   79         reason = u._("Container Wrong Type")
   80         raise exception.InvalidContainer(reason=reason)
   81 
   82     ctxt = controllers._get_barbican_context(req)
   83     inst = controllers.containers.ContainerController(container)
   84     controllers._do_enforce_rbac(inst, req,
   85                                  controllers.containers.CONTAINER_GET,
   86                                  ctxt)
   87 
   88 
   89 class ValidatorBase(object, metaclass=abc.ABCMeta):
   90     """Base class for validators."""
   91 
   92     name = ''
   93 
   94     @abc.abstractmethod
   95     def validate(self, json_data, parent_schema=None):
   96         """Validate the input JSON.
   97 
   98         :param json_data: JSON to validate against this class' internal schema.
   99         :param parent_schema: Name of the parent schema to this schema.
  100         :returns: dict -- JSON content, post-validation and
  101         :                 normalization/defaulting.
  102         :raises: schema.ValidationError on schema violations.
  103 
  104         """
  105 
  106     def _full_name(self, parent_schema=None):
  107         """Validator schema name accessor
  108 
  109         Returns the full schema name for this validator,
  110         including parent name.
  111         """
  112         schema_name = self.name
  113         if parent_schema:
  114             schema_name = u._(
  115                 "{schema_name}' within '{parent_schema_name}").format(
  116                     schema_name=self.name,
  117                     parent_schema_name=parent_schema)
  118         return schema_name
  119 
  120     def _assert_schema_is_valid(self, json_data, schema_name):
  121         """Assert that the JSON structure is valid for the given schema.
  122 
  123         :raises: InvalidObject exception if the data is not schema compliant.
  124         """
  125         try:
  126             schema.validate(json_data, self.schema)
  127         except schema.ValidationError as e:
  128             raise exception.InvalidObject(schema=schema_name,
  129                                           reason=e.message,
  130                                           property=get_invalid_property(e))
  131 
  132     def _assert_validity(self, valid_condition, schema_name, message,
  133                          property):
  134         """Assert that a certain condition is met.
  135 
  136         :raises: InvalidObject exception if the condition is not met.
  137         """
  138         if not valid_condition:
  139             raise exception.InvalidObject(schema=schema_name, reason=message,
  140                                           property=property)
  141 
  142 
  143 class NewSecretValidator(ValidatorBase):
  144     """Validate a new secret."""
  145 
  146     def __init__(self):
  147         self.name = 'Secret'
  148 
  149         # TODO(jfwood): Get the list of mime_types from the crypto plugins?
  150         self.schema = {
  151             "type": "object",
  152             "properties": {
  153                 "name": {"type": ["string", "null"], "maxLength": 255},
  154                 "algorithm": {"type": "string", "maxLength": 255},
  155                 "mode": {"type": "string", "maxLength": 255},
  156                 "bit_length": {
  157                     "type": "integer",
  158                     "minimum": 1,
  159                     "maximum": MYSQL_SMALL_INT_MAX
  160                 },
  161                 "expiration": {"type": "string", "maxLength": 255},
  162                 "payload": {"type": "string"},
  163                 "secret_type": {
  164                     "type": "string",
  165                     "maxLength": 80,
  166                     "enum": [secret_store.SecretType.SYMMETRIC,
  167                              secret_store.SecretType.PASSPHRASE,
  168                              secret_store.SecretType.PRIVATE,
  169                              secret_store.SecretType.PUBLIC,
  170                              secret_store.SecretType.CERTIFICATE,
  171                              secret_store.SecretType.OPAQUE]
  172                 },
  173                 "payload_content_type": {
  174                     "type": ["string", "null"],
  175                     "maxLength": 255
  176                 },
  177                 "payload_content_encoding": {
  178                     "type": "string",
  179                     "maxLength": 255,
  180                     "enum": [
  181                         "base64"
  182                     ]
  183                 },
  184                 "transport_key_needed": {
  185                     "type": "string",
  186                     "enum": ["true", "false"]
  187                 },
  188                 "transport_key_id": {"type": "string"},
  189             },
  190         }
  191 
  192     def validate(self, json_data, parent_schema=None):
  193         """Validate the input JSON for the schema for secrets."""
  194         schema_name = self._full_name(parent_schema)
  195         self._assert_schema_is_valid(json_data, schema_name)
  196 
  197         json_data['name'] = self._extract_name(json_data)
  198 
  199         expiration = self._extract_expiration(json_data, schema_name)
  200         self._assert_expiration_is_valid(expiration, schema_name)
  201         json_data['expiration'] = expiration
  202         content_type = json_data.get('payload_content_type')
  203 
  204         if 'payload' in json_data:
  205             content_encoding = json_data.get('payload_content_encoding')
  206             self._validate_content_parameters(content_type, content_encoding,
  207                                               schema_name)
  208 
  209             payload = self._extract_payload(json_data)
  210             self._assert_validity(payload, schema_name,
  211                                   u._("If 'payload' specified, must be non "
  212                                       "empty"),
  213                                   "payload")
  214             self._validate_payload_by_content_encoding(content_encoding,
  215                                                        payload, schema_name)
  216             json_data['payload'] = payload
  217         elif 'payload_content_type' in json_data:
  218             # parent_schema would be populated if it comes from an order.
  219             self._assert_validity(parent_schema is not None, schema_name,
  220                                   u._("payload must be provided when "
  221                                       "payload_content_type is specified"),
  222                                   "payload")
  223 
  224             if content_type:
  225                 self._assert_validity(
  226                     mime_types.is_supported(content_type),
  227                     schema_name,
  228                     u._("payload_content_type is not one of {supported}"
  229                         ).format(supported=mime_types.SUPPORTED),
  230                     "payload_content_type")
  231 
  232         return json_data
  233 
  234     def _extract_name(self, json_data):
  235         """Extracts and returns the name from the JSON data."""
  236         name = json_data.get('name')
  237         if isinstance(name, six.string_types):
  238             return name.strip()
  239         return None
  240 
  241     def _extract_expiration(self, json_data, schema_name):
  242         """Extracts and returns the expiration date from the JSON data."""
  243         expiration = None
  244         expiration_raw = json_data.get('expiration')
  245         if expiration_raw and expiration_raw.strip():
  246             try:
  247                 expiration_tz = timeutils.parse_isotime(expiration_raw.strip())
  248                 expiration = timeutils.normalize_time(expiration_tz)
  249             except ValueError:
  250                 LOG.exception("Problem parsing expiration date")
  251                 raise exception.InvalidObject(
  252                     schema=schema_name,
  253                     reason=u._("Invalid date for 'expiration'"),
  254                     property="expiration")
  255 
  256         return expiration
  257 
  258     def _assert_expiration_is_valid(self, expiration, schema_name):
  259         """Asserts that the given expiration date is valid.
  260 
  261         Expiration dates must be in the future, not the past.
  262         """
  263         if expiration:
  264             # Verify not already expired.
  265             utcnow = timeutils.utcnow()
  266             self._assert_validity(expiration > utcnow, schema_name,
  267                                   u._("'expiration' is before current time"),
  268                                   "expiration")
  269 
  270     def _validate_content_parameters(self, content_type, content_encoding,
  271                                      schema_name):
  272         """Content parameter validator.
  273 
  274         Check that the content_type, content_encoding and the parameters
  275         that they affect are valid.
  276         """
  277         self._assert_validity(
  278             content_type is not None,
  279             schema_name,
  280             u._("If 'payload' is supplied, 'payload_content_type' must also "
  281                 "be supplied."),
  282             "payload_content_type")
  283 
  284         self._assert_validity(
  285             mime_types.is_supported(content_type),
  286             schema_name,
  287             u._("payload_content_type is not one of {supported}"
  288                 ).format(supported=mime_types.SUPPORTED),
  289             "payload_content_type")
  290 
  291         self._assert_validity(
  292             mime_types.is_content_type_with_encoding_supported(
  293                 content_type,
  294                 content_encoding),
  295             schema_name,
  296             u._("payload_content_encoding is not one of {supported}").format(
  297                 supported=mime_types.get_supported_encodings(content_type)),
  298             "payload_content_encoding")
  299 
  300     def _validate_payload_by_content_encoding(self, payload_content_encoding,
  301                                               payload, schema_name):
  302         if payload_content_encoding == 'base64':
  303             try:
  304                 base64.b64decode(payload)
  305             except Exception:
  306                 LOG.exception("Problem parsing payload")
  307                 raise exception.InvalidObject(
  308                     schema=schema_name,
  309                     reason=u._("Invalid payload for payload_content_encoding"),
  310                     property="payload")
  311 
  312     def _extract_payload(self, json_data):
  313         """Extracts and returns the payload from the JSON data.
  314 
  315         :raises: LimitExceeded if the payload is too big
  316         """
  317         payload = json_data.get('payload', '')
  318         if secret_too_big(payload):
  319             raise exception.LimitExceeded()
  320 
  321         return payload.strip()
  322 
  323 
  324 class NewSecretMetadataValidator(ValidatorBase):
  325     """Validate new secret metadata."""
  326 
  327     def __init__(self):
  328         self.name = 'SecretMetadata'
  329         self.schema = {
  330             "type": "object",
  331             "$schema": "http://json-schema.org/draft-03/schema",
  332             "properties": {
  333                 "metadata": {"type": "object", "required": True},
  334             }
  335         }
  336 
  337     def validate(self, json_data, parent_schema=None):
  338         """Validate the input JSON for the schema for secret metadata."""
  339         schema_name = self._full_name(parent_schema)
  340         self._assert_schema_is_valid(json_data, schema_name)
  341         return self._extract_metadata(json_data)
  342 
  343     def _extract_metadata(self, json_data):
  344         """Extracts and returns the metadata from the JSON data."""
  345         metadata = json_data['metadata']
  346 
  347         for key in list(metadata):
  348             # make sure key is a string and url-safe.
  349             if not isinstance(key, six.string_types):
  350                 raise exception.InvalidMetadataRequest()
  351             self._check_string_url_safe(key)
  352 
  353             # make sure value is a string.
  354             value = metadata[key]
  355             if not isinstance(value, six.string_types):
  356                 raise exception.InvalidMetadataRequest()
  357 
  358             # If key is not lowercase, then change it
  359             if not key.islower():
  360                 del metadata[key]
  361                 metadata[key.lower()] = value
  362 
  363         return metadata
  364 
  365     def _check_string_url_safe(self, string):
  366         """Checks if string can be part of a URL."""
  367         if not re.match("^[A-Za-z0-9_-]*$", string):
  368             raise exception.InvalidMetadataKey()
  369 
  370 
  371 class NewSecretMetadatumValidator(ValidatorBase):
  372     """Validate new secret metadatum."""
  373 
  374     def __init__(self):
  375         self.name = 'SecretMetadatum'
  376         self.schema = {
  377             "type": "object",
  378             "$schema": "http://json-schema.org/draft-03/schema",
  379             "properties": {
  380                 "key": {
  381                     "type": "string",
  382                     "maxLength": 255,
  383                     "required": True
  384                 },
  385                 "value": {
  386                     "type": "string",
  387                     "maxLength": 255,
  388                     "required": True
  389                 },
  390             },
  391             "additionalProperties": False
  392         }
  393 
  394     def validate(self, json_data, parent_schema=None):
  395         """Validate the input JSON for the schema for secret metadata."""
  396         schema_name = self._full_name(parent_schema)
  397         self._assert_schema_is_valid(json_data, schema_name)
  398 
  399         key = self._extract_key(json_data)
  400         value = self._extract_value(json_data)
  401 
  402         return {"key": key, "value": value}
  403 
  404     def _extract_key(self, json_data):
  405         """Extracts and returns the metadata from the JSON data."""
  406         key = json_data['key']
  407         self._check_string_url_safe(key)
  408         key = key.lower()
  409         return key
  410 
  411     def _extract_value(self, json_data):
  412         """Extracts and returns the metadata from the JSON data."""
  413         value = json_data['value']
  414         return value
  415 
  416     def _check_string_url_safe(self, string):
  417         """Checks if string can be part of a URL."""
  418         if not re.match("^[A-Za-z0-9_-]*$", string):
  419             raise exception.InvalidMetadataKey()
  420 
  421 
  422 class CACommonHelpersMixin(object):
  423     def _validate_subject_dn_data(self, subject_dn):
  424         """Confirm that the subject_dn contains valid data
  425 
  426         Validate that the subject_dn string parses without error
  427         If not, raise InvalidSubjectDN
  428         """
  429         try:
  430             parse_dn(subject_dn)
  431         except ldap_exceptions.LDAPInvalidDnError:
  432             raise exception.InvalidSubjectDN(subject_dn=subject_dn)
  433 
  434 
  435 # TODO(atiwari) - Split this validator module and unit tests
  436 # into smaller modules
  437 class TypeOrderValidator(ValidatorBase, CACommonHelpersMixin):
  438     """Validate a new typed order."""
  439 
  440     def __init__(self):
  441         self.name = 'Order'
  442         self.schema = {
  443             "type": "object",
  444             "$schema": "http://json-schema.org/draft-03/schema",
  445             "properties": {
  446                 "meta": {
  447                     "type": "object",
  448                     "required": True
  449                 },
  450                 "type": {
  451                     "type": "string",
  452                     "required": True,
  453                     "enum": ['key', 'asymmetric', 'certificate']
  454                 }
  455             }
  456         }
  457 
  458     def validate(self, json_data, parent_schema=None):
  459         schema_name = self._full_name(parent_schema)
  460 
  461         self._assert_schema_is_valid(json_data, schema_name)
  462 
  463         order_type = json_data.get('type').lower()
  464 
  465         if order_type == models.OrderType.CERTIFICATE:
  466             certificate_meta = json_data.get('meta')
  467             self._validate_certificate_meta(certificate_meta, schema_name)
  468 
  469         elif order_type == models.OrderType.ASYMMETRIC:
  470             asymmetric_meta = json_data.get('meta')
  471             self._validate_asymmetric_meta(asymmetric_meta, schema_name)
  472 
  473         elif order_type == models.OrderType.KEY:
  474             key_meta = json_data.get('meta')
  475             self._validate_key_meta(key_meta, schema_name)
  476 
  477         else:
  478             self._raise_feature_not_implemented(order_type, schema_name)
  479 
  480         return json_data
  481 
  482     def _validate_key_meta(self, key_meta, schema_name):
  483         """Validation specific to meta for key type order."""
  484 
  485         secret_validator = NewSecretValidator()
  486         secret_validator.validate(key_meta, parent_schema=self.name)
  487 
  488         self._assert_validity(key_meta.get('payload') is None,
  489                               schema_name,
  490                               u._("'payload' not allowed "
  491                                   "for key type order"), "meta")
  492 
  493         # Validation secret generation related fields.
  494         # TODO(jfwood): Invoke the crypto plugin for this purpose
  495 
  496         self._validate_meta_parameters(key_meta, "key", schema_name)
  497 
  498     def _validate_asymmetric_meta(self, asymmetric_meta, schema_name):
  499         """Validation specific to meta for asymmetric type order."""
  500 
  501         # Validate secret metadata.
  502         secret_validator = NewSecretValidator()
  503         secret_validator.validate(asymmetric_meta, parent_schema=self.name)
  504 
  505         self._assert_validity(asymmetric_meta.get('payload') is None,
  506                               schema_name,
  507                               u._("'payload' not allowed "
  508                                   "for asymmetric type order"), "meta")
  509 
  510         self._validate_meta_parameters(asymmetric_meta, "asymmetric key",
  511                                        schema_name)
  512 
  513     def _get_required_metadata_value(self, metadata, key):
  514         data = metadata.get(key, None)
  515         if data is None:
  516             raise exception.MissingMetadataField(required=key)
  517         return data
  518 
  519     def _validate_certificate_meta(self, certificate_meta, schema_name):
  520         """Validation specific to meta for certificate type order."""
  521 
  522         self._assert_validity(certificate_meta.get('payload') is None,
  523                               schema_name,
  524                               u._("'payload' not allowed "
  525                                   "for certificate type order"), "meta")
  526 
  527         if 'profile' in certificate_meta:
  528             if 'ca_id' not in certificate_meta:
  529                 raise exception.MissingMetadataField(required='ca_id')
  530 
  531         jump_table = {
  532             'simple-cmc': self._validate_simple_cmc_request,
  533             'full-cmc': self._validate_full_cmc_request,
  534             'stored-key': self._validate_stored_key_request,
  535             'custom': self._validate_custom_request
  536         }
  537 
  538         request_type = certificate_meta.get("request_type", "custom")
  539         if request_type not in jump_table:
  540             raise exception.InvalidCertificateRequestType(request_type)
  541 
  542         jump_table[request_type](certificate_meta)
  543 
  544     def _validate_simple_cmc_request(self, certificate_meta):
  545         """Validates simple CMC (which are PKCS10 requests)."""
  546         request_data = self._get_required_metadata_value(
  547             certificate_meta, "request_data")
  548         self._validate_pkcs10_data(request_data)
  549 
  550     def _validate_full_cmc_request(self, certificate_meta):
  551         """Validate full CMC request.
  552 
  553         :param certificate_meta: request data from the order
  554         :raises: FullCMCNotSupported
  555         """
  556         raise exception.FullCMCNotSupported()
  557 
  558     def _validate_stored_key_request(self, certificate_meta):
  559         """Validate stored-key cert request."""
  560         self._get_required_metadata_value(
  561             certificate_meta, "container_ref")
  562         subject_dn = self._get_required_metadata_value(
  563             certificate_meta, "subject_dn")
  564         self._validate_subject_dn_data(subject_dn)
  565         # container will be validated by validate_stored_key_rsa_container()
  566 
  567         extensions = certificate_meta.get("extensions", None)
  568         if extensions:
  569             self._validate_extensions_data(extensions)
  570 
  571     def _validate_custom_request(self, certificate_meta):
  572         """Validate custom data request
  573 
  574         We cannot do any validation here because the request
  575         parameters are custom.  Validation will be done by the
  576         plugin.  We may choose to select the relevant plugin and
  577         call the supports() method to raise validation errors.
  578         """
  579         pass
  580 
  581     def _validate_pkcs10_data(self, request_data):
  582         """Confirm that the request_data is valid base64 encoded PKCS#10.
  583 
  584         Base64 decode the request, if it fails raise PayloadDecodingError.
  585         Then parse data into the ASN.1 structure defined by PKCS10 and
  586         verify the signing information.
  587         If parsing of verifying fails, raise InvalidPKCS10Data.
  588         """
  589         try:
  590             csr_pem = base64.b64decode(request_data)
  591         except Exception:
  592             raise exception.PayloadDecodingError()
  593 
  594         try:
  595             csr = crypto.load_certificate_request(crypto.FILETYPE_PEM,
  596                                                   csr_pem)
  597         except Exception:
  598             reason = u._("Bad format")
  599             raise exception.InvalidPKCS10Data(reason=reason)
  600 
  601         try:
  602             pubkey = csr.get_pubkey()
  603             csr.verify(pubkey)
  604         except Exception:
  605             reason = u._("Signing key incorrect")
  606             raise exception.InvalidPKCS10Data(reason=reason)
  607 
  608     def _validate_full_cmc_data(self, request_data):
  609         """Confirm that request_data is valid Full CMC data."""
  610         """
  611         TODO(alee-3) complete this function
  612 
  613         Parse data into the ASN.1 structure defined for full CMC.
  614         If parsing fails, raise InvalidCMCData
  615         """
  616         pass
  617 
  618     def _validate_extensions_data(self, extensions):
  619         """Confirm that the extensions data is valid.
  620 
  621         :param extensions: base 64 encoded ASN.1 string of extension data
  622         :raises: CertificateExtensionsNotSupported
  623         """
  624         """
  625         TODO(alee-3) complete this function
  626 
  627         Parse the extensions data into the correct ASN.1 structure.
  628         If the parsing fails, throw InvalidExtensionsData.
  629 
  630         For now, fail this validation because extensions parsing is not
  631         supported.
  632         """
  633         raise exception.CertificateExtensionsNotSupported()
  634 
  635     def _validate_meta_parameters(self, meta, order_type, schema_name):
  636         self._assert_validity(meta.get('algorithm'),
  637                               schema_name,
  638                               u._("'algorithm' is required field "
  639                                   "for {0} type order").format(order_type),
  640                               "meta")
  641 
  642         self._assert_validity(meta.get('bit_length'),
  643                               schema_name,
  644                               u._("'bit_length' is required field "
  645                                   "for {0} type order").format(order_type),
  646                               "meta")
  647 
  648         self._validate_bit_length(meta, schema_name)
  649 
  650     def _extract_expiration(self, json_data, schema_name):
  651         """Extracts and returns the expiration date from the JSON data."""
  652         expiration = None
  653         expiration_raw = json_data.get('expiration', None)
  654         if expiration_raw and expiration_raw.strip():
  655             try:
  656                 expiration_tz = timeutils.parse_isotime(expiration_raw)
  657                 expiration = timeutils.normalize_time(expiration_tz)
  658             except ValueError:
  659                 LOG.exception("Problem parsing expiration date")
  660                 raise exception.InvalidObject(schema=schema_name,
  661                                               reason=u._("Invalid date "
  662                                                          "for 'expiration'"),
  663                                               property="expiration")
  664 
  665         return expiration
  666 
  667     def _validate_bit_length(self, meta, schema_name):
  668 
  669         bit_length = int(meta.get('bit_length'))
  670         if bit_length % 8 != 0:
  671             raise exception.UnsupportedField(field="bit_length",
  672                                              schema=schema_name,
  673                                              reason=u._("Must be a"
  674                                                         " positive integer"
  675                                                         " that is a"
  676                                                         " multiple of 8"))
  677 
  678     def _raise_feature_not_implemented(self, order_type, schema_name):
  679         raise exception.FeatureNotImplemented(field='type',
  680                                               schema=schema_name,
  681                                               reason=u._("Feature not "
  682                                                          "implemented for "
  683                                                          "'{0}' order type")
  684                                                     .format(order_type))
  685 
  686 
  687 class ACLValidator(ValidatorBase):
  688     """Validate ACL(s)."""
  689 
  690     def __init__(self):
  691         self.name = 'ACL'
  692 
  693         self.schema = {
  694             "$schema": "http://json-schema.org/draft-04/schema#",
  695             "definitions": {
  696                 "acl_defintion": {
  697                     "type": "object",
  698                     "properties": {
  699                         "users": {
  700                             "type": "array",
  701                             "items": [
  702                                 {"type": "string", "maxLength": 255}
  703                             ]
  704                         },
  705                         "project-access": {"type": "boolean"}
  706                     },
  707                     "additionalProperties": False
  708                 }
  709             },
  710             "type": "object",
  711             "properties": {
  712                 "read": {"$ref": "#/definitions/acl_defintion"},
  713             },
  714             "additionalProperties": False
  715         }
  716 
  717     def validate(self, json_data, parent_schema=None):
  718         schema_name = self._full_name(parent_schema)
  719 
  720         self._assert_schema_is_valid(json_data, schema_name)
  721         return json_data
  722 
  723 
  724 class ContainerConsumerValidator(ValidatorBase):
  725     """Validate a Consumer."""
  726 
  727     def __init__(self):
  728         self.name = 'Consumer'
  729         self.schema = {
  730             "type": "object",
  731             "properties": {
  732                 "URL": {"type": "string", "minLength": 1},
  733                 "name": {"type": "string", "maxLength": 255, "minLength": 1}
  734             },
  735             "required": ["name", "URL"]
  736         }
  737 
  738     def validate(self, json_data, parent_schema=None):
  739         schema_name = self._full_name(parent_schema)
  740 
  741         self._assert_schema_is_valid(json_data, schema_name)
  742         return json_data
  743 
  744 
  745 class ContainerSecretValidator(ValidatorBase):
  746     """Validate a Container Secret."""
  747 
  748     def __init__(self):
  749         self.name = 'ContainerSecret'
  750         self.schema = {
  751             "type": "object",
  752             "properties": {
  753                 "name": {"type": "string", "maxLength": 255},
  754                 "secret_ref": {"type": "string", "minLength": 1}
  755             },
  756             "required": ["secret_ref"]
  757         }
  758 
  759     def validate(self, json_data, parent_schema=None):
  760         schema_name = self._full_name(parent_schema)
  761 
  762         self._assert_schema_is_valid(json_data, schema_name)
  763         return json_data
  764 
  765 
  766 class ContainerValidator(ValidatorBase):
  767     """Validator for all types of Container."""
  768 
  769     def __init__(self):
  770         self.name = 'Container'
  771         self.schema = {
  772             "type": "object",
  773             "properties": {
  774                 "name": {"type": ["string", "null"], "maxLength": 255},
  775                 "type": {
  776                     "type": "string",
  777                     # TODO(hgedikli): move this to a common location
  778                     "enum": ["generic", "rsa", "certificate"]
  779                 },
  780                 "secret_refs": {
  781                     "type": "array",
  782                     "items": {
  783                         "type": "object",
  784                         "required": ["secret_ref"],
  785                         "properties": {
  786                             "name": {
  787                                 "type": ["string", "null"], "maxLength": 255
  788                             },
  789                             "secret_ref": {"type": "string", "minLength": 1}
  790                         }
  791                     }
  792                 }
  793             },
  794             "required": ["type"]
  795         }
  796 
  797     def validate(self, json_data, parent_schema=None):
  798         schema_name = self._full_name(parent_schema)
  799 
  800         self._assert_schema_is_valid(json_data, schema_name)
  801 
  802         container_type = json_data.get('type')
  803         secret_refs = json_data.get('secret_refs')
  804 
  805         if not secret_refs:
  806             return json_data
  807 
  808         secret_refs_names = set(secret_ref.get('name', '')
  809                                 for secret_ref in secret_refs)
  810 
  811         self._assert_validity(
  812             len(secret_refs_names) == len(secret_refs),
  813             schema_name,
  814             u._("Duplicate reference names are not allowed"),
  815             "secret_refs")
  816 
  817         # The combination of container_id and secret_id is expected to be
  818         # primary key for container_secret so same secret id (ref) cannot be
  819         # used within a container
  820         secret_ids = set(self._get_secret_id_from_ref(secret_ref)
  821                          for secret_ref in secret_refs)
  822 
  823         self._assert_validity(
  824             len(secret_ids) == len(secret_refs),
  825             schema_name,
  826             u._("Duplicate secret ids are not allowed"),
  827             "secret_refs")
  828 
  829         # Ensure that our secret refs are valid relative to our config, no
  830         # spoofing allowed!
  831         req_host_href = utils.get_base_url_from_request()
  832         for secret_ref in secret_refs:
  833             if not secret_ref.get('secret_ref').startswith(req_host_href):
  834                 raise exception.UnsupportedField(
  835                     field='secret_ref',
  836                     schema=schema_name,
  837                     reason=u._(
  838                         "Secret_ref does not match the configured hostname, "
  839                         "please try again"
  840                     )
  841                 )
  842 
  843         if container_type == 'rsa':
  844             self._validate_rsa(secret_refs_names, schema_name)
  845         elif container_type == 'certificate':
  846             self._validate_certificate(secret_refs_names, schema_name)
  847 
  848         return json_data
  849 
  850     def _validate_rsa(self, secret_refs_names, schema_name):
  851         required_names = {'public_key', 'private_key'}
  852         optional_names = {'private_key_passphrase'}
  853         contains_unsupported_names = self._contains_unsupported_names(
  854             secret_refs_names, required_names | optional_names)
  855         self._assert_validity(
  856             not contains_unsupported_names,
  857             schema_name,
  858             u._("only 'private_key', 'public_key' and "
  859                 "'private_key_passphrase' reference names are "
  860                 "allowed for RSA type"),
  861             "secret_refs")
  862 
  863         self._assert_validity(
  864             self._has_minimum_required(secret_refs_names, required_names),
  865             schema_name,
  866             u._("The minimum required reference names are 'public_key' and"
  867                 "'private_key' for RSA type"),
  868             "secret_refs")
  869 
  870     def _validate_certificate(self, secret_refs_names, schema_name):
  871         required_names = {'certificate'}
  872         optional_names = {'private_key', 'private_key_passphrase',
  873                           'intermediates'}
  874         contains_unsupported_names = self._contains_unsupported_names(
  875             secret_refs_names, required_names.union(optional_names))
  876         self._assert_validity(
  877             not contains_unsupported_names,
  878             schema_name,
  879             u._("only 'private_key', 'certificate' , "
  880                 "'private_key_passphrase',  or 'intermediates' "
  881                 "reference names are allowed for Certificate type"),
  882             "secret_refs")
  883 
  884         self._assert_validity(
  885             self._has_minimum_required(secret_refs_names, required_names),
  886             schema_name,
  887             u._("The minimum required reference name is 'certificate' "
  888                 "for Certificate type"),
  889             "secret_refs")
  890 
  891     def _contains_unsupported_names(self, secret_refs_names, supported_names):
  892         if secret_refs_names.difference(supported_names):
  893             return True
  894         return False
  895 
  896     def _has_minimum_required(self, secret_refs_names, required_names):
  897         if required_names.issubset(secret_refs_names):
  898             return True
  899         return False
  900 
  901     def _get_secret_id_from_ref(self, secret_ref):
  902         secret_id = secret_ref.get('secret_ref')
  903         if secret_id.endswith('/'):
  904             secret_id = secret_id.rsplit('/', 2)[1]
  905         elif '/' in secret_id:
  906             secret_id = secret_id.rsplit('/', 1)[1]
  907 
  908         return secret_id
  909 
  910 
  911 class NewTransportKeyValidator(ValidatorBase):
  912     """Validate a new transport key."""
  913 
  914     def __init__(self):
  915         self.name = 'Transport Key'
  916 
  917         self.schema = {
  918             "type": "object",
  919             "properties": {
  920                 "plugin_name": {"type": "string"},
  921                 "transport_key": {"type": "string"},
  922             },
  923         }
  924 
  925     def validate(self, json_data, parent_schema=None):
  926         schema_name = self._full_name(parent_schema)
  927 
  928         self._assert_schema_is_valid(json_data, schema_name)
  929 
  930         plugin_name = json_data.get('plugin_name', '').strip()
  931         self._assert_validity(plugin_name,
  932                               schema_name,
  933                               u._("plugin_name must be provided"),
  934                               "plugin_name")
  935         json_data['plugin_name'] = plugin_name
  936 
  937         transport_key = json_data.get('transport_key', '').strip()
  938         self._assert_validity(transport_key,
  939                               schema_name,
  940                               u._("transport_key must be provided"),
  941                               "transport_key")
  942         json_data['transport_key'] = transport_key
  943 
  944         return json_data
  945 
  946 
  947 class ProjectQuotaValidator(ValidatorBase):
  948     """Validate a new project quota."""
  949 
  950     def __init__(self):
  951         self.name = 'Project Quota'
  952 
  953         self.schema = {
  954             'type': 'object',
  955             'properties': {
  956                 'project_quotas': {
  957                     'type': 'object',
  958                     'properties': {
  959                         'secrets': {'type': 'integer'},
  960                         'orders': {'type': 'integer'},
  961                         'containers': {'type': 'integer'},
  962                         'consumers': {'type': 'integer'},
  963                         'cas': {'type': 'integer'}
  964                     },
  965                     'additionalProperties': False,
  966                 }
  967             },
  968             'required': ['project_quotas'],
  969             'additionalProperties': False
  970         }
  971 
  972     def validate(self, json_data, parent_schema=None):
  973         schema_name = self._full_name(parent_schema)
  974 
  975         self._assert_schema_is_valid(json_data, schema_name)
  976 
  977         return json_data
  978 
  979 
  980 class NewCAValidator(ValidatorBase, CACommonHelpersMixin):
  981     """Validate new CA(s)."""
  982 
  983     def __init__(self):
  984         self.name = 'CA'
  985 
  986         self.schema = {
  987             'type': 'object',
  988             'properties': {
  989                 'name': {'type': 'string', "minLength": 1},
  990                 'subject_dn': {'type': 'string', "minLength": 1},
  991                 'parent_ca_ref': {'type': 'string', "minLength": 1},
  992                 'description': {'type': 'string'},
  993             },
  994             'required': ['name', 'subject_dn', 'parent_ca_ref'],
  995             'additionalProperties': False
  996         }
  997 
  998     def validate(self, json_data, parent_schema=None):
  999         schema_name = self._full_name(parent_schema)
 1000 
 1001         self._assert_schema_is_valid(json_data, schema_name)
 1002 
 1003         subject_dn = json_data['subject_dn']
 1004         self._validate_subject_dn_data(subject_dn)
 1005         return json_data
 1006 
 1007 
 1008 class SecretConsumerValidator(ValidatorBase):
 1009     """Validate a new Secret Consumer."""
 1010 
 1011     def __init__(self):
 1012         self.name = "Secret Consumer"
 1013 
 1014         self.schema = {
 1015             "type": "object",
 1016             "properties": {
 1017                 "service": {
 1018                     "type": "string",
 1019                     "maxLength": 255,
 1020                     "minLength": 1,
 1021                 },
 1022                 "resource_type": {
 1023                     "type": "string",
 1024                     "maxLength": 255,
 1025                     "minLength": 1,
 1026                 },
 1027                 "resource_id": {"type": "string", "minLength": 1},
 1028             },
 1029             "required": ["service", "resource_type", "resource_id"],
 1030         }
 1031 
 1032     def validate(self, json_data, parent_schema=None):
 1033         schema_name = self._full_name(parent_schema)
 1034 
 1035         self._assert_schema_is_valid(json_data, schema_name)
 1036 
 1037         return json_data