From 45be17c904489e101053fdd161c3dc466918ec61 Mon Sep 17 00:00:00 2001 From: Dmitrii Shcherbakov Date: Wed, 9 May 2018 00:28:14 +0300 Subject: [PATCH] add WebSSO support * add support for relating with subordinate charms providing Service Provider functionality via apache2 authentication modules; * retrieve protocol, identity provider and user-facing name info from keystone service provider charm subordinates; * provide trusted dashboard information to keystone charm Change-Id: I15ca0dd1616ec12c7ad47dc05961b51bb45bb770 --- hooks/horizon_contexts.py | 28 +++++++ hooks/horizon_hooks.py | 44 +++++++++++ hooks/horizon_utils.py | 3 +- ...ebsso-fid-service-provider-relation-broken | 1 + ...bsso-fid-service-provider-relation-changed | 1 + ...sso-fid-service-provider-relation-departed | 1 + ...ebsso-fid-service-provider-relation-joined | 1 + .../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 | 4 + templates/ocata/local_settings.py | 16 ++++ unit_tests/test_horizon_contexts.py | 73 +++++++++++++++++++ unit_tests/test_horizon_hooks.py | 63 +++++++++++++++- 15 files changed, 237 insertions(+), 2 deletions(-) create mode 120000 hooks/websso-fid-service-provider-relation-broken create mode 120000 hooks/websso-fid-service-provider-relation-changed create mode 120000 hooks/websso-fid-service-provider-relation-departed create mode 120000 hooks/websso-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 diff --git a/hooks/horizon_contexts.py b/hooks/horizon_contexts.py index bb99848c..6b74a835 100644 --- a/hooks/horizon_contexts.py +++ b/hooks/horizon_contexts.py @@ -44,6 +44,7 @@ from charmhelpers.core.host import pwgen from base64 import b64decode import os +import json VALID_ENDPOINT_TYPES = { 'PUBLICURL': 'publicURL', @@ -282,3 +283,30 @@ class LocalSettingsContext(OSContextGenerator): key=lambda r: r[1]['priority'])] } return ctxt + + +class WebSSOFIDServiceProviderContext(OSContextGenerator): + interfaces = ['websso-fid-service-provider'] + + def __call__(self): + websso_keys = ['protocol-name', 'idp-name', 'user-facing-name'] + + relations = [] + for rid in relation_ids("websso-fid-service-provider"): + try: + # the first unit will do - the assumption is that all + # of them should advertise the same data. This needs + # refactoring if juju gets per-application relation data + # support + unit = related_units(rid)[0] + except IndexError: + pass + else: + rdata = relation_get(unit=unit, rid=rid) + if set(rdata).issuperset(set(websso_keys)): + relations.append({k: json.loads(rdata[k]) + for k in websso_keys}) + # populate the context with data from one or more + # service providers + ctxt = {'websso_data': relations} if relations else {} + return ctxt diff --git a/hooks/horizon_hooks.py b/hooks/horizon_hooks.py index 913d925a..ba9978d7 100755 --- a/hooks/horizon_hooks.py +++ b/hooks/horizon_hooks.py @@ -31,6 +31,7 @@ from charmhelpers.core.hookenv import ( is_leader, local_unit, WARNING, + network_get, ) from charmhelpers.fetch import ( apt_update, apt_install, @@ -152,6 +153,8 @@ def config_changed(): open_port(80) open_port(443) + websso_trusted_dashboard_changed() + @hooks.hook('identity-service-relation-joined') def keystone_joined(rel_id=None): @@ -342,6 +345,47 @@ def db_changed(): log('Not running neutron database migration, not leader') +@hooks.hook('websso-fid-service-provider-relation-joined', + 'websso-fid-service-provider-relation-changed', + 'websso-fid-service-provider-relation-departed') +@restart_on_change(restart_map(), stopstart=True, sleep=3) +def websso_sp_changed(): + CONFIGS.write_all() + + +@hooks.hook('websso-trusted-dashboard-relation-joined', + 'websso-trusted-dashboard-relation-changed') +def websso_trusted_dashboard_changed(): + """ + Provide L7 endpoint details for the dashboard and also + handle any config changes that may affect those. + """ + relations = relation_ids('websso-trusted-dashboard') + if not relations: + return + + # TODO: check for vault relation in order to determine url scheme + tls_configured = config('ssl-key') or config('enforce-ssl') + scheme = 'https://' if tls_configured else 'http://' + + if config('dns-ha') or config('os-public-hostname'): + hostname = config('os-public-hostname') + elif config('vip'): + hostname = config('vip') + else: + # use an ingress-address of a given unit as a fallback + netinfo = network_get('websso-trusted-dashboard') + hostname = netinfo['ingress-addresses'][0] + + # provide trusted dashboard URL details + for rid in relations: + relation_set(relation_id=rid, relation_settings={ + "scheme": scheme, + "hostname": hostname, + "path": "/auth/websso/" + }) + + def main(): try: hooks.execute(sys.argv) diff --git a/hooks/horizon_utils.py b/hooks/horizon_utils.py index 0af36725..1c7c4722 100644 --- a/hooks/horizon_utils.py +++ b/hooks/horizon_utils.py @@ -93,7 +93,8 @@ CONFIG_FILES = OrderedDict([ horizon_contexts.IdentityServiceContext(), context.SyslogContext(), horizon_contexts.LocalSettingsContext(), - horizon_contexts.ApacheSSLContext()], + horizon_contexts.ApacheSSLContext(), + horizon_contexts.WebSSOFIDServiceProviderContext()], 'services': ['apache2', 'memcached'] }), (APACHE_CONF, { diff --git a/hooks/websso-fid-service-provider-relation-broken b/hooks/websso-fid-service-provider-relation-broken new file mode 120000 index 00000000..3195386e --- /dev/null +++ b/hooks/websso-fid-service-provider-relation-broken @@ -0,0 +1 @@ +horizon_hooks.py \ No newline at end of file diff --git a/hooks/websso-fid-service-provider-relation-changed b/hooks/websso-fid-service-provider-relation-changed new file mode 120000 index 00000000..3195386e --- /dev/null +++ b/hooks/websso-fid-service-provider-relation-changed @@ -0,0 +1 @@ +horizon_hooks.py \ No newline at end of file diff --git a/hooks/websso-fid-service-provider-relation-departed b/hooks/websso-fid-service-provider-relation-departed new file mode 120000 index 00000000..3195386e --- /dev/null +++ b/hooks/websso-fid-service-provider-relation-departed @@ -0,0 +1 @@ +horizon_hooks.py \ No newline at end of file diff --git a/hooks/websso-fid-service-provider-relation-joined b/hooks/websso-fid-service-provider-relation-joined new file mode 120000 index 00000000..3195386e --- /dev/null +++ b/hooks/websso-fid-service-provider-relation-joined @@ -0,0 +1 @@ +horizon_hooks.py \ No newline at end of file diff --git a/hooks/websso-trusted-dashboard-relation-broken b/hooks/websso-trusted-dashboard-relation-broken new file mode 120000 index 00000000..3195386e --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-broken @@ -0,0 +1 @@ +horizon_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..3195386e --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-changed @@ -0,0 +1 @@ +horizon_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..3195386e --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-departed @@ -0,0 +1 @@ +horizon_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..3195386e --- /dev/null +++ b/hooks/websso-trusted-dashboard-relation-joined @@ -0,0 +1 @@ +horizon_hooks.py \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml index 0502d80d..242aeb97 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -23,6 +23,8 @@ provides: dashboard-plugin: interface: dashboard-plugin scope: container + websso-trusted-dashboard: + interface: websso-trusted-dashboard requires: identity-service: interface: keystone @@ -31,6 +33,8 @@ requires: scope: container shared-db: interface: mysql-shared + websso-fid-service-provider: + interface: websso-fid-service-provider peers: cluster: interface: openstack-dashboard-ha diff --git a/templates/ocata/local_settings.py b/templates/ocata/local_settings.py index fd6217ba..3d43c562 100644 --- a/templates/ocata/local_settings.py +++ b/templates/ocata/local_settings.py @@ -991,3 +991,19 @@ ALLOWED_PRIVATE_SUBNET_CIDR = {'ipv4': [], 'ipv6': []} # 'phone_num': _('Phone Number'), #} {{ settings|join('\n\n') }} + +{% if websso_data %} +WEBSSO_ENABLED = True +WEBSSO_CHOICES = ( +{% for provider_data in websso_data -%} + ("{{ '{}_{}'.format(provider_data['idp-name'], provider_data['protocol-name']) }}", "{{ provider_data['user-facing-name'] }}"), +{% endfor -%} + ("credentials", _("Keystone Credentials")) +) + +WEBSSO_IDP_MAPPING = { +{% for provider_data in websso_data -%} + "{{ '{}_{}'.format(provider_data['idp-name'], provider_data['protocol-name']) }}": ("{{ provider_data['idp-name'] }}", "{{ provider_data['protocol-name'] }}"), +{% endfor -%} +} +{% endif %} diff --git a/unit_tests/test_horizon_contexts.py b/unit_tests/test_horizon_contexts.py index 90c2d071..8f5cea72 100644 --- a/unit_tests/test_horizon_contexts.py +++ b/unit_tests/test_horizon_contexts.py @@ -650,3 +650,76 @@ class TestHorizonContexts(CharmTestCase): 'BAR = False', '# horizon-plugin/0\n' 'FOO = True']}) + + def test_WebSSOFIDServiceProviderContext(self): + def relation_ids_side_effect(rname): + return { + 'websso-fid-service-provider': [ + 'websso-fid-service-provider:0', + 'websso-fid-service-provider:1', + ] + }[rname] + self.relation_ids.side_effect = relation_ids_side_effect + + def related_units_side_effect(rid): + return { + 'websso-fid-service-provider:0': [ + 'keystone-saml-mellon-red/0', + 'keystone-saml-mellon-red/1', + ], + 'websso-fid-service-provider:1': [ + 'keystone-saml-mellon-green/0', + 'keystone-saml-mellon-green/1', + ], + }[rid] + self.related_units.side_effect = related_units_side_effect + + def relation_get_side_effect(unit, rid): + return { + 'websso-fid-service-provider:0': { + 'keystone-saml-mellon-red/0': { + 'ingress-address': '10.0.0.10', + 'protocol-name': '"saml2"', + 'idp-name': '"red"', + 'user-facing-name': '"Red IDP"', + }, + 'keystone-saml-mellon-red/1': { + 'ingress-address': '10.0.0.11', + 'protocol-name': '"saml2"', + 'idp-name': '"red"', + 'user-facing-name': '"Red IDP"', + }, + }, + 'websso-fid-service-provider:1': { + 'keystone-saml-mellon-green/0': { + 'ingress-address': '10.0.0.12', + 'protocol-name': '"mapped"', + 'idp-name': '"green"', + 'user-facing-name': '"Green IDP"', + }, + 'keystone-saml-mellon-green/1': { + 'ingress-address': '10.0.0.13', + 'protocol-name': '"mapped"', + 'idp-name': '"green"', + 'user-facing-name': '"Green IDP"', + }, + }, + }[rid][unit] + self.relation_get.side_effect = relation_get_side_effect + + self.assertEqual( + horizon_contexts.WebSSOFIDServiceProviderContext()(), + { + 'websso_data': [ + { + 'protocol-name': 'saml2', + 'idp-name': 'red', + 'user-facing-name': "Red IDP", + }, + { + 'protocol-name': 'mapped', + 'idp-name': 'green', + 'user-facing-name': "Green IDP", + }, + ] + }) diff --git a/unit_tests/test_horizon_hooks.py b/unit_tests/test_horizon_hooks.py index 60f5116b..4d0d3f0e 100644 --- a/unit_tests/test_horizon_hooks.py +++ b/unit_tests/test_horizon_hooks.py @@ -260,7 +260,30 @@ class TestHorizonHooks(CharmTestCase): @patch('horizon_hooks.keystone_joined') def test_config_changed_no_upgrade(self, _joined): - self.relation_ids.return_value = ['identity/0'] + def relation_ids_side_effect(rname): + return { + 'websso-trusted-dashboard': [ + 'websso-trusted-dashboard:0', + 'websso-trusted-dashboard:1', + ], + 'identity-service': [ + 'identity/0', + ], + }[rname] + self.relation_ids.side_effect = relation_ids_side_effect + + def config_side_effect(key): + return { + 'ssl-key': 'somekey', + 'enforce-ssl': True, + 'dns-ha': True, + 'os-public-hostname': 'dashboard.intranet.test', + 'prefer-ipv6': False, + 'action-managed-upgrade': False, + 'webroot': '/horizon', + }[key] + self.config.side_effect = config_side_effect + self.openstack_upgrade_available.return_value = False self._call_hook('config-changed') _joined.assert_called_with('identity/0') @@ -331,3 +354,41 @@ class TestHorizonHooks(CharmTestCase): openstack_dir='/usr/share/openstack-dashboard', relation_id=None ) + + def test_websso_fid_service_provider_changed(self): + self._call_hook('websso-fid-service-provider-relation-changed') + self.CONFIGS.write_all.assert_called_with() + + def test_websso_trusted_dashboard_changed(self): + def relation_ids_side_effect(rname): + return { + 'websso-trusted-dashboard': [ + 'websso-trusted-dashboard:0', + 'websso-trusted-dashboard:1', + ] + }[rname] + self.relation_ids.side_effect = relation_ids_side_effect + + def config_side_effect(key): + return { + 'ssl-key': 'somekey', + 'enforce-ssl': True, + 'dns-ha': True, + 'os-public-hostname': 'dashboard.intranet.test', + }[key] + self.config.side_effect = config_side_effect + self._call_hook('websso-trusted-dashboard-relation-changed') + self.relation_set.assert_has_calls([ + call(relation_id='websso-trusted-dashboard:0', + relation_settings={ + "scheme": "https://", + "hostname": "dashboard.intranet.test", + "path": "/auth/websso/", + }), + call(relation_id='websso-trusted-dashboard:1', + relation_settings={ + "scheme": "https://", + "hostname": "dashboard.intranet.test", + "path": "/auth/websso/", + }), + ])