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 in: body
required: true required: true
type: string 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: client_ca_tls_container_ref:
description: | description: |
The ref of the `key manager service 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_member_data": 50000,
"timeout_tcp_inspect": 0, "timeout_tcp_inspect": 0,
"tags": ["test_tag"], "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_member_data": 50000,
"timeout_tcp_inspect": 0, "timeout_tcp_inspect": 0,
"tags": ["test_tag"], "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_member_data": 50000,
"timeout_tcp_inspect": 0, "timeout_tcp_inspect": 0,
"tags": ["test_tag"], "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_member_data": 100000,
"timeout_tcp_inspect": 5, "timeout_tcp_inspect": 5,
"tags": ["updated_tag"], "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_member_data": 50000,
"timeout_tcp_inspect": 0, "timeout_tcp_inspect": 0,
"tags": ["test_tag"], "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 .. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up - admin_state_up: admin_state_up
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref - client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit - connection_limit: connection_limit
- created_at: created_at - created_at: created_at
@ -137,6 +138,7 @@ Request
.. rest_parameters:: ../parameters.yaml .. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up-default-optional - admin_state_up: admin_state_up-default-optional
- client_authentication: client_authentication-optional
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional - client_ca_tls_container_ref: client_ca_tls_container_ref-optional
- connection_limit: connection_limit-optional - connection_limit: connection_limit-optional
- default_pool: pool-optional - default_pool: pool-optional
@ -206,6 +208,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml .. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up - admin_state_up: admin_state_up
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref - client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit - connection_limit: connection_limit
- created_at: created_at - created_at: created_at
@ -281,6 +284,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml .. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up - admin_state_up: admin_state_up
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref - client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit - connection_limit: connection_limit
- created_at: created_at - created_at: created_at
@ -346,6 +350,7 @@ Request
.. rest_parameters:: ../parameters.yaml .. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up-default-optional - admin_state_up: admin_state_up-default-optional
- client_authentication: client_authentication-optional
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional - client_ca_tls_container_ref: client_ca_tls_container_ref-optional
- connection_limit: connection_limit-optional - connection_limit: connection_limit-optional
- default_pool_id: default_pool_id-optional - default_pool_id: default_pool_id-optional
@ -379,6 +384,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml .. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up - admin_state_up: admin_state_up
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref - client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit - connection_limit: connection_limit
- created_at: created_at - created_at: created_at

View File

@ -364,9 +364,13 @@ contain the following:
| admin_state_up | bool | Admin state: True if up, False if | | admin_state_up | bool | Admin state: True if up, False if |
| | | down. | | | | 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. | | | | container. |
+------------------------------+--------+-------------------------------------+ +------------------------------+--------+-------------------------------------+
| connection_limit | int | The max number of connections | | 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, sni_container_data=Unset, timeout_client_data=Unset,
timeout_member_connect=Unset, timeout_member_data=Unset, timeout_member_connect=Unset, timeout_member_data=Unset,
timeout_tcp_inspect=Unset, client_ca_tls_container_ref=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.admin_state_up = admin_state_up
self.connection_limit = connection_limit self.connection_limit = connection_limit
@ -158,6 +159,7 @@ class Listener(BaseDataModel):
self.timeout_tcp_inspect = timeout_tcp_inspect self.timeout_tcp_inspect = timeout_tcp_inspect
self.client_ca_tls_container_ref = client_ca_tls_container_ref self.client_ca_tls_container_ref = client_ca_tls_container_ref
self.client_ca_tls_container_data = client_ca_tls_container_data self.client_ca_tls_container_data = client_ca_tls_container_data
self.client_authentication = client_authentication
class Pool(BaseDataModel): class Pool(BaseDataModel):

View File

@ -230,6 +230,15 @@ class ListenersController(base.BaseController):
"be provided for %s protocol listeners.") % "be provided for %s protocol listeners.") %
constants.PROTOCOL_TERMINATED_HTTPS) 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: try:
sni_containers = listener_dict.pop('sni_containers', []) sni_containers = listener_dict.pop('sni_containers', [])
tls_refs = [sni['tls_container_id'] for sni in sni_containers] tls_refs = [sni['tls_container_id'] for sni in sni_containers]
@ -382,7 +391,16 @@ class ListenersController(base.BaseController):
"%s protocol listeners.") % "%s protocol listeners.") %
constants.PROTOCOL_TERMINATED_HTTPS) 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 [] sni_containers = listener.sni_container_refs or []
tls_refs = [sni for sni in sni_containers] tls_refs = [sni for sni in sni_containers]
if listener.default_tls_container_ref: if listener.default_tls_container_ref:

View File

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

View File

@ -580,3 +580,11 @@ FLAVOR_DATA = 'flavor_data'
# Flavor metadata # Flavor metadata
LOADBALANCER_TOPOLOGY = 'loadbalancer_topology' LOADBALANCER_TOPOLOGY = 'loadbalancer_topology'
COMPUTE_FLAVOR = 'compute_flavor' 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, created_at=None, updated_at=None,
timeout_client_data=None, timeout_member_connect=None, timeout_client_data=None, timeout_member_connect=None,
timeout_member_data=None, timeout_tcp_inspect=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.id = id
self.project_id = project_id self.project_id = project_id
self.name = name self.name = name
@ -398,6 +399,7 @@ class Listener(BaseDataModel):
self.timeout_tcp_inspect = timeout_tcp_inspect self.timeout_tcp_inspect = timeout_tcp_inspect
self.tags = tags self.tags = tags
self.client_ca_tls_certificate_id = client_ca_tls_certificate_id self.client_ca_tls_certificate_id = client_ca_tls_certificate_id
self.client_authentication = client_authentication
def update(self, update_dict): def update(self, update_dict):
for key, value in update_dict.items(): for key, value in update_dict.items():

View File

@ -36,6 +36,10 @@ BALANCE_MAP = {
constants.LB_ALGORITHM_SOURCE_IP: 'source' 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 + ( ACTIVE_PENDING_STATUSES = constants.SUPPORTED_PROVISIONING_STATUSES + (
constants.DEGRADED,) constants.DEGRADED,)
@ -239,6 +243,9 @@ class JinjaTemplater(object):
ret_value['client_ca_tls_path'] = '%s' % ( ret_value['client_ca_tls_path'] = '%s' % (
os.path.join(self.base_crt_dir, listener.id, os.path.join(self.base_crt_dir, listener.id,
client_ca_filename)) client_ca_filename))
ret_value['client_auth'] = CLIENT_AUTH_MAP.get(
listener.client_authentication)
if listener.default_pool: if listener.default_pool:
ret_value['default_pool'] = self._transform_pool( ret_value['default_pool'] = self._transform_pool(
listener.default_pool, feature_compatibility) listener.default_pool, feature_compatibility)

View File

@ -38,8 +38,8 @@ peers {{ "%s_peers"|format(listener.id.replace("-", ""))|trim() }}
{% else %} {% else %}
{% set crt_dir_opt = "" %} {% set crt_dir_opt = "" %}
{% endif %} {% endif %}
{% if listener.client_ca_tls_path %} {% if listener.client_ca_tls_path and listener.client_auth %}
{% set client_ca_opt = "ca-file %s"|format(listener.client_ca_tls_path)|trim() %} {% set client_ca_opt = "ca-file %s verify %s"|format(listener.client_ca_tls_path, listener.client_auth)|trim() %}
{% else %} {% else %}
{% set client_ca_opt = "" %} {% set client_ca_opt = "" %}
{% endif %} {% 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 member
from octavia.api.v2.types import pool from octavia.api.v2.types import pool
from octavia.api.v2.types import quotas from octavia.api.v2.types import quotas
from octavia.common import constants
from octavia.common import data_models from octavia.common import data_models
from octavia.db import base_models from octavia.db import base_models
from octavia.i18n import _ 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_member_data = sa.Column(sa.Integer, nullable=True)
timeout_tcp_inspect = 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_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 = orm.relationship(
'Tags', 'Tags',
@ -762,3 +768,10 @@ class Flavor(base_models.BASE,
sa.ForeignKey("flavor_profile.id", sa.ForeignKey("flavor_profile.id",
name="fk_flavor_flavor_profile_id"), name="fk_flavor_flavor_profile_id"),
nullable=False) 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] for sni_container_id in sni_container_ids]
listener_dict['sni_containers'] = sni_containers listener_dict['sni_containers'] = sni_containers
if 'client_authentication' not in listener_dict:
listener_dict['client_authentication'] = constants.CLIENT_AUTH_NONE
return listener_dict return listener_dict

View File

@ -957,6 +957,46 @@ class TestListener(base.BaseAPITest):
listener_api = self.test_create(**optionals) listener_api = self.test_create(**optionals)
self.assertEqual(optionals['client_ca_tls_container_ref'], self.assertEqual(optionals['client_ca_tls_container_ref'],
listener_api.get('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): def test_create_with_bad_ca_cert_ref(self):
sni1 = uuidutils.generate_uuid() sni1 = uuidutils.generate_uuid()
@ -1163,6 +1203,70 @@ class TestListener(base.BaseAPITest):
self.assertNotEqual(ori_listener['client_ca_tls_container_ref'], self.assertNotEqual(ori_listener['client_ca_tls_container_ref'],
optionals['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') @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_unset_ca_cert(self, mock_cert_data): def test_update_unset_ca_cert(self, mock_cert_data):
cert1 = data_models.TLSContainer(certificate='cert 1') cert1 = data_models.TLSContainer(certificate='cert 1')
@ -1185,6 +1289,7 @@ class TestListener(base.BaseAPITest):
listener_id=listener['id']) listener_id=listener['id'])
api_listener = self.put(listener_path, body).json.get(self.root_tag) 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_ca_tls_container_ref'))
self.assertIsNone(api_listener.get('client_auth_option'))
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data') @mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_with_bad_ca_cert(self, mock_cert_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 [] expected_lb['pools'] = create_pools or []
return create_lb, expected_lb return create_lb, expected_lb
def _get_listener_bodies(self, name='listener1', protocol_port=80, def _get_listener_bodies(
create_default_pool_name=None, self, name='listener1', protocol_port=80,
create_default_pool_id=None, create_default_pool_name=None, create_default_pool_id=None,
create_l7policies=None, create_l7policies=None, expected_l7policies=None,
expected_l7policies=None, create_sni_containers=None, expected_sni_containers=None,
create_sni_containers=None, create_client_ca_tls_container=None,
expected_sni_containers=None, expected_client_ca_tls_container=None,
create_client_ca_tls_container=None, create_protocol=constants.PROTOCOL_HTTP,
expected_client_ca_tls_container=None, create_client_authentication=None,
create_protocol=constants.PROTOCOL_HTTP): expected_client_authentication=constants.CLIENT_AUTH_NONE):
create_listener = { create_listener = {
'name': name, 'name': name,
'protocol_port': protocol_port, 'protocol_port': protocol_port,
@ -2379,7 +2379,8 @@ class TestLoadBalancerGraph(base.BaseAPITest):
'timeout_member_data': constants.DEFAULT_TIMEOUT_MEMBER_DATA, 'timeout_member_data': constants.DEFAULT_TIMEOUT_MEMBER_DATA,
'timeout_tcp_inspect': constants.DEFAULT_TIMEOUT_TCP_INSPECT, 'timeout_tcp_inspect': constants.DEFAULT_TIMEOUT_TCP_INSPECT,
'tags': [], 'tags': [],
'client_ca_tls_container_ref': None 'client_ca_tls_container_ref': None,
'client_authentication': constants.CLIENT_AUTH_NONE
} }
if create_sni_containers: if create_sni_containers:
create_listener['sni_container_refs'] = 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: if create_client_ca_tls_container:
create_listener['client_ca_tls_container_ref'] = ( create_listener['client_ca_tls_container_ref'] = (
create_client_ca_tls_container) create_client_ca_tls_container)
if create_client_authentication:
create_listener['client_authentication'] = (
create_client_authentication)
if expected_sni_containers: if expected_sni_containers:
expected_listener['sni_container_refs'] = expected_sni_containers expected_listener['sni_container_refs'] = expected_sni_containers
if expected_l7policies: if expected_l7policies:
@ -2407,6 +2411,11 @@ class TestLoadBalancerGraph(base.BaseAPITest):
if expected_client_ca_tls_container: if expected_client_ca_tls_container:
expected_listener['client_ca_tls_container_ref'] = ( expected_listener['client_ca_tls_container_ref'] = (
expected_client_ca_tls_container) 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 return create_listener, expected_listener
def _get_pool_bodies(self, name='pool1', create_members=None, 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 mock_x509_cert.return_value = cert_mock
create_client_ca_tls_container = uuidutils.generate_uuid() create_client_ca_tls_container = uuidutils.generate_uuid()
expected_client_ca_tls_container = create_client_ca_tls_container 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 = ( create_sni_containers, expected_sni_containers = (
self._get_sni_container_bodies()) self._get_sni_container_bodies())
create_listener, expected_listener = self._get_listener_bodies( create_listener, expected_listener = self._get_listener_bodies(
@ -2677,7 +2688,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
create_sni_containers=create_sni_containers, create_sni_containers=create_sni_containers,
expected_sni_containers=expected_sni_containers, expected_sni_containers=expected_sni_containers,
create_client_ca_tls_container=create_client_ca_tls_container, 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_lb, expected_lb = self._get_lb_bodies(
create_listeners=[create_listener], create_listeners=[create_listener],
expected_listeners=[expected_listener]) expected_listeners=[expected_listener])

View File

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

View File

@ -382,7 +382,8 @@ class SampleDriverDataModels(object):
'timeout_member_connect': 2000, 'timeout_member_connect': 2000,
'timeout_member_data': 3000, 'timeout_member_data': 3000,
'timeout_tcp_inspect': 4000, '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) self.test_listener1_dict.update(self._common_test_dict)
@ -439,7 +440,8 @@ class SampleDriverDataModels(object):
'timeout_member_data': 3000, 'timeout_member_data': 3000,
'timeout_tcp_inspect': 4000, 'timeout_tcp_inspect': 4000,
'client_ca_tls_container_ref': self.client_ca_tls_certificate_ref, '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( self.provider_listener2_dict = copy.deepcopy(
@ -452,6 +454,8 @@ class SampleDriverDataModels(object):
del self.provider_listener2_dict['l7policies'] del self.provider_listener2_dict['l7policies']
self.provider_listener2_dict['client_ca_tls_container_ref'] = None self.provider_listener2_dict['client_ca_tls_container_ref'] = None
del self.provider_listener2_dict['client_ca_tls_container_data'] 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 = driver_dm.Listener(
**self.provider_listener1_dict) **self.provider_listener1_dict)

View File

@ -40,7 +40,7 @@ class TestHaproxyCfg(base.TestCase):
"sample_listener_id_1/tls_container_id.pem " "sample_listener_id_1/tls_container_id.pem "
"crt /var/lib/octavia/certs/sample_listener_id_1 " "crt /var/lib/octavia/certs/sample_listener_id_1 "
"ca-file /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" " mode http\n"
" default_backend sample_pool_id_1\n" " default_backend sample_pool_id_1\n"
" timeout client 50000\n\n").format( " 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,' 'l7policies, enabled, insert_headers, timeout_client_data,'
'timeout_member_connect, timeout_member_data, ' 'timeout_member_connect, timeout_member_data, '
'timeout_tcp_inspect, client_ca_tls_certificate_id,' 'timeout_tcp_inspect, client_ca_tls_certificate_id,'
'client_ca_tls_certificate') 'client_ca_tls_certificate, client_authentication')
if l7: if l7:
pools = [ pools = [
sample_pool_tuple( 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( client_ca_tls_certificate=sample_tls_container_tuple(
id='cont_id_ca', certificate=sample_certs.X509_CA_CERT, id='cont_id_ca', certificate=sample_certs.X509_CA_CERT,
primary_cn=sample_certs.X509_CA_CERT_CN 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.