Add an option to the Octavia V2 listener API for client cert

Listener API for client cerificate authentication with "None,
Optional, Mandatory" options

Story: 2002165
Task: 20019
Co-Authored-By: Michael Johnson <johnsomor@gmail.com>
Change-Id: Ia753659981d99b315504f166c09afb8f5b14f195
This commit is contained in:
ZhaoBo 2018-10-11 20:49:43 +08:00 committed by Michael Johnson
parent 0cc546a7c7
commit 7a8eb3ce22
26 changed files with 314 additions and 30 deletions

View File

@ -246,6 +246,22 @@ cert-expiration:
in: body
required: true
type: string
client_authentication:
description: |
The TLS client authentication mode. One of the options ``NONE``,
``OPTIONAL`` or ``MANDATORY``.
in: body
min_version: 2.8
required: true
type: string
client_authentication-optional:
description: |
The TLS client authentication mode. One of the options ``NONE``,
``OPTIONAL`` or ``MANDATORY``.
in: body
min_version: 2.8
required: false
type: string
client_ca_tls_container_ref:
description: |
The ref of the `key manager service

View File

@ -1 +1 @@
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"protocol": "TERMINATED_HTTPS", "description": "A great TLS listener", "admin_state_up": true, "connection_limit": 200, "protocol_port": "443", "loadbalancer_id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "name": "great_tls_listener", "insert_headers": {"X-Forwarded-For": "true", "X-Forwarded-Port": "true"}, "default_tls_container_ref": "http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "sni_container_refs": ["http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "http://198.51.100.10:9311/v1/containers/aaebb31e-7761-4826-8cb4-2b829caca3ee"], "timeout_client_data": 50000, "timeout_member_connect": 5000, "timeout_member_data": 50000, "timeout_tcp_inspect": 0, "tags": ["test_tag"], "client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5"}}' http://198.51.100.10:9876/v2/lbaas/listeners
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"protocol": "TERMINATED_HTTPS", "description": "A great TLS listener", "admin_state_up": true, "connection_limit": 200, "protocol_port": "443", "loadbalancer_id": "607226db-27ef-4d41-ae89-f2a800e9c2db", "name": "great_tls_listener", "insert_headers": {"X-Forwarded-For": "true", "X-Forwarded-Port": "true"}, "default_tls_container_ref": "http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "sni_container_refs": ["http://198.51.100.10:9311/v1/containers/a570068c-d295-4780-91d4-3046a325db51", "http://198.51.100.10:9311/v1/containers/aaebb31e-7761-4826-8cb4-2b829caca3ee"], "timeout_client_data": 50000, "timeout_member_connect": 5000, "timeout_member_data": 50000, "timeout_tcp_inspect": 0, "tags": ["test_tag"], "client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5", "client_authentication": "MANDATORY"}}' http://198.51.100.10:9876/v2/lbaas/listeners

View File

@ -21,6 +21,7 @@
"timeout_member_data": 50000,
"timeout_tcp_inspect": 0,
"tags": ["test_tag"],
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5"
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
"client_authentication": "MANDATORY"
}
}

View File

@ -36,6 +36,7 @@
"timeout_member_data": 50000,
"timeout_tcp_inspect": 0,
"tags": ["test_tag"],
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5"
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
"client_authentication": "MANDATORY"
}
}

View File

@ -36,6 +36,7 @@
"timeout_member_data": 50000,
"timeout_tcp_inspect": 0,
"tags": ["test_tag"],
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5"
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
"client_authentication": "MANDATORY"
}
}

View File

@ -36,6 +36,7 @@
"timeout_member_data": 100000,
"timeout_tcp_inspect": 5,
"tags": ["updated_tag"],
"client_ca_tls_container_ref": null
"client_ca_tls_container_ref": null,
"client_authentication": "NONE"
}
}

View File

@ -38,7 +38,8 @@
"timeout_member_data": 50000,
"timeout_tcp_inspect": 0,
"tags": ["test_tag"],
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5"
"client_ca_tls_container_ref": "http://198.51.100.10:9311/v1/containers/35649991-49f3-4625-81ce-2465fe8932e5",
"client_authentication": "NONE"
}
]
}

View File

@ -46,6 +46,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit
- created_at: created_at
@ -137,6 +138,7 @@ Request
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up-default-optional
- client_authentication: client_authentication-optional
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
- connection_limit: connection_limit-optional
- default_pool: pool-optional
@ -206,6 +208,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit
- created_at: created_at
@ -281,6 +284,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit
- created_at: created_at
@ -346,6 +350,7 @@ Request
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up-default-optional
- client_authentication: client_authentication-optional
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
- connection_limit: connection_limit-optional
- default_pool_id: default_pool_id-optional
@ -379,6 +384,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit
- created_at: created_at

View File

@ -364,9 +364,13 @@ contain the following:
| admin_state_up | bool | Admin state: True if up, False if |
| | | down. |
+------------------------------+--------+-------------------------------------+
|client_ca_tls_container_data | string | A PEM encoded certificate. |
| client_authentication | string | The TLS client authentication mode. |
| | | One of the options ``NONE``, |
| | | ``OPTIONAL`` or ``MANDATORY``. |
+------------------------------+--------+-------------------------------------+
|client_ca_tls_container_ref | string | The reference to the secrets |
| client_ca_tls_container_data | string | A PEM encoded certificate. |
+------------------------------+--------+-------------------------------------+
| client_ca_tls_container_ref | string | The reference to the secrets |
| | | container. |
+------------------------------+--------+-------------------------------------+
| connection_limit | int | The max number of connections |

View File

@ -134,7 +134,8 @@ class Listener(BaseDataModel):
sni_container_data=Unset, timeout_client_data=Unset,
timeout_member_connect=Unset, timeout_member_data=Unset,
timeout_tcp_inspect=Unset, client_ca_tls_container_ref=Unset,
client_ca_tls_container_data=Unset):
client_ca_tls_container_data=Unset,
client_authentication=Unset):
self.admin_state_up = admin_state_up
self.connection_limit = connection_limit
@ -158,6 +159,7 @@ class Listener(BaseDataModel):
self.timeout_tcp_inspect = timeout_tcp_inspect
self.client_ca_tls_container_ref = client_ca_tls_container_ref
self.client_ca_tls_container_data = client_ca_tls_container_data
self.client_authentication = client_authentication
class Pool(BaseDataModel):

View File

@ -230,6 +230,15 @@ class ListenersController(base.BaseController):
"be provided for %s protocol listeners.") %
constants.PROTOCOL_TERMINATED_HTTPS)
# Make sure we have a client CA cert if they enable client auth
if (listener_dict.get('client_authentication') !=
constants.CLIENT_AUTH_NONE and not
listener_dict.get('client_ca_tls_certificate_id')):
raise exceptions.ValidationException(detail=_(
"Client authentication setting %s requires a client CA "
"container reference.") %
listener_dict.get('client_authentication'))
try:
sni_containers = listener_dict.pop('sni_containers', [])
tls_refs = [sni['tls_container_id'] for sni in sni_containers]
@ -382,7 +391,16 @@ class ListenersController(base.BaseController):
"%s protocol listeners.") %
constants.PROTOCOL_TERMINATED_HTTPS)
# Make sure the refs are valid
# Make sure we have a client CA cert if they enable client auth
if ((listener.client_authentication != wtypes.Unset and
listener.client_authentication != constants.CLIENT_AUTH_NONE)
and not (db_listener.client_ca_tls_certificate_id or
listener.client_ca_tls_container_ref)):
raise exceptions.ValidationException(detail=_(
"Client authentication setting %s requires a client CA "
"container reference.") %
listener.client_authentication)
sni_containers = listener.sni_container_refs or []
tls_refs = [sni for sni in sni_containers]
if listener.default_tls_container_ref:

View File

@ -58,6 +58,7 @@ class ListenerResponse(BaseListenerType):
timeout_tcp_inspect = wtypes.wsattr(wtypes.IntegerType())
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
client_ca_tls_container_ref = wtypes.StringType()
client_authentication = wtypes.wsattr(wtypes.StringType())
@classmethod
def from_data_model(cls, data_model, children=False):
@ -138,6 +139,9 @@ class ListenerPOST(BaseListenerType):
default=CONF.haproxy_amphora.timeout_tcp_inspect)
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
client_ca_tls_container_ref = wtypes.StringType(max_length=255)
client_authentication = wtypes.wsattr(
wtypes.Enum(str, *constants.SUPPORTED_CLIENT_AUTH_MODES),
default=constants.CLIENT_AUTH_NONE)
class ListenerRootPOST(types.BaseType):
@ -171,6 +175,8 @@ class ListenerPUT(BaseListenerType):
maximum=constants.MAX_TIMEOUT))
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
client_ca_tls_container_ref = wtypes.StringType(max_length=255)
client_authentication = wtypes.wsattr(
wtypes.Enum(str, *constants.SUPPORTED_CLIENT_AUTH_MODES))
class ListenerRootPUT(types.BaseType):
@ -215,6 +221,9 @@ class ListenerSingleCreate(BaseListenerType):
default=CONF.haproxy_amphora.timeout_tcp_inspect)
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
client_ca_tls_container_ref = wtypes.StringType(max_length=255)
client_authentication = wtypes.wsattr(
wtypes.Enum(str, *constants.SUPPORTED_CLIENT_AUTH_MODES),
default=constants.CLIENT_AUTH_NONE)
class ListenerStatusResponse(BaseListenerType):

View File

@ -580,3 +580,11 @@ FLAVOR_DATA = 'flavor_data'
# Flavor metadata
LOADBALANCER_TOPOLOGY = 'loadbalancer_topology'
COMPUTE_FLAVOR = 'compute_flavor'
# TODO(johnsom) move to octavia_lib
# client certification authorization option
CLIENT_AUTH_NONE = 'NONE'
CLIENT_AUTH_OPTIONAL = 'OPTIONAL'
CLIENT_AUTH_MANDATORY = 'MANDATORY'
SUPPORTED_CLIENT_AUTH_MODES = [CLIENT_AUTH_NONE, CLIENT_AUTH_OPTIONAL,
CLIENT_AUTH_MANDATORY]

View File

@ -368,7 +368,8 @@ class Listener(BaseDataModel):
created_at=None, updated_at=None,
timeout_client_data=None, timeout_member_connect=None,
timeout_member_data=None, timeout_tcp_inspect=None,
tags=None, client_ca_tls_certificate_id=None):
tags=None, client_ca_tls_certificate_id=None,
client_authentication=None):
self.id = id
self.project_id = project_id
self.name = name
@ -398,6 +399,7 @@ class Listener(BaseDataModel):
self.timeout_tcp_inspect = timeout_tcp_inspect
self.tags = tags
self.client_ca_tls_certificate_id = client_ca_tls_certificate_id
self.client_authentication = client_authentication
def update(self, update_dict):
for key, value in update_dict.items():

View File

@ -36,6 +36,10 @@ BALANCE_MAP = {
constants.LB_ALGORITHM_SOURCE_IP: 'source'
}
CLIENT_AUTH_MAP = {constants.CLIENT_AUTH_NONE: 'none',
constants.CLIENT_AUTH_OPTIONAL: 'optional',
constants.CLIENT_AUTH_MANDATORY: 'required'}
ACTIVE_PENDING_STATUSES = constants.SUPPORTED_PROVISIONING_STATUSES + (
constants.DEGRADED,)
@ -239,6 +243,9 @@ class JinjaTemplater(object):
ret_value['client_ca_tls_path'] = '%s' % (
os.path.join(self.base_crt_dir, listener.id,
client_ca_filename))
ret_value['client_auth'] = CLIENT_AUTH_MAP.get(
listener.client_authentication)
if listener.default_pool:
ret_value['default_pool'] = self._transform_pool(
listener.default_pool, feature_compatibility)

View File

@ -38,8 +38,8 @@ peers {{ "%s_peers"|format(listener.id.replace("-", ""))|trim() }}
{% else %}
{% set crt_dir_opt = "" %}
{% endif %}
{% if listener.client_ca_tls_path %}
{% set client_ca_opt = "ca-file %s"|format(listener.client_ca_tls_path)|trim() %}
{% if listener.client_ca_tls_path and listener.client_auth %}
{% set client_ca_opt = "ca-file %s verify %s"|format(listener.client_ca_tls_path, listener.client_auth)|trim() %}
{% else %}
{% set client_ca_opt = "" %}
{% endif %}

View File

@ -0,0 +1,61 @@
# 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.
#
"""Add Client Auth options
Revision ID: f21ae3f21adc
Revises: 2ad093f6353f
Create Date: 2018-10-01 20:47:52.405865
"""
from alembic import op
import sqlalchemy as sa
from octavia.common import constants
# revision identifiers, used by Alembic.
revision = 'f21ae3f21adc'
down_revision = '2ad093f6353f'
def upgrade():
op.create_table(
u'client_authentication_mode',
sa.Column(u'name', sa.String(10), primary_key=True),
)
# Create temporary table for table data seeding
insert_table = sa.table(
u'client_authentication_mode',
sa.column(u'name', sa.String),
)
op.bulk_insert(
insert_table,
[
{'name': constants.CLIENT_AUTH_NONE},
{'name': constants.CLIENT_AUTH_OPTIONAL},
{'name': constants.CLIENT_AUTH_MANDATORY}
]
)
op.add_column(
u'listener',
sa.Column(u'client_authentication', sa.String(10),
sa.ForeignKey('client_authentication_mode.name'),
server_default=constants.CLIENT_AUTH_NONE, nullable=False)
)

View File

@ -33,6 +33,7 @@ from octavia.api.v2.types import load_balancer
from octavia.api.v2.types import member
from octavia.api.v2.types import pool
from octavia.api.v2.types import quotas
from octavia.common import constants
from octavia.common import data_models
from octavia.db import base_models
from octavia.i18n import _
@ -499,6 +500,11 @@ class Listener(base_models.BASE, base_models.IdMixin,
timeout_member_data = sa.Column(sa.Integer, nullable=True)
timeout_tcp_inspect = sa.Column(sa.Integer, nullable=True)
client_ca_tls_certificate_id = sa.Column(sa.String(255), nullable=True)
client_authentication = sa.Column(
sa.String(10),
sa.ForeignKey("client_authentication_mode.name",
name="fk_listener_client_authentication_mode_name"),
nullable=False, default=constants.CLIENT_AUTH_NONE)
_tags = orm.relationship(
'Tags',
@ -762,3 +768,10 @@ class Flavor(base_models.BASE,
sa.ForeignKey("flavor_profile.id",
name="fk_flavor_flavor_profile_id"),
nullable=False)
class ClientAuthenticationMode(base_models.BASE):
__tablename__ = "client_authentication_mode"
name = sa.Column(sa.String(10), primary_key=True, nullable=False)

View File

@ -101,6 +101,8 @@ def create_listener(listener_dict, lb_id):
for sni_container_id in sni_container_ids]
listener_dict['sni_containers'] = sni_containers
if 'client_authentication' not in listener_dict:
listener_dict['client_authentication'] = constants.CLIENT_AUTH_NONE
return listener_dict

View File

@ -957,6 +957,46 @@ class TestListener(base.BaseAPITest):
listener_api = self.test_create(**optionals)
self.assertEqual(optionals['client_ca_tls_container_ref'],
listener_api.get('client_ca_tls_container_ref'))
self.assertEqual(constants.CLIENT_AUTH_NONE,
listener_api.get('client_authentication'))
def test_create_with_ca_cert_and_option(self):
self.cert_manager_mock().get_secret.return_value = (
sample_certs.X509_CA_CERT)
optionals = {
'client_ca_tls_container_ref': uuidutils.generate_uuid(),
'client_authentication': constants.CLIENT_AUTH_MANDATORY
}
listener_api = self.test_create(**optionals)
self.assertEqual(optionals['client_ca_tls_container_ref'],
listener_api.get('client_ca_tls_container_ref'))
self.assertEqual(optionals['client_authentication'],
listener_api.get('client_authentication'))
def test_create_with_ca_cert_negative_cases(self):
# create just with option, no client_ca_tls_container_ref specified.
optionals = {
'client_authentication': constants.CLIENT_AUTH_MANDATORY
}
sni1 = uuidutils.generate_uuid()
sni2 = uuidutils.generate_uuid()
lb_listener = {
'name': 'listener1', 'default_pool_id': None,
'description': 'desc1',
'admin_state_up': False,
'protocol': constants.PROTOCOL_TERMINATED_HTTPS,
'protocol_port': 80,
'default_tls_container_ref': uuidutils.generate_uuid(),
'sni_container_refs': [sni1, sni2],
'project_id': self.project_id,
'loadbalancer_id': self.lb_id}
lb_listener.update(optionals)
body = self._build_body(lb_listener)
response = self.post(self.LISTENERS_PATH, body, status=400).json
self.assertEqual(
"Validation failure: Client authentication setting %s "
"requires a client CA container reference." %
constants.CLIENT_AUTH_MANDATORY, response['faultstring'])
def test_create_with_bad_ca_cert_ref(self):
sni1 = uuidutils.generate_uuid()
@ -1163,6 +1203,70 @@ class TestListener(base.BaseAPITest):
self.assertNotEqual(ori_listener['client_ca_tls_container_ref'],
optionals['client_ca_tls_container_ref'])
def test_update_with_only_client_auth_option(self):
optionals = {
'client_authentication': constants.CLIENT_AUTH_OPTIONAL
}
ori_listener, update_listener = self.test_update(**optionals)
self.assertEqual(optionals['client_authentication'],
update_listener.get('client_authentication'))
self.assertNotEqual(ori_listener['client_authentication'],
optionals['client_authentication'])
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_from_nonexist_ca_cert_to_new_ca_cert(self, mock_cert_data):
cert1 = data_models.TLSContainer(certificate='cert 1')
mock_cert_data.return_value = {'tls_cert': cert1}
self.cert_manager_mock().get_secret.return_value = (
sample_certs.X509_CA_CERT)
tls_uuid = uuidutils.generate_uuid()
listener = self.create_listener(
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
name='listener1', description='desc1',
admin_state_up=False, connection_limit=10,
default_tls_container_ref=tls_uuid,
default_pool_id=None).get(self.root_tag)
self.set_lb_status(self.lb_id)
ca_tls_uuid = uuidutils.generate_uuid()
new_listener = {
'client_ca_tls_container_ref': ca_tls_uuid}
body = self._build_body(new_listener)
listener_path = self.LISTENER_PATH.format(
listener_id=listener['id'])
api_listener = self.put(listener_path, body).json.get(self.root_tag)
update_expect = {'provisioning_status': constants.PENDING_UPDATE,
'operating_status': constants.ONLINE}
update_expect.update(new_listener)
listener.update(update_expect)
self.assertEqual(ca_tls_uuid,
api_listener['client_ca_tls_container_ref'])
self.assertEqual(constants.CLIENT_AUTH_NONE,
api_listener['client_authentication'])
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_with_ca_cert_negative_cases(self, mock_cert_data):
# update a listener, no ca cert exist
cert1 = data_models.TLSContainer(certificate='cert 1')
mock_cert_data.return_value = {'tls_cert': cert1}
tls_uuid = uuidutils.generate_uuid()
listener = self.create_listener(
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
name='listener1', description='desc1',
admin_state_up=False, connection_limit=10,
default_tls_container_ref=tls_uuid,
default_pool_id=None).get(self.root_tag)
self.set_lb_status(self.lb_id)
lb_listener = {
'client_authentication': constants.CLIENT_AUTH_OPTIONAL}
body = self._build_body(lb_listener)
listener_path = self.LISTENER_PATH.format(
listener_id=listener['id'])
response = self.put(listener_path, body, status=400).json
self.assertEqual(
"Validation failure: Client authentication setting %s "
"requires a client CA container reference." %
constants.CLIENT_AUTH_OPTIONAL, response['faultstring'])
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_unset_ca_cert(self, mock_cert_data):
cert1 = data_models.TLSContainer(certificate='cert 1')
@ -1185,6 +1289,7 @@ class TestListener(base.BaseAPITest):
listener_id=listener['id'])
api_listener = self.put(listener_path, body).json.get(self.root_tag)
self.assertIsNone(api_listener.get('client_ca_tls_container_ref'))
self.assertIsNone(api_listener.get('client_auth_option'))
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_with_bad_ca_cert(self, mock_cert_data):

View File

@ -2349,16 +2349,16 @@ class TestLoadBalancerGraph(base.BaseAPITest):
expected_lb['pools'] = create_pools or []
return create_lb, expected_lb
def _get_listener_bodies(self, name='listener1', protocol_port=80,
create_default_pool_name=None,
create_default_pool_id=None,
create_l7policies=None,
expected_l7policies=None,
create_sni_containers=None,
expected_sni_containers=None,
create_client_ca_tls_container=None,
expected_client_ca_tls_container=None,
create_protocol=constants.PROTOCOL_HTTP):
def _get_listener_bodies(
self, name='listener1', protocol_port=80,
create_default_pool_name=None, create_default_pool_id=None,
create_l7policies=None, expected_l7policies=None,
create_sni_containers=None, expected_sni_containers=None,
create_client_ca_tls_container=None,
expected_client_ca_tls_container=None,
create_protocol=constants.PROTOCOL_HTTP,
create_client_authentication=None,
expected_client_authentication=constants.CLIENT_AUTH_NONE):
create_listener = {
'name': name,
'protocol_port': protocol_port,
@ -2379,7 +2379,8 @@ class TestLoadBalancerGraph(base.BaseAPITest):
'timeout_member_data': constants.DEFAULT_TIMEOUT_MEMBER_DATA,
'timeout_tcp_inspect': constants.DEFAULT_TIMEOUT_TCP_INSPECT,
'tags': [],
'client_ca_tls_container_ref': None
'client_ca_tls_container_ref': None,
'client_authentication': constants.CLIENT_AUTH_NONE
}
if create_sni_containers:
create_listener['sni_container_refs'] = create_sni_containers
@ -2398,6 +2399,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
if create_client_ca_tls_container:
create_listener['client_ca_tls_container_ref'] = (
create_client_ca_tls_container)
if create_client_authentication:
create_listener['client_authentication'] = (
create_client_authentication)
if expected_sni_containers:
expected_listener['sni_container_refs'] = expected_sni_containers
if expected_l7policies:
@ -2407,6 +2411,11 @@ class TestLoadBalancerGraph(base.BaseAPITest):
if expected_client_ca_tls_container:
expected_listener['client_ca_tls_container_ref'] = (
expected_client_ca_tls_container)
expected_listener['client_authentication'] = (
constants.CLIENT_AUTH_NONE)
if expected_client_authentication:
expected_listener[
'client_authentication'] = expected_client_authentication
return create_listener, expected_listener
def _get_pool_bodies(self, name='pool1', create_members=None,
@ -2670,6 +2679,8 @@ class TestLoadBalancerGraph(base.BaseAPITest):
mock_x509_cert.return_value = cert_mock
create_client_ca_tls_container = uuidutils.generate_uuid()
expected_client_ca_tls_container = create_client_ca_tls_container
create_client_authentication = constants.CLIENT_AUTH_MANDATORY
expected_client_authentication = constants.CLIENT_AUTH_MANDATORY
create_sni_containers, expected_sni_containers = (
self._get_sni_container_bodies())
create_listener, expected_listener = self._get_listener_bodies(
@ -2677,7 +2688,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
create_sni_containers=create_sni_containers,
expected_sni_containers=expected_sni_containers,
create_client_ca_tls_container=create_client_ca_tls_container,
expected_client_ca_tls_container=expected_client_ca_tls_container)
expected_client_ca_tls_container=expected_client_ca_tls_container,
create_client_authentication=create_client_authentication,
expected_client_authentication=expected_client_authentication)
create_lb, expected_lb = self._get_lb_bodies(
create_listeners=[create_listener],
expected_listeners=[expected_listener])

View File

@ -79,6 +79,8 @@ class OctaviaDBTestBase(test_base.DbTestCase):
models.L7RuleCompareType)
self._seed_lookup_table(session, constants.SUPPORTED_L7POLICY_ACTIONS,
models.L7PolicyAction)
self._seed_lookup_table(session, constants.SUPPORTED_CLIENT_AUTH_MODES,
models.ClientAuthenticationMode)
def _seed_lookup_table(self, session, name_list, model_cls):
for name in name_list:

View File

@ -382,7 +382,8 @@ class SampleDriverDataModels(object):
'timeout_member_connect': 2000,
'timeout_member_data': 3000,
'timeout_tcp_inspect': 4000,
'client_ca_tls_certificate_id': self.client_ca_tls_certificate_ref
'client_ca_tls_certificate_id': self.client_ca_tls_certificate_ref,
'client_authentication': constants.CLIENT_AUTH_NONE
}
self.test_listener1_dict.update(self._common_test_dict)
@ -439,7 +440,8 @@ class SampleDriverDataModels(object):
'timeout_member_data': 3000,
'timeout_tcp_inspect': 4000,
'client_ca_tls_container_ref': self.client_ca_tls_certificate_ref,
'client_ca_tls_container_data': ca_cert
'client_ca_tls_container_data': ca_cert,
'client_authentication': constants.CLIENT_AUTH_NONE
}
self.provider_listener2_dict = copy.deepcopy(
@ -452,6 +454,8 @@ class SampleDriverDataModels(object):
del self.provider_listener2_dict['l7policies']
self.provider_listener2_dict['client_ca_tls_container_ref'] = None
del self.provider_listener2_dict['client_ca_tls_container_data']
self.provider_listener2_dict['client_authentication'] = (
constants.CLIENT_AUTH_NONE)
self.provider_listener1 = driver_dm.Listener(
**self.provider_listener1_dict)

View File

@ -40,7 +40,7 @@ class TestHaproxyCfg(base.TestCase):
"sample_listener_id_1/tls_container_id.pem "
"crt /var/lib/octavia/certs/sample_listener_id_1 "
"ca-file /var/lib/octavia/certs/sample_listener_id_1/"
"client_ca.pem\n"
"client_ca.pem verify required\n"
" mode http\n"
" default_backend sample_pool_id_1\n"
" timeout client 50000\n\n").format(

View File

@ -528,7 +528,7 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
'l7policies, enabled, insert_headers, timeout_client_data,'
'timeout_member_connect, timeout_member_data, '
'timeout_tcp_inspect, client_ca_tls_certificate_id,'
'client_ca_tls_certificate')
'client_ca_tls_certificate, client_authentication')
if l7:
pools = [
sample_pool_tuple(
@ -611,7 +611,10 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
client_ca_tls_certificate=sample_tls_container_tuple(
id='cont_id_ca', certificate=sample_certs.X509_CA_CERT,
primary_cn=sample_certs.X509_CA_CERT_CN
) if client_ca_cert else ''
) if client_ca_cert else '',
client_authentication=(
constants.CLIENT_AUTH_MANDATORY if client_ca_cert else
constants.CLIENT_AUTH_NONE)
)

View File

@ -0,0 +1,4 @@
---
features:
- |
You can now enable TLS client authentication on listeners.