diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 6553dae96e..f6d4e0d068 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -1340,6 +1340,11 @@ # administrative billing, and other (string value) #idp_contact_type=other +# Path to the Identity Provider Metadata file. This file +# should be generated with the keystone-manage +# saml_idp_metadata command. (string value) +#idp_metadata_path=/etc/keystone/saml2_idp_metadata.xml + [signing] diff --git a/keystone/common/config.py b/keystone/common/config.py index 2ae507b8ab..51f56ab5ff 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -858,6 +858,11 @@ FILE_OPTIONS = { help='Contact type. Allowed values are: ' 'technical, support, administrative ' 'billing, and other'), + cfg.StrOpt('idp_metadata_path', + default='/etc/keystone/saml2_idp_metadata.xml', + help='Path to the Identity Provider Metadata file. ' + 'This file should be generated with the ' + 'keystone-manage saml_idp_metadata command.'), ], } diff --git a/keystone/contrib/federation/controllers.py b/keystone/contrib/federation/controllers.py index fdb4097e82..a043e8073a 100644 --- a/keystone/contrib/federation/controllers.py +++ b/keystone/contrib/federation/controllers.py @@ -22,6 +22,7 @@ from keystone import config from keystone.contrib.federation import idp as keystone_idp from keystone.contrib.federation import schema from keystone.contrib.federation import utils +from keystone import exception from keystone.models import token_model @@ -332,3 +333,18 @@ class ProjectV3(controller.V3Controller): projects = self.assignment_api.list_projects_for_groups( auth_context['group_ids']) return ProjectV3.wrap_collection(context, projects) + + +class SAMLMetadataV3(_ControllerBase): + member_name = 'metadata' + + def get_metadata(self, context): + metadata_path = CONF.federation.idp_metadata_path + try: + with open(metadata_path, 'r') as metadata_handler: + metadata = metadata_handler.read() + except IOError as e: + # Raise HTTP 500 in case Metadata file cannot be read. + raise exception.MetadataFileError(reason=e) + return wsgi.render_response(body=metadata, status=('200', 'OK'), + headers=[('Content-Type', 'text/xml')]) diff --git a/keystone/contrib/federation/routers.py b/keystone/contrib/federation/routers.py index 1760b9259d..5a5b8e1b6f 100644 --- a/keystone/contrib/federation/routers.py +++ b/keystone/contrib/federation/routers.py @@ -67,8 +67,12 @@ class FederationExtension(wsgi.V3ExtensionRouter): POST /OS-FEDERATION/identity_providers/$identity_provider/ protocols/$protocol/auth + POST /auth/OS-FEDERATION/saml2 + GET /OS-FEDERATION/saml2/metadata + + """ def _construct_url(self, suffix): return "/OS-FEDERATION/%s" % suffix @@ -83,6 +87,7 @@ class FederationExtension(wsgi.V3ExtensionRouter): mapping_controller = controllers.MappingController() project_controller = controllers.ProjectV3() domain_controller = controllers.DomainV3() + saml_metadata_controller = controllers.SAMLMetadataV3() # Identity Provider CRUD operations @@ -176,3 +181,10 @@ class FederationExtension(wsgi.V3ExtensionRouter): path='/auth' + self._construct_url('saml2'), post_action='create_saml_assertion', rel=build_resource_relation(resource_name='saml2')) + + # Keystone-Identity-Provider metadata endpoint + self._add_resource( + mapper, saml_metadata_controller, + path=self._construct_url('saml2/metadata'), + get_action='get_metadata', + rel=build_resource_relation(resource_name='metadata')) diff --git a/keystone/exception.py b/keystone/exception.py index 6910cce407..6cfd12b0fc 100644 --- a/keystone/exception.py +++ b/keystone/exception.py @@ -360,6 +360,10 @@ class MappedGroupNotFound(UnexpectedError): "%(mapping_id)s was not found in the backend.") +class MetadataFileError(UnexpectedError): + message_format = _("Error while reading metadata file, %(reason)s") + + class NotImplemented(Error): message_format = _("The action you have requested has not" " been implemented.") diff --git a/keystone/tests/saml2/idp_saml2_metadata.xml b/keystone/tests/saml2/idp_saml2_metadata.xml new file mode 100644 index 0000000000..db235f7c43 --- /dev/null +++ b/keystone/tests/saml2/idp_saml2_metadata.xml @@ -0,0 +1,25 @@ + + + + + + + MIIDpTCCAo0CAREwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0xMzA3MDkxNjI1MDBaGA8yMDcyMDEwMTE2MjUwMFowgY8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3JnMREwDwYDVQQDEwhLZXlzdG9uZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMTC6IdNd9Cg1DshcrT5gRVRF36nEmjSA9QWdik7B925PK70U4F6j4pz/5JL7plIo/8rJ4jJz9ccE7m0iA+IuABtEhEwXkG9rj47Oy0J4ZyDGSh2K1Bl78PA9zxXSzysUTSjBKdAh29dPYbJY7cgZJ0uC3AtfVceYiAOIi14SdFeZ0LZLDXBuLaqUmSMrmKwJ9wAMOCb/jbBP9/3Ycd0GYjlvrSBU4Bqb8/NHasyO4DpPN68OAoyD5r5jUtV8QZN03UjIsoux8e0lrL6+MVtJo0OfWvlSrlzS5HKSryY+uqqQEuxtZKpJM2MV85ujvjc8eDSChh2shhDjBem3FIlHKUCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAed9fHgdJrk+gZcO5gsqq6uURfDOuYD66GsSdZw4BqHjYAcnyWq2da+iw7Uxkqu7iLf2k4+Hu3xjDFrce479OwZkSnbXmqB7XspTGOuM8MgT7jB/ypKTOZ6qaZKSWK1Hta995hMrVVlhUNBLh0MPGqoVWYA4d7mblujgH9vp+4mpCciJagHks8K5FBmI+pobB+uFdSYDoRzX9LTpStspK4e3IoY8baILuGcdKimRNBv6ItG4hMrntAe1/nWMJyUu5rDTGf2V/vAaS0S/faJBwQSz1o38QHMTWHNspfwIdX3yMqI9u7/vYlz3rLy5WdBdUgZrZ3/VLmJTiJVZu5Owq4Q== + + + + + + + openstack + openstack + openstack + + + openstack + first + lastname + admin@example.com + 555-555-5555 + + diff --git a/keystone/tests/test_v3_federation.py b/keystone/tests/test_v3_federation.py index caa1b70ed4..ba1dff6d6d 100644 --- a/keystone/tests/test_v3_federation.py +++ b/keystone/tests/test_v3_federation.py @@ -1652,6 +1652,11 @@ def _is_xmlsec1_installed(): return not bool(p.wait()) +def _load_xml(filename): + with open(os.path.join(XMLDIR, filename), 'r') as xml: + return xml.read() + + class SAMLGenerationTests(FederationTests): ISSUER = 'https://acme.com/FIM/sps/openstack/saml20' @@ -1664,11 +1669,7 @@ class SAMLGenerationTests(FederationTests): def setUp(self): super(SAMLGenerationTests, self).setUp() self.signed_assertion = saml2.create_class_from_xml_string( - saml.Assertion, self._load_xml('signed_saml2_assertion.xml')) - - def _load_xml(self, filename): - with open(os.path.join(XMLDIR, filename), 'r') as xml: - return xml.read() + saml.Assertion, _load_xml('signed_saml2_assertion.xml')) def test_samlize_token_values(self): """Test the SAML generator produces a SAML object. @@ -1901,6 +1902,8 @@ class SAMLGenerationTests(FederationTests): class IdPMetadataGenerationTests(FederationTests): """A class for testing Identity Provider Metadata generation.""" + METADATA_URL = '/OS-FEDERATION/saml2/metadata' + def setUp(self): super(IdPMetadataGenerationTests, self).setUp() self.generator = keystone_idp.MetadataGenerator() @@ -2011,3 +2014,15 @@ class IdPMetadataGenerationTests(FederationTests): idp_entity_id=None) self.assertRaises(exception.ValidationError, self.generator.generate_metadata) + + def test_get_metadata_with_no_metadata_file_configured(self): + self.get(self.METADATA_URL, expected_status=500) + + def test_get_metadata(self): + CONF.federation.idp_metadata_path = XMLDIR + '/idp_saml2_metadata.xml' + r = self.get(self.METADATA_URL, response_content_type='text/xml', + expected_status=200) + self.assertEqual('text/xml', r.headers.get('Content-Type')) + + reference_file = _load_xml('idp_saml2_metadata.xml') + self.assertEqual(reference_file, r.result)