validate.py 17.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# Copyright 2016 Blue Box, an IBM Company
# All Rights Reserved.
#
#    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.

"""
Several handy validation functions that go beyond simple type checking.
Defined here so these can also be used at deeper levels than the API.
"""

21

22
import ipaddress
23 24
import re

25
import netaddr
26
from oslo_config import cfg
27
import rfc3986
28
import six
29
from wsme import types as wtypes
30 31 32

from octavia.common import constants
from octavia.common import exceptions
33
from octavia.common import utils
Dong Jun's avatar
Dong Jun committed
34
from octavia.i18n import _
35

36 37 38
CONF = cfg.CONF


39
def url(url, require_scheme=True):
40
    """Raises an error if the url doesn't look like a URL."""
41
    try:
42
        if not rfc3986.is_valid_uri(url, require_scheme=require_scheme):
43 44
            raise exceptions.InvalidURL(url=url)
        p_url = rfc3986.urlparse(rfc3986.normalize_uri(url))
45 46 47
        if require_scheme:
            if p_url.scheme != 'http' and p_url.scheme != 'https':
                raise exceptions.InvalidURL(url=url)
48
    except Exception:
49 50
        raise exceptions.InvalidURL(url=url)
    return True
51 52


cheng's avatar
cheng committed
53 54 55 56 57
def url_path(url_path):
    """Raises an error if the url_path doesn't look like a URL Path."""
    try:
        p_url = rfc3986.urlparse(rfc3986.normalize_uri(url_path))

58
        invalid_path = (
cheng's avatar
cheng committed
59 60
            p_url.scheme or p_url.userinfo or p_url.host or
            p_url.port or
61 62
            p_url.path is None or
            not p_url.path.startswith('/')
63 64 65
        )

        if invalid_path:
cheng's avatar
cheng committed
66 67 68 69 70 71
            raise exceptions.InvalidURLPath(url_path=url_path)
    except Exception:
        raise exceptions.InvalidURLPath(url_path=url_path)
    return True


72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
def header_name(header, what=None):
    """Raises an error if header does not look like an HTML header name."""
    p = re.compile(constants.HTTP_HEADER_NAME_REGEX)
    if not p.match(header):
        raise exceptions.InvalidString(what=what)
    return True


def cookie_value_string(value, what=None):
    """Raises an error if the value string contains invalid characters."""
    p = re.compile(constants.HTTP_COOKIE_VALUE_REGEX)
    if not p.match(value):
        raise exceptions.InvalidString(what=what)
    return True


def header_value_string(value, what=None):
    """Raises an error if the value string contains invalid characters."""
    p = re.compile(constants.HTTP_HEADER_VALUE_REGEX)
    q = re.compile(constants.HTTP_QUOTED_HEADER_VALUE_REGEX)
    if not p.match(value) and not q.match(value):
        raise exceptions.InvalidString(what=what)
    return True


def regex(regex):
    """Raises an error if the string given is not a valid regex."""
    try:
        re.compile(regex)
    except Exception as e:
        raise exceptions.InvalidRegex(e=str(e))
    return True


# Note that we can evaluate this outside the context of any L7 Policy because
# L7 rules must be internally consistent.
def l7rule_data(l7rule):
    """Raises an error if the l7rule given is invalid in some way."""
    if l7rule.type == constants.L7RULE_TYPE_HEADER:
        if not l7rule.key:
            raise exceptions.InvalidL7Rule(msg='L7 rule type requires a key')
        header_name(l7rule.key, what='key')
        if l7rule.compare_type == constants.L7RULE_COMPARE_TYPE_REGEX:
            regex(l7rule.value)
        elif l7rule.compare_type in (
                constants.L7RULE_COMPARE_TYPE_STARTS_WITH,
                constants.L7RULE_COMPARE_TYPE_ENDS_WITH,
                constants.L7RULE_COMPARE_TYPE_CONTAINS,
                constants.L7RULE_COMPARE_TYPE_EQUAL_TO):
            header_value_string(l7rule.value, what='header value')
        else:
            raise exceptions.InvalidL7Rule(msg='invalid comparison type '
                                           'for rule type')

    elif l7rule.type == constants.L7RULE_TYPE_COOKIE:
        if not l7rule.key:
            raise exceptions.InvalidL7Rule(msg='L7 rule type requires a key')
        header_name(l7rule.key, what='key')
        if l7rule.compare_type == constants.L7RULE_COMPARE_TYPE_REGEX:
            regex(l7rule.value)
        elif l7rule.compare_type in (
                constants.L7RULE_COMPARE_TYPE_STARTS_WITH,
                constants.L7RULE_COMPARE_TYPE_ENDS_WITH,
                constants.L7RULE_COMPARE_TYPE_CONTAINS,
                constants.L7RULE_COMPARE_TYPE_EQUAL_TO):
            cookie_value_string(l7rule.value, what='cookie value')
        else:
            raise exceptions.InvalidL7Rule(msg='invalid comparison type '
                                           'for rule type')

    elif l7rule.type in (constants.L7RULE_TYPE_HOST_NAME,
                         constants.L7RULE_TYPE_PATH):
        if l7rule.compare_type in (
                constants.L7RULE_COMPARE_TYPE_STARTS_WITH,
                constants.L7RULE_COMPARE_TYPE_ENDS_WITH,
                constants.L7RULE_COMPARE_TYPE_CONTAINS,
                constants.L7RULE_COMPARE_TYPE_EQUAL_TO):
            header_value_string(l7rule.value, what='comparison value')
        elif l7rule.compare_type == constants.L7RULE_COMPARE_TYPE_REGEX:
            regex(l7rule.value)
        else:
            raise exceptions.InvalidL7Rule(msg='invalid comparison type '
                                           'for rule type')

    elif l7rule.type == constants.L7RULE_TYPE_FILE_TYPE:
        if l7rule.compare_type == constants.L7RULE_COMPARE_TYPE_REGEX:
            regex(l7rule.value)
        elif l7rule.compare_type == constants.L7RULE_COMPARE_TYPE_EQUAL_TO:
            header_value_string(l7rule.value, what='comparison value')
        else:
            raise exceptions.InvalidL7Rule(msg='invalid comparison type '
                                           'for rule type')

165 166 167 168 169
    elif l7rule.type in [constants.L7RULE_TYPE_SSL_CONN_HAS_CERT,
                         constants.L7RULE_TYPE_SSL_VERIFY_RESULT,
                         constants.L7RULE_TYPE_SSL_DN_FIELD]:
        validate_l7rule_ssl_types(l7rule)

170 171 172
    else:
        raise exceptions.InvalidL7Rule(msg='invalid rule type')
    return True
173 174


175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
def validate_l7rule_ssl_types(l7rule):
    if not l7rule.type or l7rule.type not in [
       constants.L7RULE_TYPE_SSL_CONN_HAS_CERT,
       constants.L7RULE_TYPE_SSL_VERIFY_RESULT,
       constants.L7RULE_TYPE_SSL_DN_FIELD]:
        return

    rule_type = None if l7rule.type == wtypes.Unset else l7rule.type
    req_key = None if l7rule.key == wtypes.Unset else l7rule.key
    req_value = None if l7rule.value == wtypes.Unset else l7rule.value
    compare_type = (None if l7rule.compare_type == wtypes.Unset else
                    l7rule.compare_type)
    msg = None
    if rule_type == constants.L7RULE_TYPE_SSL_CONN_HAS_CERT:
        # key and value are not allowed
        if req_key:
            # log error or raise
            msg = 'L7rule type {0} does not use the "key" field.'.format(
                rule_type)
        elif req_value.lower() != 'true':
            msg = 'L7rule value {0} is not a boolean True string.'.format(
                req_value)
        elif compare_type != constants.L7RULE_COMPARE_TYPE_EQUAL_TO:
            msg = 'L7rule type {0} only supports the {1} compare type.'.format(
                rule_type, constants.L7RULE_COMPARE_TYPE_EQUAL_TO)

    if rule_type == constants.L7RULE_TYPE_SSL_VERIFY_RESULT:
        if req_key:
            # log or raise req_key not used
            msg = 'L7rule type {0} does not use the "key" field.'.format(
                rule_type)
        elif not req_value.isdigit() or int(req_value) < 0:
            # log or raise req_value must be int
            msg = 'L7rule type {0} needs a int value, which is >= 0'.format(
                rule_type)
        elif compare_type != constants.L7RULE_COMPARE_TYPE_EQUAL_TO:
            msg = 'L7rule type {0} only supports the {1} compare type.'.format(
                rule_type, constants.L7RULE_COMPARE_TYPE_EQUAL_TO)

    if rule_type == constants.L7RULE_TYPE_SSL_DN_FIELD:
        dn_regex = re.compile(constants.DISTINGUISHED_NAME_FIELD_REGEX)
        if compare_type == constants.L7RULE_COMPARE_TYPE_REGEX:
            regex(l7rule.value)

        if not req_key or not req_value:
            # log or raise key and value must be specified.
            msg = 'L7rule type {0} needs to specify a key and a value.'.format(
                rule_type)
        # log or raise the key must be splited by '-'
        elif not dn_regex.match(req_key):
            msg = ('Invalid L7rule distinguished name field.')

    if msg:
        raise exceptions.InvalidL7Rule(msg=msg)


231 232 233 234 235 236 237 238 239 240 241 242 243
def sanitize_l7policy_api_args(l7policy, create=False):
    """Validate and make consistent L7Policy API arguments.

    This method is mainly meant to sanitize L7 Policy create and update
    API dictionaries, so that we strip 'None' values that don't apply for
    our particular update. This method does *not* verify that any
    redirect_pool_id exists in the database, but will raise an
    error if a redirect_url doesn't look like a URL.

    :param l7policy: The L7 Policy dictionary we are santizing / validating
    """
    if 'action' in l7policy.keys():
        if l7policy['action'] == constants.L7POLICY_ACTION_REJECT:
244 245
            l7policy.update({'redirect_url': None})
            l7policy.update({'redirect_pool_id': None})
246 247
            l7policy.pop('redirect_pool', None)
        elif l7policy['action'] == constants.L7POLICY_ACTION_REDIRECT_TO_URL:
248 249 250 251
            if not l7policy.get('redirect_url'):
                raise exceptions.InvalidL7PolicyArgs(
                    msg='redirect_url must not be None')
            l7policy.update({'redirect_pool_id': None})
252 253
            l7policy.pop('redirect_pool', None)
        elif l7policy['action'] == constants.L7POLICY_ACTION_REDIRECT_TO_POOL:
254 255
            if (not l7policy.get('redirect_pool_id') and
                    not l7policy.get('redirect_pool')):
256 257 258
                raise exceptions.InvalidL7PolicyArgs(
                    msg='redirect_pool_id or redirect_pool must not be None')
            l7policy.update({'redirect_url': None})
259 260 261 262
        elif l7policy['action'] == constants.L7POLICY_ACTION_REDIRECT_PREFIX:
            if not l7policy.get('redirect_prefix'):
                raise exceptions.InvalidL7PolicyArgs(
                    msg='redirect_prefix must not be None')
263 264 265
        else:
            raise exceptions.InvalidL7PolicyAction(
                action=l7policy['action'])
266 267
    if ((l7policy.get('redirect_pool_id') or l7policy.get('redirect_pool')) and
            (l7policy.get('redirect_url') or l7policy.get('redirect_prefix'))):
268
        raise exceptions.InvalidL7PolicyArgs(
269 270
            msg='Cannot specify redirect_pool_id and redirect_url or '
                'redirect_prefix at the same time')
271 272 273 274 275
    if l7policy.get('redirect_pool_id'):
        l7policy.update({
            'action': constants.L7POLICY_ACTION_REDIRECT_TO_POOL})
        l7policy.update({'redirect_url': None})
        l7policy.pop('redirect_pool', None)
276
        l7policy.update({'redirect_prefix': None})
277
        l7policy.update({'redirect_http_code': None})
278 279 280 281 282
    if l7policy.get('redirect_pool'):
        l7policy.update({
            'action': constants.L7POLICY_ACTION_REDIRECT_TO_POOL})
        l7policy.update({'redirect_url': None})
        l7policy.pop('redirect_pool_id', None)
283
        l7policy.update({'redirect_prefix': None})
284
        l7policy.update({'redirect_http_code': None})
285 286 287 288 289
    if l7policy.get('redirect_url'):
        url(l7policy['redirect_url'])
        l7policy.update({
            'action': constants.L7POLICY_ACTION_REDIRECT_TO_URL})
        l7policy.update({'redirect_pool_id': None})
290 291
        l7policy.update({'redirect_prefix': None})
        l7policy.pop('redirect_pool', None)
292 293
        if not l7policy.get('redirect_http_code'):
            l7policy.update({'redirect_http_code': 302})
294 295 296 297 298 299
    if l7policy.get('redirect_prefix'):
        url(l7policy['redirect_prefix'])
        l7policy.update({
            'action': constants.L7POLICY_ACTION_REDIRECT_PREFIX})
        l7policy.update({'redirect_pool_id': None})
        l7policy.update({'redirect_url': None})
300
        l7policy.pop('redirect_pool', None)
301 302
        if not l7policy.get('redirect_http_code'):
            l7policy.update({'redirect_http_code': 302})
303 304 305 306 307 308

    # If we are creating, we need an action at this point
    if create and 'action' not in l7policy.keys():
        raise exceptions.InvalidL7PolicyAction(action='None')

    # See if we have anything left after that...
309
    if not l7policy.keys():
310 311
        raise exceptions.InvalidL7PolicyArgs(msg='Invalid update options')
    return l7policy
312 313


314 315 316 317 318 319 320 321 322 323
def port_exists(port_id):
    """Raises an exception when a port does not exist."""
    network_driver = utils.get_network_driver()
    try:
        port = network_driver.get_port(port_id)
    except Exception:
        raise exceptions.InvalidSubresource(resource='Port', id=port_id)
    return port


324 325 326 327 328 329 330 331 332
def check_port_in_use(port):
    """Raise an exception when a port is used."""
    if port.device_id:
        raise exceptions.ValidationException(detail=_(
            "Port %(port_id)s is already used by device %(device_id)s ") %
            {'port_id': port.id, 'device_id': port.device_id})
    return False


333
def subnet_exists(subnet_id):
334
    """Raises an exception when a subnet does not exist."""
335 336
    network_driver = utils.get_network_driver()
    try:
337
        subnet = network_driver.get_subnet(subnet_id)
338
    except Exception:
339 340 341 342
        raise exceptions.InvalidSubresource(resource='Subnet', id=subnet_id)
    return subnet


343 344
def qos_policy_exists(qos_policy_id):
    network_driver = utils.get_network_driver()
345
    qos_extension_enabled(network_driver)
346 347 348 349 350 351 352 353
    try:
        qos_policy = network_driver.get_qos_policy(qos_policy_id)
    except Exception:
        raise exceptions.InvalidSubresource(resource='qos_policy',
                                            id=qos_policy_id)
    return qos_policy


354 355 356 357 358 359
def qos_extension_enabled(network_driver):
    if not network_driver.qos_enabled():
        raise exceptions.ValidationException(detail=_(
            "VIP QoS policy is not allowed in this deployment."))


360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
def network_exists_optionally_contains_subnet(network_id, subnet_id=None):
    """Raises an exception when a network does not exist.

    If a subnet is provided, also validate the network contains that subnet.
    """
    network_driver = utils.get_network_driver()
    try:
        network = network_driver.get_network(network_id)
    except Exception:
        raise exceptions.InvalidSubresource(resource='Network', id=network_id)
    if subnet_id:
        if not network.subnets or subnet_id not in network.subnets:
            raise exceptions.InvalidSubresource(resource='Subnet',
                                                id=subnet_id)
    return network
375 376 377 378 379 380 381 382 383


def network_allowed_by_config(network_id):
    if CONF.networking.valid_vip_networks:
        valid_networks = map(str.lower, CONF.networking.valid_vip_networks)
        if network_id not in valid_networks:
            raise exceptions.ValidationException(detail=_(
                'Supplied VIP network_id is not allowed by the configuration '
                'of this deployment.'))
384 385 386 387 388 389


def is_ip_member_of_cidr(address, cidr):
    if netaddr.IPAddress(address) in netaddr.IPNetwork(cidr):
        return True
    return False
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415


def check_session_persistence(SP_dict):
    try:
        if SP_dict['cookie_name']:
            if SP_dict['type'] != constants.SESSION_PERSISTENCE_APP_COOKIE:
                raise exceptions.ValidationException(detail=_(
                    'Field "cookie_name" can only be specified with session '
                    'persistence of type "APP_COOKIE".'))
            bad_cookie_name = re.compile(r'[\x00-\x20\x22\x28-\x29\x2c\x2f'
                                         r'\x3a-\x40\x5b-\x5d\x7b\x7d\x7f]+')
            valid_chars = re.compile(r'[\x00-\xff]+')
            if (bad_cookie_name.search(SP_dict['cookie_name']) or
                    not valid_chars.search(SP_dict['cookie_name'])):
                raise exceptions.ValidationException(detail=_(
                    'Supplied "cookie_name" is invalid.'))
        if (SP_dict['type'] == constants.SESSION_PERSISTENCE_APP_COOKIE and
                not SP_dict['cookie_name']):
            raise exceptions.ValidationException(detail=_(
                'Field "cookie_name" must be specified when using the '
                '"APP_COOKIE" session persistence type.'))
    except exceptions.ValidationException:
        raise
    except Exception:
        raise exceptions.ValidationException(detail=_(
            'Invalid session_persistence provided.'))
416 417 418 419 420 421 422 423


def ip_not_reserved(ip_address):
    ip_address = (
        ipaddress.ip_address(six.text_type(ip_address)).exploded.upper())
    if ip_address in CONF.networking.reserved_ips:
        raise exceptions.InvalidOption(value=ip_address,
                                       option='member address')
424 425 426 427 428 429 430 431


def is_flavor_spares_compatible(flavor):
    if flavor:
        # If a compute flavor is specified, the flavor is not spares compatible
        if flavor.get(constants.COMPUTE_FLAVOR, None):
            return False
    return True