ndg_saml
NDG SAML is a Python implementation of SAML 2.0 developed for the NERC DataGrid and ESG (Earth System Grid) security architecture. Both use a federated model for access control and SAML 2.0 was selected to provide the interfaces for attribute and authorisation decision queries. This implementation is based on the Java OpenSAML code.
The code uses ElementTree for serialisation to and parsing from XML but an API makes it easily extendable to use other Python XML parsers if desired.
Releases
0.5.1
Minor fixes to 0.5 release:
- Fix for date time parsing where no seconds fraction is present
- fixed error message for InResponseTo ID check for Subject Query.
0.5
Adds support for SOAP binding to query/request profile with WSGI middleware for server side applications and client side package.
0.4
Initial release to PyPI includes SAML core for attribute and authorisation decision queries.
Installation
The code is available on PyPI at http://pypi.python.org/pypi/ndg-saml/.
$ sudo easy_install ndg_saml
Example Code
These samples are taken direct from the unit tests.
Attribute Query
This example creates an attribute query:
from ndg.saml.saml2.core import (AttributeQuery, SAMLVersion, Issuer, Subject,
NameID, Attribute, XSStringAttributeValue)
from uuid import uuid4
from datetime import datetime
attributeQuery = AttributeQuery()
attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
attributeQuery.id = str(uuid4())
attributeQuery.issueInstant = datetime.utcnow()
attributeQuery.issuer = Issuer()
attributeQuery.issuer.format = Issuer.X509_SUBJECT
attributeQuery.issuer.value = '/O=NDG/OU=BADC/CN=PolicyInformationPoint'
attributeQuery.subject = Subject()
attributeQuery.subject.nameID = NameID()
attributeQuery.subject.nameID.format = NameID.X509_SUBJECT
attributeQuery.subject.nameID.value = '/O=NDG/OU=BADC/CN=PhilipKershaw'
# special case handling for 'LastName' attribute
emailAddressAttribute = Attribute()
emailAddressAttribute.name = "urn:esg:email:address"
emailAddressAttribute.nameFormat = "%s#%s" % (
XSStringAttributeValue.TYPE_NAME.namespaceURI,
XSStringAttributeValue.TYPE_NAME.localPart)
emailAddress = XSStringAttributeValue()
emailAddress.value = 'pjk@somewhere.ac.uk'
emailAddressAttribute.attributeValues.append(emailAddress)
attributeQuery.attributes.append(emailAddressAttribute)
# Convert to ElementTree representation
from ndg.saml.xml.etree import AttributeQueryElementTree, prettyPrint
elem = AttributeQueryElementTree.toXML(attributeQuery)
# Serialise as string
xmlOut = prettyPrint(elem)
print(xmlOut)
Produces:
<samlp:AttributeQuery xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Version="2.0" IssueInstant="2010-06-01T13:19:50.690263Z" ID="1c15e748-0f74-41f1-848c-1fbdfeef2a06">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:x509SubjectName">/O=NDG/OU=BADC/CN=PolicyInformationPoint</saml:Issuer>
<saml:Subject xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:x509SubjectName">/O=NDG/OU=BADC/CN=PhilipKershaw</saml:NameID>
</saml:Subject>
<saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="urn:esg:email:address" NameFormat="http://www.w3.org/2001/XMLSchema#string">
<saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">pjk@somewhere.ac.uk</saml:AttributeValue>
</saml:Attribute>
</samlp:AttributeQuery>
Query/Request? Profile with SOAP Binding
This example shows how a simple Attribute Service can be created with WSGI together with a client call.
The server is configured using a Paste ini file. A pipeline arranges a custom filter AttributeAuthorityFilter, in this case upstream of the SAMLSoapAttributeInterfaceFilter available with this package. TestApp is a simple WSGI application to terminate the chain.
[pipeline:main] pipeline = AttributeAuthorityFilter SAMLSoapAttributeInterfaceFilter TestApp
SAMLSoapAttributeInterfaceFilter uses the ndg.saml.saml2.binding.soap.server.wsgi.queryinterface:SOAPQueryInterfaceMiddleware class. The settings define the path for where the service will be mounted and the serialisation/parsing classes for handling the SAML XML content. the ElementTree? based ones have been used.
[filter:SAMLSoapAttributeInterfaceFilter] paste.filter_app_factory = ndg.saml.saml2.binding.soap.server.wsgi.queryinterface:SOAPQueryInterfaceMiddleware.filter_app_factory prefix = saml. saml.mountPath = /attributeauthority saml.queryInterfaceKeyName = attributeQueryInterface saml.deserialise = ndg.saml.xml.etree:AttributeQueryElementTree.fromXML saml.serialise = ndg.saml.xml.etree:ResponseElementTree.toXML
The saml.queryInterfaceKeyName setting above is important. This sets the keyword in environ that this class will use to get a callback function. This callback handles the actual query. It has the pattern:
attributeQuery(query, response)
where query is the parsed SAML query and response is the SAML response object which needs to be populated. For covenience, the caller set a number of values in this object such as for example the issuer instant, ID and the inResponseTo values. As the callback is read from environ, any custom middleware upstream of this filter can set it. This is what the AttributeAuthorityFilter does in this example. Looking at the details in the ini file, it sets an environ key named attributeQueryInterface which assigns to a callback function it creates:
[filter:AttributeAuthorityFilter] # This filter is a container for a binding to a SOAP based interface to the # Attribute Authority paste.filter_app_factory = ndg.saml.test.binding.soap.test_attributeservice:TestAttributeServiceMiddleware queryInterfaceKeyName = attributeQueryInterface
Looking at the ndg.saml.test.binding.soap.test_attributeservice:TestAttributeServiceMiddleware implementation, a factory method creates the callback (shortened for illustration):
def attributeQueryFactory(self):
"""Makes the attribute query method"""
def attributeQuery(query, response):
"""Attribute Query interface called by the next middleware in the
stack the SAML SOAP Query interface middleware instance
(ndg.saml.saml2.binding.soap.server.wsgi.queryinterface.SOAPQueryInterfaceMiddleware)
"""
<...>
assertion = Assertion()
assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
assertion.id = str(uuid4())
assertion.issueInstant = response.issueInstant
assertion.conditions = Conditions()
assertion.conditions.notBefore = assertion.issueInstant
assertion.conditions.notOnOrAfter = \
assertion.conditions.notBefore + timedelta(seconds=60*60*8)
assertion.subject = Subject()
assertion.subject.nameID = NameID()
assertion.subject.nameID.format = query.subject.nameID.format
assertion.subject.nameID.value = query.subject.nameID.value
<...>
response.assertions.append(assertion)
response.status.statusCode.value = StatusCode.SUCCESS_URI
The middleware's __call__ method invokes this method to set the callback in environ:
def __call__(self, environ, start_response):
environ[self.queryInterfaceKeyName] = self.attributeQueryFactory()
return self._app(environ, start_response)
The client request is created using the SOAP binding and sent with SSL. ndg.saml.saml2.binding.soap.client.attributequery.AttributeQuerySslSOAPBinding is a wrapper class to perform this task taking SAML attribute query inputs and SSL settings. In this example, the subject is queried for an e-mail address. The connection is mutually authenticated with the client passing a certificate in the SSL handshake:
from ndg.soap.utils.etree import prettyPrint
from ndg.saml.saml2.core import Attribute, StatusCode
from ndg.saml.xml.etree import ResponseElementTree
from ndg.saml.saml2.binding.soap.client.attributequery import \
AttributeQuerySslSOAPBinding
attributeQuery = AttributeQuerySslSOAPBinding()
attributeQuery.subjectID = "https://openid.localhost/philip.kershaw"
attributeQuery.subjectIdFormat = "urn:ndg:saml:openid"
attributeQuery.clockSkewTolerance = 2.
attributeQuery.issuerName = '/O=Site A/CN=Authorisation Service'
attribute = Attribute()
attribute.name = 'urn:ndg:saml:emailaddress'
attribute.friendlyName = 'emailAddress'
attribute.nameFormat = 'http://www.w3.org/2001/XMLSchema'
attributeQuery.queryAttributes.append(attribute)
attributeQuery.sslCACertDir = "./trustroots"
attributeQuery.sslCertFilePath = "./client.crt"
attributeQuery.sslPriKeyFilePath = "./client.key"
response = attributeQuery.send(uri='https://localhost:5443/attributeauthority')
# Convert back to ElementTree instance read for string output
samlResponseElem = ResponseElementTree.toXML(response)
print("Pretty print SAML Response ...")
print(prettyPrint(samlResponseElem))
The response is generated:
<samlp:Response ID="c61f8c17-8d2d-4b6a-ad7f-3acd6b4c3adc" InResponseTo="6fb92e0e-4570-4c03-8df9-6ee76ecfb03f" IssueInstant="2010-10-01T13:24:20.446046Z" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">/O=NDG/OU=BADC/CN=attributeauthority.badc.rl.ac.uk</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status><saml:Assertion ID="bcd28e09-1eb9-47f6-95c7-5b4133783aec" IssueInstant="2010-10-01T13:24:20.446046Z" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:Subject><saml:NameID Format="urn:ndg:saml:openid">https://openid.localhost/philip.kershaw</saml:NameID></saml:Subject><saml:Conditions NotBefore="2010-10-01T13:24:20.446046Z" NotOnOrAfter="2010-10-01T21:24:20.446046Z" /><saml:AttributeStatement><saml:Attribute FriendlyName="emailAddress" Name="urn:ndg:saml:emailaddress" NameFormat="http://www.w3.org/2001/XMLSchema"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">pkershaw@somewhere.ac.uk</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>
Pretty print SAML Response ...
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" IssueInstant="2010-10-01T13:24:20.446046Z" InResponseTo="6fb92e0e-4570-4c03-8df9-6ee76ecfb03f" Version="2.0" ID="c61f8c17-8d2d-4b6a-ad7f-3acd6b4c3adc">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">/O=NDG/OU=BADC/CN=attributeauthority.badc.rl.ac.uk</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"></samlp:StatusCode>
</samlp:Status>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" IssueInstant="2010-10-01T13:24:20.446046Z" ID="bcd28e09-1eb9-47f6-95c7-5b4133783aec">
<saml:Subject>
<saml:NameID Format="urn:ndg:saml:openid">https://openid.localhost/philip.kershaw</saml:NameID>
</saml:Subject>
<saml:Conditions NotOnOrAfter="2010-10-01T21:24:20.446046Z" NotBefore="2010-10-01T13:24:20.446046Z"></saml:Conditions>
<saml:AttributeStatement>
<saml:Attribute FriendlyName="emailAddress" Name="urn:ndg:saml:emailaddress" NameFormat="http://www.w3.org/2001/XMLSchema">
<saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">pkershaw@somewhere.ac.uk</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
Repository
http://proj.badc.rl.ac.uk/svn/ndg-security/trunk/ndg_saml
Unit Tests
See the ndg.saml.test.test_saml module and the ndg.saml.test.binding.soap package.
Philip Kershaw 20/10/10
