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

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

Don't set default query attributes - set entirely from config settings as config settings may only append existing settings and cannot reset defaults. Re-release eggs with this fix.

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    __PRIVATE_ATTR_PREFIX = '__'
121    __slots__ = tuple(
122        [__PRIVATE_ATTR_PREFIX + i
123         for i in CONFIG_FILE_OPTNAMES + (ATTRIBUTE_QUERY_ATTRNAME,)]
124    )
125    del i
126   
127    def __init__(self):
128        self.__attributeAuthorityURI = None
129        self.__connectionString = None
130        self.__openIdSqlQuery = None
131        self.__attributeQuery = AttributeQuerySslSOAPBinding() 
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(uri=self.attributeAuthorityURI)
151       
152        try:
153            assertionStr = self.serialiseAssertion(response.assertions[0])
154           
155        except (IndexError, TypeError):
156            raise CertExtAppRetrieveError("Error accessing assertion from "
157                                          "Attribute Authority SAML response: "
158                                          "%s" % traceback.format_exc())
159           
160        return assertionStr
161
162    def readConfig(self, cfg, prefix='', section='DEFAULT'):
163        '''Read config file settings
164        @type cfg: basestring /ConfigParser derived type
165        @param cfg: configuration file path or ConfigParser type object
166        @type prefix: basestring
167        @param prefix: prefix for option names e.g. "certExtApp."
168        @type section: baestring
169        @param section: configuration file section from which to extract
170        parameters.
171        '''
172        if isinstance(cfg, basestring):
173            cfgFilePath = os.path.expandvars(cfg)
174            _cfg = CaseSensitiveConfigParser()
175            _cfg.read(cfgFilePath)
176           
177        elif isinstance(cfg, ConfigParser):
178            _cfg = cfg   
179        else:
180            raise AttributeError('Expecting basestring or ConfigParser type '
181                                 'for "cfg" attribute; got %r type' % type(cfg))
182       
183        prefixLen = len(prefix)
184        for optName, val in _cfg.items(section):
185            if prefix:
186                # Filter attributes based on prefix
187                if optName.startswith(prefix):
188                    setattr(self, optName[prefixLen:], val)
189            else:
190                # No prefix set - attempt to set all attributes   
191                setattr(self, optName, val)
192           
193    def __setattr__(self, name, value):
194        """Enable setting of AttributeQuerySslSOAPBinding attributes from
195        names starting with attributeQuery.* / attributeQuery_*.  Addition for
196        setting these values from ini file
197        """
198        try:
199            super(CertExtApp, self).__setattr__(name, value)
200           
201        except AttributeError:
202            # Coerce into setting AttributeQuerySslSOAPBinding attributes -
203            # names must start with 'attributeQuery\W' e.g.
204            # attributeQuery.clockSkew or attributeQuery_issuerDN
205            if name.startswith(CertExtApp.ATTRIBUTE_QUERY_ATTRNAME):               
206                setattr(self.__attributeQuery, 
207                        name[CertExtApp.LEN_ATTRIBUTE_QUERY_ATTRNAME+1:], 
208                        value)
209            else:
210                raise
211
212    @property
213    def attributeQuery(self):
214        """SAML SOAP Attribute Query client binding object"""
215        return self.__attributeQuery
216   
217    def _getAttributeAuthorityURI(self):
218        return self.__attributeAuthorityURI
219
220    def _setAttributeAuthorityURI(self, value):
221        if not isinstance(value, basestring):
222            raise TypeError('Expecting string type for "attributeAuthorityURI";'
223                            ' got %r instead' % type(value))
224        self.__attributeAuthorityURI = value
225
226    attributeAuthorityURI = property(_getAttributeAuthorityURI,
227                                     _setAttributeAuthorityURI, 
228                                     doc="Attribute Authority SOAP SAML URI")
229
230    def _getConnectionString(self):
231        return self.__connectionString
232
233    def _setConnectionString(self, value):
234        if not isinstance(value, basestring):
235            raise TypeError('Expecting string type for "%s" attribute; got %r'%
236                            (CertExtApp.CONNECTION_STRING_OPTNAME,
237                             type(value)))
238        self.__connectionString = os.path.expandvars(value)
239
240    connectionString = property(fget=_getConnectionString, 
241                                fset=_setConnectionString, 
242                                doc="Database connection string")
243
244    def _getOpenIdSqlQuery(self):
245        return self.__openIdSqlQuery
246
247    def _setOpenIdSqlQuery(self, value):
248        if not isinstance(value, basestring):
249            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
250                        (CertExtApp.OPENID_SQLQUERY_OPTNAME,
251                         type(value)))
252        self.__openIdSqlQuery = value
253
254    openIdSqlQuery = property(fget=_getOpenIdSqlQuery, 
255                        fset=_setOpenIdSqlQuery, 
256                        doc="SQL Query for authentication request")
257       
258    def __getstate__(self):
259        '''Specific implementation needed with __slots__'''
260        return dict([(attrName, getattr(self, attrName)) 
261                     for attrName in CertExtApp.__slots__])
262       
263    def __setstate__(self, attrDict):
264        '''Specific implementation needed with __slots__'''
265        for attr, val in attrDict.items():
266            setattr(self, attr, val)
267   
268    def serialiseAssertion(self, assertion):
269        """Convert SAML assertion object into a string"""
270        samlAssertionElem = AssertionElementTree.toXML(assertion)
271        return ElementTree.tostring(samlAssertionElem)
272   
273    def queryOpenId(self, username):
274        """Given a username, query for user OpenID from the user
275        database
276
277        @type username: basestring
278        @param username: username
279        @rtype: basestring
280        @return: the OpenID identifier corresponding to the input username
281        """
282
283        try:
284            dbEngine = create_engine(self.connectionString)
285        except ImportError, e:
286            raise CertExtAppConfigError("Missing database engine for "
287                                        "SQLAlchemy: %s" % e)
288        connection = dbEngine.connect()
289       
290        try:
291            queryInputs = dict(username=username)
292            query = Template(self.openIdSqlQuery).substitute(queryInputs)
293            result = connection.execute(query)
294
295        except exc.ProgrammingError:
296            raise CertExtAppSqlError("Error with SQL Syntax: %s" %
297                                     traceback.format_exc())
298        finally:
299            connection.close()
300
301        try:
302            openId = [r for r in result][0][0]
303       
304        except Exception:
305            raise CertExtAppRetrieveError("Error with result set: %s" %
306                                          traceback.format_exc())
307       
308        log.debug('Query succeeded for user %r' % username)
309        return openId
310   
311   
312import optparse
313import os
314
315class CertExtConsoleApp(CertExtApp):
316    """Extend CertExtApp with functionality for command line options"""
317
318    DEBUG_ENVVAR_NAME = 'NDGSEC_MYPROXY_CERT_EXT_APP_DEBUG'
319   
320    # Essential to have slots declaration otherwise superclass __setattr__
321    # will not behave correctly
322    __slots__ = ()
323   
324    @classmethod
325    def run(cls):
326        """Parse command line arguments and run the query specified"""
327
328        if cls.DEBUG_ENVVAR_NAME in os.environ:
329            import pdb
330            pdb.set_trace()
331
332        parser = optparse.OptionParser()
333
334        parser.add_option("-f",
335                          "--config-file",
336                          dest="configFilePath",
337                          help="ini style configuration file path containing "
338                               "the options: connectionString, "
339                               "openIdSqlQuery, identityUriTemplate, "
340                               "attributeAuthorityURI and issuerDN.  The file "
341                               "can also contain sections to configure logging "
342                               "using the standard logging module log file "
343                               "format")
344
345        parser.add_option("-u",
346                          "--username",
347                          dest="username",
348                          help="username to generate a SAML assertion for")
349
350        opt = parser.parse_args()[0]
351
352        if not opt.configFilePath:
353            msg = "Error: no configuration file set.\n\n" + parser.format_help()
354            raise SystemExit(msg)
355        elif not opt.username:
356            msg = "Error: no username set.\n\n" + parser.format_help()
357            raise SystemExit(msg)
358       
359        # Enable the setting of logging configuration from config file
360        from logging.config import fileConfig
361        from ConfigParser import NoSectionError
362        try:
363            fileConfig(opt.configFilePath)
364        except NoSectionError:
365            pass
366
367        certExtApp = cls.fromConfigFile(opt.configFilePath)
368        assertion = certExtApp(opt.username)
369        print(assertion)
370
Note: See TracBrowser for help on using the repository browser.