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

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

Incomplete - task 2: XACML-Security Integration

  • migrating to ndg.saml and ndg.soap imports now that SAML WSGI middleware has moved to ndg.saml egg.
  • Property svn:keywords set to Id
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 ndg.saml.utils import SAMLDateTime
28from ndg.saml.common.xml import SAMLConstants
29from ndg.saml.saml2.core import (Attribute, 
30                             SAMLVersion, 
31                             Subject, 
32                             NameID, 
33                             Issuer, 
34                             AttributeQuery, 
35                             XSStringAttributeValue, 
36                             Status,
37                             StatusCode,
38                             StatusMessage)
39from ndg.saml.xml.etree import AssertionElementTree, ResponseElementTree
40   
41from ndg.saml.saml2.binding.soap.client.attributequery import \
42                                                AttributeQuerySslSOAPBinding
43from ndg.security.common.saml_utils.esg import (EsgSamlNamespaces,
44                                                EsgDefaultQueryAttributes)
45from ndg.security.common.utils.etree import prettyPrint
46from ndg.security.common.X509 import X500DN
47from ndg.security.server.wsgi.openid.provider import IdentityMapping
48from ndg.security.common.utils.configfileparsers import (     
49                                                    CaseSensitiveConfigParser,)
50
51class CertExtAppError(Exception):
52    """Base class for CertExtApp class exceptions"""
53   
54   
55class CertExtAppConfigError(CertExtAppError):
56    """Configuration fault for CertExtApp instance"""
57
58
59class CertExtAppRetrieveError(CertExtAppError):
60    """Error retrieving results from user database or attribute authority"""
61   
62
63class CertExtAppSqlError(CertExtAppError):   
64    """Error with SQL query syntax"""
65   
66   
67class CertExtAppSamlResponseError(CertExtAppError):
68    """Attribute Authority returned a SAML Response error code"""
69    def __init__(self, *arg, **kw):
70        CertExtAppError.__init__(self, *arg, **kw)
71        self.__status = Status()
72        self.__status.statusCode = StatusCode()
73        self.__status.statusMessage = StatusMessage()
74   
75    def _getStatus(self):
76        '''Gets the Status of this response.
77       
78        @return the Status of this response
79        '''
80        return self.__status
81
82    def _setStatus(self, value):
83        '''Sets the Status of this response.
84       
85        @param value: the Status of this response
86        '''
87        if not isinstance(value, Status):
88            raise TypeError('"status" must be a %r, got %r' % (Status,
89                                                               type(value)))
90        self.__status = value
91       
92    status = property(fget=_getStatus, fset=_setStatus, 
93                      doc="Attribute Authority SAML Response error status")
94   
95    def __str__(self):
96        if self.status is not None:
97            return self.status.statusMessage.value or ''
98        else:
99            return ''
100   
101           
102class CertExtApp(object):
103    """Application to create a X.509 certificate extension containing a SAML
104    assertion for inclusion by MyProxy into an issued certificate
105    """
106    DEFAULT_QUERY_ATTRIBUTES = EsgDefaultQueryAttributes.ATTRIBUTES
107    N_DEFAULT_QUERY_ATTRIBUTES = len(DEFAULT_QUERY_ATTRIBUTES)
108    ESG_NAME_ID_FORMAT = EsgSamlNamespaces.NAMEID_FORMAT
109   
110    CONNECTION_STRING_OPTNAME = 'connectionString'
111    OPENID_SQLQUERY_OPTNAME = 'openIdSqlQuery'
112    ATTRIBUTE_AUTHORITY_URI_OPTNAME = 'attributeAuthorityURI'
113   
114    CONFIG_FILE_OPTNAMES = (
115        ATTRIBUTE_AUTHORITY_URI_OPTNAME,
116        CONNECTION_STRING_OPTNAME,
117        OPENID_SQLQUERY_OPTNAME,
118    )
119    ATTRIBUTE_QUERY_ATTRNAME = 'attributeQuery'
120    LEN_ATTRIBUTE_QUERY_ATTRNAME = len(ATTRIBUTE_QUERY_ATTRNAME)
121    __PRIVATE_ATTR_PREFIX = '__'
122    __slots__ = tuple(
123        [__PRIVATE_ATTR_PREFIX + i
124         for i in CONFIG_FILE_OPTNAMES + (ATTRIBUTE_QUERY_ATTRNAME,)]
125    )
126    del i
127   
128    def __init__(self):
129        self.__attributeAuthorityURI = None
130        self.__connectionString = None
131        self.__openIdSqlQuery = None
132        self.__attributeQuery = AttributeQuerySslSOAPBinding() 
133
134    @classmethod
135    def fromConfigFile(cls, configFilePath, **kw):
136        '''Alternative constructor makes object from config file settings
137        @type configFilePath: basestring
138        @param configFilePath: configuration file path
139        '''
140        certExtApp = cls()
141        certExtApp.readConfig(configFilePath, **kw)
142       
143        return certExtApp
144       
145    def __call__(self, username):
146        """Main method - create SAML assertion by querying the user's OpenID
147        identifier from the user database and using this to query the
148        Attribute Authority for attributes
149        """
150        self.__attributeQuery.subjectID = self.queryOpenId(username)
151        response = self.__attributeQuery.send(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    # Essential to have slots declaration otherwise superclass __setattr__
322    # will not behave correctly
323    __slots__ = ()
324   
325    @classmethod
326    def run(cls):
327        """Parse command line arguments and run the query specified"""
328
329        if cls.DEBUG_ENVVAR_NAME in os.environ:
330            import pdb
331            pdb.set_trace()
332
333        parser = optparse.OptionParser()
334
335        parser.add_option("-f",
336                          "--config-file",
337                          dest="configFilePath",
338                          help="ini style configuration file path containing "
339                               "the options: connectionString, "
340                               "openIdSqlQuery, identityUriTemplate, "
341                               "attributeAuthorityURI and issuerDN.  The file "
342                               "can also contain sections to configure logging "
343                               "using the standard logging module log file "
344                               "format")
345
346        parser.add_option("-u",
347                          "--username",
348                          dest="username",
349                          help="username to generate a SAML assertion for")
350
351        opt = parser.parse_args()[0]
352
353        if not opt.configFilePath:
354            msg = "Error: no configuration file set.\n\n" + parser.format_help()
355            raise SystemExit(msg)
356        elif not opt.username:
357            msg = "Error: no username set.\n\n" + parser.format_help()
358            raise SystemExit(msg)
359       
360        # Enable the setting of logging configuration from config file
361        from logging.config import fileConfig
362        from ConfigParser import NoSectionError
363        try:
364            fileConfig(opt.configFilePath)
365        except NoSectionError:
366            pass
367
368        certExtApp = cls.fromConfigFile(opt.configFilePath)
369        assertion = certExtApp(opt.username)
370        print(assertion)
371
Note: See TracBrowser for help on using the repository browser.