Add API to create ecp wrapped saml assertion
Create a new API that gives users the option to wrap a token based SAML assertion in an ECP envelope. Co-Authored-By: Rodrigo Duarte Sousa <rodrigods@lsd.ufcg.edu.br> Change-Id: Ieffeb6cf34b225f0704321fa64fe6dfc227add8e Closes-Bug: 1426128 bp: ecp-wrapped-saml-assertions
This commit is contained in:
parent
659529a4ad
commit
84c89ae649
|
@ -311,21 +311,12 @@ class Auth(auth_controllers.Auth):
|
|||
return webob.Response(body=body, status='200',
|
||||
headerlist=headers)
|
||||
|
||||
@validation.validated(schema.saml_create, 'auth')
|
||||
def create_saml_assertion(self, context, auth):
|
||||
"""Exchange a scoped token for a SAML assertion.
|
||||
|
||||
:param auth: Dictionary that contains a token and service provider id
|
||||
:returns: SAML Assertion based on properties from the token
|
||||
"""
|
||||
|
||||
def _create_base_saml_assertion(self, context, auth):
|
||||
issuer = CONF.saml.idp_entity_id
|
||||
sp_id = auth['scope']['service_provider']['id']
|
||||
service_provider = self.federation_api.get_sp(sp_id)
|
||||
utils.assert_enabled_service_provider_object(service_provider)
|
||||
|
||||
sp_url = service_provider.get('sp_url')
|
||||
auth_url = service_provider.get('auth_url')
|
||||
|
||||
token_id = auth['identity']['token']['id']
|
||||
token_data = self.token_provider_api.validate_token(token_id)
|
||||
|
@ -342,6 +333,20 @@ class Auth(auth_controllers.Auth):
|
|||
generator = keystone_idp.SAMLGenerator()
|
||||
response = generator.samlize_token(issuer, sp_url, subject, roles,
|
||||
project)
|
||||
return (response, service_provider)
|
||||
|
||||
@validation.validated(schema.saml_create, 'auth')
|
||||
def create_saml_assertion(self, context, auth):
|
||||
"""Exchange a scoped token for a SAML assertion.
|
||||
|
||||
:param auth: Dictionary that contains a token and service provider ID
|
||||
:returns: SAML Assertion based on properties from the token
|
||||
"""
|
||||
|
||||
t = self._create_base_saml_assertion(context, auth)
|
||||
(response, service_provider) = t
|
||||
sp_url = service_provider.get('sp_url')
|
||||
auth_url = service_provider.get('auth_url')
|
||||
|
||||
return wsgi.render_response(body=response.to_string(),
|
||||
status=('200', 'OK'),
|
||||
|
@ -351,6 +356,32 @@ class Auth(auth_controllers.Auth):
|
|||
('X-auth-url',
|
||||
six.binary_type(auth_url))])
|
||||
|
||||
@validation.validated(schema.saml_create, 'auth')
|
||||
def create_ecp_assertion(self, context, auth):
|
||||
"""Exchange a scoped token for an ECP assertion.
|
||||
|
||||
:param auth: Dictionary that contains a token and service provider ID
|
||||
:returns: ECP Assertion based on properties from the token
|
||||
"""
|
||||
|
||||
t = self._create_base_saml_assertion(context, auth)
|
||||
(saml_assertion, service_provider) = t
|
||||
sp_url = service_provider.get('sp_url')
|
||||
auth_url = service_provider.get('auth_url')
|
||||
relay_state_prefix = service_provider.get('relay_state_prefix')
|
||||
|
||||
generator = keystone_idp.ECPGenerator()
|
||||
ecp_assertion = generator.generate_ecp(saml_assertion,
|
||||
relay_state_prefix)
|
||||
|
||||
return wsgi.render_response(body=ecp_assertion.to_string(),
|
||||
status=('200', 'OK'),
|
||||
headers=[('Content-Type', 'text/xml'),
|
||||
('X-sp-url',
|
||||
six.binary_type(sp_url)),
|
||||
('X-auth-url',
|
||||
six.binary_type(auth_url))])
|
||||
|
||||
|
||||
@dependency.requires('assignment_api', 'resource_api')
|
||||
class DomainV3(controller.V3Controller):
|
||||
|
|
|
@ -19,9 +19,12 @@ from oslo_config import cfg
|
|||
from oslo_log import log
|
||||
from oslo_utils import timeutils
|
||||
import saml2
|
||||
from saml2 import client_base
|
||||
from saml2 import md
|
||||
from saml2.profile import ecp
|
||||
from saml2 import saml
|
||||
from saml2 import samlp
|
||||
from saml2.schema import soapenv
|
||||
from saml2 import sigver
|
||||
import xmldsig
|
||||
|
||||
|
@ -556,3 +559,31 @@ class MetadataGenerator(object):
|
|||
if value is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class ECPGenerator(object):
|
||||
"""A class for generating an ECP assertion."""
|
||||
|
||||
@staticmethod
|
||||
def generate_ecp(saml_assertion, relay_state_prefix):
|
||||
ecp_generator = ECPGenerator()
|
||||
header = ecp_generator._create_header(relay_state_prefix)
|
||||
body = ecp_generator._create_body(saml_assertion)
|
||||
envelope = soapenv.Envelope(header=header, body=body)
|
||||
return envelope
|
||||
|
||||
def _create_header(self, relay_state_prefix):
|
||||
relay_state_text = relay_state_prefix + uuid.uuid4().hex
|
||||
relay_state = ecp.RelayState(actor=client_base.ACTOR,
|
||||
must_understand='1',
|
||||
text=relay_state_text)
|
||||
header = soapenv.Header()
|
||||
header.extension_elements = (
|
||||
[saml2.element_to_extension_element(relay_state)])
|
||||
return header
|
||||
|
||||
def _create_body(self, saml_assertion):
|
||||
body = soapenv.Body()
|
||||
body.extension_elements = (
|
||||
[saml2.element_to_extension_element(saml_assertion)])
|
||||
return body
|
||||
|
|
|
@ -74,6 +74,7 @@ class FederationExtension(wsgi.V3ExtensionRouter):
|
|||
protocols/$protocol/auth
|
||||
|
||||
POST /auth/OS-FEDERATION/saml2
|
||||
POST /auth/OS-FEDERATION/saml2/ecp
|
||||
GET /OS-FEDERATION/saml2/metadata
|
||||
|
||||
GET /auth/OS-FEDERATION/websso/{protocol_id}
|
||||
|
@ -209,6 +210,11 @@ class FederationExtension(wsgi.V3ExtensionRouter):
|
|||
path='/auth' + self._construct_url('saml2'),
|
||||
post_action='create_saml_assertion',
|
||||
rel=build_resource_relation(resource_name='saml2'))
|
||||
self._add_resource(
|
||||
mapper, auth_controller,
|
||||
path='/auth' + self._construct_url('saml2/ecp'),
|
||||
post_action='create_ecp_assertion',
|
||||
rel=build_resource_relation(resource_name='ecp'))
|
||||
self._add_resource(
|
||||
mapper, auth_controller,
|
||||
path='/auth' + self._construct_url('websso/{protocol_id}'),
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import os
|
||||
import random
|
||||
import subprocess
|
||||
from testtools import matchers
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
|
@ -2938,6 +2939,7 @@ class SAMLGenerationTests(FederationTests):
|
|||
ROLES = ['admin', 'member']
|
||||
PROJECT = 'development'
|
||||
SAML_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2'
|
||||
ECP_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2/ecp'
|
||||
ASSERTION_VERSION = "2.0"
|
||||
SERVICE_PROVDIER_ID = 'ACME'
|
||||
|
||||
|
@ -2957,7 +2959,9 @@ class SAMLGenerationTests(FederationTests):
|
|||
self.signed_assertion = saml2.create_class_from_xml_string(
|
||||
saml.Assertion, _load_xml('signed_saml2_assertion.xml'))
|
||||
self.sp = self.sp_ref()
|
||||
self.federation_api.create_sp(self.SERVICE_PROVDIER_ID, self.sp)
|
||||
url = '/OS-FEDERATION/service_providers/' + self.SERVICE_PROVDIER_ID
|
||||
self.put(url, body={'service_provider': self.sp},
|
||||
expected_status=201)
|
||||
|
||||
def test_samlize_token_values(self):
|
||||
"""Test the SAML generator produces a SAML object.
|
||||
|
@ -3252,6 +3256,52 @@ class SAMLGenerationTests(FederationTests):
|
|||
self.SERVICE_PROVDIER_ID)
|
||||
self.post(self.SAML_GENERATION_ROUTE, body=body, expected_status=404)
|
||||
|
||||
def test_generate_ecp_route(self):
|
||||
"""Test that the ECP generation endpoint produces XML.
|
||||
|
||||
The ECP endpoint /v3/auth/OS-FEDERATION/saml2/ecp should take the same
|
||||
input as the SAML generation endpoint (scoped token ID + Service
|
||||
Provider ID).
|
||||
The controller should return a SAML assertion that is wrapped in a
|
||||
SOAP envelope.
|
||||
"""
|
||||
|
||||
self.config_fixture.config(group='saml', idp_entity_id=self.ISSUER)
|
||||
token_id = self._fetch_valid_token()
|
||||
body = self._create_generate_saml_request(token_id,
|
||||
self.SERVICE_PROVDIER_ID)
|
||||
|
||||
with mock.patch.object(keystone_idp, '_sign_assertion',
|
||||
return_value=self.signed_assertion):
|
||||
http_response = self.post(self.ECP_GENERATION_ROUTE, body=body,
|
||||
response_content_type='text/xml',
|
||||
expected_status=200)
|
||||
|
||||
env_response = etree.fromstring(http_response.result)
|
||||
header = env_response[0]
|
||||
|
||||
# Verify the relay state starts with 'ss:mem'
|
||||
prefix = CONF.saml.relay_state_prefix
|
||||
self.assertThat(header[0].text, matchers.StartsWith(prefix))
|
||||
|
||||
# Verify that the content in the body matches the expected assertion
|
||||
body = env_response[1]
|
||||
response = body[0]
|
||||
issuer = response[0]
|
||||
assertion = response[2]
|
||||
|
||||
self.assertEqual(self.RECIPIENT, response.get('Destination'))
|
||||
self.assertEqual(self.ISSUER, issuer.text)
|
||||
|
||||
user_attribute = assertion[4][0]
|
||||
self.assertIsInstance(user_attribute[0].text, str)
|
||||
|
||||
role_attribute = assertion[4][1]
|
||||
self.assertIsInstance(role_attribute[0].text, str)
|
||||
|
||||
project_attribute = assertion[4][2]
|
||||
self.assertIsInstance(project_attribute[0].text, str)
|
||||
|
||||
|
||||
class IdPMetadataGenerationTests(FederationTests):
|
||||
"""A class for testing Identity Provider Metadata generation."""
|
||||
|
|
|
@ -352,6 +352,8 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = {
|
|||
'href': '/OS-FEDERATION/projects'},
|
||||
_build_federation_rel(resource_name='saml2'): {
|
||||
'href': '/auth/OS-FEDERATION/saml2'},
|
||||
_build_federation_rel(resource_name='ecp'): {
|
||||
'href': '/auth/OS-FEDERATION/saml2/ecp'},
|
||||
_build_federation_rel(resource_name='metadata'): {
|
||||
'href': '/OS-FEDERATION/saml2/metadata'},
|
||||
_build_federation_rel(resource_name='identity_providers'): {
|
||||
|
|
Loading…
Reference in New Issue