source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/myproxy/certificate_extapp/saml_attribute_assertion.py @ 5929

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/myproxy/certificate_extapp/saml_attribute_assertion.py@5929
Revision 5929, 13.8 KB checked in by pjkersha, 10 years ago (diff)

Working unit tests for MyProxy? SAML Attribute assertion callout. TODO: add console script entry point.

Line 
1"""X.509 certificate extension application for adding SAML assertions into
2certificates issued by MyProxy
3
4NERC DataGrid Project
5"""
6__author__ = "P J Kershaw"
7__date__ = "29/10/09"
8__copyright__ = "(C) 2009 Science and Technology Facilities Council"
9__license__ = "BSD - see LICENSE file in top-level directory"
10__contact__ = "Philip.Kershaw@stfc.ac.uk"
11__revision__ = '$Id: $'
12import logging
13log = logging.getLogger(__name__)
14
15import traceback
16from datetime import datetime
17from uuid import uuid4
18from string import Template
19
20from sqlalchemy import create_engine, exc
21
22try: # >= python 2.5
23    from xml.etree import ElementTree
24except ImportError:
25    import ElementTree
26
27from saml.common.xml import SAMLConstants
28from saml.saml2.core import (
29    Assertion, Attribute, 
30    SAMLVersion, Subject, NameID, Issuer, AttributeQuery, 
31    XSStringAttributeValue, 
32    StatusCode)
33from saml.xml.etree import AssertionElementTree
34   
35from ndg.security.common.saml.bindings import SOAPBinding as SamlSoapBinding
36from ndg.security.common.X509 import X500DN
37from ndg.security.server.wsgi.openid.provider import IdentityMapping
38
39
40class SamlAssertionMyProxyCertExtAppError(Exception):
41    """Base class for SamlAssertionMyProxyCertExtApp class exceptions"""
42   
43   
44class SamlAssertionMyProxyCertExtAppConfigError(
45                                        SamlAssertionMyProxyCertExtAppError):
46    """Configuration fault for SamlAssertionMyProxyCertExtApp instance"""
47
48
49class SamlAssertionMyProxyCertExtAppRetrieveError(
50                                        SamlAssertionMyProxyCertExtAppError):
51    """Error retrieving results from user database or attribute authority"""
52   
53
54class SamlAssertionMyProxyCertExtAppSqlError(
55                                        SamlAssertionMyProxyCertExtAppError):   
56    """Error with SQL query syntax"""
57   
58   
59class SamlAssertionMyProxyCertExtApp(object):
60    """Application to create a X.509 certificate extension containing a SAML
61    assertion for inclusion by MyProxy into an issued certificate
62   
63    @type DEFAULT_ATTR_DESCR: tuple
64    @cvar DEFAULT_ATTR_DESCR: a tuple of tuples describing the default
65    SAML attributes to be queried from the Attribute Authority.  The format is,
66   
67    ((<name>, <friendlyName>, <format>), (...), ...)
68   
69    FriendlyName can be defaulted to None in which case it will be omitted from
70    the query
71    """
72    XSSTRING_NS = "%s#%s" % (
73        SAMLConstants.XSD_NS,
74        XSStringAttributeValue.TYPE_LOCAL_NAME
75    )
76    N_ATTR_DESCR_ELEM_ITEMS = 3
77    DEFAULT_ATTR_DESCR = (
78        ("urn:esg:first:name", "FirstName", XSSTRING_NS),
79        ("urn:esg:last:name", "LastName", XSSTRING_NS),
80        ("urn:esg:email:address", "emailAddress", XSSTRING_NS),
81    )
82    ESG_NAME_ID_FORMAT = "urn:esg:openid"
83   
84    CONNECTION_STRING_OPTNAME = 'connectionString'
85    OPENID_SQLQUERY_OPTNAME = 'openIdSqlQuery'
86    OPENID_IDENTITY_URI_TMPL_OPTNAME = 'identityUriTemplate'
87   
88    __slots__ = (
89       'attributeAuthorityURI',
90       'userOpenID',
91       'issuerDN',
92       'attributeDescr',
93       CONNECTION_STRING_OPTNAME,
94       OPENID_SQLQUERY_OPTNAME,
95       OPENID_IDENTITY_URI_TMPL_OPTNAME
96    )
97    __PRIVATE_ATTR_PREFIX = '_SamlAssertionMyProxyCertExtApp__'
98    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__])
99   
100    def __init__(self):
101        self.__attributeAuthorityURI = None
102        self.__userOpenID = None
103        self.__issuerDN = None
104        self.__connectionString = None
105        self.__openIdSqlQuery = None
106        self.__identityUriTemplate = None 
107               
108        # Use property here as a safeguard in case DEFAULT_ATTR_DESCR has been
109        # altered
110        self.attributeDescr = SamlAssertionMyProxyCertExtApp.DEFAULT_ATTR_DESCR
111
112    def __call__(self, username):
113        """Main method - create SAML assertion by querying the user's OpenID
114        identifier from the user database and using this to query the
115        Attribute Authority for attributes
116        """
117        identifier = self.queryOpenId(username)
118        self.userOpenID = IdentityMapping.userIdentifier2IdentityURI(
119                                                    self.identityUriTemplate, 
120                                                    identifier)
121       
122        response = self.attributeQuery()
123       
124        try:
125            assertionStr = self.serialiseAssertion(response.assertions[0])
126           
127        except (IndexError, TypeError):
128            raise SamlAssertionMyProxyCertExtAppRetrieveError("Error accessing "
129                    "assertion from Attribute Authority SAML response: %s" %
130                    traceback.format_exc())
131           
132        return assertionStr
133
134    def _getAttributeDescr(self):
135        return self.__attributeDescr
136
137    def _setAttributeDescr(self, value):
138        if not isinstance(value, tuple):
139            raise TypeError('Expecting tuple type for "attributeDescr";'
140                            ' got %r instead' % type(value))
141           
142        for i in value:
143            if not isinstance(value, tuple):
144                raise TypeError('Expecting tuple type for "attributeDescr" '
145                                'tuple sub-elements; got %r instead' % 
146                                type(value))
147            if len(i) != SamlAssertionMyProxyCertExtApp.N_ATTR_DESCR_ELEM_ITEMS:
148                raise TypeError('Expecting %d element tuple for '
149                    '"attributeDescr" sub-elements; got %d elements instead' % 
150                    (SamlAssertionMyProxyCertExtApp.N_ATTR_DESCR_ELEM_ITEMS,
151                    len(i)))
152               
153        self.__attributeDescr = value
154   
155    attributeDescr = property(_getAttributeDescr, 
156                              _setAttributeDescr, 
157                              doc="List of name, friendly name, format tuples "
158                                  "determining attributes to query from the "
159                                  "Attribute Authority")
160
161    def _getAttributeAuthorityURI(self):
162        return self.__attributeAuthorityURI
163
164    def _setAttributeAuthorityURI(self, value):
165        if not isinstance(value, basestring):
166            raise TypeError('Expecting string type for "attributeAuthorityURI";'
167                            ' got %r instead' % type(value))
168        self.__attributeAuthorityURI = value
169
170    attributeAuthorityURI = property(_getAttributeAuthorityURI,
171                                     _setAttributeAuthorityURI, 
172                                     doc="Attribute Authority SOAP SAML URI")
173
174    def _getUserOpenID(self):
175        return self.__userOpenID
176
177    def _setUserOpenID(self, value):
178        if not isinstance(value, basestring):
179            raise TypeError('Expecting string type for "userOpenID"; got %r '
180                            'instead' % type(value))
181        self.__userOpenID = value
182
183    userOpenID = property(_getUserOpenID, _setUserOpenID, 
184                          doc="OpenID corresponding to user certificate to "
185                              "be issued")
186
187    def _getIssuerDN(self):
188        return self.__issuerDN
189
190    def _setIssuerDN(self, value):
191        if isinstance(value, basestring):
192            self.__issuerDN = X500DN.fromString(value)
193           
194        elif isinstance(value, X500DN):
195            self.__issuerDN = value
196        else:
197            raise TypeError('Expecting string or X500DN type for "issuerDN"; '
198                            'got %r instead' % type(value))
199        self.__issuerDN = value
200
201    issuerDN = property(_getIssuerDN, _setIssuerDN, 
202                        doc="Distinguished Name of issuer of SAML Attribute "
203                            "Query to Attribute Authority")
204
205    def _getConnectionString(self):
206        return self.__connectionString
207
208    def _setConnectionString(self, value):
209        if not isinstance(value, basestring):
210            raise TypeError('Expecting string type for "%s" attribute; got %r'%
211                    (SamlAssertionMyProxyCertExtApp.CONNECTION_STRING_OPTNAME,
212                     type(value)))
213        self.__connectionString = value
214
215    connectionString = property(fget=_getConnectionString, 
216                                fset=_setConnectionString, 
217                                doc="Database connection string")
218
219    def _getOpenIdSqlQuery(self):
220        return self.__openIdSqlQuery
221
222    def _setOpenIdSqlQuery(self, value):
223        if not isinstance(value, basestring):
224            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
225                        (SamlAssertionMyProxyCertExtApp.OPENID_SQLQUERY_OPTNAME,
226                         type(value)))
227        self.__openIdSqlQuery = value
228
229    openIdSqlQuery = property(fget=_getOpenIdSqlQuery, 
230                        fset=_setOpenIdSqlQuery, 
231                        doc="SQL Query for authentication request")
232
233    def _getIdentityUriTemplate(self):
234        return self.__identityUriTemplate
235
236    def _setIdentityUriTemplate(self, value):
237        if not isinstance(value, basestring):
238            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
239            (SamlAssertionMyProxyCertExtApp.OPENID_IDENTITY_URI_TMPL_OPTNAME,
240             type(value)))
241        self.__identityUriTemplate = value
242
243    identityUriTemplate = property(_getIdentityUriTemplate, 
244                                   _setIdentityUriTemplate, 
245                                   doc="Identity URI template string - sets "
246                                       "the common component of user's "
247                                       "identity URI.  It should contain the "
248                                       "${userIdentifier} template "
249                                       "substitution parameter")
250       
251    def __getstate__(self):
252        '''Specific implementation needed with __slots__'''
253        return dict([(attrName, getattr(self, attrName)) \
254                     for attrName in SamlAssertionMyProxyCertExtApp.__slots__])
255       
256    def __setstate__(self, attrDict):
257        '''Specific implementation needed with __slots__'''
258        for attr, val in attrDict.items():
259            setattr(self, attr, val)
260   
261    def serialiseAssertion(self, assertion):
262        """Convert SAML assertion object into a string"""
263        samlAssertionElem = AssertionElementTree.toXML(assertion)
264        return ElementTree.tostring(samlAssertionElem)
265       
266    def attributeQuery(self):
267        """Query an Attribute Authority to retrieve an assertion for the
268        given user"""
269               
270        # Create a SAML attribute query
271        attributeQuery = AttributeQuery()
272        attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
273        attributeQuery.id = str(uuid4())
274        attributeQuery.issueInstant = datetime.utcnow()
275       
276        attributeQuery.issuer = Issuer()
277        attributeQuery.issuer.format = Issuer.X509_SUBJECT
278        attributeQuery.issuer.value = self.issuerDN
279                       
280        attributeQuery.subject = Subject() 
281        attributeQuery.subject.nameID = NameID()
282        attributeQuery.subject.nameID.format = \
283                            SamlAssertionMyProxyCertExtApp.ESG_NAME_ID_FORMAT
284        attributeQuery.subject.nameID.value = self.userOpenID
285                 
286        # Add list of attributes to query                     
287        for name, friendlyName, format in self.attributeDescr:
288            attribute = Attribute()
289            attribute.name = name
290            attribute.nameFormat = format
291            attribute.friendlyName = friendlyName
292   
293            attributeQuery.attributes.append(attribute)
294
295        # Make query over SOAP interface to remote service
296        binding = SamlSoapBinding()
297        response = binding.attributeQuery(attributeQuery, 
298                                          self.attributeAuthorityURI)
299       
300        assert(response.status.statusCode.value==StatusCode.SUCCESS_URI)
301       
302        # Check Query ID matches the query ID the service received
303        assert(response.inResponseTo == attributeQuery.id)
304       
305        now = datetime.utcnow()
306        assert(response.issueInstant < now)
307        assert(response.assertions[-1].issueInstant < now)       
308        assert(response.assertions[-1].conditions.notBefore < now) 
309        assert(response.assertions[-1].conditions.notOnOrAfter > now)
310       
311        return response
312   
313    def queryOpenId(self, username):
314        """Given a username, query for user OpenID identifier from the user
315        database
316
317        @type username: basestring
318        @param username: username
319        @rtype: basestring
320        @return: the OpenID identifier corresponding to the input username
321        """
322
323        try:
324            dbEngine = create_engine(self.connectionString)
325        except ImportError, e:
326            raise SamlAssertionMyProxyCertExtAppConfigError("Missing database "
327                                                            "engine for "
328                                                            "SQLAlchemy: %s" % 
329                                                            e)
330        connection = dbEngine.connect()
331       
332        try:
333            queryInputs = dict(username=username)
334            query = Template(self.openIdSqlQuery).substitute(queryInputs)
335            result = connection.execute(query)
336
337        except exc.ProgrammingError:
338            raise SamlAssertionMyProxyCertExtAppSqlError(
339                                                "Error with SQL Syntax: %s" %
340                                                traceback.format_exc())
341        finally:
342            connection.close()
343
344        try:
345            identifier = [r for r in result][0][0]
346       
347        except Exception:
348            raise SamlAssertionMyProxyCertExtAppRetrieveError(
349                                                "Error with result set: %s" %
350                                                traceback.format_exc())
351       
352        log.debug('Query succeeded for user %r' % username)
353        return identifier
Note: See TracBrowser for help on using the repository browser.