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

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

1.3.0 Release

Completed MyProxy? SAML Attribute assertion callout and added 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
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 sys
386import os
387
388class CertExtConsoleApp(CertExtApp):
389    """Extend CertExtApp with functionality for command line options"""
390
391    DEBUG_ENVVAR_NAME = 'NDGSEC_MYPROXY_CERT_EXT_APP_DEBUG'
392
393    @classmethod
394    def run(cls):
395        """Parse command line arguments and run the query specified"""
396
397        if cls.DEBUG_ENVVAR_NAME in os.environ:
398            import pdb
399            pdb.set_trace()
400
401        parser = optparse.OptionParser()
402
403        parser.add_option("-f",
404                          "--config-file",
405                          dest="configFilePath",
406                          help="ini style configuration file path containing "
407                               "the options: connectionString, "
408                               "openIdSqlQuery, identityUriTemplate, "
409                               "attributeAuthorityURI and issuerDN")
410
411        parser.add_option("-u",
412                          "--username",
413                          dest="username",
414                          help="username to generate a SAML assertion for")
415
416        opt = parser.parse_args()[0]
417
418        if not opt.configFilePath:
419            msg = "Error: no configuration file set.\n\n" + parser.format_help()
420            raise SystemExit(msg)
421        elif not opt.username:
422            msg = "Error: no username set.\n\n" + parser.format_help()
423            raise SystemExit(msg)
424       
425        certExtApp = cls.fromConfigFile(opt.configFilePath)
426        assertion = certExtApp(opt.username)
427        print(assertion)
428
Note: See TracBrowser for help on using the repository browser.