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
This commit is contained in:
Dmitrii Shcherbakov 2018-05-09 00:28:14 +03:00
parent 0707590487
commit 45be17c904
15 changed files with 237 additions and 2 deletions

View File

@ -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

View File

@ -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)

View File

@ -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, {

View File

@ -0,0 +1 @@
horizon_hooks.py

View File

@ -0,0 +1 @@
horizon_hooks.py

View File

@ -0,0 +1 @@
horizon_hooks.py

View File

@ -0,0 +1 @@
horizon_hooks.py

View File

@ -0,0 +1 @@
horizon_hooks.py

View File

@ -0,0 +1 @@
horizon_hooks.py

View File

@ -0,0 +1 @@
horizon_hooks.py

View File

@ -0,0 +1 @@
horizon_hooks.py

View File

@ -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

View File

@ -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 %}

View File

@ -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",
},
]
})

View File

@ -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/",
}),
])