# Copyright 2016-2017 FUJITSU LIMITED # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re import falcon import six from monasca_log_api import conf from monasca_log_api.app.base import exceptions from oslo_log import log LOG = log.getLogger(__name__) CONF = conf.CONF APPLICATION_TYPE_CONSTRAINTS = { 'MAX_LENGTH': 255, 'PATTERN': re.compile('^[a-zA-Z0-9_.\\-]+$') } """Application type constraint used in validation. See :py:func:`Validations.validate_application_type` """ DIMENSION_NAME_CONSTRAINTS = { 'MAX_LENGTH': 255, 'PATTERN': re.compile('[^><={}(), \'";&]+$') } """Constraint for name of single dimension. See :py:func:`Validations.validate_dimensions` """ DIMENSION_VALUE_CONSTRAINTS = { 'MAX_LENGTH': 255 } """Constraint for value of single dimension. See :py:func:`Validations.validate_dimensions` """ def validate_application_type(application_type=None): """Validates application type. Validation won't take place if application_type is None. For details see: :py:data:`APPLICATION_TYPE_CONSTRAINTS` :param str application_type: application type """ def validate_length(): if (len(application_type) > APPLICATION_TYPE_CONSTRAINTS['MAX_LENGTH']): msg = ('Application type {type} must be ' '{length} characters or less') raise exceptions.HTTPUnprocessableEntity( msg.format( type=application_type, length=APPLICATION_TYPE_CONSTRAINTS[ 'MAX_LENGTH'] ) ) def validate_match(): if (not APPLICATION_TYPE_CONSTRAINTS['PATTERN'] .match(application_type)): raise exceptions.HTTPUnprocessableEntity( 'Application type %s may only contain: "a-z A-Z 0-9 _ - ."' % application_type ) if application_type: validate_length() validate_match() def _validate_dimension_name(name): try: if len(name) > DIMENSION_NAME_CONSTRAINTS['MAX_LENGTH']: raise exceptions.HTTPUnprocessableEntity( 'Dimension name %s must be 255 characters or less' % name ) if name[0] == '_': raise exceptions.HTTPUnprocessableEntity( 'Dimension name %s cannot start with underscore (_)' % name ) if not DIMENSION_NAME_CONSTRAINTS['PATTERN'].match(name): raise exceptions.HTTPUnprocessableEntity( 'Dimension name %s may not contain: %s' % (name, '> < = { } ( ) \' " , ; &') ) except (TypeError, IndexError): raise exceptions.HTTPUnprocessableEntity( 'Dimension name cannot be empty' ) def _validate_dimension_value(value): try: value[0] if len(value) > DIMENSION_VALUE_CONSTRAINTS['MAX_LENGTH']: raise exceptions.HTTPUnprocessableEntity( 'Dimension value %s must be 255 characters or less' % value ) except (TypeError, IndexError): raise exceptions.HTTPUnprocessableEntity( 'Dimension value cannot be empty' ) def validate_dimensions(dimensions): """Validates dimensions type. Empty dimensions are not being validated. For details see: :param dict dimensions: dimensions to validate * :py:data:`DIMENSION_NAME_CONSTRAINTS` * :py:data:`DIMENSION_VALUE_CONSTRAINTS` """ try: for dim_name, dim_value in dimensions.items(): _validate_dimension_name(dim_name) _validate_dimension_value(dim_value) except AttributeError: raise exceptions.HTTPUnprocessableEntity( 'Dimensions %s must be a dictionary (map)' % dimensions) def validate_content_type(req, allowed): """Validates content type. Method validates request against correct content type. If content-type cannot be established (i.e. header is missing), :py:class:`falcon.HTTPMissingHeader` is thrown. If content-type is not **application/json** or **text/plain**, :py:class:`falcon.HTTPUnsupportedMediaType` is thrown. :param falcon.Request req: current request :param iterable allowed: allowed content type :exception: :py:class:`falcon.HTTPMissingHeader` :exception: :py:class:`falcon.HTTPUnsupportedMediaType` """ content_type = req.content_type LOG.debug('Content-Type is %s', content_type) if content_type is None or len(content_type) == 0: raise falcon.HTTPMissingHeader('Content-Type') if content_type not in allowed: sup_types = ', '.join(allowed) details = ('Only [%s] are accepted as logs representations' % str(sup_types)) raise falcon.HTTPUnsupportedMediaType(description=details) def validate_payload_size(req): """Validates payload size. Method validates sent payload size. It expects that http header **Content-Length** is present. If it does not, method raises :py:class:`falcon.HTTPLengthRequired`. Otherwise values is being compared with :: [service] max_log_size = 1048576 **max_log_size** refers to the maximum allowed content length. If it is exceeded :py:class:`falcon.HTTPRequestEntityTooLarge` is thrown. :param falcon.Request req: current request :exception: :py:class:`falcon.HTTPLengthRequired` :exception: :py:class:`falcon.HTTPRequestEntityTooLarge` """ payload_size = req.content_length max_size = CONF.service.max_log_size LOG.debug('Payload (content-length) is %s', str(payload_size)) if payload_size is None: raise falcon.HTTPLengthRequired( title='Content length header is missing', description='Content length is required to estimate if ' 'payload can be processed' ) if payload_size >= max_size: raise falcon.HTTPRequestEntityTooLarge( title='Log payload size exceeded', description='Maximum allowed size is %d bytes' % max_size ) def validate_is_delegate(roles): delegate_roles = CONF.roles_middleware.delegate_roles if roles and delegate_roles: roles = roles.split(',') if isinstance(roles, six.string_types) \ else roles return any(x in set(delegate_roles) for x in roles) return False def validate_cross_tenant(tenant_id, cross_tenant_id, roles): if not validate_is_delegate(roles): if cross_tenant_id: raise falcon.HTTPForbidden( 'Permission denied', 'Projects %s cannot POST cross tenant logs' % tenant_id ) def validate_log_message(log_object): """Validates log property. Log property should have message property. Args: log_object (dict): log property """ if 'message' not in log_object: raise exceptions.HTTPUnprocessableEntity( 'Log property should have message' ) def validate_authorization(http_request, authorized_rules_list): """Validates whether is authorized according to provided policy rules list. If authorization fails, 401 is thrown with appropriate description. Additionally response specifies 'WWW-Authenticate' header with 'Token' value challenging the client to use different token (the one with different set of roles which can access the service). """ challenge = 'Token' for rule in authorized_rules_list: try: http_request.can(rule) return except Exception as ex: LOG.debug(ex) raise falcon.HTTPUnauthorized('Forbidden', 'The request does not have access to this service', challenge)