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

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@5953
Revision 5953, 16.0 KB checked in by pjkersha, 12 years ago (diff)

Important fix for parsing Distinguished Names - handle the case where the CN contains a slash as used for MyProxy/Globus? host certificates e.g. /.../CN=host/myhost.mydomain

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
38from ndg.security.common.utils.configfileparsers import (     
39                                                    CaseSensitiveConfigParser,)
40
41class CertExtAppError(Exception):
42    """Base class for CertExtApp class exceptions"""
43   
44   
45class CertExtAppConfigError(CertExtAppError):
46    """Configuration fault for CertExtApp instance"""
47
48
49class CertExtAppRetrieveError(CertExtAppError):
50    """Error retrieving results from user database or attribute authority"""
51   
52
53class CertExtAppSqlError(CertExtAppError):   
54    """Error with SQL query syntax"""
55   
56   
57class CertExtApp(object):
58    """Application to create a X.509 certificate extension containing a SAML
59    assertion for inclusion by MyProxy into an issued certificate
60   
61    @type DEFAULT_ATTR_DESCR: tuple
62    @cvar DEFAULT_ATTR_DESCR: a tuple of tuples describing the default
63    SAML attributes to be queried from the Attribute Authority.  The format is,
64   
65    ((<name>, <friendlyName>, <format>), (...), ...)
66   
67    FriendlyName can be defaulted to None in which case it will be omitted from
68    the query
69    """
70    XSSTRING_NS = "%s#%s" % (
71        SAMLConstants.XSD_NS,
72        XSStringAttributeValue.TYPE_LOCAL_NAME
73    )
74    N_ATTR_DESCR_ELEM_ITEMS = 3
75    DEFAULT_ATTR_DESCR = (
76        ("urn:esg:first:name", "FirstName", XSSTRING_NS),
77        ("urn:esg:last:name", "LastName", XSSTRING_NS),
78        ("urn:esg:email:address", "emailAddress", XSSTRING_NS),
79    )
80    ESG_NAME_ID_FORMAT = "urn:esg:openid"
81   
82    CONNECTION_STRING_OPTNAME = 'connectionString'
83    OPENID_SQLQUERY_OPTNAME = 'openIdSqlQuery'
84    OPENID_IDENTITY_URI_TMPL_OPTNAME = 'identityUriTemplate'
85    ATTRIBUTE_AUTHORITY_URI_OPTNAME = 'attributeAuthorityURI'
86    ISSUER_DN_OPTNAME = 'issuerDN'
87   
88    CONFIG_FILE_OPTNAMES = (
89        ATTRIBUTE_AUTHORITY_URI_OPTNAME,
90        ISSUER_DN_OPTNAME,                 
91        CONNECTION_STRING_OPTNAME,
92        OPENID_SQLQUERY_OPTNAME,
93        OPENID_IDENTITY_URI_TMPL_OPTNAME                   
94    )
95    __slots__ = (
96       'userOpenID',
97       'attributeDescr'
98    )
99    __slots__ += CONFIG_FILE_OPTNAMES
100    __PRIVATE_ATTR_PREFIX = '_CertExtApp__'
101    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__])
102   
103    def __init__(self):
104        self.__attributeAuthorityURI = None
105        self.__userOpenID = None
106        self.__issuerDN = None
107        self.__connectionString = None
108        self.__openIdSqlQuery = None
109        self.__identityUriTemplate = None 
110               
111        # Use property here as a safeguard in case DEFAULT_ATTR_DESCR has been
112        # altered
113        self.attributeDescr = CertExtApp.DEFAULT_ATTR_DESCR
114
115    @classmethod
116    def fromConfigFile(cls, configFilePath, **kw):
117        '''Alternative constructor makes object from config file settings
118        @type configFilePath: basestring
119        @param configFilePath: configuration file path
120        '''
121        certExtApp = cls()
122        certExtApp.readConfig(configFilePath, **kw)
123       
124        return certExtApp
125       
126    def __call__(self, username):
127        """Main method - create SAML assertion by querying the user's OpenID
128        identifier from the user database and using this to query the
129        Attribute Authority for attributes
130        """
131        identifier = self.queryOpenId(username)
132        self.userOpenID = IdentityMapping.userIdentifier2IdentityURI(
133                                                    self.identityUriTemplate, 
134                                                    identifier)
135       
136        response = self.attributeQuery()
137       
138        try:
139            assertionStr = self.serialiseAssertion(response.assertions[0])
140           
141        except (IndexError, TypeError):
142            raise CertExtAppRetrieveError("Error accessing assertion from "
143                                          "Attribute Authority SAML response: "
144                                          "%s" % traceback.format_exc())
145           
146        return assertionStr
147
148    def readConfig(self, configFilePath, prefix='', section='DEFAULT'):
149        '''Read config file settings
150        @type configFilePath: basestring
151        @param configFilePath: configuration file path
152        @type prefix: basestring
153        @param prefix: prefix for option names e.g. "certExtApp."
154        @type section: baestring
155        @param section: configuration file section from which to extract
156        parameters.
157        '''
158        cfg = CaseSensitiveConfigParser()
159        cfg.read(os.path.expandvars(configFilePath))
160       
161        for optName in CertExtApp.CONFIG_FILE_OPTNAMES:
162            val = cfg.get(section, prefix+optName)
163            setattr(self, optName, val)
164                   
165    def _getAttributeDescr(self):
166        return self.__attributeDescr
167
168    def _setAttributeDescr(self, value):
169        if not isinstance(value, tuple):
170            raise TypeError('Expecting tuple type for "attributeDescr";'
171                            ' got %r instead' % type(value))
172           
173        for i in value:
174            if not isinstance(value, tuple):
175                raise TypeError('Expecting tuple type for "attributeDescr" '
176                                'tuple sub-elements; got %r instead' % 
177                                type(value))
178            if len(i) != CertExtApp.N_ATTR_DESCR_ELEM_ITEMS:
179                raise TypeError('Expecting %d element tuple for '
180                                '"attributeDescr" sub-elements; got %d '
181                                'elements instead' % 
182                                (CertExtApp.N_ATTR_DESCR_ELEM_ITEMS,
183                                 len(i)))
184               
185        self.__attributeDescr = value
186   
187    attributeDescr = property(_getAttributeDescr, 
188                              _setAttributeDescr, 
189                              doc="List of name, friendly name, format tuples "
190                                  "determining attributes to query from the "
191                                  "Attribute Authority")
192
193    def _getAttributeAuthorityURI(self):
194        return self.__attributeAuthorityURI
195
196    def _setAttributeAuthorityURI(self, value):
197        if not isinstance(value, basestring):
198            raise TypeError('Expecting string type for "attributeAuthorityURI";'
199                            ' got %r instead' % type(value))
200        self.__attributeAuthorityURI = value
201
202    attributeAuthorityURI = property(_getAttributeAuthorityURI,
203                                     _setAttributeAuthorityURI, 
204                                     doc="Attribute Authority SOAP SAML URI")
205
206    def _getUserOpenID(self):
207        return self.__userOpenID
208
209    def _setUserOpenID(self, value):
210        if not isinstance(value, basestring):
211            raise TypeError('Expecting string type for "userOpenID"; got %r '
212                            'instead' % type(value))
213        self.__userOpenID = value
214
215    userOpenID = property(_getUserOpenID, _setUserOpenID, 
216                          doc="OpenID corresponding to user certificate to "
217                              "be issued")
218
219    def _getIssuerDN(self):
220        return self.__issuerDN
221
222    def _setIssuerDN(self, value):
223        if isinstance(value, basestring):
224            self.__issuerDN = X500DN.fromString(value)
225           
226        elif isinstance(value, X500DN):
227            self.__issuerDN = value
228        else:
229            raise TypeError('Expecting string or X500DN type for "issuerDN"; '
230                            'got %r instead' % type(value))
231        self.__issuerDN = value
232
233    issuerDN = property(_getIssuerDN, _setIssuerDN, 
234                        doc="Distinguished Name of issuer of SAML Attribute "
235                            "Query to Attribute Authority")
236
237    def _getConnectionString(self):
238        return self.__connectionString
239
240    def _setConnectionString(self, value):
241        if not isinstance(value, basestring):
242            raise TypeError('Expecting string type for "%s" attribute; got %r'%
243                            (CertExtApp.CONNECTION_STRING_OPTNAME,
244                             type(value)))
245        self.__connectionString = value
246
247    connectionString = property(fget=_getConnectionString, 
248                                fset=_setConnectionString, 
249                                doc="Database connection string")
250
251    def _getOpenIdSqlQuery(self):
252        return self.__openIdSqlQuery
253
254    def _setOpenIdSqlQuery(self, value):
255        if not isinstance(value, basestring):
256            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
257                        (CertExtApp.OPENID_SQLQUERY_OPTNAME,
258                         type(value)))
259        self.__openIdSqlQuery = value
260
261    openIdSqlQuery = property(fget=_getOpenIdSqlQuery, 
262                        fset=_setOpenIdSqlQuery, 
263                        doc="SQL Query for authentication request")
264
265    def _getIdentityUriTemplate(self):
266        return self.__identityUriTemplate
267
268    def _setIdentityUriTemplate(self, value):
269        if not isinstance(value, basestring):
270            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
271                            (CertExtApp.OPENID_IDENTITY_URI_TMPL_OPTNAME,
272                             type(value)))
273        self.__identityUriTemplate = value
274
275    identityUriTemplate = property(_getIdentityUriTemplate, 
276                                   _setIdentityUriTemplate, 
277                                   doc="Identity URI template string - sets "
278                                       "the common component of user's "
279                                       "identity URI.  It should contain the "
280                                       "${userIdentifier} template "
281                                       "substitution parameter")
282       
283    def __getstate__(self):
284        '''Specific implementation needed with __slots__'''
285        return dict([(attrName, getattr(self, attrName)) \
286                     for attrName in CertExtApp.__slots__])
287       
288    def __setstate__(self, attrDict):
289        '''Specific implementation needed with __slots__'''
290        for attr, val in attrDict.items():
291            setattr(self, attr, val)
292   
293    def serialiseAssertion(self, assertion):
294        """Convert SAML assertion object into a string"""
295        samlAssertionElem = AssertionElementTree.toXML(assertion)
296        return ElementTree.tostring(samlAssertionElem)
297       
298    def attributeQuery(self):
299        """Query an Attribute Authority to retrieve an assertion for the
300        given user"""
301               
302        # Create a SAML attribute query
303        attributeQuery = AttributeQuery()
304        attributeQuery.version = SAMLVersion(SAMLVersion.VERSION_20)
305        attributeQuery.id = str(uuid4())
306        attributeQuery.issueInstant = datetime.utcnow()
307       
308        attributeQuery.issuer = Issuer()
309        attributeQuery.issuer.format = Issuer.X509_SUBJECT
310        attributeQuery.issuer.value = self.issuerDN
311                       
312        attributeQuery.subject = Subject() 
313        attributeQuery.subject.nameID = NameID()
314        attributeQuery.subject.nameID.format = \
315                            CertExtApp.ESG_NAME_ID_FORMAT
316        attributeQuery.subject.nameID.value = self.userOpenID
317                 
318        # Add list of attributes to query                     
319        for name, friendlyName, format in self.attributeDescr:
320            attribute = Attribute()
321            attribute.name = name
322            attribute.nameFormat = format
323            attribute.friendlyName = friendlyName
324   
325            attributeQuery.attributes.append(attribute)
326
327        # Make query over SOAP interface to remote service
328        binding = SamlSoapBinding()
329        response = binding.attributeQuery(attributeQuery, 
330                                          self.attributeAuthorityURI)
331       
332        assert(response.status.statusCode.value==StatusCode.SUCCESS_URI)
333       
334        # Check Query ID matches the query ID the service received
335        assert(response.inResponseTo == attributeQuery.id)
336       
337        now = datetime.utcnow()
338        assert(response.issueInstant < now)
339        assert(response.assertions[-1].issueInstant < now)       
340        assert(response.assertions[-1].conditions.notBefore < now) 
341        assert(response.assertions[-1].conditions.notOnOrAfter > now)
342       
343        return response
344   
345    def queryOpenId(self, username):
346        """Given a username, query for user OpenID identifier from the user
347        database
348
349        @type username: basestring
350        @param username: username
351        @rtype: basestring
352        @return: the OpenID identifier corresponding to the input username
353        """
354
355        try:
356            dbEngine = create_engine(self.connectionString)
357        except ImportError, e:
358            raise CertExtAppConfigError("Missing database engine for "
359                                        "SQLAlchemy: %s" % e)
360        connection = dbEngine.connect()
361       
362        try:
363            queryInputs = dict(username=username)
364            query = Template(self.openIdSqlQuery).substitute(queryInputs)
365            result = connection.execute(query)
366
367        except exc.ProgrammingError:
368            raise CertExtAppSqlError("Error with SQL Syntax: %s" %
369                                     traceback.format_exc())
370        finally:
371            connection.close()
372
373        try:
374            identifier = [r for r in result][0][0]
375       
376        except Exception:
377            raise CertExtAppRetrieveError("Error with result set: %s" %
378                                          traceback.format_exc())
379       
380        log.debug('Query succeeded for user %r' % username)
381        return identifier
382   
383   
384import optparse
385import os
386
387class CertExtConsoleApp(CertExtApp):
388    """Extend CertExtApp with functionality for command line options"""
389
390    DEBUG_ENVVAR_NAME = 'NDGSEC_MYPROXY_CERT_EXT_APP_DEBUG'
391
392    @classmethod
393    def run(cls):
394        """Parse command line arguments and run the query specified"""
395
396        if cls.DEBUG_ENVVAR_NAME in os.environ:
397            import pdb
398            pdb.set_trace()
399
400        parser = optparse.OptionParser()
401
402        parser.add_option("-f",
403                          "--config-file",
404                          dest="configFilePath",
405                          help="ini style configuration file path containing "
406                               "the options: connectionString, "
407                               "openIdSqlQuery, identityUriTemplate, "
408                               "attributeAuthorityURI and issuerDN")
409
410        parser.add_option("-u",
411                          "--username",
412                          dest="username",
413                          help="username to generate a SAML assertion for")
414
415        opt = parser.parse_args()[0]
416
417        if not opt.configFilePath:
418            msg = "Error: no configuration file set.\n\n" + parser.format_help()
419            raise SystemExit(msg)
420        elif not opt.username:
421            msg = "Error: no username set.\n\n" + parser.format_help()
422            raise SystemExit(msg)
423       
424        certExtApp = cls.fromConfigFile(opt.configFilePath)
425        assertion = certExtApp(opt.username)
426        print(assertion)
427
Note: See TracBrowser for help on using the repository browser.