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:
Steve Martinelli 2015-03-10 01:21:35 -04:00
parent 659529a4ad
commit 84c89ae649
5 changed files with 131 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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'): {