add support for ldaps and starttls via config opts

* Both ldaps and ldap + StartTLS require a CA certificate configuration
option;
* use_tls option should only be used for StartTLS and will result in an
error if used with a URL that starts with ldaps;
* if a certificate is specified then LDAP backend server's certificate
validation is considered mandatory ("demand" option).

Depends-On: Ied4b6ed64354e3de3c78e6ac809666ee9ae29d1a
Change-Id: I659683ffec91560ebbd77969840c27e3d7048689
Closes-Bug: #1728155
This commit is contained in:
Dmitrii Shcherbakov 2018-01-07 01:54:21 +03:00
parent b5fe0ef6c9
commit 5d644391ad
4 changed files with 157 additions and 4 deletions

View File

@ -7,9 +7,15 @@ options:
type: string
default:
description: |
LDAP server URL for keystone identity backend.
.
Example: ldap://10.10.10.10/
LDAP server URL for keystone LDAP identity backend.
Examples:
ldap://10.10.10.10/
ldaps://10.10.10.10/
ldap://example.com:389,ldaps://ldaps.example.com:636
Usage of ldap:// urls with tls_ca_ldap option specified or certificates relation
presence will result in mandatory StartTLS usage.
ldap-user:
type: string
default:
@ -42,3 +48,13 @@ options:
type: boolean
default: True
description: LDAP identity server backend readonly to keystone.
tls-ca-ldap:
type: string
default: null
description: |
This option controls which certificate (or a chain) will be used to connect
to an ldap server(s) over TLS. Certificate contents should be either used
directly or included via include-file://
An LDAP url should also be considered as ldaps and StartTLS are both valid
methods of using TLS (see RFC 4513) with StartTLS using a non-ldaps url which,
of course, still requires a CA certificate.

View File

@ -35,6 +35,7 @@ from charms_openstack.charm.core import (
OPENSTACK_RELEASE_KEY = 'charmers.openstack-release-version'
DOMAIN_CONF = "/etc/keystone/domains/keystone.{}.conf"
BACKEND_CA_CERT = "/usr/share/ca-certificates/{}.crt"
KEYSTONE_CONF_TEMPLATE = "keystone.conf"
@ -65,6 +66,15 @@ class KeystoneLDAPConfigurationAdapter(
hookenv.config('ldap-config-flags')
)
@property
def backend_ca_file(self):
return BACKEND_CA_CERT.format(hookenv.service_name())
@property
def use_tls(self):
ldap_srv = hookenv.config('ldap-server')
return not ldap_srv.startswith('ldaps') if ldap_srv else False
class KeystoneLDAPCharm(charms_openstack.charm.OpenStackCharm):
@ -104,6 +114,7 @@ class KeystoneLDAPCharm(charms_openstack.charm.OpenStackCharm):
'ldap_password': hookenv.config('ldap-password'),
'ldap_suffix': hookenv.config('ldap-suffix'),
}
return all(required_config.values())
@property
@ -131,7 +142,22 @@ class KeystoneLDAPCharm(charms_openstack.charm.OpenStackCharm):
'templates/', self.release),
target=self.configuration_file,
context=self.adapters_instance)
if checksum != ch_host.file_hash(self.configuration_file):
tmpl_changed = (checksum !=
ch_host.file_hash(self.configuration_file))
cert = hookenv.config('tls-ca-ldap')
cert_changed = False
if cert:
ca_file = self.options.backend_ca_file
old_cert_csum = ch_host.file_hash(ca_file)
ch_host.write_file(ca_file, cert,
owner='root', group='root', perms=0o644)
cert_csum = ch_host.file_hash(ca_file)
cert_changed = (old_cert_csum != cert_csum)
if tmpl_changed or cert_changed:
restart_trigger()
def remove_config(self):
@ -141,3 +167,7 @@ class KeystoneLDAPCharm(charms_openstack.charm.OpenStackCharm):
"""
if os.path.exists(self.configuration_file):
os.unlink(self.configuration_file)
if (hookenv.config('tls-ca-ldap') and
os.path.exists(self.options.backend_ca_file)):
os.unlink(self.options.backend_ca_file)

View File

@ -12,6 +12,12 @@ group_allow_create = {{ not options.ldap_readonly }}
group_allow_update = {{ not options.ldap_readonly }}
group_allow_delete = {{ not options.ldap_readonly }}
{% if options.tls_ca_ldap -%}
use_tls = {{ options.use_tls }}
tls_req_cert = demand
tls_cacertfile = {{ options.backend_ca_file }}
{% endif -%}
# User supplied configuration flags
{% if options.ldap_options -%}
{% for key, value in options.ldap_options.items() -%}

View File

@ -13,6 +13,7 @@ from __future__ import absolute_import
from __future__ import print_function
import mock
import textwrap
import charms_openstack.test_utils as test_utils
@ -155,6 +156,106 @@ class TestKeystoneLDAPCharm(Helper):
kldap_charm.render_config(mock_trigger)
self.assertTrue(mock_trigger.called)
@mock.patch('charmhelpers.core.hookenv.config')
@mock.patch('charmhelpers.core.hookenv.service_name')
def test_render_config_tls(self, service_name, config):
self.patch_object(keystone_ldap.ch_host, 'file_hash')
self.patch_object(keystone_ldap.ch_host, 'write_file')
self.patch_object(keystone_ldap.core.templating, 'render')
self.patch_object(keystone_ldap.core.templating, 'render')
reply = {
'ldap-server': 'myserver',
'ldap-user': 'myusername',
'ldap-password': 'mypassword',
'ldap-suffix': 'suffix',
'domain-name': 'userdomain',
'tls-ca-ldap': textwrap.dedent("""
-----BEGIN CERTIFICATE-----
MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB
qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf
Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw
MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV
BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw
NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j
LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG
A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs
W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta
3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk
6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6
Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J
NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA
MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP
r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU
DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz
YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX
xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2
/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/
LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7
jVaMaA==
-----END CERTIFICATE-----
""")
}
def mock_config(key=None):
if key:
return reply.get(key)
return reply
config.side_effect = mock_config
svc_name = 'keystone_ldap'
service_name.return_value = svc_name
self.file_hash.side_effect = [
'templatehash',
'templatehash',
'de3d5930e6e6b3fdb385f60a05206588',
'de3d5930e6e6b3fdb385f60a05206588',
]
mock_trigger = mock.MagicMock()
with provide_charm_instance() as kldap_charm:
# Ensure a basic level of function from render_config
kldap_charm.render_config(mock_trigger)
self.render.assert_called_with(
source=keystone_ldap.KEYSTONE_CONF_TEMPLATE,
template_loader=mock.ANY,
target='/etc/keystone/domains/keystone.userdomain.conf',
context=mock.ANY
)
self.write_file.assert_called_with(
keystone_ldap.BACKEND_CA_CERT.format(svc_name),
reply['tls-ca-ldap'],
owner='root',
group='root',
perms=0o644,
)
self.assertFalse(mock_trigger.called)
# template file change leads to restart without a change
# in a cert
self.file_hash.side_effect = [
'oldtemplatehash',
'newtemplatehash',
'de3d5930e6e6b3fdb385f60a05206588',
'de3d5930e6e6b3fdb385f60a05206588',
]
kldap_charm.render_config(mock_trigger)
self.assertTrue(mock_trigger.called)
# cert change without template change
self.file_hash.side_effect = [
'templatehash',
'templatehash',
'deadbeefdeadbeefdeadbeefdeadbeef',
'de3d5930e6e6b3fdb385f60a05206588',
]
kldap_charm.render_config(mock_trigger)
self.assertTrue(mock_trigger.called)
@mock.patch('charmhelpers.core.hookenv.config')
@mock.patch('os.path.exists')
@mock.patch('os.unlink')