From 6f3751cc96a910b07a122171cf43eee0b2852ecb Mon Sep 17 00:00:00 2001 From: Dmitrii Shcherbakov Date: Thu, 26 Apr 2018 00:36:02 +0300 Subject: [PATCH] add support for Federated IDentity (FID) and WebSSO * add support for relating with subordinate charms providing Service Provider functionality via apache2 authentication modules; * enable additional authentication methods on the keystone side to accept parsed assertion data provided via apache2 authentication module variables exported to WSGI environment; * move https frontend and WSGI API apache config files to keystone instead of relying on charm-helpers as modifications are needed there to add IncludeOptional directives. openstack_https_frontend.conf is added on purpose as ServerName cannot be correctly determined after ProxyPass which results in TLS errors during SAML exchange process; * add an additional relation to openstack-dashboard to provide URL information necessary to trust 'origin' parameter in WebSSO URLs used by horizon during the authentication process. Also add a context to render the federation section that is used to render this information in keystone.conf; Subordinates can choose to use different apache2 authentication modules. If those modules support vhost-level variables then multiple subordinates for the same module can be used. For example, mod_auth_mellon can be used multiple times in different vhosts to protect federated token endpoints related to different identity provider and protocol combinations). Trusted dashboard relation could be used to provide dashboard origin URL from a different site via cross-model relations. NOTE: this functionality will be triggered only on Ocata+ (inclusive) Change-Id: I1ef623b0b0e2a9f68cec4be550965c5e15e5f561 --- ...stone-fid-service-provider-relation-broken | 1 + ...tone-fid-service-provider-relation-changed | 1 + ...one-fid-service-provider-relation-departed | 1 + ...stone-fid-service-provider-relation-joined | 1 + hooks/keystone_context.py | 47 ++++ hooks/keystone_hooks.py | 95 ++++++++- hooks/keystone_utils.py | 13 +- .../websso-trusted-dashboard-relation-broken | 1 + .../websso-trusted-dashboard-relation-changed | 1 + ...websso-trusted-dashboard-relation-departed | 1 + .../websso-trusted-dashboard-relation-joined | 1 + metadata.yaml | 5 + templates/ocata/keystone.conf | 4 +- templates/openstack_https_frontend.conf | 30 +++ templates/parts/section-federation | 10 + templates/wsgi-openstack-api.conf | 94 ++++++++ unit_tests/test_keystone_contexts.py | 201 ++++++++++++++++++ unit_tests/test_keystone_hooks.py | 99 ++++++++- 18 files changed, 592 insertions(+), 14 deletions(-) create mode 120000 hooks/keystone-fid-service-provider-relation-broken create mode 120000 hooks/keystone-fid-service-provider-relation-changed create mode 120000 hooks/keystone-fid-service-provider-relation-departed create mode 120000 hooks/keystone-fid-service-provider-relation-joined create mode 120000 hooks/websso-trusted-dashboard-relation-broken create mode 120000 hooks/websso-trusted-dashboard-relation-changed create mode 120000 hooks/websso-trusted-dashboard-relation-departed create mode 120000 hooks/websso-trusted-dashboard-relation-joined create mode 100644 templates/openstack_https_frontend.conf create mode 100644 templates/parts/section-federation create mode 100644 templates/wsgi-openstack-api.conf diff --git a/hooks/keystone-fid-service-provider-relation-broken b/hooks/keystone-fid-service-provider-relation-broken new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/keystone-fid-service-provider-relation-broken @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone-fid-service-provider-relation-changed b/hooks/keystone-fid-service-provider-relation-changed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/keystone-fid-service-provider-relation-changed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone-fid-service-provider-relation-departed b/hooks/keystone-fid-service-provider-relation-departed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/keystone-fid-service-provider-relation-departed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone-fid-service-provider-relation-joined b/hooks/keystone-fid-service-provider-relation-joined new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/keystone-fid-service-provider-relation-joined @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone_context.py b/hooks/keystone_context.py index edf138f6..6ff8aa00 100644 --- a/hooks/keystone_context.py +++ b/hooks/keystone_context.py @@ -14,6 +14,7 @@ import hashlib import os +import json from base64 import b64decode @@ -39,6 +40,9 @@ from charmhelpers.core.hookenv import ( leader_get, DEBUG, INFO, + related_units, + relation_ids, + relation_get, ) from charmhelpers.core.strutils import ( @@ -405,3 +409,46 @@ class TokenFlushContext(context.OSContextGenerator): 'token_flush': is_elected_leader(DC_RESOURCE_NAME) } return ctxt + + +class KeystoneFIDServiceProviderContext(context.OSContextGenerator): + interfaces = ['keystone-fid-service-provider'] + + def __call__(self): + fid_sp_keys = ['protocol-name', 'remote-id-attribute'] + fid_sps = [] + for rid in relation_ids("keystone-fid-service-provider"): + for unit in related_units(rid): + rdata = relation_get(unit=unit, rid=rid) + if set(rdata).issuperset(set(fid_sp_keys)): + fid_sps.append({ + k: json.loads(v) for k, v in rdata.items() + if k in fid_sp_keys + }) + # populate the context with data from one or more + # service providers + ctxt = ({'fid_sps': fid_sps} + if fid_sps else {}) + return ctxt + + +class WebSSOTrustedDashboardContext(context.OSContextGenerator): + interfaces = ['websso-trusted-dashboard'] + + def __call__(self): + trusted_dashboard_keys = ['scheme', 'hostname', 'path'] + trusted_dashboards = set() + for rid in relation_ids("websso-trusted-dashboard"): + for unit in related_units(rid): + rdata = relation_get(unit=unit, rid=rid) + if set(rdata).issuperset(set(trusted_dashboard_keys)): + scheme = rdata.get('scheme') + hostname = rdata.get('hostname') + path = rdata.get('path') + url = '{}{}{}'.format(scheme, hostname, path) + trusted_dashboards.add(url) + # populate the context with data from one or more + # service providers + ctxt = ({'trusted_dashboards': trusted_dashboards} + if trusted_dashboards else {}) + return ctxt diff --git a/hooks/keystone_hooks.py b/hooks/keystone_hooks.py index ca56c1ec..d7407f60 100755 --- a/hooks/keystone_hooks.py +++ b/hooks/keystone_hooks.py @@ -40,6 +40,7 @@ from charmhelpers.core.hookenv import ( status_set, open_port, is_leader, + relation_id, ) from charmhelpers.core.host import ( @@ -121,7 +122,7 @@ from keystone_utils import ( ADMIN_DOMAIN, ADMIN_PROJECT, create_or_show_domain, - keystone_service, + restart_keystone, ) from charmhelpers.contrib.hahelpers.cluster import ( @@ -272,6 +273,7 @@ def config_changed_postupgrade(): update_all_identity_relation_units() update_all_domain_backends() + update_all_fid_backends() # Ensure sync request is sent out (needed for any/all ssl change) send_ssl_sync_request() @@ -381,6 +383,17 @@ def update_all_domain_backends(): domain_backend_changed(relation_id=rid, unit=unit) +def update_all_fid_backends(): + if CompareOpenStackReleases(os_release('keystone-common')) < 'ocata': + log('Ignoring keystone-fid-service-provider relation as it is' + ' not supported on releases older than Ocata') + return + """If there are any config changes, e.g. for domain or service port + make sure to update those for all relation-level buckets""" + for rid in relation_ids('keystone-fid-service-provider'): + update_keystone_fid_service_provider(relation_id=rid) + + def leader_init_db_if_ready(use_current_context=False): """ Initialise the keystone db if it is ready and mark it as initialised. @@ -784,11 +797,7 @@ def domain_backend_changed(relation_id=None, unit=None): domain_nonce_key = 'domain-restart-nonce-{}'.format(domain_name) db = unitdata.kv() if restart_nonce != db.get(domain_nonce_key): - if not is_unit_paused_set(): - if snap_install_requested(): - service_restart('snap.keystone.*') - else: - service_restart(keystone_service()) + restart_keystone() db.set(domain_nonce_key, restart_nonce) db.flush() @@ -869,6 +878,80 @@ def update_nrpe_config(): nrpe_setup.write() +@hooks.hook('keystone-fid-service-provider-relation-joined', + 'keystone-fid-service-provider-relation-changed') +def keystone_fid_service_provider_changed(): + if get_api_version() < 3: + log('Identity federation is only supported with keystone v3') + return + if CompareOpenStackReleases(os_release('keystone-common')) < 'ocata': + log('Ignoring keystone-fid-service-provider relation as it is' + ' not supported on releases older than Ocata') + return + # for the join case a keystone public-facing hostname and service + # port need to be set + update_keystone_fid_service_provider(relation_id=relation_id()) + + # handle relation data updates (if any), e.g. remote_id_attribute + # and a restart will be handled via a nonce, not restart_on_change + CONFIGS.write(KEYSTONE_CONF) + + # The relation is container-scoped so this keystone unit's unitdata + # will only contain a nonce of a single fid subordinate for a given + # fid backend (relation id) + restart_nonce = relation_get('restart-nonce') + if restart_nonce: + nonce = json.loads(restart_nonce) + # multiplex by relation id for multiple federated identity + # provider charms + fid_nonce_key = 'fid-restart-nonce-{}'.format(relation_id()) + db = unitdata.kv() + if restart_nonce != db.get(fid_nonce_key): + restart_keystone() + db.set(fid_nonce_key, nonce) + db.flush() + + +@hooks.hook('keystone-fid-service-provider-relation-broken') +def keystone_fid_service_provider_broken(): + if CompareOpenStackReleases(os_release('keystone-common')) < 'ocata': + log('Ignoring keystone-fid-service-provider relation as it is' + ' not supported on releases older than Ocata') + return + + restart_keystone() + + +@hooks.hook('websso-trusted-dashboard-relation-joined', + 'websso-trusted-dashboard-relation-changed', + 'websso-trusted-dashboard-relation-broken') +@restart_on_change(restart_map(), restart_functions=restart_function_map()) +def websso_trusted_dashboard_changed(): + if get_api_version() < 3: + log('WebSSO is only supported with keystone v3') + return + if CompareOpenStackReleases(os_release('keystone-common')) < 'ocata': + log('Ignoring WebSSO relation as it is not supported on' + ' releases older than Ocata') + return + CONFIGS.write(KEYSTONE_CONF) + + +def update_keystone_fid_service_provider(relation_id=None): + tls_enabled = (config('ssl_cert') is not None and + config('ssl_key') is not None) + # reactive endpoints implementation on the other side, hence + # json-encoded values + fid_settings = { + 'hostname': json.dumps(config('os-public-hostname')), + 'port': json.dumps(config('service-port')), + 'tls-enabled': json.dumps(tls_enabled), + } + + relation_set(relation_id=relation_id, + relation_settings=fid_settings) + + def main(): try: hooks.execute(sys.argv) diff --git a/hooks/keystone_utils.py b/hooks/keystone_utils.py index ec7e077e..0002e19a 100644 --- a/hooks/keystone_utils.py +++ b/hooks/keystone_utils.py @@ -72,6 +72,7 @@ from charmhelpers.contrib.openstack.utils import ( install_os_snaps, get_snaps_install_info_from_origin, enable_memcache, + is_unit_paused_set, ) from charmhelpers.core.strutils import ( @@ -245,7 +246,9 @@ BASE_RESOURCE_MAP = OrderedDict([ keystone_context.HAProxyContext(), context.BindHostContext(), context.WorkerConfigContext(), - context.MemcacheContext(package='keystone')], + context.MemcacheContext(package='keystone'), + keystone_context.KeystoneFIDServiceProviderContext(), + keystone_context.WebSSOTrustedDashboardContext()], }), (KEYSTONE_LOGGER_CONF, { 'contexts': [keystone_context.KeystoneLoggingContext()], @@ -2574,3 +2577,11 @@ def post_snap_install(): if os.path.exists(PASTE_SRC): log("Perfoming post snap install tasks", INFO) shutil.copy(PASTE_SRC, PASTE_DST) + + +def restart_keystone(): + if not is_unit_paused_set(): + if snap_install_requested(): + service_restart('snap.keystone.*') + else: + service_restart(keystone_service()) diff --git a/hooks/websso-trusted-dashboard-relation-broken b/hooks/websso-trusted-dashboard-relation-broken new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-broken @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/websso-trusted-dashboard-relation-changed b/hooks/websso-trusted-dashboard-relation-changed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-changed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/websso-trusted-dashboard-relation-departed b/hooks/websso-trusted-dashboard-relation-departed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-departed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/websso-trusted-dashboard-relation-joined b/hooks/websso-trusted-dashboard-relation-joined new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-joined @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml index 3dadf6af..2e80061d 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -39,6 +39,11 @@ requires: domain-backend: interface: keystone-domain-backend scope: container + keystone-fid-service-provider: + interface: keystone-fid-service-provider + scope: container + websso-trusted-dashboard: + interface: websso-trusted-dashboard peers: cluster: interface: keystone-ha diff --git a/templates/ocata/keystone.conf b/templates/ocata/keystone.conf index 4ce530da..6fc6bd98 100644 --- a/templates/ocata/keystone.conf +++ b/templates/ocata/keystone.conf @@ -67,7 +67,7 @@ driver = {{ assignment_backend }} [oauth1] [auth] -methods = external,password,token,oauth1 +methods = external,password,token,oauth1,mapped,openid password = keystone.auth.plugins.password.Password token = keystone.auth.plugins.token.Token oauth1 = keystone.auth.plugins.oauth1.OAuth @@ -115,3 +115,5 @@ group_allow_delete = False admin_project_domain_name = {{ admin_domain_name }} admin_project_name = admin {% endif -%} + +{% include "parts/section-federation" %} diff --git a/templates/openstack_https_frontend.conf b/templates/openstack_https_frontend.conf new file mode 100644 index 00000000..e0e42296 --- /dev/null +++ b/templates/openstack_https_frontend.conf @@ -0,0 +1,30 @@ +{% if endpoints -%} +{% for ext_port in ext_ports -%} +Listen {{ ext_port }} +{% endfor -%} +{% for address, endpoint, ext, int in endpoints -%} + + ServerName {{ endpoint }} + SSLEngine on + SSLProtocol +TLSv1 +TLSv1.1 +TLSv1.2 + SSLCipherSuite HIGH:!RC4:!MD5:!aNULL:!eNULL:!EXP:!LOW:!MEDIUM + SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }} + # See LP 1484489 - this is to support <= 2.4.7 and >= 2.4.8 + SSLCertificateChainFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }} + SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }} + ProxyPass / http://localhost:{{ int }}/ + ProxyPassReverse / http://localhost:{{ int }}/ + ProxyPreserveHost on + RequestHeader set X-Forwarded-Proto "https" + IncludeOptional /etc/apache2/mellon*/sp-location*.conf + +{% endfor -%} + + Order deny,allow + Allow from all + + + Order allow,deny + Allow from all + +{% endif -%} diff --git a/templates/parts/section-federation b/templates/parts/section-federation new file mode 100644 index 00000000..65ee99ed --- /dev/null +++ b/templates/parts/section-federation @@ -0,0 +1,10 @@ +{% if trusted_dashboards %} +[federation] +{% for dashboard_url in trusted_dashboards -%} +trusted_dashboard = {{ dashboard_url }} +{% endfor -%} +{% endif %} +{% for sp in fid_sps -%} +[{{ sp['protocol-name'] }}] +remote_id_attribute = {{ sp['remote-id-attribute'] }} +{% endfor -%} diff --git a/templates/wsgi-openstack-api.conf b/templates/wsgi-openstack-api.conf new file mode 100644 index 00000000..942e2b29 --- /dev/null +++ b/templates/wsgi-openstack-api.conf @@ -0,0 +1,94 @@ +# Configuration file maintained by Juju. Local changes may be overwritten. + +{% if port -%} +Listen {{ port }} +{% endif -%} + +{% if admin_port -%} +Listen {{ admin_port }} +{% endif -%} + +{% if public_port -%} +Listen {{ public_port }} +{% endif -%} + +{% if port -%} + + WSGIDaemonProcess {{ service_name }} processes={{ processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ + display-name=%{GROUP} + WSGIProcessGroup {{ service_name }} + WSGIScriptAlias / {{ script }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog /var/log/apache2/{{ service_name }}_error.log + CustomLog /var/log/apache2/{{ service_name }}_access.log combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + IncludeOptional /etc/apache2/mellon*/sp-location*.conf + +{% endif -%} + +{% if admin_port -%} + + WSGIDaemonProcess {{ service_name }}-admin processes={{ admin_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ + display-name=%{GROUP} + WSGIProcessGroup {{ service_name }}-admin + WSGIScriptAlias / {{ admin_script }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog /var/log/apache2/{{ service_name }}_error.log + CustomLog /var/log/apache2/{{ service_name }}_access.log combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + IncludeOptional /etc/apache2/mellon*/sp-location*.conf + +{% endif -%} + +{% if public_port -%} + + WSGIDaemonProcess {{ service_name }}-public processes={{ public_processes }} threads={{ threads }} user={{ service_name }} group={{ service_name }} \ + display-name=%{GROUP} + WSGIProcessGroup {{ service_name }}-public + WSGIScriptAlias / {{ public_script }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog /var/log/apache2/{{ service_name }}_error.log + CustomLog /var/log/apache2/{{ service_name }}_access.log combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + IncludeOptional /etc/apache2/mellon*/sp-location*.conf + +{% endif -%} diff --git a/unit_tests/test_keystone_contexts.py b/unit_tests/test_keystone_contexts.py index d8d256e0..42a7b7e5 100644 --- a/unit_tests/test_keystone_contexts.py +++ b/unit_tests/test_keystone_contexts.py @@ -217,3 +217,204 @@ class TestKeystoneContexts(CharmTestCase): mock_is_elected_leader.return_value = True self.assertEqual({'token_flush': True}, ctxt()) + + @patch.object(context, 'relation_ids') + @patch.object(context, 'related_units') + @patch.object(context, 'relation_get') + def test_keystone_fid_service_provider_rdata( + self, mock_relation_get, mock_related_units, + mock_relation_ids): + os.environ['JUJU_UNIT_NAME'] = 'keystone' + + def relation_ids_side_effect(rname): + return { + 'keystone-fid-service-provider': { + 'keystone-fid-service-provider:0', + 'keystone-fid-service-provider:1', + 'keystone-fid-service-provider:2' + } + }[rname] + + mock_relation_ids.side_effect = relation_ids_side_effect + + def related_units_side_effect(rid): + return { + 'keystone-fid-service-provider:0': ['sp-mellon/0'], + 'keystone-fid-service-provider:1': ['sp-shib/0'], + 'keystone-fid-service-provider:2': ['sp-oidc/0'], + }[rid] + mock_related_units.side_effect = related_units_side_effect + + def relation_get_side_effect(unit, rid): + # one unit only as the relation is container-scoped + return { + "keystone-fid-service-provider:0": { + "sp-mellon/0": { + "ingress-address": '10.0.0.10', + "protocol-name": '"saml2"', + "remote-id-attribute": '"MELLON_IDP"', + }, + }, + "keystone-fid-service-provider:1": { + "sp-shib/0": { + "ingress-address": '10.0.0.10', + "protocol-name": '"mapped"', + "remote-id-attribute": '"Shib-Identity-Provider"', + }, + }, + "keystone-fid-service-provider:2": { + "sp-oidc/0": { + "ingress-address": '10.0.0.10', + "protocol-name": '"oidc"', + "remote-id-attribute": '"HTTP_OIDC_ISS"', + }, + }, + }[rid][unit] + + mock_relation_get.side_effect = relation_get_side_effect + ctxt = context.KeystoneFIDServiceProviderContext() + + self.maxDiff = None + self.assertItemsEqual( + ctxt(), + { + "fid_sps": [ + { + "protocol-name": "saml2", + "remote-id-attribute": "MELLON_IDP", + }, + { + "protocol-name": "mapped", + "remote-id-attribute": "Shib-Identity-Provider", + }, + { + "protocol-name": "oidc", + "remote-id-attribute": "HTTP_OIDC_ISS", + }, + ] + } + ) + + @patch.object(context, 'relation_ids') + def test_keystone_fid_service_provider_empty( + self, mock_relation_ids): + os.environ['JUJU_UNIT_NAME'] = 'keystone' + + def relation_ids_side_effect(rname): + return { + 'keystone-fid-service-provider': {} + }[rname] + + mock_relation_ids.side_effect = relation_ids_side_effect + ctxt = context.KeystoneFIDServiceProviderContext() + + self.maxDiff = None + self.assertItemsEqual(ctxt(), {}) + + @patch.object(context, 'relation_ids') + @patch.object(context, 'related_units') + @patch.object(context, 'relation_get') + def test_websso_trusted_dashboard_urls_generated( + self, mock_relation_get, mock_related_units, + mock_relation_ids): + os.environ['JUJU_UNIT_NAME'] = 'keystone' + + def relation_ids_side_effect(rname): + return { + 'websso-trusted-dashboard': { + 'websso-trusted-dashboard:0', + 'websso-trusted-dashboard:1', + 'websso-trusted-dashboard:2' + } + }[rname] + + mock_relation_ids.side_effect = relation_ids_side_effect + + def related_units_side_effect(rid): + return { + 'websso-trusted-dashboard:0': ['dashboard-blue/0', + 'dashboard-blue/1'], + 'websso-trusted-dashboard:1': ['dashboard-red/0', + 'dashboard-red/1'], + 'websso-trusted-dashboard:2': ['dashboard-green/0', + 'dashboard-green/1'] + }[rid] + mock_related_units.side_effect = related_units_side_effect + + def relation_get_side_effect(unit, rid): + return { + "websso-trusted-dashboard:0": { + "dashboard-blue/0": { # dns-ha + "ingress-address": '10.0.0.10', + "scheme": "https://", + "hostname": "horizon.intranet.test", + "path": "/auth/websso/", + }, + "dashboard-blue/1": { # dns-ha + "ingress-address": '10.0.0.11', + "scheme": "https://", + "hostname": "horizon.intranet.test", + "path": "/auth/websso/", + }, + }, + "websso-trusted-dashboard:1": { + "dashboard-red/0": { # vip + "ingress-address": '10.0.0.12', + "scheme": "https://", + "hostname": "10.0.0.100", + "path": "/auth/websso/", + }, + "dashboard-red/1": { # vip + "ingress-address": '10.0.0.13', + "scheme": "https://", + "hostname": "10.0.0.100", + "path": "/auth/websso/", + }, + }, + "websso-trusted-dashboard:2": { + "dashboard-green/0": { # vip-less, dns-ha-less + "ingress-address": '10.0.0.14', + "scheme": "http://", + "hostname": "10.0.0.14", + "path": "/auth/websso/", + }, + "dashboard-green/1": { + "ingress-address": '10.0.0.15', + "scheme": "http://", + "hostname": "10.0.0.15", + "path": "/auth/websso/", + }, + }, + }[rid][unit] + + mock_relation_get.side_effect = relation_get_side_effect + ctxt = context.WebSSOTrustedDashboardContext() + + self.maxDiff = None + self.assertEqual( + ctxt(), + { + 'trusted_dashboards': set([ + 'https://horizon.intranet.test/auth/websso/', + 'https://10.0.0.100/auth/websso/', + 'http://10.0.0.14/auth/websso/', + 'http://10.0.0.15/auth/websso/', + ]) + } + ) + + @patch.object(context, 'relation_ids') + def test_websso_trusted_dashboard_empty( + self, mock_relation_ids): + os.environ['JUJU_UNIT_NAME'] = 'keystone' + + def relation_ids_side_effect(rname): + return { + 'websso-trusted-dashboard': {} + }[rname] + + mock_relation_ids.side_effect = relation_ids_side_effect + ctxt = context.WebSSOTrustedDashboardContext() + + self.maxDiff = None + self.assertItemsEqual(ctxt(), {}) diff --git a/unit_tests/test_keystone_hooks.py b/unit_tests/test_keystone_hooks.py index b3c8c8d8..a214b757 100644 --- a/unit_tests/test_keystone_hooks.py +++ b/unit_tests/test_keystone_hooks.py @@ -93,7 +93,6 @@ TO_PATCH = [ 'update_nrpe_config', 'ensure_ssl_dirs', 'is_db_ready', - 'keystone_service', 'create_or_show_domain', 'get_api_version', # other @@ -441,6 +440,7 @@ class KeystoneRelationTests(CharmTestCase): self.assertTrue(update.called) self.assertTrue(mock_update_domains.called) + @patch.object(hooks, 'os_release') @patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'initialise_pki') @patch.object(hooks, 'is_db_initialised') @@ -460,7 +460,9 @@ class KeystoneRelationTests(CharmTestCase): ensure_ssl_dir, mock_db_init, mock_initialise_pki, - mock_run_in_apache): + mock_run_in_apache, + os_release): + os_release.return_value = 'ocata' self.enable_memcache.return_value = False mock_run_in_apache.return_value = False ensure_ssl_cert.return_value = False @@ -1087,9 +1089,14 @@ class KeystoneRelationTests(CharmTestCase): @patch.object(hooks, 'is_unit_paused_set') @patch.object(hooks, 'is_db_initialised') + @patch.object(utils, 'run_in_apache') + @patch.object(utils, 'service_restart') def test_domain_backend_changed_complete(self, + service_restart, + run_in_apache, is_db_initialised, is_unit_paused_set): + run_in_apache.return_value = True self.get_api_version.return_value = 3 self.relation_get.side_effect = ['mydomain', 'nonce2'] self.is_leader.return_value = True @@ -1099,7 +1106,6 @@ class KeystoneRelationTests(CharmTestCase): mock_kv.get.return_value = None self.unitdata.kv.return_value = mock_kv is_unit_paused_set.return_value = False - self.keystone_service.return_value = 'apache2' hooks.domain_backend_changed() @@ -1113,16 +1119,21 @@ class KeystoneRelationTests(CharmTestCase): rid=None), ]) self.create_or_show_domain.assert_called_with('mydomain') - self.service_restart.assert_called_with('apache2') + service_restart.assert_called_with('apache2') mock_kv.set.assert_called_with('domain-restart-nonce-mydomain', 'nonce2') self.assertTrue(mock_kv.flush.called) @patch.object(hooks, 'is_unit_paused_set') @patch.object(hooks, 'is_db_initialised') + @patch.object(utils, 'run_in_apache') + @patch.object(utils, 'service_restart') def test_domain_backend_changed_complete_follower(self, + service_restart, + run_in_apache, is_db_initialised, is_unit_paused_set): + run_in_apache.return_value = True self.get_api_version.return_value = 3 self.relation_get.side_effect = ['mydomain', 'nonce2'] self.is_leader.return_value = False @@ -1132,7 +1143,6 @@ class KeystoneRelationTests(CharmTestCase): mock_kv.get.return_value = None self.unitdata.kv.return_value = mock_kv is_unit_paused_set.return_value = False - self.keystone_service.return_value = 'apache2' hooks.domain_backend_changed() @@ -1147,7 +1157,84 @@ class KeystoneRelationTests(CharmTestCase): ]) # Only lead unit will create the domain self.assertFalse(self.create_or_show_domain.called) - self.service_restart.assert_called_with('apache2') + service_restart.assert_called_with('apache2') mock_kv.set.assert_called_with('domain-restart-nonce-mydomain', 'nonce2') self.assertTrue(mock_kv.flush.called) + + @patch.object(hooks, 'os_release') + @patch.object(hooks, 'relation_id') + @patch.object(hooks, 'is_unit_paused_set') + @patch.object(hooks, 'is_db_initialised') + @patch.object(utils, 'run_in_apache') + @patch.object(utils, 'service_restart') + def test_fid_service_provider_changed_complete( + self, + service_restart, + run_in_apache, + is_db_initialised, + is_unit_paused_set, + relation_id, os_release): + os_release.return_value = 'ocata' + rel = 'keystone-fid-service-provider:0' + relation_id.return_value = rel + run_in_apache.return_value = True + self.get_api_version.return_value = 3 + self.relation_get.side_effect = ['"nonce2"'] + self.is_leader.return_value = True + self.is_db_ready.return_value = True + is_db_initialised.return_value = True + mock_kv = MagicMock() + mock_kv.get.return_value = None + self.unitdata.kv.return_value = mock_kv + is_unit_paused_set.return_value = False + + hooks.keystone_fid_service_provider_changed() + + self.assertTrue(self.get_api_version.called) + self.relation_get.assert_has_calls([ + call('restart-nonce'), + ]) + service_restart.assert_called_with('apache2') + mock_kv.set.assert_called_with( + 'fid-restart-nonce-{}'.format(rel), 'nonce2') + self.assertTrue(mock_kv.flush.called) + + @patch.object(hooks, 'os_release') + @patch.object(hooks, 'relation_id') + @patch.object(hooks, 'is_unit_paused_set') + @patch.object(hooks, 'is_db_initialised') + @patch.object(utils, 'run_in_apache') + @patch.object(utils, 'service_restart') + def test_fid_service_provider_changed_complete_follower( + self, + service_restart, + run_in_apache, + is_db_initialised, + is_unit_paused_set, + relation_id, os_release): + os_release.return_value = 'ocata' + rel = 'keystone-fid-service-provider:0' + relation_id.return_value = rel + run_in_apache.return_value = True + self.get_api_version.return_value = 3 + self.relation_get.side_effect = ['"nonce2"'] + self.is_leader.return_value = False + self.is_db_ready.return_value = True + is_db_initialised.return_value = True + mock_kv = MagicMock() + mock_kv.get.return_value = None + self.unitdata.kv.return_value = mock_kv + is_unit_paused_set.return_value = False + + hooks.keystone_fid_service_provider_changed() + + self.assertTrue(self.get_api_version.called) + self.relation_get.assert_has_calls([ + call('restart-nonce'), + ]) + service_restart.assert_called_with('apache2') + mock_kv.set.assert_called_with( + 'fid-restart-nonce-{}'.format(rel), + 'nonce2') + self.assertTrue(mock_kv.flush.called)