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

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

Refactoring MyProxy? Cert Extension app following updates to the SAML Attribute Query client interface

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, timedelta
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.utils import SAMLDateTime
28from saml.common.xml import SAMLConstants
29from saml.saml2.core import (Attribute, 
30                             SAMLVersion, 
31                             Subject, 
32                             NameID, 
33                             Issuer, 
34                             AttributeQuery, 
35                             XSStringAttributeValue, 
36                             Status,
37                             StatusCode,
38                             StatusMessage)
39from saml.xml.etree import AssertionElementTree, ResponseElementTree
40   
41from ndg.security.common.saml_utils.bindings import AttributeQuerySslSOAPBinding
42from ndg.security.common.saml_utils.esg import (EsgSamlNamespaces,
43                                                EsgDefaultQueryAttributes)
44from ndg.security.common.utils.etree import prettyPrint
45from ndg.security.common.X509 import X500DN
46from ndg.security.server.wsgi.openid.provider import IdentityMapping
47from ndg.security.common.utils.configfileparsers import (     
48                                                    CaseSensitiveConfigParser,)
49
50class CertExtAppError(Exception):
51    """Base class for CertExtApp class exceptions"""
52   
53   
54class CertExtAppConfigError(CertExtAppError):
55    """Configuration fault for CertExtApp instance"""
56
57
58class CertExtAppRetrieveError(CertExtAppError):
59    """Error retrieving results from user database or attribute authority"""
60   
61
62class CertExtAppSqlError(CertExtAppError):   
63    """Error with SQL query syntax"""
64   
65   
66class CertExtAppSamlResponseError(CertExtAppError):
67    """Attribute Authority returned a SAML Response error code"""
68    def __init__(self, *arg, **kw):
69        CertExtAppError.__init__(self, *arg, **kw)
70        self.__status = Status()
71        self.__status.statusCode = StatusCode()
72        self.__status.statusMessage = StatusMessage()
73   
74    def _getStatus(self):
75        '''Gets the Status of this response.
76       
77        @return the Status of this response
78        '''
79        return self.__status
80
81    def _setStatus(self, value):
82        '''Sets the Status of this response.
83       
84        @param value: the Status of this response
85        '''
86        if not isinstance(value, Status):
87            raise TypeError('"status" must be a %r, got %r' % (Status,
88                                                               type(value)))
89        self.__status = value
90       
91    status = property(fget=_getStatus, fset=_setStatus, 
92                      doc="Attribute Authority SAML Response error status")
93   
94    def __str__(self):
95        if self.status is not None:
96            return self.status.statusMessage.value or ''
97        else:
98            return ''
99   
100           
101class CertExtApp(object):
102    """Application to create a X.509 certificate extension containing a SAML
103    assertion for inclusion by MyProxy into an issued certificate
104    """
105    DEFAULT_QUERY_ATTRIBUTES = EsgDefaultQueryAttributes.ATTRIBUTES
106    N_DEFAULT_QUERY_ATTRIBUTES = len(DEFAULT_QUERY_ATTRIBUTES)
107    ESG_NAME_ID_FORMAT = EsgSamlNamespaces.NAMEID_FORMAT
108   
109    CONNECTION_STRING_OPTNAME = 'connectionString'
110    OPENID_SQLQUERY_OPTNAME = 'openIdSqlQuery'
111    ATTRIBUTE_AUTHORITY_URI_OPTNAME = 'attributeAuthorityURI'
112   
113    CONFIG_FILE_OPTNAMES = (
114        ATTRIBUTE_AUTHORITY_URI_OPTNAME,
115        CONNECTION_STRING_OPTNAME,
116        OPENID_SQLQUERY_OPTNAME,
117    )
118    ATTRIBUTE_QUERY_ATTRNAME = 'attributeQuery'
119    LEN_ATTRIBUTE_QUERY_ATTRNAME = len(ATTRIBUTE_QUERY_ATTRNAME)
120    __slots__ = CONFIG_FILE_OPTNAMES + (ATTRIBUTE_QUERY_ATTRNAME,)
121    __PRIVATE_ATTR_PREFIX = '_CertExtApp__'
122    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__])
123    del i
124   
125    def __init__(self):
126        self.__attributeAuthorityURI = None
127        self.__connectionString = None
128        self.__openIdSqlQuery = None
129        self.__attributeQuery = AttributeQuerySslSOAPBinding() 
130        self.__attributeQuery.queryAttributes = \
131            CertExtApp.DEFAULT_QUERY_ATTRIBUTES
132
133    @classmethod
134    def fromConfigFile(cls, configFilePath, **kw):
135        '''Alternative constructor makes object from config file settings
136        @type configFilePath: basestring
137        @param configFilePath: configuration file path
138        '''
139        certExtApp = cls()
140        certExtApp.readConfig(configFilePath, **kw)
141       
142        return certExtApp
143       
144    def __call__(self, username):
145        """Main method - create SAML assertion by querying the user's OpenID
146        identifier from the user database and using this to query the
147        Attribute Authority for attributes
148        """
149        self.__attributeQuery.subjectID = self.queryOpenId(username)
150        response = self.__attributeQuery.send(
151                                                uri=self.attributeAuthorityURI)
152       
153        try:
154            assertionStr = self.serialiseAssertion(response.assertions[0])
155           
156        except (IndexError, TypeError):
157            raise CertExtAppRetrieveError("Error accessing assertion from "
158                                          "Attribute Authority SAML response: "
159                                          "%s" % traceback.format_exc())
160           
161        return assertionStr
162
163    def readConfig(self, cfg, prefix='', section='DEFAULT'):
164        '''Read config file settings
165        @type cfg: basestring /ConfigParser derived type
166        @param cfg: configuration file path or ConfigParser type object
167        @type prefix: basestring
168        @param prefix: prefix for option names e.g. "certExtApp."
169        @type section: baestring
170        @param section: configuration file section from which to extract
171        parameters.
172        '''
173        if isinstance(cfg, basestring):
174            cfgFilePath = os.path.expandvars(cfg)
175            _cfg = CaseSensitiveConfigParser()
176            _cfg.read(cfgFilePath)
177           
178        elif isinstance(cfg, ConfigParser):
179            _cfg = cfg   
180        else:
181            raise AttributeError('Expecting basestring or ConfigParser type '
182                                 'for "cfg" attribute; got %r type' % type(cfg))
183       
184        prefixLen = len(prefix)
185        for optName, val in _cfg.items(section):
186            if prefix:
187                # Filter attributes based on prefix
188                if optName.startswith(prefix):
189                    setattr(self, optName[prefixLen:], val)
190            else:
191                # No prefix set - attempt to set all attributes   
192                setattr(self, optName, val)
193           
194    def __setattr__(self, name, value):
195        """Enable setting of AttributeQuerySslSOAPBinding attributes from
196        names starting with attributeQuery.* / attributeQuery_*.  Addition for
197        setting these values from ini file
198        """
199        try:
200            super(CertExtApp, self).__setattr__(name, value)
201           
202        except AttributeError:
203            # Coerce into setting AttributeQuerySslSOAPBinding attributes -
204            # names must start with 'attributeQuery\W' e.g.
205            # attributeQuery.clockSkew or attributeQuery_issuerDN
206            if name.startswith(CertExtApp.ATTRIBUTE_QUERY_ATTRNAME):               
207                setattr(self.__attributeQuery, 
208                        name[CertExtApp.LEN_ATTRIBUTE_QUERY_ATTRNAME+1:], 
209                        value)
210            else:
211                raise
212
213    @property
214    def attributeQuery(self):
215        """SAML SOAP Attribute Query client binding object"""
216        return self.__attributeQuery
217   
218    def _getAttributeAuthorityURI(self):
219        return self.__attributeAuthorityURI
220
221    def _setAttributeAuthorityURI(self, value):
222        if not isinstance(value, basestring):
223            raise TypeError('Expecting string type for "attributeAuthorityURI";'
224                            ' got %r instead' % type(value))
225        self.__attributeAuthorityURI = value
226
227    attributeAuthorityURI = property(_getAttributeAuthorityURI,
228                                     _setAttributeAuthorityURI, 
229                                     doc="Attribute Authority SOAP SAML URI")
230
231    def _getConnectionString(self):
232        return self.__connectionString
233
234    def _setConnectionString(self, value):
235        if not isinstance(value, basestring):
236            raise TypeError('Expecting string type for "%s" attribute; got %r'%
237                            (CertExtApp.CONNECTION_STRING_OPTNAME,
238                             type(value)))
239        self.__connectionString = os.path.expandvars(value)
240
241    connectionString = property(fget=_getConnectionString, 
242                                fset=_setConnectionString, 
243                                doc="Database connection string")
244
245    def _getOpenIdSqlQuery(self):
246        return self.__openIdSqlQuery
247
248    def _setOpenIdSqlQuery(self, value):
249        if not isinstance(value, basestring):
250            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
251                        (CertExtApp.OPENID_SQLQUERY_OPTNAME,
252                         type(value)))
253        self.__openIdSqlQuery = value
254
255    openIdSqlQuery = property(fget=_getOpenIdSqlQuery, 
256                        fset=_setOpenIdSqlQuery, 
257                        doc="SQL Query for authentication request")
258       
259    def __getstate__(self):
260        '''Specific implementation needed with __slots__'''
261        return dict([(attrName, getattr(self, attrName)) 
262                     for attrName in CertExtApp.__slots__])
263       
264    def __setstate__(self, attrDict):
265        '''Specific implementation needed with __slots__'''
266        for attr, val in attrDict.items():
267            setattr(self, attr, val)
268   
269    def serialiseAssertion(self, assertion):
270        """Convert SAML assertion object into a string"""
271        samlAssertionElem = AssertionElementTree.toXML(assertion)
272        return ElementTree.tostring(samlAssertionElem)
273   
274    def queryOpenId(self, username):
275        """Given a username, query for user OpenID from the user
276        database
277
278        @type username: basestring
279        @param username: username
280        @rtype: basestring
281        @return: the OpenID identifier corresponding to the input username
282        """
283
284        try:
285            dbEngine = create_engine(self.connectionString)
286        except ImportError, e:
287            raise CertExtAppConfigError("Missing database engine for "
288                                        "SQLAlchemy: %s" % e)
289        connection = dbEngine.connect()
290       
291        try:
292            queryInputs = dict(username=username)
293            query = Template(self.openIdSqlQuery).substitute(queryInputs)
294            result = connection.execute(query)
295
296        except exc.ProgrammingError:
297            raise CertExtAppSqlError("Error with SQL Syntax: %s" %
298                                     traceback.format_exc())
299        finally:
300            connection.close()
301
302        try:
303            openId = [r for r in result][0][0]
304       
305        except Exception:
306            raise CertExtAppRetrieveError("Error with result set: %s" %
307                                          traceback.format_exc())
308       
309        log.debug('Query succeeded for user %r' % username)
310        return openId
311   
312   
313import optparse
314import os
315
316class CertExtConsoleApp(CertExtApp):
317    """Extend CertExtApp with functionality for command line options"""
318
319    DEBUG_ENVVAR_NAME = 'NDGSEC_MYPROXY_CERT_EXT_APP_DEBUG'
320
321    @classmethod
322    def run(cls):
323        """Parse command line arguments and run the query specified"""
324
325        if cls.DEBUG_ENVVAR_NAME in os.environ:
326            import pdb
327            pdb.set_trace()
328
329        parser = optparse.OptionParser()
330
331        parser.add_option("-f",
332                          "--config-file",
333                          dest="configFilePath",
334                          help="ini style configuration file path containing "
335                               "the options: connectionString, "
336                               "openIdSqlQuery, identityUriTemplate, "
337                               "attributeAuthorityURI and issuerDN.  The file "
338                               "can also contain sections to configure logging "
339                               "using the standard logging module log file "
340                               "format")
341
342        parser.add_option("-u",
343                          "--username",
344                          dest="username",
345                          help="username to generate a SAML assertion for")
346
347        opt = parser.parse_args()[0]
348
349        if not opt.configFilePath:
350            msg = "Error: no configuration file set.\n\n" + parser.format_help()
351            raise SystemExit(msg)
352        elif not opt.username:
353            msg = "Error: no username set.\n\n" + parser.format_help()
354            raise SystemExit(msg)
355       
356        # Enable the setting of logging configuration from config file
357        from logging.config import fileConfig
358        from ConfigParser import NoSectionError
359        try:
360            fileConfig(opt.configFilePath)
361        except NoSectionError:
362            pass
363
364        certExtApp = cls.fromConfigFile(opt.configFilePath)
365        assertion = certExtApp(opt.username)
366        print(assertion)
367
Note: See TracBrowser for help on using the repository browser.