Add client_ca_tls_container_ref to listener API

This patch add 'client_ca_tls_container_ref' into listener API for front
client authentication.

Story: 2002165
Task: 20018
Co-Authored-By: Michael Johnson <johnsomor@gmail.com>
Change-Id: I8a96d6fdfe53a16d1abcfd09bc6afedd6c490de2
changes/67/612267/20
ZhaoBo 2018-11-07 18:51:50 +08:00 committed by Michael Johnson
parent 6e0bed1c54
commit 0cc546a7c7
44 changed files with 1001 additions and 200 deletions

View File

@ -246,6 +246,26 @@ cert-expiration:
in: body
required: true
type: string
client_ca_tls_container_ref:
description: |
The ref of the `key manager service
<https://docs.openstack.org/castellan/latest/>`__ secret containing a
PEM format client CA certificate bundle for ``TERMINATED_TLS``
listeners.
in: body
min_version: 2.8
required: true
type: string
client_ca_tls_container_ref-optional:
description: |
The ref of the `key manager service
<https://docs.openstack.org/castellan/latest/>`__ secret containing a
PEM format client CA certificate bundle for ``TERMINATED_TLS``
listeners.
in: body
min_version: 2.8
required: false
type: string
compute-flavor:
description: |
The ID of the compute flavor used for the amphora.

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"]}}' 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"}}' http://198.51.100.10:9876/v2/lbaas/listeners

View File

@ -20,6 +20,7 @@
"timeout_member_connect": 5000,
"timeout_member_data": 50000,
"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"
}
}

View File

@ -35,6 +35,7 @@
"timeout_member_connect": 5000,
"timeout_member_data": 50000,
"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"
}
}

View File

@ -35,6 +35,7 @@
"timeout_member_connect": 5000,
"timeout_member_data": 50000,
"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"
}
}

View File

@ -1 +1 @@
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"description": "An updated great TLS listener", "admin_state_up": true, "connection_limit": 200, "name": "great_updated_tls_listener", "insert_headers": {"X-Forwarded-For": "false", "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": 100000, "timeout_member_connect": 1000, "timeout_member_data": 100000, "timeout_tcp_inspect": 5, "tags": ["updated_tag"]}}' http://198.51.100.10:9876/v2/lbaas/listeners/023f2e34-7806-443b-bfae-16c324569a3d
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"listener": {"description": "An updated great TLS listener", "admin_state_up": true, "connection_limit": 200, "name": "great_updated_tls_listener", "insert_headers": {"X-Forwarded-For": "false", "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": 100000, "timeout_member_connect": 1000, "timeout_member_data": 100000, "timeout_tcp_inspect": 5, "tags": ["updated_tag"], "client_ca_tls_container_ref": null}}' http://198.51.100.10:9876/v2/lbaas/listeners/023f2e34-7806-443b-bfae-16c324569a3d

View File

@ -18,6 +18,7 @@
"timeout_member_connect": 1000,
"timeout_member_data": 100000,
"timeout_tcp_inspect": 5,
"tags": ["updated_tag"]
"tags": ["updated_tag"],
"client_ca_tls_container_ref": null
}
}

View File

@ -35,6 +35,7 @@
"timeout_member_connect": 1000,
"timeout_member_data": 100000,
"timeout_tcp_inspect": 5,
"tags": ["updated_tag"]
"tags": ["updated_tag"],
"client_ca_tls_container_ref": null
}
}

View File

@ -37,7 +37,8 @@
"timeout_member_connect": 5000,
"timeout_member_data": 50000,
"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"
}
]
}

View File

@ -46,6 +46,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit
- created_at: created_at
- default_pool_id: default_pool_id
@ -136,6 +137,7 @@ Request
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up-default-optional
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
- connection_limit: connection_limit-optional
- default_pool: pool-optional
- default_pool_id: default_pool_id-optional
@ -204,6 +206,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit
- created_at: created_at
- default_pool_id: default_pool_id
@ -278,6 +281,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit
- created_at: created_at
- default_pool_id: default_pool_id
@ -342,6 +346,7 @@ Request
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up-default-optional
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
- connection_limit: connection_limit-optional
- default_pool_id: default_pool_id-optional
- default_tls_container_ref: default_tls_container_ref-optional
@ -374,6 +379,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- client_ca_tls_container_ref: client_ca_tls_container_ref
- connection_limit: connection_limit
- created_at: created_at
- default_pool_id: default_pool_id

View File

@ -358,68 +358,73 @@ PEM format.
As of the writing of this specification the create listener object may
contain the following:
+----------------------------+--------+-------------------------------------+
| Name | Type | Description |
+============================+========+=====================================+
| admin_state_up | bool | Admin state: True if up, False if |
| | | down. |
+----------------------------+--------+-------------------------------------+
| connection_limit | int | The max number of connections |
| | | permitted for this listener. Default|
| | | is -1, which is infinite |
| | | connections. |
+----------------------------+--------+-------------------------------------+
| default_pool | object | A `Pool object`_. |
+----------------------------+--------+-------------------------------------+
| default_pool_id | string | The ID of the pool used by the |
| | | listener if no L7 policies match. |
+----------------------------+--------+-------------------------------------+
| default_tls_container_data | dict | A `TLS container`_ dict. |
+----------------------------+--------+-------------------------------------+
| default_tls_container_refs | string | The reference to the secrets |
| | | container. |
+----------------------------+--------+-------------------------------------+
| description | string | A human-readable description for the|
| | | listener. |
+----------------------------+--------+-------------------------------------+
| insert_headers | dict | A dictionary of optional headers to |
| | | insert into the request before it is|
| | | sent to the backend member. See |
| | | `Supported HTTP Header Insertions`_.|
| | | Keys and values are specified as |
| | | strings. |
+----------------------------+--------+-------------------------------------+
| l7policies | list | A list of `L7policy objects`_. |
+----------------------------+--------+-------------------------------------+
| listener_id | string | ID of listener to create. |
+----------------------------+--------+-------------------------------------+
| loadbalancer_id | string | ID of load balancer. |
+----------------------------+--------+-------------------------------------+
| name | string | Human-readable name of the listener.|
+----------------------------+--------+-------------------------------------+
| protocol | string | Protocol type: One of HTTP, HTTPS, |
| | | TCP, or TERMINATED_HTTPS. |
+----------------------------+--------+-------------------------------------+
| protocol_port | int | Protocol port number. |
+----------------------------+--------+-------------------------------------+
| sni_container_data | list | A list of `TLS container`_ dict. |
+----------------------------+--------+-------------------------------------+
| sni_container_refs | list | A list of references to the SNI |
| | | secrets containers. |
+----------------------------+--------+-------------------------------------+
| timeout_client_data | int | Frontend client inactivity timeout |
| | | in milliseconds. |
+----------------------------+--------+-------------------------------------+
| timeout_member_connect | int | Backend member connection timeout in|
| | | milliseconds. |
+----------------------------+--------+-------------------------------------+
| timeout_member_data | int | Backend member inactivity timeout in|
| | | milliseconds. |
+----------------------------+--------+-------------------------------------+
| timeout_tcp_inspect | int | Time, in milliseconds, to wait for |
| | | additional TCP packets for content |
| | | inspection. |
+----------------------------+--------+-------------------------------------+
+------------------------------+--------+-------------------------------------+
| Name | Type | Description |
+==============================+========+=====================================+
| admin_state_up | bool | Admin state: True if up, False if |
| | | down. |
+------------------------------+--------+-------------------------------------+
|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 |
| | | permitted for this listener. Default|
| | | is -1, which is infinite |
| | | connections. |
+------------------------------+--------+-------------------------------------+
| default_pool | object | A `Pool object`_. |
+------------------------------+--------+-------------------------------------+
| default_pool_id | string | The ID of the pool used by the |
| | | listener if no L7 policies match. |
+------------------------------+--------+-------------------------------------+
| default_tls_container_data | dict | A `TLS container`_ dict. |
+------------------------------+--------+-------------------------------------+
| default_tls_container_refs | string | The reference to the secrets |
| | | container. |
+------------------------------+--------+-------------------------------------+
| description | string | A human-readable description for the|
| | | listener. |
+------------------------------+--------+-------------------------------------+
| insert_headers | dict | A dictionary of optional headers to |
| | | insert into the request before it is|
| | | sent to the backend member. See |
| | | `Supported HTTP Header Insertions`_.|
| | | Keys and values are specified as |
| | | strings. |
+------------------------------+--------+-------------------------------------+
| l7policies | list | A list of `L7policy objects`_. |
+------------------------------+--------+-------------------------------------+
| listener_id | string | ID of listener to create. |
+------------------------------+--------+-------------------------------------+
| loadbalancer_id | string | ID of load balancer. |
+------------------------------+--------+-------------------------------------+
| name | string | Human-readable name of the listener.|
+------------------------------+--------+-------------------------------------+
| protocol | string | Protocol type: One of HTTP, HTTPS, |
| | | TCP, or TERMINATED_HTTPS. |
+------------------------------+--------+-------------------------------------+
| protocol_port | int | Protocol port number. |
+------------------------------+--------+-------------------------------------+
| sni_container_data | list | A list of `TLS container`_ dict. |
+------------------------------+--------+-------------------------------------+
| sni_container_refs | list | A list of references to the SNI |
| | | secrets containers. |
+------------------------------+--------+-------------------------------------+
| timeout_client_data | int | Frontend client inactivity timeout |
| | | in milliseconds. |
+------------------------------+--------+-------------------------------------+
| timeout_member_connect | int | Backend member connection timeout in|
| | | milliseconds. |
+------------------------------+--------+-------------------------------------+
| timeout_member_data | int | Backend member inactivity timeout in|
| | | milliseconds. |
+------------------------------+--------+-------------------------------------+
| timeout_tcp_inspect | int | Time, in milliseconds, to wait for |
| | | additional TCP packets for content |
| | | inspection. |
+------------------------------+--------+-------------------------------------+
.. _TLS container:

View File

@ -17,6 +17,7 @@ import hashlib
import time
import warnings
from oslo_context import context as oslo_context
from oslo_log import log as logging
import requests
import simplejson
@ -115,11 +116,15 @@ class HaproxyAmphoraLoadBalancerDriver(
timeout_dict=timeout_dict)
else:
certs = self._process_tls_certificates(listener)
client_ca_filename = self._process_secret(
listener, listener.client_ca_tls_certificate_id)
# Generate HaProxy configuration from listener object
config = self.jinja.build_config(
host_amphora=amp, listener=listener,
tls_cert=certs['tls_cert'],
haproxy_versions=haproxy_versions)
haproxy_versions=haproxy_versions,
client_ca_filename=client_ca_filename)
self.client.upload_config(amp, listener.id, config,
timeout_dict=timeout_dict)
self.client.reload_listener(amp, listener.id,
@ -149,6 +154,8 @@ class HaproxyAmphoraLoadBalancerDriver(
# Process listener certificate info
certs = self._process_tls_certificates(listener)
client_ca_filename = self._process_secret(
listener, listener.client_ca_tls_certificate_id)
for amp in listener.load_balancer.amphorae:
if amp.status != consts.DELETED:
@ -159,7 +166,8 @@ class HaproxyAmphoraLoadBalancerDriver(
config = self.jinja.build_config(
host_amphora=amp, listener=listener,
tls_cert=certs['tls_cert'],
haproxy_versions=haproxy_versions)
haproxy_versions=haproxy_versions,
client_ca_filename=client_ca_filename)
self.client.upload_config(amp, listener.id, config)
self.client.reload_listener(amp, listener.id)
@ -278,6 +286,21 @@ class HaproxyAmphoraLoadBalancerDriver(
return {'tls_cert': tls_cert, 'sni_certs': sni_certs}
def _process_secret(self, listener, secret_ref):
"""Get the secret from the cert manager and upload it to the amp.
:returns: The filename of the secret in the amp.
"""
if not secret_ref:
return None
context = oslo_context.RequestContext(project_id=listener.project_id)
secret = self.cert_manager.get_secret(context, secret_ref)
md5 = hashlib.md5(secret.encode('utf-8')).hexdigest() # nosec
id = hashlib.sha1(secret.encode('utf-8')).hexdigest() # nosec
name = '{id}.pem'.format(id=id)
self._apply(self._upload_cert, listener, None, secret, md5, name)
return name
def _upload_cert(self, amp, listener_id, pem, md5, name):
try:
if self.client.get_cert_md5sum(

View File

@ -133,7 +133,8 @@ class Listener(BaseDataModel):
protocol_port=Unset, sni_container_refs=Unset,
sni_container_data=Unset, timeout_client_data=Unset,
timeout_member_connect=Unset, timeout_member_data=Unset,
timeout_tcp_inspect=Unset):
timeout_tcp_inspect=Unset, client_ca_tls_container_ref=Unset,
client_ca_tls_container_data=Unset):
self.admin_state_up = admin_state_up
self.connection_limit = connection_limit
@ -155,6 +156,8 @@ class Listener(BaseDataModel):
self.timeout_member_connect = timeout_member_connect
self.timeout_member_data = timeout_member_data
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
class Pool(BaseDataModel):

View File

@ -17,6 +17,7 @@ import copy
import six
from oslo_config import cfg
from oslo_context import context as oslo_context
from oslo_log import log as logging
from stevedore import driver as stevedore_driver
@ -154,6 +155,15 @@ def db_listener_to_provider_listener(db_listener):
return provider_listener
def _get_secret_data(cert_manager, listener, secret_ref):
"""Get the secret from the certificate manager and upload it to the amp.
:returns: The secret data.
"""
context = oslo_context.RequestContext(project_id=listener.project_id)
return cert_manager.get_secret(context, secret_ref)
def listener_dict_to_provider_dict(listener_dict):
new_listener_dict = _base_to_provider_dict(listener_dict)
new_listener_dict['listener_id'] = new_listener_dict.pop('id')
@ -171,8 +181,13 @@ def listener_dict_to_provider_dict(listener_dict):
if 'sni_container_refs' in listener_dict:
listener_dict['sni_containers'] = listener_dict.pop(
'sni_container_refs')
if 'client_ca_tls_certificate_id' in new_listener_dict:
new_listener_dict['client_ca_tls_container_ref'] = (
new_listener_dict.pop('client_ca_tls_certificate_id'))
listener_obj = data_models.Listener(**listener_dict)
if listener_obj.tls_certificate_id or listener_obj.sni_containers:
if (listener_obj.tls_certificate_id or listener_obj.sni_containers or
listener_obj.client_ca_tls_certificate_id):
SNI_objs = []
for sni in listener_obj.sni_containers:
if isinstance(sni, dict):
@ -192,15 +207,20 @@ def listener_dict_to_provider_dict(listener_dict):
).driver
cert_dict = cert_parser.load_certificates_data(cert_manager,
listener_obj)
if 'tls_cert' in cert_dict:
if 'tls_cert' in cert_dict and cert_dict['tls_cert']:
new_listener_dict['default_tls_container_data'] = (
cert_dict['tls_cert'].to_dict())
if 'sni_certs' in cert_dict:
if 'sni_certs' in cert_dict and cert_dict['sni_certs']:
sni_data_list = []
for sni in cert_dict['sni_certs']:
sni_data_list.append(sni.to_dict())
new_listener_dict['sni_container_data'] = sni_data_list
if listener_obj.client_ca_tls_certificate_id:
cert = _get_secret_data(cert_manager, listener_obj,
listener_obj.client_ca_tls_certificate_id)
new_listener_dict['client_ca_tls_container_data'] = cert
# Remove the DB back references
if 'load_balancer' in new_listener_dict:
del new_listener_dict['load_balancer']

View File

@ -89,6 +89,9 @@ class RootController(rest.RestController):
self._add_a_version(versions, 'v2.6', 'v2', 'SUPPORTED',
'2019-01-25T00:00:00Z', host_url)
# Amphora Config update
self._add_a_version(versions, 'v2.7', 'v2', 'CURRENT',
self._add_a_version(versions, 'v2.7', 'v2', 'SUPPORTED',
'2018-01-25T12:00:00Z', host_url)
# TLS client authentication
self._add_a_version(versions, 'v2.8', 'v2', 'CURRENT',
'2019-02-12T00:00:00Z', host_url)
return {'versions': versions}

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from oslo_config import cfg
from oslo_db import exception as odb_exceptions
from oslo_log import log as logging
@ -140,11 +142,48 @@ class ListenersController(base.BaseController):
if bad_refs:
raise exceptions.CertificateRetrievalException(ref=bad_refs)
def _validate_client_ca_ref(self, client_ca_ref):
context = pecan.request.context.get('octavia_context')
bad_refs = []
try:
self.cert_manager.set_acls(context, client_ca_ref)
ca_pem = self.cert_manager.get_secret(context, client_ca_ref)
except Exception:
bad_refs.append(client_ca_ref)
# This will be used in a later patch
if bad_refs:
raise exceptions.CertificateRetrievalException(ref=bad_refs)
try:
# Test if it needs to be UTF-8 encoded
try:
ca_pem = ca_pem.encode('utf-8')
except AttributeError:
pass
x509.load_pem_x509_certificate(ca_pem, default_backend())
except Exception as e:
raise exceptions.ValidationException(detail=_(
"The client authentication CA certificate is invalid. "
"It must be a valid x509 PEM format certificate. "
"Error: %s") % str(e))
def _has_tls_container_refs(self, listener_dict):
return (listener_dict.get('tls_certificate_id') or
listener_dict.get('client_ca_tls_container_id') or
listener_dict.get('sni_containers'))
def _is_tls_or_insert_header(self, listener_dict):
return (self._has_tls_container_refs(listener_dict) or
listener_dict.get('insert_headers'))
def _validate_create_listener(self, lock_session, listener_dict):
"""Validate listener for wrong protocol or duplicate listeners
Update the load balancer db when provisioning status changes.
"""
listener_protocol = listener_dict.get('protocol')
if (listener_dict and
listener_dict.get('insert_headers') and
list(set(listener_dict['insert_headers'].keys()) -
@ -153,12 +192,53 @@ class ListenersController(base.BaseController):
value=listener_dict.get('insert_headers'),
option='insert_headers')
# Check for UDP compatibility
if (listener_protocol == constants.PROTOCOL_UDP and
self._is_tls_or_insert_header(listener_dict)):
raise exceptions.ValidationException(detail=_(
"%s protocol listener does not support TLS or header "
"insertion.") % constants.PROTOCOL_UDP)
# Check for TLS disabled
if (not CONF.api_settings.allow_tls_terminated_listeners and
listener_protocol == constants.PROTOCOL_TERMINATED_HTTPS):
raise exceptions.DisabledOption(
value=constants.PROTOCOL_TERMINATED_HTTPS, option='protocol')
# Check for certs when not TERMINATED_HTTPS
if (listener_protocol != constants.PROTOCOL_TERMINATED_HTTPS and
self._has_tls_container_refs(listener_dict)):
raise exceptions.ValidationException(detail=_(
"Certificate container references are only allowed on "
"%s protocol listeners.") %
constants.PROTOCOL_TERMINATED_HTTPS)
# Make sure a base certificate exists if specifying a client ca
if (listener_dict.get('client_ca_tls_certificate_id') and
not (listener_dict.get('tls_certificate_id') or
listener_dict.get('sni_containers'))):
raise exceptions.ValidationException(detail=_(
"An SNI or default certificate container reference must "
"be provided with a client CA container reference."))
# Make sure a certificate container is specified for TERMINATED_HTTPS
if (listener_protocol == constants.PROTOCOL_TERMINATED_HTTPS and
not (listener_dict.get('tls_certificate_id') or
listener_dict.get('sni_containers'))):
raise exceptions.ValidationException(detail=_(
"An SNI or default certificate container reference must "
"be provided for %s protocol listeners.") %
constants.PROTOCOL_TERMINATED_HTTPS)
try:
sni_containers = listener_dict.pop('sni_containers', [])
tls_refs = [sni['tls_container_id'] for sni in sni_containers]
if listener_dict.get('tls_certificate_id'):
tls_refs.append(listener_dict.get('tls_certificate_id'))
self._validate_tls_refs(tls_refs)
if listener_dict.get('client_ca_tls_certificate_id'):
self._validate_client_ca_ref(
listener_dict.get('client_ca_tls_certificate_id'))
db_listener = self.repositories.listener.create(
lock_session, **listener_dict)
if sni_containers:
@ -183,10 +263,6 @@ class ListenersController(base.BaseController):
raise exceptions.InvalidOption(value=listener_dict.get('protocol'),
option='protocol')
def _is_tls_or_insert_header(self, listener):
return (listener.default_tls_container_ref or
listener.sni_container_refs or listener.insert_headers)
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse,
body=listener_types.ListenerRootPOST, status_code=201)
def post(self, listener_):
@ -200,15 +276,6 @@ class ListenersController(base.BaseController):
self._auth_validate_action(context, listener.project_id,
constants.RBAC_POST)
if (listener.protocol == constants.PROTOCOL_UDP and
self._is_tls_or_insert_header(listener)):
raise exceptions.ValidationException(detail=_(
"%s protocol listener does not support TLS or header "
"insertion.") % constants.PROTOCOL_UDP)
if (not CONF.api_settings.allow_tls_terminated_listeners and
listener.protocol == constants.PROTOCOL_TERMINATED_HTTPS):
raise exceptions.DisabledOption(
value=constants.PROTOCOL_TERMINATED_HTTPS, option='protocol')
# Load the driver early as it also provides validation
driver = driver_factory.get_driver(provider)
@ -293,6 +360,37 @@ class ListenersController(base.BaseController):
db_listener.l7policies = new_l7ps
return db_listener
def _validate_listener_PUT(self, listener, db_listener):
# TODO(rm_work): Do we need something like this? What do we do on an
# empty body for a PUT?
if not listener:
raise exceptions.ValidationException(
detail='No listener object supplied.')
# Check for UDP compatibility
if (db_listener.protocol == constants.PROTOCOL_UDP and
self._is_tls_or_insert_header(listener.to_dict())):
raise exceptions.ValidationException(detail=_(
"%s protocol listener does not support TLS or header "
"insertion.") % constants.PROTOCOL_UDP)
# Check for certs when not TERMINATED_HTTPS
if (db_listener.protocol != constants.PROTOCOL_TERMINATED_HTTPS and
self._has_tls_container_refs(listener.to_dict())):
raise exceptions.ValidationException(detail=_(
"Certificate container references are only allowed on "
"%s protocol listeners.") %
constants.PROTOCOL_TERMINATED_HTTPS)
# Make sure the refs are valid
sni_containers = listener.sni_container_refs or []
tls_refs = [sni for sni in sni_containers]
if listener.default_tls_container_ref:
tls_refs.append(listener.default_tls_container_ref)
self._validate_tls_refs(tls_refs)
if listener.client_ca_tls_container_ref:
self._validate_client_ca_ref(listener.client_ca_tls_container_ref)
@wsme_pecan.wsexpose(listener_types.ListenerRootResponse, wtypes.text,
body=listener_types.ListenerRootPUT, status_code=200)
def put(self, id, listener_):
@ -308,28 +406,12 @@ class ListenersController(base.BaseController):
self._auth_validate_action(context, project_id, constants.RBAC_PUT)
# TODO(rm_work): Do we need something like this? What do we do on an
# empty body for a PUT?
if not listener:
raise exceptions.ValidationException(
detail='No listener object supplied.')
if (db_listener.protocol == constants.PROTOCOL_UDP and
self._is_tls_or_insert_header(listener)):
raise exceptions.ValidationException(detail=_(
"%s protocol listener does not support TLS or header "
"insertion.") % constants.PROTOCOL_UDP)
self._validate_listener_PUT(listener, db_listener)
if listener.default_pool_id:
self._validate_pool(context.session, load_balancer_id,
listener.default_pool_id, db_listener.protocol)
sni_containers = listener.sni_container_refs or []
tls_refs = [sni for sni in sni_containers]
if listener.default_tls_container_ref:
tls_refs.append(listener.default_tls_container_ref)
self._validate_tls_refs(tls_refs)
# Load the driver early as it also provides validation
driver = driver_factory.get_driver(provider)

View File

@ -25,8 +25,10 @@ CONF.import_group('haproxy_amphora', 'octavia.common.config')
class BaseListenerType(types.BaseType):
_type_to_model_map = {'admin_state_up': 'enabled',
'default_tls_container_ref': 'tls_certificate_id'}
_type_to_model_map = {
'admin_state_up': 'enabled',
'default_tls_container_ref': 'tls_certificate_id',
'client_ca_tls_container_ref': 'client_ca_tls_certificate_id'}
_child_map = {}
@ -55,6 +57,7 @@ class ListenerResponse(BaseListenerType):
timeout_member_data = wtypes.wsattr(wtypes.IntegerType())
timeout_tcp_inspect = wtypes.wsattr(wtypes.IntegerType())
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
client_ca_tls_container_ref = wtypes.StringType()
@classmethod
def from_data_model(cls, data_model, children=False):
@ -63,7 +66,6 @@ class ListenerResponse(BaseListenerType):
listener.sni_container_refs = [
sni_c.tls_container_id for sni_c in data_model.sni_containers]
if cls._full_response():
del listener.loadbalancers
l7policy_type = l7policy.L7PolicyFullResponse
@ -135,6 +137,7 @@ class ListenerPOST(BaseListenerType):
maximum=constants.MAX_TIMEOUT),
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)
class ListenerRootPOST(types.BaseType):
@ -167,6 +170,7 @@ class ListenerPUT(BaseListenerType):
wtypes.IntegerType(minimum=constants.MIN_TIMEOUT,
maximum=constants.MAX_TIMEOUT))
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
client_ca_tls_container_ref = wtypes.StringType(max_length=255)
class ListenerRootPUT(types.BaseType):
@ -210,6 +214,7 @@ class ListenerSingleCreate(BaseListenerType):
maximum=constants.MAX_TIMEOUT),
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)
class ListenerStatusResponse(BaseListenerType):

View File

@ -159,3 +159,24 @@ class BarbicanCertManager(cert_mgr.CertManager):
# the legacy driver is complete.
legacy_mgr = barbican_legacy.BarbicanCertManager(auth=self.auth)
legacy_mgr.unset_acls(context, cert_ref)
def get_secret(self, context, secret_ref):
"""Retrieves a secret payload by reference.
:param context: Oslo context of the request
:param secret_ref: The secret reference ID
:return: The secret payload
:raises CertificateStorageException: if retrieval fails
"""
connection = self.auth.get_barbican_client(context.project_id)
LOG.info('Loading secret %s from Barbican.', secret_ref)
try:
secret = connection.secrets.get(secret_ref=secret_ref)
return secret.payload
except Exception as e:
LOG.error("Failed to access secret for %s due to: %s.",
secret_ref, str(e))
raise exceptions.CertificateStorageException(
msg="Secret could not be accessed.")

View File

@ -226,3 +226,7 @@ class BarbicanCertManager(cert_mgr.CertManager):
if cert_container.intermediates:
self.auth.revoke_secret_access(
context, cert_container.intermediates.secret_ref)
def get_secret(self, context, secret_ref):
# The legacy driver doesn't need get_secret
return None

View File

@ -71,3 +71,14 @@ class CastellanCertManager(cert_mgr.CertManager):
# We don't manage ACL based access for things retrieved via Castellan
# because we assume we have elevated access to the secret store.
pass
def get_secret(self, context, secret_ref):
try:
certbag = self.manager.get(context, secret_ref)
certbag_data = certbag.get_encoded()
except Exception as e:
LOG.error("Failed to access secret for %s due to: %s.",
secret_ref, str(e))
raise exceptions.CertificateStorageException(
msg="Secret could not be accessed.")
return certbag_data

View File

@ -72,3 +72,11 @@ class CertManager(object):
If the specified cert does not exist or the removal of ACLs fails for
any reason, a CertificateStorageException should be raised.
"""
@abc.abstractmethod
def get_secret(self, context, secret_ref):
"""Retrieves a secret payload by reference.
If the specified secret does not exist, a CertificateStorageException
should be raised.
"""

View File

@ -168,3 +168,33 @@ class LocalCertManager(cert_mgr.CertManager):
def unset_acls(self, context, cert_ref):
# There is no security on this store, because it's really dumb
pass
@staticmethod
def get_secret(context, secret_ref):
"""Retrieves a secret payload by reference.
:param context: Ignored in this implementation
:param secret_ref: The secret reference ID
:return: The secret payload
:raises CertificateStorageException: if secret retrieval fails
"""
LOG.info("Loading secret %s from the local filesystem.", secret_ref)
filename_base = os.path.join(CONF.certificates.storage_path,
secret_ref)
filename_secret = "{0}.pem".format(filename_base)
secret_data = None
flags = os.O_RDONLY
try:
with os.fdopen(os.open(filename_secret, flags)) as secret_file:
secret_data = secret_file.read()
except IOError:
LOG.error("Failed to read secret for %s.", secret_ref)
raise exceptions.CertificateStorageException(
msg="secret could not be read.")
return secret_data

View File

@ -368,7 +368,7 @@ 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):
tags=None, client_ca_tls_certificate_id=None):
self.id = id
self.project_id = project_id
self.name = name
@ -397,6 +397,7 @@ class Listener(BaseDataModel):
self.timeout_member_data = timeout_member_data
self.timeout_tcp_inspect = timeout_tcp_inspect
self.tags = tags
self.client_ca_tls_certificate_id = client_ca_tls_certificate_id
def update(self, update_dict):
for key, value in update_dict.items():

View File

@ -78,7 +78,8 @@ class JinjaTemplater(object):
self.connection_logging = connection_logging
def build_config(self, host_amphora, listener, tls_cert,
haproxy_versions, socket_path=None):
haproxy_versions, socket_path=None,
client_ca_filename=None):
"""Convert a logical configuration to the HAProxy version
:param host_amphora: The Amphora this configuration is hosted on
@ -99,7 +100,8 @@ class JinjaTemplater(object):
return self.render_loadbalancer_obj(
host_amphora, listener, tls_cert=tls_cert, socket_path=socket_path,
feature_compatibility=feature_compatibility)
feature_compatibility=feature_compatibility,
client_ca_filename=client_ca_filename)
def _get_template(self):
"""Returns the specified Jinja configuration template."""
@ -117,12 +119,14 @@ class JinjaTemplater(object):
def render_loadbalancer_obj(self, host_amphora, listener,
tls_cert=None, socket_path=None,
feature_compatibility=None):
feature_compatibility=None,
client_ca_filename=None):
"""Renders a templated configuration from a load balancer object
:param host_amphora: The Amphora this configuration is hosted on
:param listener: The listener configuration
:param tls_cert: The TLS certificates for the listener
:param client_ca_filename: The CA certificate for client authorization
:param socket_path: The socket path for Haproxy process
:return: Rendered configuration
"""
@ -132,7 +136,8 @@ class JinjaTemplater(object):
listener.load_balancer,
listener,
tls_cert,
feature_compatibility)
feature_compatibility,
client_ca_filename=client_ca_filename)
if not socket_path:
socket_path = '%s/%s.sock' % (self.base_amp_path, listener.id)
return self._get_template().render(
@ -144,13 +149,15 @@ class JinjaTemplater(object):
constants=constants)
def _transform_loadbalancer(self, host_amphora, loadbalancer, listener,
tls_cert, feature_compatibility):
tls_cert, feature_compatibility,
client_ca_filename=None):
"""Transforms a load balancer into an object that will
be processed by the templating system
"""
t_listener = self._transform_listener(
listener, tls_cert, feature_compatibility)
listener, tls_cert, feature_compatibility,
client_ca_filename=client_ca_filename)
ret_value = {
'id': loadbalancer.id,
'vip_address': loadbalancer.vip.ip_address,
@ -189,7 +196,8 @@ class JinjaTemplater(object):
'vrrp_priority': amphora.vrrp_priority
}
def _transform_listener(self, listener, tls_cert, feature_compatibility):
def _transform_listener(self, listener, tls_cert, feature_compatibility,
client_ca_filename=None):
"""Transforms a listener into an object that will
be processed by the templating system
@ -227,6 +235,10 @@ class JinjaTemplater(object):
tls_cert.id))
if listener.sni_containers:
ret_value['crt_dir'] = os.path.join(self.base_crt_dir, listener.id)
if listener.client_ca_tls_certificate_id:
ret_value['client_ca_tls_path'] = '%s' % (
os.path.join(self.base_crt_dir, listener.id,
client_ca_filename))
if listener.default_pool:
ret_value['default_pool'] = self._transform_pool(
listener.default_pool, feature_compatibility)

View File

@ -38,8 +38,13 @@ 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() %}
{% else %}
{% set client_ca_opt = "" %}
{% endif %}
bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{
"%s %s"|format(def_crt_opt, crt_dir_opt)|trim() }}
"%s %s %s"|format(def_crt_opt, crt_dir_opt, client_ca_opt)|trim() }}
{% endmacro %}

View File

@ -328,7 +328,9 @@ def build_pem(tls_container):
:param tls_container: Object container TLS certificates
:returns: Pem encoded certificate file
"""
pem = [tls_container.certificate, tls_container.private_key]
pem = [tls_container.certificate]
if tls_container.private_key:
pem.append(tls_container.private_key)
if tls_container.intermediates:
pem.extend(tls_container.intermediates[:])
return b'\n'.join(pem) + b'\n'

View File

@ -0,0 +1,37 @@
# 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 listener client_ca_tls_certificate_id column
Revision ID: 2ad093f6353f
Revises: 11e4bb2bb8ef
Create Date: 2019-02-13 08:32:43.009997
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2ad093f6353f'
down_revision = '11e4bb2bb8ef'
def upgrade():
op.add_column(
u'listener',
sa.Column(u'client_ca_tls_certificate_id', sa.String(255),
nullable=True)
)

View File

@ -463,7 +463,7 @@ class Listener(base_models.BASE, base_models.IdMixin,
sa.String(36),
sa.ForeignKey("load_balancer.id", name="fk_listener_load_balancer_id"),
nullable=True)
tls_certificate_id = sa.Column(sa.String(36), nullable=True)
tls_certificate_id = sa.Column(sa.String(255), nullable=True)
default_pool_id = sa.Column(
sa.String(36),
sa.ForeignKey("pool.id", name="fk_listener_pool_id"),
@ -498,6 +498,7 @@ class Listener(base_models.BASE, base_models.IdMixin,
timeout_member_connect = sa.Column(sa.Integer, nullable=True)
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)
_tags = orm.relationship(
'Tags',

View File

@ -90,10 +90,6 @@ def create_listener(listener_dict, lb_id):
if 'tls_termination' in listener_dict:
del listener_dict['tls_termination']
if 'default_tls_container_ref' in listener_dict:
listener_dict['tls_certificate_id'] = (
listener_dict.pop('default_tls_container_ref'))
if 'sni_containers' in listener_dict:
sni_container_ids = listener_dict.pop('sni_containers') or []
elif 'sni_container_refs' in listener_dict:
@ -104,6 +100,7 @@ def create_listener(listener_dict, lb_id):
'tls_container_id': sni_container_id}
for sni_container_id in sni_container_ids]
listener_dict['sni_containers'] = sni_containers
return listener_dict

View File

@ -46,7 +46,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
versions = self._get_versions_with_config(
api_v1_enabled=True, api_v2_enabled=True)
version_ids = tuple(v.get('id') for v in versions)
self.assertEqual(9, len(version_ids))
self.assertEqual(10, len(version_ids))
self.assertIn('v1', version_ids)
self.assertIn('v2.0', version_ids)
self.assertIn('v2.1', version_ids)
@ -56,6 +56,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
self.assertIn('v2.5', version_ids)
self.assertIn('v2.6', version_ids)
self.assertIn('v2.7', version_ids)
self.assertIn('v2.8', version_ids)
# Each version should have a 'self' 'href' to the API version URL
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]
@ -75,7 +76,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
def test_api_v1_disabled(self):
versions = self._get_versions_with_config(
api_v1_enabled=False, api_v2_enabled=True)
self.assertEqual(8, len(versions))
self.assertEqual(9, len(versions))
self.assertEqual('v2.0', versions[0].get('id'))
self.assertEqual('v2.1', versions[1].get('id'))
self.assertEqual('v2.2', versions[2].get('id'))
@ -84,6 +85,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
self.assertEqual('v2.5', versions[5].get('id'))
self.assertEqual('v2.6', versions[6].get('id'))
self.assertEqual('v2.7', versions[7].get('id'))
self.assertEqual('v2.8', versions[8].get('id'))
def test_api_v2_disabled(self):
versions = self._get_versions_with_config(

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import random
import mock
@ -25,6 +26,7 @@ import octavia.common.context
from octavia.common import data_models
from octavia.common import exceptions
from octavia.tests.functional.api.v2 import base
from octavia.tests.unit.common.sample_configs import sample_certs
class TestListener(base.BaseAPITest):
@ -564,7 +566,7 @@ class TestListener(base.BaseAPITest):
lb_listener = {'name': 'listener1', 'default_pool_id': None,
'description': 'desc1',
'admin_state_up': False,
'protocol': constants.PROTOCOL_HTTP,
'protocol': constants.PROTOCOL_TERMINATED_HTTPS,
'protocol_port': 80, 'connection_limit': 10,
'default_tls_container_ref': uuidutils.generate_uuid(),
'sni_container_refs': [sni1, sni2],
@ -724,6 +726,47 @@ class TestListener(base.BaseAPITest):
self.assertNotIn(sni2, response['faultstring'])
self.assertIn(tls_ref, response['faultstring'])
def test_create_with_certs_not_terminated_https(self):
optionals = {
'default_tls_container_ref': uuidutils.generate_uuid(),
'protocol': constants.PROTOCOL_TCP
}
resp = self.test_create(response_status=400, **optionals).json
fault = resp.get('faultstring')
self.assertIn(
'Certificate container references are only allowed on ', fault)
self.assertIn(
'{} protocol listeners.'.format(
constants.PROTOCOL_TERMINATED_HTTPS), fault)
def test_create_without_certs_if_terminated_https(self):
optionals = {
'default_tls_container_ref': None,
'sni_container_refs': None,
'protocol': constants.PROTOCOL_TERMINATED_HTTPS
}
resp = self.test_create(response_status=400, **optionals).json
fault = resp.get('faultstring')
self.assertIn(
'An SNI or default certificate container reference must ', fault)
self.assertIn(
'be provided for {} protocol listeners.'.format(
constants.PROTOCOL_TERMINATED_HTTPS), fault)
def test_create_client_ca_cert_without_tls_cert(self):
optionals = {
'default_tls_container_ref': None,
'sni_container_refs': None,
'client_ca_tls_container_ref': uuidutils.generate_uuid(),
'protocol': constants.PROTOCOL_TERMINATED_HTTPS
}
resp = self.test_create(response_status=400, **optionals).json
fault = resp.get('faultstring')
self.assertIn(
'An SNI or default certificate container reference must ', fault)
self.assertIn(
'be provided with a client CA container reference.', fault)
def test_create_with_default_pool_id(self):
lb_listener = {'name': 'listener1',
'default_pool_id': self.pool_id,
@ -824,23 +867,14 @@ class TestListener(base.BaseAPITest):
self.assertIn('Provider \'bad_driver\' reports error: broken',
response.json.get('faultstring'))
# TODO(johnsom) Fix this when there is a noop certificate manager
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_create_authorized(self, mock_cert_data, **optionals):
cert1 = data_models.TLSContainer(certificate='cert 1')
cert2 = data_models.TLSContainer(certificate='cert 2')
cert3 = data_models.TLSContainer(certificate='cert 3')
mock_cert_data.return_value = {'tls_cert': cert1,
'sni_certs': [cert2, cert3]}
sni1 = uuidutils.generate_uuid()
sni2 = uuidutils.generate_uuid()
def test_create_authorized(self, **optionals):
lb_listener = {'name': 'listener1', 'default_pool_id': None,
'description': 'desc1',
'admin_state_up': False,
'protocol': constants.PROTOCOL_HTTP,
'protocol_port': 80, 'connection_limit': 10,
'default_tls_container_ref': uuidutils.generate_uuid(),
'sni_container_refs': [sni1, sni2],
'default_tls_container_ref': None,
'sni_container_refs': None,
'insert_headers': {},
'project_id': self.project_id,
'loadbalancer_id': self.lb_id}
@ -881,12 +915,6 @@ class TestListener(base.BaseAPITest):
for key, value in optionals.items():
self.assertEqual(value, lb_listener.get(key))
lb_listener['id'] = listener_api.get('id')
lb_listener.pop('sni_container_refs')
sni_ex = [sni1, sni2]
sni_resp = listener_api.pop('sni_container_refs')
self.assertEqual(2, len(sni_resp))
for sni in sni_resp:
self.assertIn(sni, sni_ex)
self.assertIsNotNone(listener_api.pop('created_at'))
self.assertIsNone(listener_api.pop('updated_at'))
self.assertNotEqual(lb_listener, listener_api)
@ -895,15 +923,13 @@ class TestListener(base.BaseAPITest):
self.assert_final_listener_statuses(self.lb_id, listener_api.get('id'))
def test_create_not_authorized(self, **optionals):
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_HTTP,
'protocol_port': 80, 'connection_limit': 10,
'default_tls_container_ref': uuidutils.generate_uuid(),
'sni_container_refs': [sni1, sni2],
'default_tls_container_ref': None,
'sni_container_refs': None,
'insert_headers': {},
'project_id': self.project_id,
'loadbalancer_id': self.lb_id}
@ -921,6 +947,63 @@ class TestListener(base.BaseAPITest):
self.conf.config(group='api_settings', auth_strategy=auth_strategy)
self.assertEqual(self.NOT_AUTHORIZED_BODY, response.json)
def test_create_with_ca_cert(self):
self.cert_manager_mock().get_secret.return_value = (
sample_certs.X509_CA_CERT)
optionals = {
'client_ca_tls_container_ref': uuidutils.generate_uuid()
}
listener_api = self.test_create(**optionals)
self.assertEqual(optionals['client_ca_tls_container_ref'],
listener_api.get('client_ca_tls_container_ref'))