Merge "Add client_ca_tls_container_ref to listener API"

This commit is contained in:
Zuul 2019-02-26 10:54:59 +00:00 committed by Gerrit Code Review
commit a569a6e935
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'))
def test_create_with_bad_ca_cert_ref(self):
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,
'client_ca_tls_container_ref': uuidutils.generate_uuid()}
body = self._build_body(lb_listener)
self.cert_manager_mock().get_cert.side_effect = [
'cert 1', 'cert 2', 'cert 3']
self.cert_manager_mock().get_secret.side_effect = [
Exception('bad ca cert')]
response = self.post(self.LISTENERS_PATH, body, status=400).json
self.assertIn(lb_listener['client_ca_tls_container_ref'],
response['faultstring'])
def test_create_with_bad_ca_cert(self):
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,
'client_ca_tls_container_ref': uuidutils.generate_uuid()}
body = self._build_body(lb_listener)
self.cert_manager_mock().get_cert.side_effect = [
'cert 1', 'cert 2', 'cert 3']
self.cert_manager_mock().get_secret.return_value = 'bad cert'
response = self.post(self.LISTENERS_PATH, body, status=400).json
self.assertIn("The client authentication CA certificate is invalid. "
"It must be a valid x509 PEM format certificate.",
response['faultstring'])
@mock.patch('octavia.api.drivers.utils.call_provider')
def test_update_with_bad_provider(self, mock_provider):
api_listener = self.create_listener(
@ -957,16 +1040,21 @@ class TestListener(base.BaseAPITest):
# TODO(johnsom) Fix this when there is a noop certificate manager
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update(self, mock_cert_data):
def test_update(self, mock_cert_data, **options):
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()
ca_tls_uuid = uuidutils.generate_uuid()
listener = self.create_listener(
constants.PROTOCOL_TCP, 80, self.lb_id,
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, tags=['old_tag']).get(self.root_tag)
default_pool_id=None, tags=['old_tag'],
client_ca_tls_container_ref=ca_tls_uuid).get(self.root_tag)
ori_listener = copy.deepcopy(listener)
self.set_lb_status(self.lb_id)
new_listener = {'name': 'listener2', 'admin_state_up': True,
'default_pool_id': self.pool_id,
@ -975,6 +1063,7 @@ class TestListener(base.BaseAPITest):
'timeout_member_data': 3,
'timeout_tcp_inspect': 4,
'tags': ['new_tag']}
new_listener.update(options)
body = self._build_body(new_listener)
listener_path = self.LISTENER_PATH.format(
listener_id=listener['id'])
@ -991,6 +1080,7 @@ class TestListener(base.BaseAPITest):
constants.PENDING_UPDATE)
self.assert_final_listener_statuses(self.lb_id,
api_listener['id'])
return ori_listener, api_listener
def test_negative_update_udp_case(self):
api_listener = self.create_listener(constants.PROTOCOL_UDP, 6666,
@ -1040,17 +1130,95 @@ class TestListener(base.BaseAPITest):
self.assert_final_listener_statuses(self.lb_id,
listener['listener']['id'])
# TODO(johnsom) Fix this when there is a noop certificate manager
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_authorized(self, mock_cert_data):
cert1 = data_models.TLSContainer(certificate='cert 1')
mock_cert_data.return_value = {'tls_cert': cert1}
tls_uuid = uuidutils.generate_uuid()
def test_update_with_certs_not_terminated_https(self):
listener = self.create_listener(
constants.PROTOCOL_TCP, 80, self.lb_id,
name='listener1', description='desc1',
admin_state_up=False, connection_limit=10,
default_pool_id=None,).get(self.root_tag)
self.set_lb_status(self.lb_id)
lb_listener = {
'default_tls_container_ref': uuidutils.generate_uuid()}
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
fault = response.get('faultstring')
self.assertIn(
'Certificate container references are only allowed on ', fault)
self.assertIn(
'{} protocol listeners.'.format(
constants.PROTOCOL_TERMINATED_HTTPS), fault)
def test_update_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()
}
ori_listener, update_listener = self.test_update(**optionals)
self.assertEqual(optionals['client_ca_tls_container_ref'],
update_listener.get('client_ca_tls_container_ref'))
self.assertNotEqual(ori_listener['client_ca_tls_container_ref'],
optionals['client_ca_tls_container_ref'])
@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')
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()
ca_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,
client_ca_tls_container_ref=ca_tls_uuid).get(self.root_tag)
self.set_lb_status(self.lb_id)
lb_listener = {'client_ca_tls_container_ref': None}
body = self._build_body(lb_listener)
listener_path = self.LISTENER_PATH.format(
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'))
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_with_bad_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()
ca_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,
client_ca_tls_container_ref=ca_tls_uuid).get(self.root_tag)
self.set_lb_status(self.lb_id)
self.cert_manager_mock().get_secret.side_effect = Exception(
'bad ca cert')
lb_listener = {
'client_ca_tls_container_ref': uuidutils.generate_uuid()}
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.assertIn(lb_listener['client_ca_tls_container_ref'],
response['faultstring'])
def test_update_authorized(self):
listener = self.create_listener(
constants.PROTOCOL_TCP, 80, self.lb_id,
name='listener1', description='desc1',
admin_state_up=False, connection_limit=10,
default_pool_id=None).get(self.root_tag)
self.set_lb_status(self.lb_id)
new_listener = {'name': 'listener2', 'admin_state_up': True,
@ -1098,17 +1266,11 @@ class TestListener(base.BaseAPITest):
self.assert_final_listener_statuses(self.lb_id,
api_listener['id'])
# TODO(johnsom) Fix this when there is a noop certificate manager
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_not_authorized(self, mock_cert_data):
cert1 = data_models.TLSContainer(certificate='cert 1')
mock_cert_data.return_value = {'tls_cert': cert1}
tls_uuid = uuidutils.generate_uuid()
def test_update_not_authorized(self):
listener = self.create_listener(
constants.PROTOCOL_TCP, 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)
new_listener = {'name': 'listener2', 'admin_state_up': True,
@ -1454,16 +1616,19 @@ class TestListener(base.BaseAPITest):
# TODO(johnsom) Fix this when there is a noop certificate manager
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_with_tls_termination_data(self, mock_cert_data):
cert_id_orig = uuidutils.generate_uuid()
cert1 = data_models.TLSContainer(certificate='cert 1')
mock_cert_data.return_value = {'tls_cert': cert1}
cert_id = uuidutils.generate_uuid()
listener = self.create_listener(constants.PROTOCOL_TERMINATED_HTTPS,
80, self.lb_id)
listener = self.create_listener(
constants.PROTOCOL_TERMINATED_HTTPS, 80, self.lb_id,
default_tls_container_ref=cert_id_orig)
self.set_lb_status(self.lb_id)
listener_path = self.LISTENER_PATH.format(
listener_id=listener['listener']['id'])
get_listener = self.get(listener_path).json['listener']
self.assertIsNone(get_listener.get('default_tls_container_ref'))
self.assertEqual(cert_id_orig,
get_listener.get('default_tls_container_ref'))
self.put(listener_path,
self._build_body({'default_tls_container_ref': cert_id}))
get_listener = self.get(listener_path).json['listener']
@ -1493,8 +1658,8 @@ class TestListener(base.BaseAPITest):
'sni_certs': [cert2, cert3]}
sni_id1 = uuidutils.generate_uuid()
sni_id2 = uuidutils.generate_uuid()
listener = self.create_listener(constants.PROTOCOL_HTTP, 80,
self.lb_id,
listener = self.create_listener(constants.PROTOCOL_TERMINATED_HTTPS,
80, self.lb_id,
sni_container_refs=[sni_id1, sni_id2])
listener_path = self.LISTENER_PATH.format(
listener_id=listener['listener']['id'])

View File

@ -2355,11 +2355,14 @@ class TestLoadBalancerGraph(base.BaseAPITest):
create_l7policies=None,
expected_l7policies=None,
create_sni_containers=None,
expected_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_listener = {
'name': name,
'protocol_port': protocol_port,
'protocol': constants.PROTOCOL_HTTP
'protocol': create_protocol
}
expected_listener = {
'description': '',
@ -2375,7 +2378,8 @@ class TestLoadBalancerGraph(base.BaseAPITest):
'timeout_member_connect': constants.DEFAULT_TIMEOUT_MEMBER_CONNECT,
'timeout_member_data': constants.DEFAULT_TIMEOUT_MEMBER_DATA,
'timeout_tcp_inspect': constants.DEFAULT_TIMEOUT_TCP_INSPECT,
'tags': []
'tags': [],
'client_ca_tls_container_ref': None
}
if create_sni_containers:
create_listener['sni_container_refs'] = create_sni_containers
@ -2391,12 +2395,18 @@ class TestLoadBalancerGraph(base.BaseAPITest):
if create_l7policies:
l7policies = create_l7policies
create_listener['l7policies'] = l7policies
if create_client_ca_tls_container:
create_listener['client_ca_tls_container_ref'] = (
create_client_ca_tls_container)
if expected_sni_containers:
expected_listener['sni_container_refs'] = expected_sni_containers
if expected_l7policies:
expected_listener['l7policies'] = expected_l7policies
else:
expected_listener['l7policies'] = []
if expected_client_ca_tls_container:
expected_listener['client_ca_tls_container_ref'] = (
expected_client_ca_tls_container)
return create_listener, expected_listener
def _get_pool_bodies(self, name='pool1', create_members=None,
@ -2633,6 +2643,7 @@ class TestLoadBalancerGraph(base.BaseAPITest):
create_sni_containers, expected_sni_containers = (
self._get_sni_container_bodies())
create_listener, expected_listener = self._get_listener_bodies(
create_protocol=constants.PROTOCOL_TERMINATED_HTTPS,
create_sni_containers=create_sni_containers,
expected_sni_containers=expected_sni_containers)
create_lb, expected_lb = self._get_lb_bodies(
@ -2643,6 +2654,38 @@ class TestLoadBalancerGraph(base.BaseAPITest):
api_lb = response.json.get(self.root_tag)
self._assert_graphs_equal(expected_lb, api_lb)
@mock.patch('cryptography.hazmat.backends.default_backend')
@mock.patch('cryptography.x509.load_pem_x509_certificate')
@mock.patch('octavia.api.drivers.utils._get_secret_data')
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_with_full_listener_certs(self, mock_cert_data, mock_get_secret,
mock_x509_cert, mock_backend):
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]}
mock_get_secret.side_effect = ['ca cert']
cert_mock = mock.MagicMock()
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_sni_containers, expected_sni_containers = (
self._get_sni_container_bodies())
create_listener, expected_listener = self._get_listener_bodies(
create_protocol=constants.PROTOCOL_TERMINATED_HTTPS,
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)
create_lb, expected_lb = self._get_lb_bodies(
create_listeners=[create_listener],
expected_listeners=[expected_listener])
body = self._build_body(create_lb)
response = self.post(self.LBS_PATH, body)
api_lb = response.json.get(self.root_tag)
self._assert_graphs_equal(expected_lb, api_lb)
def test_with_l7policy_redirect_pool_no_rule(self):
create_pool, expected_pool = self._get_pool_bodies(create_members=[],
expected_members=[])
@ -2824,6 +2867,7 @@ class TestLoadBalancerGraph(base.BaseAPITest):
expected_l7rules=expected_l7rules)
create_listener, expected_listener = self._get_listener_bodies(
create_default_pool_name=create_pool['name'],
create_protocol=constants.PROTOCOL_TERMINATED_HTTPS,
create_l7policies=create_l7policies,
expected_l7policies=expected_l7policies,
create_sni_containers=create_sni_containers,
@ -2924,7 +2968,6 @@ class TestLoadBalancerGraph(base.BaseAPITest):
self.start_quota_mock(data_models.HealthMonitor)
self.post(self.LBS_PATH, body, status=403)
# 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_over_quota_sanity_check(self, mock_cert_data):
cert1 = data_models.TLSContainer(certificate='cert 1')

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import hashlib
import mock
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
@ -63,7 +65,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.driver.udp_jinja = mock.MagicMock()
# Build sample Listener and VIP configs
self.sl = sample_configs.sample_listener_tuple(tls=True, sni=True)
self.sl = sample_configs.sample_listener_tuple(
tls=True, sni=True, client_ca_cert=True)
self.sl_udp = sample_configs.sample_listener_tuple(
proto=constants.PROTOCOL_UDP,
persistence_type=constants.SESSION_PERSISTENCE_SOURCE_IP,
@ -99,13 +102,17 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
constants.CONN_MAX_RETRIES: 3,
constants.CONN_RETRY_INTERVAL: 4}
@mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.'
'HaproxyAmphoraLoadBalancerDriver._process_secret')
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_update_amphora_listeners(self, mock_load_cert):
def test_update_amphora_listeners(self, mock_load_cert, mock_secret):
mock_amphora = mock.MagicMock()
mock_amphora.id = uuidutils.generate_uuid()
mock_listener = mock.MagicMock()
mock_listener.id = uuidutils.generate_uuid()
mock_load_cert.return_value = {'tls_cert': None, 'sni_certs': []}
mock_secret.return_value = 'filename.pem'
mock_load_cert.return_value = {'tls_cert': None, 'sni_certs': [],
'client_ca_cert': None}
self.driver.jinja.build_config.return_value = 'the_config'
self.driver.update_amphora_listeners(None, 1, [],
@ -135,19 +142,24 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.driver.client.upload_config.assert_not_called()
self.driver.client.reload_listener.assert_not_called()
@mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.'
'HaproxyAmphoraLoadBalancerDriver._process_secret')
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
@mock.patch('octavia.common.tls_utils.cert_parser.get_host_names')
def test_update(self, mock_cert, mock_load_crt):
def test_update(self, mock_cert, mock_load_crt, mock_secret):
mock_cert.return_value = {'cn': sample_certs.X509_CERT_CN}
mock_secret.return_value = 'filename.pem'
sconts = []
for sni_container in self.sl.sni_containers:
sconts.append(sni_container.tls_container)
mock_load_crt.return_value = {
'tls_cert': self.sl.default_tls_container,
'sni_certs': sconts
'sni_certs': sconts,
'client_ca_cert': self.sl.client_ca_tls_certificate
}
self.driver.client.get_cert_md5sum.side_effect = [
exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']
exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
'CA_CERT_MD5']
self.driver.jinja.build_config.side_effect = ['fake_config']
self.driver.client.get_listener_status.side_effect = [
dict(status='ACTIVE')]
@ -156,7 +168,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.driver.update(self.sl, self.sv)
# verify result
# this is called 3 times
# this is called 4 times
gcm_calls = [
mock.call(self.amp, self.sl.id,
self.sl.default_tls_container.id + '.pem',
@ -164,7 +176,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
mock.call(self.amp, self.sl.id,
sconts[0].id + '.pem', ignore=(404,)),
mock.call(self.amp, self.sl.id,
sconts[1].id + '.pem', ignore=(404,))
sconts[1].id + '.pem', ignore=(404,)),
]
self.driver.client.get_cert_md5sum.assert_has_calls(gcm_calls,
any_order=True)
@ -179,13 +191,14 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
fp3 = b'\n'.join([sample_certs.X509_CERT_3,
sample_certs.X509_CERT_KEY_3,
sample_certs.X509_IMDS]) + b'\n'
ucp_calls = [
mock.call(self.amp, self.sl.id,
self.sl.default_tls_container.id + '.pem', fp1),
mock.call(self.amp, self.sl.id,
sconts[0].id + '.pem', fp2),
mock.call(self.amp, self.sl.id,
sconts[1].id + '.pem', fp3)
sconts[1].id + '.pem', fp3),
]
self.driver.client.upload_cert_pem.assert_has_calls(ucp_calls,
any_order=True)
@ -216,6 +229,90 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.driver.client.update_cert_for_rotation.assert_called_once_with(
self.amp, six.b('test'))
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test__process_tls_certificates_no_ca_cert(self, mock_load_crt):
sample_listener = sample_configs.sample_listener_tuple(
tls=True, sni=True)
sconts = []
for sni_container in sample_listener.sni_containers:
sconts.append(sni_container.tls_container)
mock_load_crt.return_value = {
'tls_cert': self.sl.default_tls_container,
'sni_certs': sconts,
'client_ca_cert': None
}
self.driver.client.get_cert_md5sum.side_effect = [
exc.NotFound, 'Fake_MD5', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']
self.driver._process_tls_certificates(sample_listener)
gcm_calls = [
mock.call(self.amp, self.sl.id,
self.sl.default_tls_container.id + '.pem',
ignore=(404,)),
mock.call(self.amp, self.sl.id,
sconts[0].id + '.pem', ignore=(404,)),
mock.call(self.amp, self.sl.id,
sconts[1].id + '.pem', ignore=(404,))
]
self.driver.client.get_cert_md5sum.assert_has_calls(gcm_calls,
any_order=True)
fp1 = b'\n'.join([sample_certs.X509_CERT,
sample_certs.X509_CERT_KEY,
sample_certs.X509_IMDS]) + b'\n'
fp2 = b'\n'.join([sample_certs.X509_CERT_2,
sample_certs.X509_CERT_KEY_2,
sample_certs.X509_IMDS]) + b'\n'
fp3 = b'\n'.join([sample_certs.X509_CERT_3,
sample_certs.X509_CERT_KEY_3,
sample_certs.X509_IMDS]) + b'\n'
ucp_calls = [
mock.call(self.amp, self.sl.id,
self.sl.default_tls_container.id + '.pem', fp1),
mock.call(self.amp, self.sl.id,
sconts[0].id + '.pem', fp2),
mock.call(self.amp, self.sl.id,
sconts[1].id + '.pem', fp3)
]
self.driver.client.upload_cert_pem.assert_has_calls(ucp_calls,
any_order=True)
self.assertEqual(3, self.driver.client.upload_cert_pem.call_count)
@mock.patch('oslo_context.context.RequestContext')
@mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.'
'HaproxyAmphoraLoadBalancerDriver._apply')
def test_process_secret(self, mock_apply, mock_oslo):
# Test bypass if no secret_ref
sample_listener = sample_configs.sample_listener_tuple(
tls=True, sni=True)
result = self.driver._process_secret(sample_listener, None)
self.assertIsNone(result)
self.driver.cert_manager.get_secret.assert_not_called()
# Test the secret process
sample_listener = sample_configs.sample_listener_tuple(
tls=True, sni=True, client_ca_cert=True)
fake_context = 'fake context'
fake_secret = 'fake cert'
mock_oslo.return_value = fake_context
self.driver.cert_manager.get_secret.reset_mock()
self.driver.cert_manager.get_secret.return_value = fake_secret
ref_md5 = hashlib.md5(fake_secret.encode('utf-8')).hexdigest() # nosec
ref_id = hashlib.sha1(fake_secret.encode('utf-8')).hexdigest() # nosec
ref_name = '{id}.pem'.format(id=ref_id)
result = self.driver._process_secret(
sample_listener, sample_listener.client_ca_tls_certificate_id)
mock_oslo.assert_called_once_with(
project_id=sample_listener.project_id)
self.driver.cert_manager.get_secret.assert_called_once_with(
fake_context, sample_listener.client_ca_tls_certificate_id)
mock_apply.assert_called_once_with(
self.driver._upload_cert, sample_listener, None, fake_secret,
ref_md5, ref_name)
self.assertEqual(ref_name, result)
def test_stop(self):
self.driver.client.stop_listener.__name__ = 'stop_listener'
# Execute driver method

View File

@ -37,6 +37,7 @@ class SampleDriverDataModels(object):
self.default_tls_container_ref = uuidutils.generate_uuid()
self.sni_container_ref_1 = uuidutils.generate_uuid()
self.sni_container_ref_2 = uuidutils.generate_uuid()
self.client_ca_tls_certificate_ref = uuidutils.generate_uuid()
self.pool1_id = uuidutils.generate_uuid()
self.pool2_id = uuidutils.generate_uuid()
@ -380,7 +381,9 @@ class SampleDriverDataModels(object):
'timeout_client_data': 1000,
'timeout_member_connect': 2000,
'timeout_member_data': 3000,
'timeout_tcp_inspect': 4000}
'timeout_tcp_inspect': 4000,
'client_ca_tls_certificate_id': self.client_ca_tls_certificate_ref
}
self.test_listener1_dict.update(self._common_test_dict)
@ -392,6 +395,7 @@ class SampleDriverDataModels(object):
self.test_listener2_dict['default_pool'] = self.test_pool2_dict
del self.test_listener2_dict['l7policies']
del self.test_listener2_dict['sni_containers']
del self.test_listener2_dict['client_ca_tls_certificate_id']
self.test_listeners = [self.test_listener1_dict,
self.test_listener2_dict]
@ -410,6 +414,7 @@ class SampleDriverDataModels(object):
cert1 = data_models.TLSContainer(certificate='cert 1')
cert2 = data_models.TLSContainer(certificate='cert 2')
cert3 = data_models.TLSContainer(certificate='cert 3')
ca_cert = 'ca cert'
self.provider_listener1_dict = {
'admin_state_up': True,
@ -432,7 +437,10 @@ class SampleDriverDataModels(object):
'timeout_client_data': 1000,
'timeout_member_connect': 2000,
'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_data': ca_cert
}
self.provider_listener2_dict = copy.deepcopy(
self.provider_listener1_dict)
@ -442,6 +450,8 @@ class SampleDriverDataModels(object):
self.provider_listener2_dict['default_pool_id'] = self.pool2_id
self.provider_listener2_dict['default_pool'] = self.provider_pool2_dict
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_listener1 = driver_dm.Listener(
**self.provider_listener1_dict)

View File

@ -84,11 +84,13 @@ class TestUtils(base.TestCase):
self.assertEqual({'admin_state_up': True},
result_dict)
@mock.patch('octavia.api.drivers.utils._get_secret_data')
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_lb_dict_to_provider_dict(self, mock_load_cert):
def test_lb_dict_to_provider_dict(self, mock_load_cert, mock_secret):
cert1 = data_models.TLSContainer(certificate='cert 1')
cert2 = data_models.TLSContainer(certificate='cert 2')
cert3 = data_models.TLSContainer(certificate='cert 3')
mock_secret.return_value = 'ca cert'
mock_load_cert.return_value = {'tls_cert': cert1,
'sni_certs': [cert2, cert3]}
test_lb_dict = {'name': 'lb1',
@ -156,8 +158,11 @@ class TestUtils(base.TestCase):
self.assertEqual(ref_provider_list.to_dict(render_unsets=True),
provider_list.to_dict(render_unsets=True))
@mock.patch('octavia.api.drivers.utils._get_secret_data')
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_db_listeners_to_provider_listeners(self, mock_load_cert):
def test_db_listeners_to_provider_listeners(self, mock_load_cert,
mock_secret):
mock_secret.return_value = 'ca cert'
cert1 = data_models.TLSContainer(certificate='cert 1')
cert2 = data_models.TLSContainer(certificate='cert 2')
cert3 = data_models.TLSContainer(certificate='cert 3')
@ -168,8 +173,10 @@ class TestUtils(base.TestCase):
self.assertEqual(self.sample_data.provider_listeners,
provider_listeners)
@mock.patch('octavia.api.drivers.utils._get_secret_data')
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_listener_dict_to_provider_dict(self, mock_load_cert):
def test_listener_dict_to_provider_dict(self, mock_load_cert, mock_secret):
mock_secret.return_value = 'ca cert'
cert1 = data_models.TLSContainer(certificate='cert 1')
cert2 = data_models.TLSContainer(certificate='cert 2')
cert3 = data_models.TLSContainer(certificate='cert 3')
@ -180,8 +187,11 @@ class TestUtils(base.TestCase):
self.assertEqual(self.sample_data.provider_listener1_dict,
provider_listener)
@mock.patch('octavia.api.drivers.utils._get_secret_data')
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
def test_listener_dict_to_provider_dict_SNI(self, mock_load_cert):
def test_listener_dict_to_provider_dict_SNI(self, mock_load_cert,
mock_secret):
mock_secret.return_value = 'ca cert'
cert1 = data_models.TLSContainer(certificate='cert 1')
cert2 = data_models.TLSContainer(certificate='cert 2')
cert3 = data_models.TLSContainer(certificate='cert 3')

View File

@ -20,6 +20,7 @@ import mock
import octavia.certificates.common.barbican as barbican_common
import octavia.certificates.common.cert as cert
import octavia.certificates.manager.barbican as barbican_cert_mgr
from octavia.common import exceptions
import octavia.tests.unit.base as base
import octavia.tests.unit.common.sample_configs.sample_certs as sample
@ -39,11 +40,15 @@ class TestBarbicanManager(base.TestCase):
)
self.name = 'My Fancy Cert'
self.secret = secrets.Secret(
self.secret_pkcs12 = secrets.Secret(
api=mock.MagicMock(),
payload=sample.PKCS12_BUNDLE
)
self.fake_secret = 'Fake secret'
self.secret = secrets.Secret(api=mock.MagicMock(),
payload=self.fake_secret)
self.empty_secret = mock.Mock(spec=secrets.Secret)
# Mock out the client
@ -111,7 +116,7 @@ class TestBarbicanManager(base.TestCase):
def test_get_cert(self):
# Mock out the client
self.bc.secrets.get.return_value = self.secret
self.bc.secrets.get.return_value = self.secret_pkcs12
# Get the secret data
data = self.cert_manager.get_cert(
@ -174,3 +179,25 @@ class TestBarbicanManager(base.TestCase):
self.cert_manager.auth.revoke_secret_access.assert_called_once_with(
self.context, self.secret_ref
)
def test_get_secret(self):
# Mock out the client
self.bc.secrets.get.side_effect = [self.secret, Exception]
# Get the secret data
data = self.cert_manager.get_secret(
context=self.context,
secret_ref=self.secret_ref,
)
# 'get_secret' should be called once with the secret_ref
self.bc.secrets.get.assert_called_once_with(
secret_ref=self.secret_ref
)
self.assertEqual(self.fake_secret, data)
# Test with a failure
self.assertRaises(exceptions.CertificateStorageException,
self.cert_manager.get_secret,
context=self.context, secret_ref=self.secret_ref)

View File

@ -298,3 +298,7 @@ class TestBarbicanManager(base.TestCase):
mock.call(self.context, self.private_key_uuid),
mock.call(self.context, self.private_key_passphrase_uuid)
], any_order=True)
def test_get_secret(self):
self.assertIsNone(self.cert_manager.get_secret('fake context',
'fake secret ref'))

View File

@ -0,0 +1,49 @@
# Copyright 2019 Rackspace, US Inc.
#
# 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.
import mock
from octavia.certificates.manager import castellan_mgr
from octavia.common import exceptions
import octavia.tests.unit.base as base
class TestCastellanCertManager(base.TestCase):
def setUp(self):
self.fake_secret = 'Fake secret'
self.manager = mock.MagicMock()
self.certbag = mock.MagicMock()
self.manager.get.return_value = self.certbag
super(TestCastellanCertManager, self).setUp()
@mock.patch('castellan.key_manager.API')
def test_get_secret(self, mock_api):
mock_api.return_value = self.manager
castellan_mgr_obj = castellan_mgr.CastellanCertManager()
self.certbag.get_encoded.side_effect = [self.fake_secret,
Exception('boom')]
result = castellan_mgr_obj.get_secret('context', 'secret_ref')
self.assertEqual(self.fake_secret, result)
self.manager.get.assert_called_once_with('context', 'secret_ref')
self.certbag.get_encoded.assert_called_once()
self.assertRaises(exceptions.CertificateStorageException,
castellan_mgr_obj.get_secret, 'context',
'secret_ref')

View File

@ -18,9 +18,11 @@ import stat
import mock
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
from oslo_utils import uuidutils
import octavia.certificates.common.cert as cert
import octavia.certificates.manager.local as local_cert_mgr
from octavia.common import exceptions
import octavia.tests.unit.base as base
@ -133,3 +135,25 @@ class TestLocalManager(base.TestCase):
# Delete the cert
self._delete_cert(cert_id)
def test_get_secret(self):
fd_mock = mock.mock_open()
open_mock = mock.Mock()
secret_id = uuidutils.generate_uuid()
# Attempt to retrieve the secret
with mock.patch('os.open', open_mock), mock.patch.object(
os, 'fdopen', fd_mock):
local_cert_mgr.LocalCertManager.get_secret(None, secret_id)
# Verify the correct files were opened
flags = os.O_RDONLY
open_mock.assert_called_once_with('/tmp/{0}.pem'.format(secret_id),
flags)
# Test failure path
with mock.patch('os.open', open_mock), mock.patch.object(
os, 'fdopen', fd_mock) as mock_open:
mock_open.side_effect = IOError
self.assertRaises(exceptions.CertificateStorageException,
local_cert_mgr.LocalCertManager.get_secret,
None, secret_id)

View File

@ -38,7 +38,9 @@ class TestHaproxyCfg(base.TestCase):
" bind 10.0.0.2:443 "
"ssl crt /var/lib/octavia/certs/"
"sample_listener_id_1/tls_container_id.pem "
"crt /var/lib/octavia/certs/sample_listener_id_1\n"
"crt /var/lib/octavia/certs/sample_listener_id_1 "
"ca-file /var/lib/octavia/certs/sample_listener_id_1/"
"client_ca.pem\n"
" mode http\n"
" default_backend sample_pool_id_1\n"
" timeout client 50000\n\n").format(
@ -68,8 +70,9 @@ class TestHaproxyCfg(base.TestCase):
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs.sample_amphora_tuple(),
sample_configs.sample_listener_tuple(proto='TERMINATED_HTTPS',
tls=True, sni=True),
tls_tupe)
tls=True, sni=True,
client_ca_cert=True),
tls_tupe, client_ca_filename='client_ca.pem')
self.assertEqual(
sample_configs.sample_base_expected_config(
frontend=fe, backend=be),

View File

@ -818,3 +818,41 @@ uJIQ
PKCS12_BUNDLE = pkg_resources.resource_string(
'octavia.tests.unit.common.sample_configs', 'sample_pkcs12.p12')
X509_CA_CERT_CN = 'ca.example.org'
X509_CA_CERT_SHA1 = '3d52837151662dbe7c01a97fad0aab5f61f78280'
X509_CA_CERT = b"""-----BEGIN CERTIFICATE-----
MIIFoDCCA4igAwIBAgIJAPBfmRtfTNF2MA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV
BAYTAlVTMQ8wDQYDVQQIDAZPcmVnb24xEjAQBgNVBAoMCU9wZW5TdGFjazEQMA4G
A1UECwwHT2N0YXZpYTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5vcmcwHhcNMTkwMjE0
MDQ1MjQwWhcNMjkwMjExMDQ1MjQwWjBdMQswCQYDVQQGEwJVUzEPMA0GA1UECAwG
T3JlZ29uMRIwEAYDVQQKDAlPcGVuU3RhY2sxEDAOBgNVBAsMB09jdGF2aWExFzAV
BgNVBAMMDmNhLmV4YW1wbGUub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAshn5CRt949+edmECCpaQtrCnjiA8KFsNCb9Dv70LkK9XbHtFkJuUgJR1
VE1OhGK057k/z1gEYUIFxw8s9wKMaAxta7CwxkpJR8oMa60nx4hbNLF1Q5xO0P40
YW/fSxuBmztI8EtYGUCGDLpktUTrewWu68nnWV2Wyx5B69Z14qrDGk7b6VH2atWD
qJwDGrPkekNSUiE2Z/cCcTDH2t1jqtlGsiS8tDDH4h35ywm6fY3V/11hHT76dxDz
LhrLa2aVXeVtqGMTOHkXOFEwcQNfh78z7qBOZy9O8bCCepCmJ56ff9E3kXd1jam2
6TiZikOVWhDOv668IosYzCU2gllKYG++7PITb+12VaVqJwWf8G9rFQ0xptZuXmHE
BTFCzxWxK8vSs85aBYWFd8eLmWrEZyEk1JfD7jU4OZm9BK3qoRvfwDwzPnmZIpCt
YPhYVi5F1W/w3Iw1mTqxkEMuy6mlMn14nKmA2seSAkPSJ+b5C92dqhwN1cvgUVhL
bIl3Yurj3ayvT+vRCYadQZJif+e/dxUrcRZ7oPpV23QxVgEZ+Yd+++3XA09LSdhQ
lLl/3/I+MNvCxHEKx4imCGLAmMOFL7u9Af/delFRVKDXferYb/HIxkiJGJco96J5
RvYsXGr2wTCQcCRZjv1+LlAlKUAgJMeVkszKQ56pCI7tvyB2gG8CAwEAAaNjMGEw
HQYDVR0OBBYEFN/4bLQKWNMwoLzQ2du9NT33x7+DMB8GA1UdIwQYMBaAFN/4bLQK
WNMwoLzQ2du9NT33x7+DMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGG
MA0GCSqGSIb3DQEBCwUAA4ICAQB2nU0y43nVDKgL1PPIdVDnYa2vjH+DBkSAVaTv
73OKdimh4Kzy0YYlrKzeNiE2k4Q/nUjTbAN13DvQbjRFQZx17L2Gckv+cMFyB7yb
vlsBeySarJKhYeKhlLrd20Qn7GiyHGkXUshnSVQm9/HFlegoMMjQyExGsA1PYU6W
mycNYv5yWTLgbaFNfIYjL6AcIVtxMMZoD4XgpVpETwNIoble+B3sYQ05dTYxMyT0
aHjafUPedasqXFoo5TJCJ7Wcq92dBwUXpgkHsf3PPKy8VVukWUaCP9ECAxHLmEPj
0tyElkvy55lauzVing7F/uRF6DIlRz6fH0y92qFJ5/t46L9C3V23+zIF80CJeZ21
/goal0NlAyjhI4zfpwwAUeqnAElncNhFcmTWHLyTGQyA4rYHDl5fZIhk6MFYdLwi
ml96m+T1z8iPqmrTtd6P3SVmEkRvSt8L7ItL82VcDELUCXJoSKEm5im84yEiPdUs
emQtJbioTM4+Vze32U6MSznelKiK3dkNPnNiKA6xsjxNC+Hp2LzcANg3/SUUC9ea
pDEMmP7TJMJ3dG63RtAzQiGfRO18BIVOrRUfQpR32FkrYd9wCE02cnv0QZzY9NYt
6hAlAa6Motve8UFewoO4pNknj3MBEN+64wDzHaP6VPysNJwrAlgaHfGDU6xJffAd
uCWDmw==
-----END CERTIFICATE-----"""

View File

@ -511,7 +511,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
timeout_client_data=50000,
timeout_member_connect=5000,
timeout_member_data=50000,
timeout_tcp_inspect=0):
timeout_tcp_inspect=0,
client_ca_cert=False):
proto = 'HTTP' if proto is None else proto
if be_proto is None:
be_proto = 'HTTP' if proto is 'TERMINATED_HTTPS' else proto
@ -526,7 +527,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
'sni_containers, load_balancer, peer_port, pools, '
'l7policies, enabled, insert_headers, timeout_client_data,'
'timeout_member_connect, timeout_member_data, '
'timeout_tcp_inspect',)
'timeout_tcp_inspect, client_ca_tls_certificate_id,'
'client_ca_tls_certificate')
if l7:
pools = [
sample_pool_tuple(
@ -604,7 +606,12 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
timeout_client_data=timeout_client_data,
timeout_member_connect=timeout_member_connect,
timeout_member_data=timeout_member_data,
timeout_tcp_inspect=timeout_tcp_inspect
timeout_tcp_inspect=timeout_tcp_inspect,
client_ca_tls_certificate_id='cont_id_ca' if client_ca_cert else '',
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 ''
)

View File

@ -142,8 +142,10 @@ class TestTLSParseUtils(base.TestCase):
exceptions.UnreadableCert,
cert_parser._get_x509_from_der_bytes, b'bad data')
def test_load_certificates(self):
listener = sample_configs.sample_listener_tuple(tls=True, sni=True)
@mock.patch('oslo_context.context.RequestContext')
def test_load_certificates(self, mock_oslo):
listener = sample_configs.sample_listener_tuple(tls=True, sni=True,
client_ca_cert=True)
client = mock.MagicMock()
context = mock.Mock()
context.project_id = '12345'
@ -162,6 +164,19 @@ class TestTLSParseUtils(base.TestCase):
]
client.assert_has_calls(calls_cert_mngr)
# Test asking for nothing
listener = sample_configs.sample_listener_tuple(tls=False, sni=False,
client_ca_cert=False)
client = mock.MagicMock()
with mock.patch.object(cert_parser,
'_map_cert_tls_container') as mock_map:
result = cert_parser.load_certificates_data(client, listener)
mock_map.assert_not_called()
ref_empty_dict = {'tls_cert': None, 'sni_certs': []}
self.assertEqual(ref_empty_dict, result)
mock_oslo.assert_called()
@mock.patch('octavia.certificates.common.cert.Cert')
def test_map_cert_tls_container(self, cert_mock):
tls = data_models.TLSContainer(

View File

@ -0,0 +1,5 @@
---
features:
- |
You can now specify a certificate authority certificate reference, on
listeners, for use with TLS client authentication.