From f77d7d0220515dd78c03beb97663337b5bb46c3a Mon Sep 17 00:00:00 2001 From: ZhaoBo Date: Thu, 18 Oct 2018 17:38:19 +0800 Subject: [PATCH] L7rule support client certificate cases This patch add 4 new types for SSL connection ACL configuration. Which are: L7RULE_TYPE_SSL_CONN_HAS_CERT L7RULE_TYPE_VERIFY_RESULT L7RULE_TYPE_DN_FIELD The first type can just accept the compare type "EQUAL_TO" and value "True" string. The second can just accept the int value string to check the certificate verify result, also just support "EQUAL_TO" compare type. The third can accept key, the distinguished name field and a match string, this one supports all kind compare types. Story: 2002165 Task: 20025 Co-Authored-By: Michael Johnson Change-Id: I71b57d0f32d4839a770396645d2b9945d24f2853 --- api-ref/source/parameters.yaml | 6 +- doc/source/user/guides/l7-cookbook.rst | 64 +++++++ doc/source/user/guides/l7.rst | 10 + octavia/common/constants.py | 8 +- .../common/jinja/haproxy/templates/macros.j2 | 8 + octavia/common/validate.py | 62 ++++++ ...1afc932f1ca2_l7rule_support_client_cert.py | 44 +++++ .../tests/functional/api/v2/test_l7rule.py | 176 ++++++++++++++++++ .../common/jinja/haproxy/test_jinja_cfg.py | 82 ++++++++ .../common/sample_configs/sample_configs.py | 42 ++++- ...lient-authentication-22e3ae29aaf7fc26.yaml | 6 + 11 files changed, 504 insertions(+), 4 deletions(-) create mode 100644 octavia/db/migration/alembic_migrations/versions/1afc932f1ca2_l7rule_support_client_cert.py create mode 100644 releasenotes/notes/Adds-L7rule-support-for-TLS-client-authentication-22e3ae29aaf7fc26.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index c41fb0fbd6..fb62354466 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -770,14 +770,16 @@ l7rule-key-optional: l7rule-type: description: | The L7 rule type. One of ``COOKIE``, ``FILE_TYPE``, ``HEADER``, - ``HOST_NAME``, or ``PATH``. + ``HOST_NAME``, ``PATH``, ``SSL_CONN_HAS_CERT``, ``SSL_VERIFY_RESULT``, + or ``SSL_DN_FIELD``. in: body required: true type: string l7rule-type-optional: description: | The L7 rule type. One of ``COOKIE``, ``FILE_TYPE``, ``HEADER``, - ``HOST_NAME``, or ``PATH``. + ``HOST_NAME``, ``PATH``, ``SSL_CONN_HAS_CERT``, ``SSL_VERIFY_RESULT``, + or ``SSL_DN_FIELD``. in: body required: false type: string diff --git a/doc/source/user/guides/l7-cookbook.rst b/doc/source/user/guides/l7-cookbook.rst index d5da60a1c4..f5c8fb34fd 100644 --- a/doc/source/user/guides/l7-cookbook.rst +++ b/doc/source/user/guides/l7-cookbook.rst @@ -354,3 +354,67 @@ sent to *static_pool_B*, which is why *policy2* needs to be evaluated before openstack loadbalancer l7rule create --compare-type EQUAL_TO --key site_version --type COOKIE --value B policy2 openstack loadbalancer l7policy create --action REDIRECT_TO_POOL --redirect-pool pool_B --name policy3 --position 2 listener1 openstack loadbalancer l7rule create --compare-type EQUAL_TO --key site_version --type COOKIE --value B policy3 + + +Redirect requests with an invalid TLS client authentication certificate +----------------------------------------------------------------------- +**Scenario description**: + +* Listener *listener1* on load balancer *lb1* is configured for ``OPTIONAL`` + client_authentication. +* Web clients that do not present a TLS client authentication certificate + should be redirected to a signup page at *http://www.example.com/signup*. + +**Solution**: + +1. Create the load balancer *lb1*. +2. Create a listener *listner1* of type ``TERMINATED_TLS`` with a + client_ca_tls_container_ref and client_authentication ``OPTIONAL``. +3. Create a L7 Policy *policy1* on *listener1* with action ``REDIRECT_TO_URL`` + pointed at the URL *http://www.example.com/signup*. +4. Add an L7 Rule to *policy1* that does not match ``SSL_CONN_HAS_CERT``. + +**CLI commands**: + +.. code-block:: bash + + openstack loadbalancer create --name lb1 --vip-subnet-id public-subnet + openstack loadbalancer listener create --name listener1 --protocol TERMINATED_HTTPS --client-authentication OPTIONAL --protocol-port 443 --default-tls-container-ref http://192.0.2.15:9311/v1/secrets/697c2a6d-ffbe-40b8-be5e-7629fd636bca --client-ca-tls-container-ref http://192.0.2.15:9311/v1/secrets/dba60b77-8dad-4171-8a96-f21e1ca5fb46 lb1 + openstack loadbalancer l7policy create --action REDIRECT_TO_URL --redirect-url http://www.example.com/signup --name policy1 listener1 + openstack loadbalancer l7rule create --type SSL_CONN_HAS_CERT --invert --compare-type EQUAL_TO --value True policy1 + + +Send users from the finance department to pool2 +----------------------------------------------- +**Scenario description**: + +* Users from the finance department have client certificates with the OU field + of the distinguished name set to ``finance``. +* Only users with valid finance department client certificates should be able + to access ``pool2``. Others will be rejected. + +**Solution**: + +1. Create the load balancer *lb1*. +2. Create a listener *listner1* of type ``TERMINATED_TLS`` with a + client_ca_tls_container_ref and client_authentication ``MANDATORY``. +3. Create a pool *pool2* on load balancer *lb1*. +4. Create a L7 Policy *policy1* on *listener1* with action ``REDIRECT_TO_POOL`` + pointed at *pool2*. +5. Add an L7 Rule to *policy1* that matches ``SSL_CONN_HAS_CERT``. +6. Add an L7 Rule to *policy1* that matches ``SSL_VERIFY_RESULT`` with a value + of 0. +7. Add an L7 Rule to *policy1* of type ``SSL_DN_FIELD`` that looks for + "finance" in the "OU" field of the client authentication distinguished name. + +**CLI commands**: + +.. code-block:: bash + + openstack loadbalancer create --name lb1 --vip-subnet-id public-subnet + openstack loadbalancer listener create --name listener1 --protocol TERMINATED_HTTPS --client-authentication MANDATORY --protocol-port 443 --default-tls-container-ref http://192.0.2.15:9311/v1/secrets/697c2a6d-ffbe-40b8-be5e-7629fd636bca --client-ca-tls-container-ref http://192.0.2.15:9311/v1/secrets/dba60b77-8dad-4171-8a96-f21e1ca5fb46 lb1 + openstack loadbalancer pool create --lb-algorithm ROUND_ROBIN --loadbalancer lb1 --name pool2 --protocol HTTP + openstack loadbalancer l7policy create --action REDIRECT_TO_POOL --redirect-pool pool2 --name policy1 listener1 + openstack loadbalancer l7rule create --type SSL_CONN_HAS_CERT --compare-type EQUAL_TO --value True policy1 + openstack loadbalancer l7rule create --type SSL_VERIFY_RESULT --compare-type EQUAL_TO --value 0 policy1 + openstack loadbalancer l7rule create --type SSL_DN_FIELD --compare-type EQUAL_TO --key OU --value finance policy1 diff --git a/doc/source/user/guides/l7.rst b/doc/source/user/guides/l7.rst index 04450e20e7..7949ad8f28 100644 --- a/doc/source/user/guides/l7.rst +++ b/doc/source/user/guides/l7.rst @@ -86,6 +86,15 @@ L7 rules have the following types: compares it against the value parameter in the rule. * ``COOKIE``: The rule looks for a cookie named by the key parameter and compares it against the value parameter in the rule. +* ``SSL_CONN_HAS_CERT``: The rule will match if the client has presented a + certificate for TLS client authentication. This does not imply the + certificate is valid. +* ``SSL_VERIFY_RESULT``: This rule will match the TLS client authentication + certificate validation result. A value of '0' means the certificate was + successfully validated. A value greater than '0' means the certificate + failed validation. This value follows the `openssl-verify result codes `_. +* ``SSL_DN_FIELD``: The rule looks for a Distinguished Name feild defined in + the key parameter and compares it against the value parameter in the rule. Comparison types ________________ @@ -183,3 +192,4 @@ Useful links * `Octavia API Reference `_ * `LBaaS Layer 7 rules `_ * `Using ACLs and fetching samples `_ +* `OpenSSL openssl-verify command `_ diff --git a/octavia/common/constants.py b/octavia/common/constants.py index 29eeea4133..fd343cc767 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -143,9 +143,15 @@ L7RULE_TYPE_PATH = 'PATH' L7RULE_TYPE_FILE_TYPE = 'FILE_TYPE' L7RULE_TYPE_HEADER = 'HEADER' L7RULE_TYPE_COOKIE = 'COOKIE' +L7RULE_TYPE_SSL_CONN_HAS_CERT = 'SSL_CONN_HAS_CERT' +L7RULE_TYPE_SSL_VERIFY_RESULT = 'SSL_VERIFY_RESULT' +L7RULE_TYPE_SSL_DN_FIELD = 'SSL_DN_FIELD' SUPPORTED_L7RULE_TYPES = (L7RULE_TYPE_HOST_NAME, L7RULE_TYPE_PATH, L7RULE_TYPE_FILE_TYPE, L7RULE_TYPE_HEADER, - L7RULE_TYPE_COOKIE) + L7RULE_TYPE_COOKIE, L7RULE_TYPE_SSL_CONN_HAS_CERT, + L7RULE_TYPE_SSL_VERIFY_RESULT, + L7RULE_TYPE_SSL_DN_FIELD) +DISTINGUISHED_NAME_FIELD_REGEX = '^([a-zA-Z][A-Za-z0-9-]*)$' L7RULE_COMPARE_TYPE_REGEX = 'REGEX' L7RULE_COMPARE_TYPE_STARTS_WITH = 'STARTS_WITH' diff --git a/octavia/common/jinja/haproxy/templates/macros.j2 b/octavia/common/jinja/haproxy/templates/macros.j2 index 41b1e8d76b..e84d3a6f58 100644 --- a/octavia/common/jinja/haproxy/templates/macros.j2 +++ b/octavia/common/jinja/haproxy/templates/macros.j2 @@ -86,6 +86,14 @@ bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{ acl {{ l7rule.id }} req.cook({{ l7rule.key }}) {{ l7rule_compare_type_macro( constants, l7rule.compare_type) }} {{ l7rule.value }} + {% elif l7rule.type == constants.L7RULE_TYPE_SSL_CONN_HAS_CERT %} + acl {{ l7rule.id }} ssl_c_used + {% elif l7rule.type == constants.L7RULE_TYPE_SSL_VERIFY_RESULT %} + acl {{ l7rule.id }} ssl_c_verify eq {{ l7rule.value }} + {% elif l7rule.type == constants.L7RULE_TYPE_SSL_DN_FIELD %} + acl {{ l7rule.id }} ssl_c_s_dn({{ l7rule.key }}) {{ + l7rule_compare_type_macro( + constants, l7rule.compare_type) }} {{ l7rule.value }} {% endif %} {% endmacro %} diff --git a/octavia/common/validate.py b/octavia/common/validate.py index 5f64ad9871..1d7a7388a9 100644 --- a/octavia/common/validate.py +++ b/octavia/common/validate.py @@ -26,6 +26,7 @@ import netaddr from oslo_config import cfg import rfc3986 import six +from wsme import types as wtypes from octavia.common import constants from octavia.common import exceptions @@ -161,11 +162,72 @@ def l7rule_data(l7rule): raise exceptions.InvalidL7Rule(msg='invalid comparison type ' 'for rule type') + 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) + else: raise exceptions.InvalidL7Rule(msg='invalid rule type') return True +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) + + def sanitize_l7policy_api_args(l7policy, create=False): """Validate and make consistent L7Policy API arguments. diff --git a/octavia/db/migration/alembic_migrations/versions/1afc932f1ca2_l7rule_support_client_cert.py b/octavia/db/migration/alembic_migrations/versions/1afc932f1ca2_l7rule_support_client_cert.py new file mode 100644 index 0000000000..cd68e1faf4 --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/1afc932f1ca2_l7rule_support_client_cert.py @@ -0,0 +1,44 @@ +# Copyright 2018 Huawei +# +# 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. +# + +"""Extend the l7rule type for support client certificate cases + +Revision ID: 1afc932f1ca2 +Revises: ffad172e98c1 +Create Date: 2018-10-03 20:47:52.405865 + +""" + + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import sql + +# revision identifiers, used by Alembic. +revision = '1afc932f1ca2' +down_revision = 'ffad172e98c1' + +new_fields = ['SSL_CONN_HAS_CERT', 'SSL_VERIFY_RESULT', 'SSL_DN_FIELD'] + + +def upgrade(): + + insert_table = sql.table( + u'l7rule_type', + sql.column(u'name', sa.String), + sql.column(u'description', sa.String) + ) + cows = [{'name': field} for field in new_fields] + op.bulk_insert(insert_table, cows) diff --git a/octavia/tests/functional/api/v2/test_l7rule.py b/octavia/tests/functional/api/v2/test_l7rule.py index b7850506d0..46a19a7fbf 100644 --- a/octavia/tests/functional/api/v2/test_l7rule.py +++ b/octavia/tests/functional/api/v2/test_l7rule.py @@ -682,6 +682,135 @@ class TestL7Rule(base.BaseAPITest): self.assertIn('Provider \'bad_driver\' reports error: broken', response.json.get('faultstring')) + def test_create_with_ssl_rule_types(self): + test_mapping = { + constants.L7RULE_TYPE_SSL_CONN_HAS_CERT: { + 'value': 'tRuE', + 'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO}, + constants.L7RULE_TYPE_SSL_VERIFY_RESULT: { + 'value': '0', + 'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO}, + constants.L7RULE_TYPE_SSL_DN_FIELD: { + 'key': 'st-1', 'value': 'ST-FIELD1-PREFIX', + 'compare_type': constants.L7RULE_COMPARE_TYPE_STARTS_WITH} + } + for l7rule_type, test_body in test_mapping.items(): + self.set_lb_status(self.lb_id) + test_body.update({'type': l7rule_type}) + api_l7rule = self.create_l7rule( + self.l7policy_id, l7rule_type, + test_body['compare_type'], test_body['value'], + key=test_body.get('key')).get(self.root_tag) + self.assertEqual(l7rule_type, api_l7rule.get('type')) + self.assertEqual(test_body['compare_type'], + api_l7rule.get('compare_type')) + self.assertEqual(test_body['value'], api_l7rule.get('value')) + if test_body.get('key'): + self.assertEqual(test_body['key'], api_l7rule.get('key')) + self.assertFalse(api_l7rule.get('invert')) + self.assert_correct_status( + lb_id=self.lb_id, listener_id=self.listener_id, + l7policy_id=self.l7policy_id, l7rule_id=api_l7rule.get('id'), + lb_prov_status=constants.PENDING_UPDATE, + listener_prov_status=constants.PENDING_UPDATE, + l7policy_prov_status=constants.PENDING_UPDATE, + l7rule_prov_status=constants.PENDING_CREATE, + l7rule_op_status=constants.OFFLINE) + + def _test_bad_cases_with_ssl_rule_types(self, is_create=True, + rule_id=None): + if is_create: + req_func = self.post + first_req_arg = self.l7rules_path + else: + req_func = self.put + first_req_arg = self.l7rule_path.format(l7rule_id=rule_id) + + # test bad cases of L7RULE_TYPE_SSL_CONN_HAS_CERT + l7rule = {'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO, + 'invert': False, + 'type': constants.L7RULE_TYPE_SSL_CONN_HAS_CERT, + 'value': 'true', + 'admin_state_up': True, + 'key': 'no-need-key'} + response = req_func(first_req_arg, self._build_body(l7rule), + status=400).json + self.assertIn('L7rule type {0} does not use the "key" field.'.format( + constants.L7RULE_TYPE_SSL_CONN_HAS_CERT), + response.get('faultstring')) + + l7rule.pop('key') + l7rule['value'] = 'not-true-string' + response = req_func(first_req_arg, self._build_body(l7rule), + status=400).json + self.assertIn( + 'L7rule value {0} is not a boolean True string.'.format( + l7rule['value']), response.get('faultstring')) + + l7rule['value'] = 'tRUe' + l7rule['compare_type'] = constants.L7RULE_COMPARE_TYPE_STARTS_WITH + response = req_func(first_req_arg, self._build_body(l7rule), + status=400).json + self.assertIn( + 'L7rule type {0} only supports the {1} compare type.'.format( + constants.L7RULE_TYPE_SSL_CONN_HAS_CERT, + constants.L7RULE_COMPARE_TYPE_EQUAL_TO), + response.get('faultstring')) + + # test bad cases of L7RULE_TYPE_SSL_VERIFY_RES + l7rule = {'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO, + 'invert': False, + 'type': constants.L7RULE_TYPE_SSL_VERIFY_RESULT, + 'value': 'true', + 'admin_state_up': True, + 'key': 'no-need-key'} + response = req_func(first_req_arg, self._build_body(l7rule), + status=400).json + self.assertIn( + 'L7rule type {0} does not use the "key" field.'.format( + l7rule['type']), response.get('faultstring')) + + l7rule.pop('key') + response = req_func(first_req_arg, self._build_body(l7rule), + status=400).json + self.assertIn( + 'L7rule type {0} needs a int value, which is >= 0'.format( + l7rule['type']), response.get('faultstring')) + + l7rule['value'] = '0' + l7rule['compare_type'] = constants.L7RULE_COMPARE_TYPE_STARTS_WITH + response = req_func(first_req_arg, self._build_body(l7rule), + status=400).json + self.assertIn( + 'L7rule type {0} only supports the {1} compare type.'.format( + l7rule['type'], constants.L7RULE_COMPARE_TYPE_EQUAL_TO), + response.get('faultstring')) + + # test bad cases of L7RULE_TYPE_SSL_DN_FIELD + l7rule = {'compare_type': constants.L7RULE_COMPARE_TYPE_REGEX, + 'invert': False, + 'type': constants.L7RULE_TYPE_SSL_DN_FIELD, + 'value': 'bad regex\\', + 'admin_state_up': True} + # This case just test that fail to parse the regex from the value + req_func(first_req_arg, self._build_body(l7rule), status=400).json + + l7rule['value'] = '^.test*$' + response = req_func(first_req_arg, self._build_body(l7rule), + status=400).json + self.assertIn( + 'L7rule type {0} needs to specify a key and a value.'.format( + l7rule['type']), response.get('faultstring')) + + l7rule['key'] = 'NOT_SUPPORTED_DN_FIELD' + response = req_func(first_req_arg, self._build_body(l7rule), + status=400).json + self.assertIn('Invalid L7rule distinguished name field.', + response.get('faultstring')) + + def test_create_bad_cases_with_ssl_rule_types(self): + self._test_bad_cases_with_ssl_rule_types() + def test_update(self): api_l7rule = self.create_l7rule( self.l7policy_id, constants.L7RULE_TYPE_PATH, @@ -811,6 +940,53 @@ class TestL7Rule(base.BaseAPITest): l7policy_id=self.l7policy_id, l7rule_id=api_l7rule.get('id'), l7rule_prov_status=constants.ACTIVE) + def test_update_with_ssl_rule_types(self): + test_mapping = { + constants.L7RULE_TYPE_SSL_CONN_HAS_CERT: { + 'value': 'tRuE', + 'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO}, + constants.L7RULE_TYPE_SSL_VERIFY_RESULT: { + 'value': '0', + 'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO}, + constants.L7RULE_TYPE_SSL_DN_FIELD: { + 'key': 'st-1', 'value': 'ST-FIELD1-PREFIX', + 'compare_type': constants.L7RULE_COMPARE_TYPE_STARTS_WITH} + } + + for l7rule_type, test_body in test_mapping.items(): + self.set_lb_status(self.lb_id) + api_l7rule = self.create_l7rule( + self.l7policy_id, constants.L7RULE_TYPE_PATH, + constants.L7RULE_COMPARE_TYPE_STARTS_WITH, + '/api').get(self.root_tag) + self.set_lb_status(self.lb_id) + test_body.update({'type': l7rule_type}) + response = self.put(self.l7rule_path.format( + l7rule_id=api_l7rule.get('id')), + self._build_body(test_body)).json.get(self.root_tag) + self.assertEqual(l7rule_type, response.get('type')) + self.assertEqual(test_body['compare_type'], + response.get('compare_type')) + self.assertEqual(test_body['value'], response.get('value')) + if test_body.get('key'): + self.assertEqual(test_body['key'], response.get('key')) + self.assertFalse(response.get('invert')) + self.assert_correct_status( + lb_id=self.lb_id, listener_id=self.listener_id, + l7policy_id=self.l7policy_id, l7rule_id=response.get('id'), + lb_prov_status=constants.PENDING_UPDATE, + listener_prov_status=constants.PENDING_UPDATE, + l7policy_prov_status=constants.PENDING_UPDATE, + l7rule_prov_status=constants.PENDING_UPDATE) + + def test_update_bad_cases_with_ssl_rule_types(self): + api_l7rule = self.create_l7rule( + self.l7policy_id, constants.L7RULE_TYPE_PATH, + constants.L7RULE_COMPARE_TYPE_STARTS_WITH, + '/api').get(self.root_tag) + self._test_bad_cases_with_ssl_rule_types( + is_create=False, rule_id=api_l7rule.get('id')) + def test_delete(self): api_l7rule = self.create_l7rule( self.l7policy_id, constants.L7RULE_TYPE_PATH, diff --git a/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py b/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py index 7247ee744e..e4d9fdcfd3 100644 --- a/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py +++ b/octavia/tests/unit/common/jinja/haproxy/test_jinja_cfg.py @@ -942,3 +942,85 @@ class TestHaproxyCfg(base.TestCase): self.assertEqual( sample_configs.sample_base_expected_config(backend=be), rendered_obj) + + def test_ssl_types_l7rules(self): + j_cfg = jinja_cfg.JinjaTemplater( + base_amp_path='/var/lib/octavia', + base_crt_dir='/var/lib/octavia/certs') + fe = ("frontend sample_listener_id_1\n" + " option httplog\n" + " maxconn 1000000\n" + " redirect scheme https if !{ ssl_fc }\n" + " bind 10.0.0.2:443\n" + " mode http\n" + " acl sample_l7rule_id_1 path -m beg /api\n" + " use_backend sample_pool_id_2 if sample_l7rule_id_1\n" + " acl sample_l7rule_id_2 req.hdr(Some-header) -m sub " + "This\\ string\\\\\\ with\\ stuff\n" + " acl sample_l7rule_id_3 req.cook(some-cookie) -m reg " + "this.*|that\n" + " redirect location http://www.example.com " + "if !sample_l7rule_id_2 sample_l7rule_id_3\n" + " acl sample_l7rule_id_4 path_end -m str jpg\n" + " acl sample_l7rule_id_5 req.hdr(host) -i -m end " + ".example.com\n" + " http-request deny " + "if sample_l7rule_id_4 sample_l7rule_id_5\n" + " acl sample_l7rule_id_2 req.hdr(Some-header) -m sub " + "This\\ string\\\\\\ with\\ stuff\n" + " acl sample_l7rule_id_3 req.cook(some-cookie) -m reg " + "this.*|that\n" + " redirect prefix https://example.com " + "if !sample_l7rule_id_2 sample_l7rule_id_3\n" + " acl sample_l7rule_id_7 ssl_c_used\n" + " acl sample_l7rule_id_8 ssl_c_verify eq 1\n" + " acl sample_l7rule_id_9 ssl_c_s_dn(STREET) -m reg " + "^STREET.*NO\\\\.$\n" + " acl sample_l7rule_id_10 ssl_c_s_dn(OU-3) -m beg " + "Orgnization\\ Bala\n" + " acl sample_l7rule_id_11 path -m beg /api\n" + " redirect location http://www.ssl-type-l7rule-test.com " + "if sample_l7rule_id_7 !sample_l7rule_id_8 !sample_l7rule_id_9 " + "!sample_l7rule_id_10 sample_l7rule_id_11\n" + " default_backend sample_pool_id_1\n" + " timeout client 50000\n\n") + be = ("backend sample_pool_id_1\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /index.html\n" + " http-check expect rstatus 418\n" + " fullconn 1000000\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_1 10.0.0.99:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_1\n" + " server sample_member_id_2 10.0.0.98:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_2\n\n" + "backend sample_pool_id_2\n" + " mode http\n" + " balance roundrobin\n" + " cookie SRV insert indirect nocache\n" + " timeout check 31s\n" + " option httpchk GET /healthmon.html\n" + " http-check expect rstatus 418\n" + " fullconn 1000000\n" + " option allbackups\n" + " timeout connect 5000\n" + " timeout server 50000\n" + " server sample_member_id_3 10.0.0.97:82 weight 13 check " + "inter 30s fall 3 rise 2 cookie sample_member_id_3\n\n") + sample_listener = sample_configs.sample_listener_tuple( + proto=constants.PROTOCOL_TERMINATED_HTTPS, l7=True, + ssl_type_l7=True) + rendered_obj = j_cfg.build_config( + sample_configs.sample_amphora_tuple(), + sample_listener, + tls_cert=None, + haproxy_versions=("1", "5", "18")) + self.assertEqual( + sample_configs.sample_base_expected_config( + frontend=fe, backend=be), + rendered_obj) diff --git a/octavia/tests/unit/common/sample_configs/sample_configs.py b/octavia/tests/unit/common/sample_configs/sample_configs.py index d91ab630e5..3fdb3cf9de 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs.py @@ -512,7 +512,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, timeout_member_connect=5000, timeout_member_data=50000, timeout_tcp_inspect=0, - client_ca_cert=False, client_crl_cert=False): + client_ca_cert=False, client_crl_cert=False, + ssl_type_l7=False): proto = 'HTTP' if proto is None else proto if be_proto is None: be_proto = 'HTTP' if proto is 'TERMINATED_HTTPS' else proto @@ -550,6 +551,9 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, sample_l7policy_tuple('sample_l7policy_id_5', sample_policy=5), sample_l7policy_tuple('sample_l7policy_id_6', sample_policy=6), sample_l7policy_tuple('sample_l7policy_id_7', sample_policy=7)] + if ssl_type_l7: + l7policies.append(sample_l7policy_tuple( + 'sample_l7policy_id_8', sample_policy=8)) else: pools = [ sample_pool_tuple( @@ -799,6 +803,14 @@ def sample_l7policy_tuple(id, redirect_prefix = 'https://example.com' l7rules = [sample_l7rule_tuple('sample_l7rule_id_2', sample_rule=2), sample_l7rule_tuple('sample_l7rule_id_3', sample_rule=3)] + elif sample_policy == 8: + action = constants.L7POLICY_ACTION_REDIRECT_TO_URL + redirect_url = 'http://www.ssl-type-l7rule-test.com' + l7rules = [sample_l7rule_tuple('sample_l7rule_id_7', sample_rule=7), + sample_l7rule_tuple('sample_l7rule_id_8', sample_rule=8), + sample_l7rule_tuple('sample_l7rule_id_9', sample_rule=9), + sample_l7rule_tuple('sample_l7rule_id_10', sample_rule=10), + sample_l7rule_tuple('sample_l7rule_id_11', sample_rule=11)] return in_l7policy( id=id, action=action, @@ -855,6 +867,34 @@ def sample_l7rule_tuple(id, value = '.example.com' invert = False enabled = False + if sample_rule == 7: + type = constants.L7RULE_TYPE_SSL_CONN_HAS_CERT + compare_type = constants.L7RULE_COMPARE_TYPE_EQUAL_TO + key = None + value = 'tRuE' + invert = False + enabled = True + if sample_rule == 8: + type = constants.L7RULE_TYPE_SSL_VERIFY_RESULT + compare_type = constants.L7RULE_COMPARE_TYPE_EQUAL_TO + key = None + value = '1' + invert = True + enabled = True + if sample_rule == 9: + type = constants.L7RULE_TYPE_SSL_DN_FIELD + compare_type = constants.L7RULE_COMPARE_TYPE_REGEX + key = 'STREET' + value = '^STREET.*NO\.$' + invert = True + enabled = True + if sample_rule == 10: + type = constants.L7RULE_TYPE_SSL_DN_FIELD + compare_type = constants.L7RULE_COMPARE_TYPE_STARTS_WITH + key = 'OU-3' + value = 'Orgnization Bala' + invert = True + enabled = True return in_l7rule( id=id, type=type, diff --git a/releasenotes/notes/Adds-L7rule-support-for-TLS-client-authentication-22e3ae29aaf7fc26.yaml b/releasenotes/notes/Adds-L7rule-support-for-TLS-client-authentication-22e3ae29aaf7fc26.yaml new file mode 100644 index 0000000000..30d819591c --- /dev/null +++ b/releasenotes/notes/Adds-L7rule-support-for-TLS-client-authentication-22e3ae29aaf7fc26.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds the ability to define L7 rules based on TLS client authentication + information. The new L7 rules are\: "L7RULE_TYPE_SSL_CONN_HAS_CERT", + "L7RULE_TYPE_VERIFY_RESULT", and "L7RULE_TYPE_DN_FIELD".