Merge pull request #230 from thedac/saml
Keystone SAML Mellon functional testing
This commit is contained in:
committed by
Chris MacNaughton
parent
278107e391
commit
ebf9aa085e
@@ -1099,6 +1099,18 @@ disk_formats = ami,ari,aki,vhd,vmdk,raw,qcow2,vdi,iso,root-tar
|
||||
["juju", "set-series", "-m", self.model_name,
|
||||
_application, _to_series])
|
||||
|
||||
def test_attach_resource(self):
|
||||
self.patch_object(model, 'get_juju_model',
|
||||
return_value=self.model_name)
|
||||
self.patch_object(model, 'subprocess')
|
||||
_application = "application"
|
||||
_resource_name = "myresource"
|
||||
_resource_path = "/path/to/{}.tar.gz".format(_resource_name)
|
||||
model.attach_resource(_application, _resource_name, _resource_path)
|
||||
self.subprocess.check_call.assert_called_once_with(
|
||||
["juju", "attach-resource", "-m", self.model_name,
|
||||
_application, "{}={}".format(_resource_name, _resource_path)])
|
||||
|
||||
|
||||
class AsyncModelTests(aiounittest.AsyncTestCase):
|
||||
|
||||
|
||||
14
zaza/charm_tests/saml_mellon/__init__.py
Normal file
14
zaza/charm_tests/saml_mellon/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Copyright 2019 Canonical Ltd.
|
||||
#
|
||||
# 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.
|
||||
"""Collection of code for setting up and testing keystone-saml-mellon."""
|
||||
160
zaza/charm_tests/saml_mellon/setup.py
Normal file
160
zaza/charm_tests/saml_mellon/setup.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# Copyright 2019 Canonical Ltd.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Code for setting up keystone federation."""
|
||||
|
||||
import json
|
||||
import keystoneauth1
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import zaza.charm_lifecycle.utils as charm_lifecycle_utils
|
||||
import zaza.model
|
||||
from zaza.utilities import (
|
||||
cert as cert_utils,
|
||||
cli as cli_utils,
|
||||
openstack as openstack_utils,
|
||||
)
|
||||
|
||||
|
||||
APP_NAME = "keystone-saml-mellon"
|
||||
|
||||
FEDERATED_DOMAIN = "federated_domain"
|
||||
FEDERATED_GROUP = "federated_users"
|
||||
MEMBER = "Member"
|
||||
IDP = "samltest"
|
||||
REMOTE_ID = "https://samltest.id/saml/idp"
|
||||
MAP_NAME = "{}_mapping".format(IDP)
|
||||
PROTOCOL_NAME = "mapped"
|
||||
MAP_TEMPLATE = '''
|
||||
[{{
|
||||
"local": [
|
||||
{{
|
||||
"user": {{
|
||||
"name": "{{0}}"
|
||||
}},
|
||||
"group": {{
|
||||
"name": "federated_users",
|
||||
"domain": {{
|
||||
"id": "{domain_id}"
|
||||
}}
|
||||
}},
|
||||
"projects": [
|
||||
{{
|
||||
"name": "{{0}}_project",
|
||||
"roles": [
|
||||
{{
|
||||
"name": "Member"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
],
|
||||
"remote": [
|
||||
{{
|
||||
"type": "MELLON_NAME_ID"
|
||||
}}
|
||||
]
|
||||
}}]
|
||||
'''
|
||||
|
||||
SP_SIGNING_KEY_INFO_XML_TEMPLATE = '''
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
{}
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
'''
|
||||
|
||||
|
||||
def keystone_federation_setup():
|
||||
"""Configure Keystone Federation."""
|
||||
cli_utils.setup_logging()
|
||||
keystone_session = openstack_utils.get_overcloud_keystone_session()
|
||||
keystone_client = openstack_utils.get_keystone_session_client(
|
||||
keystone_session)
|
||||
|
||||
try:
|
||||
domain = keystone_client.domains.find(name=FEDERATED_DOMAIN)
|
||||
except keystoneauth1.exceptions.http.NotFound:
|
||||
domain = keystone_client.domains.create(
|
||||
FEDERATED_DOMAIN,
|
||||
description="Federated Domain",
|
||||
enabled=True)
|
||||
|
||||
try:
|
||||
group = keystone_client.groups.find(
|
||||
name=FEDERATED_GROUP, domain=domain)
|
||||
except keystoneauth1.exceptions.http.NotFound:
|
||||
group = keystone_client.groups.create(
|
||||
FEDERATED_GROUP,
|
||||
domain=domain,
|
||||
enabled=True)
|
||||
|
||||
role = keystone_client.roles.find(name=MEMBER)
|
||||
keystone_client.roles.grant(role, group=group, domain=domain)
|
||||
|
||||
try:
|
||||
idp = keystone_client.federation.identity_providers.find(
|
||||
name=IDP, domain_id=domain.id)
|
||||
except keystoneauth1.exceptions.http.NotFound:
|
||||
idp = keystone_client.federation.identity_providers.create(
|
||||
IDP,
|
||||
remote_ids=[REMOTE_ID],
|
||||
domain_id=domain.id,
|
||||
enabled=True)
|
||||
|
||||
JSON_RULES = json.loads(MAP_TEMPLATE.format(domain_id=domain.id))
|
||||
|
||||
try:
|
||||
keystone_client.federation.mappings.find(name=MAP_NAME)
|
||||
except keystoneauth1.exceptions.http.NotFound:
|
||||
keystone_client.federation.mappings.create(
|
||||
MAP_NAME, rules=JSON_RULES)
|
||||
|
||||
try:
|
||||
keystone_client.federation.protocols.get(IDP, PROTOCOL_NAME)
|
||||
except keystoneauth1.exceptions.http.NotFound:
|
||||
keystone_client.federation.protocols.create(
|
||||
PROTOCOL_NAME, mapping=MAP_NAME, identity_provider=idp)
|
||||
|
||||
|
||||
def attach_saml_resources(application="keystone-saml-mellon"):
|
||||
"""Attach resource to the Keystone SAML Mellon charm."""
|
||||
test_idp_metadata_xml = "samltest.xml"
|
||||
idp_metadata_xml_file = os.path.join(
|
||||
charm_lifecycle_utils.BUNDLE_DIR, test_idp_metadata_xml)
|
||||
|
||||
idp_metadata_name = "idp-metadata"
|
||||
sp_private_key_name = "sp-private-key"
|
||||
sp_signing_keyinfo_name = "sp-signing-keyinfo"
|
||||
|
||||
zaza.model.attach_resource(
|
||||
application, idp_metadata_name, idp_metadata_xml_file)
|
||||
|
||||
(key, cert) = cert_utils.generate_cert('SP Signing Key')
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.pem') as fp:
|
||||
fp.write(key.decode())
|
||||
fp.flush()
|
||||
zaza.model.attach_resource(application, sp_private_key_name, fp.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml') as fp:
|
||||
fp.write(SP_SIGNING_KEY_INFO_XML_TEMPLATE.format(key.decode()))
|
||||
fp.flush()
|
||||
zaza.model.attach_resource(
|
||||
application, sp_signing_keyinfo_name, fp.name)
|
||||
144
zaza/charm_tests/saml_mellon/tests.py
Normal file
144
zaza/charm_tests/saml_mellon/tests.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# Copyright 2018 Canonical Ltd.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Keystone SAML Mellon Testing."""
|
||||
|
||||
import logging
|
||||
from lxml import etree
|
||||
import requests
|
||||
|
||||
import zaza.model
|
||||
from zaza.charm_tests.keystone import BaseKeystoneTest
|
||||
|
||||
|
||||
class FailedToReachIDP(Exception):
|
||||
"""Custom Exception for failing to reach the IDP."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CharmKeystoneSAMLMellonTest(BaseKeystoneTest):
|
||||
"""Charm Keystone SAML Mellon tests."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Run class setup for running Keystone SAML Mellon charm tests."""
|
||||
super(CharmKeystoneSAMLMellonTest, cls).setUpClass()
|
||||
cls.action = "get-sp-metadata"
|
||||
|
||||
def test_run_get_sp_metadata_action(self):
|
||||
"""Validate the get-sp-metadata action."""
|
||||
if self.vip:
|
||||
ip = self.vip
|
||||
else:
|
||||
unit = zaza.model.get_units(self.application_name)[0]
|
||||
ip = unit.public_address
|
||||
|
||||
action = zaza.model.run_action(unit.entity_id, self.action)
|
||||
if "failed" in action.data["status"]:
|
||||
raise Exception(
|
||||
"The action failed: {}".format(action.data["message"]))
|
||||
|
||||
output = action.data["results"]["output"]
|
||||
root = etree.fromstring(output)
|
||||
for item in root.items():
|
||||
if "entityID" in item[0]:
|
||||
assert ip in item[1]
|
||||
|
||||
for appt in root.getchildren():
|
||||
for elem in appt.getchildren():
|
||||
for item in elem.items():
|
||||
if "Location" in item[0]:
|
||||
assert ip in item[1]
|
||||
|
||||
logging.info("Successul get-sp-metadata action")
|
||||
|
||||
def test_saml_mellon_redirects(self):
|
||||
"""Validate the horizon -> keystone -> IDP redirects."""
|
||||
if self.vip:
|
||||
keystone_ip = self.vip
|
||||
else:
|
||||
unit = zaza.model.get_units(self.application_name)[0]
|
||||
keystone_ip = unit.public_address
|
||||
|
||||
horizon = "openstack-dashboard"
|
||||
horizon_vip = (zaza.model.get_application_config(horizon)
|
||||
.get("vip").get("value"))
|
||||
if horizon_vip:
|
||||
horizon_ip = horizon_vip
|
||||
else:
|
||||
unit = zaza.model.get_units("openstack-dashboard")[0]
|
||||
horizon_ip = unit.public_address
|
||||
|
||||
if self.tls_rid:
|
||||
proto = "https"
|
||||
else:
|
||||
proto = "http"
|
||||
|
||||
url = "{}://{}/horizon/auth/login/".format(proto, horizon_ip)
|
||||
region = "{}://{}:5000/v3".format(proto, keystone_ip)
|
||||
horizon_expect = ('<option value="samltest_mapped">'
|
||||
'samltest.id</option>')
|
||||
|
||||
# This is the message samltest.id gives when it has not had
|
||||
# SP XML uploaded. It still shows we have been directed to:
|
||||
# horizon -> keystone -> samltest.id
|
||||
idp_expect = ("The application you have accessed is not registered "
|
||||
"for use with this service.")
|
||||
|
||||
def _do_redirect_check(url, region, idp_expect, horizon_expect):
|
||||
|
||||
# start session, get csrftoken
|
||||
client = requests.session()
|
||||
# Verify=False see note below
|
||||
login_page = client.get(url, verify=False)
|
||||
|
||||
# Validate SAML method is available
|
||||
assert horizon_expect in login_page.text
|
||||
|
||||
# Get cookie
|
||||
if "csrftoken" in client.cookies:
|
||||
csrftoken = client.cookies["csrftoken"]
|
||||
else:
|
||||
raise Exception("Missing csrftoken")
|
||||
|
||||
# Build and send post request
|
||||
form_data = {
|
||||
"auth_type": "samltest_mapped",
|
||||
"csrfmiddlewaretoken": csrftoken,
|
||||
"next": "/horizon/project/api_access",
|
||||
"region": region,
|
||||
}
|
||||
|
||||
# Verify=False due to CA certificate bundles.
|
||||
# If we point to the CA for keystone/horizon they work but
|
||||
# samltest.id does not.
|
||||
# If we don't set it validation fails for keystone/horizon
|
||||
# We would have to install the keystone CA onto the system
|
||||
# to validate end to end.
|
||||
response = client.post(
|
||||
url, data=form_data,
|
||||
headers={"Referer": url},
|
||||
allow_redirects=True,
|
||||
verify=False)
|
||||
|
||||
if idp_expect not in response.text:
|
||||
msg = "FAILURE code={} text={}".format(response, response.text)
|
||||
# Raise a custom exception.
|
||||
raise FailedToReachIDP(msg)
|
||||
|
||||
# Execute the check
|
||||
# We may need to try/except to allow horizon to build its pages
|
||||
_do_redirect_check(url, region, idp_expect, horizon_expect)
|
||||
logging.info("SUCCESS")
|
||||
@@ -1341,3 +1341,21 @@ def set_series(application, to_series):
|
||||
cmd = ["juju", "set-series", "-m", juju_model,
|
||||
application, to_series]
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def attach_resource(application, resource_name, resource_path):
|
||||
"""Attach resource to charm.
|
||||
|
||||
:param application: Application to get leader settings from.
|
||||
:type application: str
|
||||
:param resource_name: The name of the resource as defined in metadata.yaml
|
||||
:type resource_name: str
|
||||
:param resource_path: The path to the resource on disk
|
||||
:type resource_path: str
|
||||
:returns: None
|
||||
:rtype: None
|
||||
"""
|
||||
juju_model = get_juju_model()
|
||||
cmd = ["juju", "attach-resource", "-m", juju_model,
|
||||
application, "{}={}".format(resource_name, resource_path)]
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
Reference in New Issue
Block a user