source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/attributeauthority.py @ 5971

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/attributeauthority.py@5971
Revision 5971, 94.6 KB checked in by pjkersha, 10 years ago (diff)

Adding SQLAlchemy based AttributeInterface? class for Attribute Authority

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""NDG Attribute Authority server side code
2
3handles security user attribute (role) allocation
4
5NERC Data Grid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "15/04/05"
9__copyright__ = "(C) 2009 Science and Technology Facilities Council"
10__license__ = "BSD - see LICENSE file in top-level directory"
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = '$Id:attributeauthority.py 4367 2008-10-29 09:27:59Z pjkersha $'
13import logging
14log = logging.getLogger(__name__)
15
16import os
17import re
18
19# For parsing of properties file
20try: # python 2.5
21    from xml.etree import cElementTree as ElementTree
22except ImportError:
23    # if you've installed it yourself it comes this way
24    import cElementTree as ElementTree
25
26# SAML 2.0 Attribute Query Support - added 20/08/2009
27from uuid import uuid4
28from datetime import datetime, timedelta
29
30from saml.utils import SAMLDateTime
31from saml.saml2.core import (Response, Assertion, Attribute, AttributeValue, 
32    AttributeStatement, SAMLVersion, Subject, NameID, Issuer, AttributeQuery, 
33    XSStringAttributeValue, XSGroupRoleAttributeValue, Conditions, Status, 
34    StatusCode, StatusMessage)
35   
36from saml.common.xml import SAMLConstants
37from saml.xml.etree import (AssertionElementTree, AttributeQueryElementTree, 
38    ResponseElementTree, XSGroupRoleAttributeValueElementTree)
39
40from ndg.security.common.utils import TypedList
41from ndg.security.common.utils.classfactory import instantiateClass
42from ndg.security.common.utils.configfileparsers import (
43    CaseSensitiveConfigParser, )
44   
45# X.509 Certificate handling
46from ndg.security.common.X509 import X509Cert, X500DN
47
48# NDG Attribute Certificate
49from ndg.security.common.AttCert import AttCert
50
51
52class AttributeAuthorityError(Exception):
53    """Exception handling for NDG Attribute Authority class."""
54    def __init__(self, msg):
55        log.error(msg)
56        Exception.__init__(self, msg)
57
58
59class AttributeAuthorityConfigError(Exception):
60    """NDG Attribute Authority error with configuration. e.g. properties file
61    directory permissions or role mapping file"""
62    def __init__(self, msg):
63        log.error(msg)
64        Exception.__init__(self, msg)
65       
66       
67class AttributeAuthorityAccessDenied(AttributeAuthorityError):
68    """NDG Attribute Authority - access denied exception.
69
70    Raise from getAttCert method where no roles are available for the user
71    but that the request is otherwise valid.  In all other error cases raise
72    AttributeAuthorityError"""   
73   
74   
75class AttributeAuthorityNoTrustedHosts(AttributeAuthorityError):
76    """Raise from getTrustedHosts if there are no trusted hosts defined in
77    the map configuration"""
78
79
80class AttributeAuthorityNoMatchingRoleInTrustedHosts(AttributeAuthorityError):
81    """Raise from getTrustedHosts if there is no mapping to any of the
82    trusted hosts for the given input role name"""
83
84
85class AttributeAuthority(object):
86    """NDG Attribute Authority - service for allocation of user authorization
87    tokens - attribute certificates.
88   
89    @type propertyDefaults: dict
90    @cvar propertyDefaults: valid configuration property keywords
91   
92    @type attributeInterfacePropertyDefaults: dict
93    @cvar attributeInterfacePropertyDefaults: valid configuration property
94    keywords for the Attribute Interface plugin
95   
96    @type mapConfigHostDefaults: dict
97    @cvar mapConfigHostDefaults: valid configuration property
98    keywords for the Map Configuration XML Host element
99   
100    @type DEFAULT_CONFIG_DIRNAME: string
101    @cvar DEFAULT_CONFIG_DIRNAME: configuration directory under $NDGSEC_DIR -
102    default location for properties file
103   
104    @type DEFAULT_PROPERTY_FILENAME: string
105    @cvar DEFAULT_PROPERTY_FILENAME: default file name for properties file
106    under DEFAULT_CONFIG_DIRNAME
107   
108    @type ATTRIBUTE_INTERFACE_KEYNAME: basestring
109    @param ATTRIBUTE_INTERFACE_KEYNAME: attribute interface parameters key
110    name - see initAttributeInterface for details
111    """
112
113    # Code designed from NERC Data Grid Enterprise and Information Viewpoint
114    # documents.
115    #
116    # Also, draws from Neil Bennett's ACServer class used in the Java
117    # implementation of NDG Security
118
119    DEFAULT_CONFIG_DIRNAME = "conf"
120    DEFAULT_PROPERTY_FILENAME = "attributeAuthority.cfg"
121    ATTRIBUTE_INTERFACE_KEYNAME = 'attributeInterface'
122    CONFIG_LIST_SEP_PAT = re.compile(',\W*')
123   
124    attributeInterfacePropertyDefaults = {
125        'modFilePath':  '',
126        'modName':      '',
127        'className':    ''
128    }
129   
130    # valid configuration property keywords with accepted default values. 
131    # Values set to not NotImplemented here denote keys which must be specified
132    # in the config
133    propertyDefaults = { 
134        'name':                         '',
135        'signingCertFilePath':          '',
136        'signingPriKeyFilePath':        '',
137        'signingPriKeyPwd':             None,
138        'caCertFilePathList':           [],
139        'attCertLifetime':              -1,
140        'attCertNotBeforeOff':          0.,
141        'clockSkew':                    timedelta(seconds=0.),
142        'attCertFileName':              '',
143        'attCertFileLogCnt':            0,
144        'mapConfigFilePath':            '',
145        'attCertDir':                   '',
146        'dnSeparator':                  '/',
147        ATTRIBUTE_INTERFACE_KEYNAME:    attributeInterfacePropertyDefaults
148    }
149   
150    mapConfigHostDefaults = {
151        'siteName':                 None,
152        'aaURI':                    NotImplemented,
153        'aaDN':                     NotImplemented,
154        'loginURI':                 NotImplemented,
155        'loginServerDN':            NotImplemented,
156        'loginRequestServerDN':     NotImplemented
157    }
158
159    def __init__(self):
160        """Create new Attribute Authority instance"""
161        log.info("Initialising service ...")
162       
163        # Initial config file property based attributes
164        for name, val in AttributeAuthority.propertyDefaults.items():
165            setattr(self, '_AttributeAuthority__%s' % name, val)
166       
167        self.__caCertFilePathList = TypedList(basestring)
168       
169        self.__propFilePath = None       
170        self.__propFileSection = 'DEFAULT'
171        self.__propPrefix = ''
172       
173        # Initialise role mapping look-ups - These are set in readMapConfig()
174        self.__mapConfig = None
175        self.__localRole2RemoteRole = None
176        self.__remoteRole2LocalRole = None
177       
178        self.__cert = None
179       
180        # Issuer details - serialise using the separator string set in the
181        # properties file
182        self.__issuer = None
183        self.__issuerSerialNumber = None
184        self.__attCertLog = None
185        self.__name = None
186       
187        self.__attributeInterfaceCfg = {}
188
189    def _getMapConfig(self):
190        return self.__mapConfig
191
192    def _getCert(self):
193        return self.__cert
194
195    def _getIssuer(self):
196        return self.__issuer
197
198    def _getIssuerSerialNumber(self):
199        return self.__issuerSerialNumber
200
201    def _getAttCertLog(self):
202        return self.__attCertLog
203
204    def _getName(self):
205        return self.__name
206
207    def _getAttCertLifetime(self):
208        return self.__attCertLifetime
209
210    def _getAttCertNotBeforeOff(self):
211        return self.__attCertNotBeforeOff
212
213    def _getClockSkew(self):
214        return self.__clockSkew
215
216    def _getAttCertDir(self):
217        return self.__attCertDir
218
219    def _getAttributeInterface(self):
220        return self.__attributeInterface
221
222    def _getTrustedHostInfo(self):
223        return self.__trustedHostInfo
224
225    def _setCert(self, value):
226        if not isinstance(value, X509Cert):
227            raise TypeError('Expecting %r type for "cert"; got %r' %
228                            (X509Cert, type(value)))
229           
230        self.__cert = value
231
232    def _setIssuer(self, value):
233        self.__issuer = value
234
235    def _setIssuerSerialNumber(self, value):
236        if not isinstance(value, (long, int)):
237            raise TypeError('Expecting long or int type for "name"; got %r' %
238                            type(value))
239        self.__issuerSerialNumber = value
240
241    def _setAttCertLog(self, value):
242        if not isinstance(value, AttCertLog):
243            raise TypeError('Expecting %r type for "attCertLog"; got %r' %
244                            (AttCertLog, type(value)))
245        self.__attCertLog = value
246
247    def _setName(self, value):
248        if not isinstance(value, basestring):
249            raise TypeError('Expecting string type for "name"; got %r' %
250                            type(value))
251        self.__name = value
252
253    def _setAttCertLifetime(self, value):
254        if isinstance(value, float):
255            self.__attCertLifetime = value
256           
257        elif isinstance(value, (basestring, int, long)):
258            self.__attCertLifetime = float(value)
259        else:
260            raise TypeError('Expecting float, int, long or string type for '
261                            '"attCertLifetime"; got %r' % type(value))
262
263    def _setAttCertNotBeforeOff(self, value):
264        if isinstance(value, float):
265            self.__attCertNotBeforeOff = value
266           
267        elif isinstance(value, (basestring, int, long)):
268            self.__attCertNotBeforeOff = float(value)
269        else:
270            raise TypeError('Expecting float, int, long or string type for '
271                            '"attCertNotBeforeOff"; got %r' % type(value))
272
273    def _setClockSkew(self, value):
274        if isinstance(value, (float, int, long)):
275            self.__clockSkew = timedelta(seconds=value)
276           
277        elif isinstance(value, basestring):
278            self.__clockSkew = timedelta(seconds=float(value))
279        else:
280            raise TypeError('Expecting float, int, long or string type for '
281                            '"clockSkew"; got %r' % type(value))
282
283    def _setAttCertDir(self, value):
284        if not isinstance(value, basestring):
285            raise TypeError('Expecting string type for "attCertDir"; got %r' % 
286                            type(value))
287
288        # Check directory path
289        try:
290            dirList = os.listdir(value)
291
292        except OSError, osError:
293            raise AttributeAuthorityConfigError('Invalid directory path for '
294                                                'Attribute Certificates store '
295                                                '"%s": %s' % 
296                                                (value, osError.strerror))
297        self.__attCertDir = value
298
299    def _setTrustedHostInfo(self, value):
300        self.__trustedHostInfo = value
301
302    def _get_caCertFilePathList(self):
303        return self.__caCertFilePathList
304
305    def _set_caCertFilePathList(self, val):
306        if not isinstance(val, (list, tuple)):
307            raise TypeError('Expecting list or tuple type for '
308                            '"caCertFilePathList"; got %r' % type(val))
309           
310        # Overwrite any original settings
311        self.__caCertFilePathList = TypedList(basestring)
312       
313        # Update with new items
314        self.__caCertFilePathList += val
315   
316    caCertFilePathList = property(fget=_get_caCertFilePathList,
317                                  fset=_set_caCertFilePathList,
318                                  doc="list of file paths for CA certificates "
319                                      "used to validate an Attribute "
320                                      "Certificate")
321   
322    def _get_signingCertFilePath(self):
323        return self.__signingCertFilePath
324   
325    def _set_signingCertFilePath(self, value):
326        if not isinstance(value, basestring):
327            raise TypeError('Expecting string type for "signingCertFilePath"; '
328                            'got %r' % type(value))
329        self.__signingCertFilePath = value
330         
331    signingCertFilePath = property(fget=_get_signingCertFilePath, 
332                                   fset=_set_signingCertFilePath,
333                                   doc="X.509 certificate used for Attribute "
334                                       "certificate signature")
335   
336    def _get_signingPriKeyFilePath(self):
337        return self.__signingPriKeyFilePath
338   
339    def _set_signingPriKeyFilePath(self, value):
340        if not isinstance(value, basestring):
341            raise TypeError('Expecting string type for '
342                            '"signingPriKeyFilePath"; got %r' % type(value))
343        self.__signingPriKeyFilePath = value
344         
345    signingPriKeyFilePath = property(fget=_get_signingPriKeyFilePath, 
346                                     fset=_set_signingPriKeyFilePath,
347                                     doc="File Path for private key used to "
348                                         "sign Attribute certificate")
349   
350    def _get_signingPriKeyPwd(self):
351        return self.__signingPriKeyPwd
352   
353    def _set_signingPriKeyPwd(self, value):
354        if not isinstance(value, (type(None), basestring)):
355            raise TypeError('Expecting string or None type for '
356                            '"signingPriKeyPwd"; got %r' % type(value))
357        self.__signingPriKeyPwd = value
358         
359    signingPriKeyPwd = property(fget=_get_signingPriKeyPwd, 
360                                fset=_set_signingPriKeyPwd,
361                                doc="Password for private key file used to "
362                                    "for Attribute certificate signature")
363
364    def _get_attributeInterfaceCfg(self):
365        return self.__attributeInterfaceCfg
366   
367    attributeInterfaceCfg = property(fget=_get_attributeInterfaceCfg,
368                                     doc="Settings for Attribute Interface "
369                                         "initialisation")
370   
371    def _get_attCertFileName(self):
372        return self.__attCertFileName
373   
374    def _set_attCertFileName(self, value):
375        if not isinstance(value, basestring):
376            raise TypeError('Expecting string type for "attCertFileName"; got '
377                            '%r' % type(value))
378           
379        self.__attCertFileName = value
380         
381    attCertFileName = property(fget=_get_attCertFileName, 
382                                fset=_set_attCertFileName,
383                                doc="Attribute certificate file name for log "
384                                    "initialisation")
385   
386    def _get_attCertFileLogCnt(self):
387        return self.__attCertFileLogCnt
388   
389    def _set_attCertFileLogCnt(self, value):
390        if isinstance(value, int):
391            self.__attCertFileLogCnt = value
392        elif isinstance(value, basestring):
393            self.__attCertFileLogCnt = int(value)
394        else:
395            raise TypeError('Expecting int or string type for '
396                            '"attCertFileLogCnt"; got %r' % type(value))
397         
398    attCertFileLogCnt = property(fget=_get_attCertFileLogCnt, 
399                                 fset=_set_attCertFileLogCnt,
400                                 doc="Counter for Attribute Certificate log "
401                                     "rotating file handler")
402   
403    def _get_dnSeparator(self):
404        return self.__dnSeparator
405   
406    def _set_dnSeparator(self, value):
407        if not isinstance(value, basestring):
408            raise TypeError('Expecting string type for "dnSeparator"; got '
409                            '%r' % type(value))
410        self.__dnSeparator = value
411         
412    dnSeparator = property(fget=_get_dnSeparator, 
413                           fset=_set_dnSeparator,
414                           doc="Distinguished Name separator character used "
415                               "with X.509 Certificate issuer certificate")
416           
417    def _getMapConfigFilePath(self):
418        return self.__mapConfigFilePath
419   
420    def _setMapConfigFilePath(self, val):
421        if not isinstance(val, basestring):
422            raise AttributeAuthorityConfigError("Input Map Configuration "
423                                                "file path must be a "
424                                                "valid string.")
425        self.__mapConfigFilePath = val
426         
427    mapConfigFilePath = property(fget=_getMapConfigFilePath,
428                                 fset=_setMapConfigFilePath,
429                                 doc="File path for Role Mapping "
430                                     "configuration") 
431
432    def setPropFilePath(self, val=None):
433        """Set properties file from input or based on environment variable
434        settings
435       
436        @type val: basestring
437        @param val: properties file path"""
438        log.debug("Setting property file path")
439        if not val:
440            if 'NDGSEC_AA_PROPFILEPATH' in os.environ:
441                val = os.environ['NDGSEC_AA_PROPFILEPATH']
442               
443            elif 'NDGSEC_DIR' in os.environ:
444                val = os.path.join(os.environ['NDGSEC_DIR'], 
445                                   AttributeAuthority.DEFAULT_CONFIG_DIRNAME,
446                                   AttributeAuthority.DEFAULT_PROPERTY_FILENAME)
447            else:
448                raise AttributeError('Unable to set default Attribute '
449                                     'Authority properties file path: neither '
450                                     '"NDGSEC_AA_PROPFILEPATH" or "NDGSEC_DIR"'
451                                     ' environment variables are set')
452               
453        if not isinstance(val, basestring):
454            raise AttributeError("Input Properties file path "
455                                 "must be a valid string.")
456     
457        self.__propFilePath = os.path.expandvars(val)
458        log.debug("Path set to: %s" % val)
459       
460    def getPropFilePath(self):
461        '''Get the properties file path
462       
463        @rtype: basestring
464        @return: properties file path'''
465        return self.__propFilePath
466       
467    # Also set up as a property
468    propFilePath = property(fset=setPropFilePath,
469                            fget=getPropFilePath,
470                            doc="path to file containing Attribute Authority "
471                                "configuration parameters.  It defaults to "
472                                "$NDGSEC_AA_PROPFILEPATH or if not set, "
473                                "$NDGSEC_DIR/conf/attributeAuthority.cfg")   
474   
475    def setPropFileSection(self, val=None):
476        """Set section name to read properties from ini file.  This is set from
477        input or based on environment variable setting
478        NDGSEC_AA_PROPFILESECTION
479       
480        @type val: basestring
481        @param val: section name"""
482        if not val:
483            val = os.environ.get('NDGSEC_AA_PROPFILESECTION', 'DEFAULT')
484               
485        if not isinstance(val, basestring):
486            raise AttributeError("Input Properties file section name "
487                                 "must be a valid string.")
488     
489        self.__propFileSection = val
490        log.debug("Properties file section set to: \"%s\"" % val)
491       
492    def getPropFileSection(self):
493        '''Get the section name to extract properties from an ini file -
494        DOES NOT apply to XML file properties
495       
496        @rtype: basestring
497        @return: section name'''
498        return self.__propFileSection
499       
500    # Also set up as a property
501    propFileSection = property(fset=setPropFileSection,
502                               fget=getPropFileSection,
503                               doc="Set the file section name for ini file "
504                                   "properties")   
505   
506    def setPropPrefix(self, val=None):
507        """Set prefix for properties read from ini file.  This is set from
508        input or based on environment variable setting
509        NDGSEC_AA_PROPFILEPREFIX
510       
511        DOES NOT apply to XML file properties
512       
513        @type val: basestring
514        @param val: section name"""
515        log.debug("Setting property file section name")
516        if val is None:
517            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
518               
519        if not isinstance(val, basestring):
520            raise AttributeError("Input Properties file section name "
521                                 "must be a valid string.")
522     
523        self.__propPrefix = val
524        log.debug("Properties file section set to: %s" % val)
525       
526    def getPropPrefix(self):
527        '''Get the prefix name used for properties in an ini file -
528        DOES NOT apply to XML file properties
529       
530        @rtype: basestring
531        @return: section name'''
532        return self.__propPrefix
533   
534       
535    # Also set up as a property
536    propPrefix = property(fset=setPropPrefix,
537                          fget=getPropPrefix,
538                          doc="Set a prefix for ini file properties")   
539   
540    mapConfig = property(fget=_getMapConfig, 
541                         doc="MapConfig object")
542
543    cert = property(fget=_getCert, 
544                    fset=_setCert, 
545                    doc="X.509 Issuer Certificate")
546
547    issuer = property(fget=_getIssuer, 
548                      fset=_setIssuer, 
549                      doc="Issuer name")
550
551    issuerSerialNumber = property(fget=_getIssuerSerialNumber, 
552                                  fset=_setIssuerSerialNumber, 
553                                  doc="Issuer Serial Number")
554
555    attCertLog = property(fget=_getAttCertLog,
556                          fset=_setAttCertLog, 
557                          doc="Attribute certificate logging object")
558
559    name = property(fget=_getName, 
560                    fset=_setName, 
561                    doc="Issuer organisation name")
562
563    attCertLifetime = property(fget=_getAttCertLifetime, 
564                               fset=_setAttCertLifetime, 
565                               doc="Attribute certificate lifetime")
566
567    attCertNotBeforeOff = property(fget=_getAttCertNotBeforeOff, 
568                                   fset=_setAttCertNotBeforeOff, 
569                                   doc="Attribute certificate clock skew in "
570                                       "seconds")
571
572    clockSkew = property(fget=_getClockSkew, 
573                         fset=_setClockSkew, 
574                         doc="Allow a clock skew in seconds for SAML Attribute"
575                             " Query issueInstant parameter check")
576
577    attCertDir = property(fget=_getAttCertDir, 
578                          fset=_setAttCertDir, 
579                          doc="Attribute certificate log directory")
580
581    attributeInterface = property(fget=_getAttributeInterface, 
582                                  doc="Attribute Interface object")
583
584    name = property(fget=_getName, fset=_setName, doc="Organisation Name")
585
586    trustedHostInfo = property(fget=_getTrustedHostInfo, 
587                               fset=_setTrustedHostInfo, 
588                               doc="Dictionary of trusted organisations")
589       
590    @classmethod
591    def fromPropertyFile(cls, propFilePath=None, propFileSection='DEFAULT',
592                         propPrefix='attributeauthority.', 
593                         bReadMapConfig=True):
594        """Create new NDG Attribute Authority instance from the property file
595        settings
596
597        @type propFilePath: string
598        @param propFilePath: path to file containing Attribute Authority
599        configuration parameters.  It defaults to $NDGSEC_AA_PROPFILEPATH or
600        if not set, $NDGSEC_DIR/conf/attributeAuthority.cfg
601        @type propFileSection: basestring
602        @param propFileSection: section of properties file to read from.
603        properties files
604        @type propPrefix: basestring
605        @param propPrefix: set a prefix for filtering attribute authority
606        property names - useful where properties are being parsed from a file
607        section containing parameter names for more than one application
608        @type bReadMapConfig: boolean
609        @param bReadMapConfig: by default the Map Configuration file is
610        read.  Set this flag to False to override.
611        """
612           
613        attributeAuthority = AttributeAuthority()
614        if propFileSection:
615            attributeAuthority.propFileSection = propFileSection
616           
617        if propPrefix:
618            attributeAuthority.propPrefix = propPrefix
619
620        attributeAuthority.propFilePath = propFilePath           
621        attributeAuthority.readProperties()
622        attributeAuthority.initialise(bReadMapConfig=bReadMapConfig)
623   
624        return attributeAuthority
625
626       
627    @classmethod
628    def fromProperties(cls, propPrefix='attributeauthority.', 
629                       bReadMapConfig=True, **prop):
630        """Create new NDG Attribute Authority instance from input property
631        keywords
632
633        @type propPrefix: basestring
634        @param propPrefix: set a prefix for filtering attribute authority
635        property names - useful where properties are being parsed from a file
636        section containing parameter names for more than one application
637        @type bReadMapConfig: boolean
638        @param bReadMapConfig: by default the Map Configuration file is
639        read.  Set this flag to False to override.
640        """
641        attributeAuthority = AttributeAuthority()
642        if propPrefix:
643            attributeAuthority.propPrefix = propPrefix
644               
645        attributeAuthority.setProperties(**prop)
646        attributeAuthority.initialise(bReadMapConfig=bReadMapConfig)
647       
648        return attributeAuthority
649   
650    def initialise(self, bReadMapConfig=True):
651        """Convenience method for set up of Attribute Interface, map
652        configuration and PKI"""
653       
654        # Read the Map Configuration file
655        if bReadMapConfig:
656            self.readMapConfig()
657
658        # Instantiate Certificate object
659        log.debug("Reading and checking Attribute Authority X.509 cert. ...")
660        self.cert = X509Cert.Read(self.signingCertFilePath)
661
662        # Check it's valid
663        try:
664            self.cert.isValidTime(raiseExcep=True)
665           
666        except Exception, e:
667            raise AttributeAuthorityError("Attribute Authority's certificate "
668                                          "is invalid: %s" % e)
669       
670        # Check CA certificate
671        log.debug("Reading and checking X.509 CA certificate ...")
672        for caCertFile in self.caCertFilePathList:
673            caCert = X509Cert(caCertFile)
674            caCert.read()
675           
676            try:
677                caCert.isValidTime(raiseExcep=True)
678               
679            except Exception, e:
680                raise AttributeAuthorityError('CA certificate "%s" is '
681                                              'invalid: %s'% (caCert.dn, e))
682       
683        # Issuer details - serialise using the separator string set in the
684        # properties file
685        self.issuer = self.cert.dn.serialise(separator=self.dnSeparator)
686
687        self.issuerSerialNumber = self.cert.serialNumber
688       
689        # Load user - user attribute look-up plugin
690        self.initAttributeInterface()
691       
692        attCertFilePath = os.path.join(self.attCertDir, self.attCertFileName)
693               
694        # Rotating file handler used for logging attribute certificates
695        # issued.
696        self.attCertLog = AttCertLog(attCertFilePath,
697                                     backUpCnt=self.attCertFileLogCnt)
698
699    def setProperties(self, **prop):
700        """Set configuration from an input property dictionary
701        @type prop: dict
702        @param prop: properties dictionary containing configuration items
703        to be set
704        """
705        lenPropPrefix = len(self.propPrefix)
706       
707        # '+ 1' allows for the dot separator
708        lenAttributeInterfacePrefix = len(
709                            AttributeAuthority.ATTRIBUTE_INTERFACE_KEYNAME) + 1
710       
711        for name, val in prop.items():
712            if name.startswith(self.propPrefix):
713                name = name[lenPropPrefix:]
714           
715            if name.startswith(AttributeAuthority.ATTRIBUTE_INTERFACE_KEYNAME):
716                name = name[lenAttributeInterfacePrefix:]
717                self.attributeInterfaceCfg[name] = val
718                continue
719           
720            if name not in AttributeAuthority.propertyDefaults:
721                raise AttributeError('Invalid attribute name "%s"' % name)
722           
723            if isinstance(val, basestring):
724                val = os.path.expandvars(val)
725           
726            if isinstance(AttributeAuthority.propertyDefaults[name], list):
727                val = AttributeAuthority.CONFIG_LIST_SEP_PAT.split(val)
728               
729            # This makes an implicit call to the appropriate property method
730            try:
731                setattr(self, name, val)
732            except AttributeError:
733                raise AttributeError("Can't set attribute \"%s\"" % name)         
734           
735    def readProperties(self):
736        '''Read the properties files and do some checking/converting of input
737        values
738        '''
739        if not os.path.isfile(self.propFilePath):
740            raise IOError('Error parsing properties file "%s": No such file' % 
741                          self.propFilePath)
742           
743        defaultItems = {'here': os.path.dirname(self.propFilePath)}
744       
745        cfg = CaseSensitiveConfigParser(defaults=defaultItems)
746        cfg.read(self.propFilePath)
747       
748        cfgItems = dict([(name, val) 
749                         for name, val in cfg.items(self.propFileSection)
750                         if name != 'here'])
751        self.setProperties(**cfgItems)
752
753    def initAttributeInterface(self):
754        '''Load host sites custom user roles interface to enable the AA to
755        # assign roles in an attribute certificate on a getAttCert request'''
756        classProperties = {}
757        classProperties.update(self.attributeInterfaceCfg)
758       
759        modName = classProperties.pop('modName')
760        className = classProperties.pop('className') 
761       
762        # file path may be omitted   
763        modFilePath = classProperties.pop('modFilePath', None) 
764                     
765        self.__attributeInterface = instantiateClass(modName,
766                                             className,
767                                             moduleFilePath=modFilePath,
768                                             objectType=AttributeInterface,
769                                             classProperties=classProperties)
770
771    def getAttCert(self,
772                   userId=None,
773                   holderX509Cert=None,
774                   holderX509CertFilePath=None,
775                   userAttCert=None,
776                   userAttCertFilePath=None):
777
778        """Request a new Attribute Certificate for use in authorisation
779
780        getAttCert([userId=uid][holderX509Cert=x509Cert|
781                    holderX509CertFilePath=x509CertFile, ]
782                   [userAttCert=cert|userAttCertFilePath=certFile])
783         
784        @type userId: string
785        @param userId: identifier for the user who is entitled to the roles
786        in the certificate that is issued.  If this keyword is omitted, then
787        the userId will be set to the DN of the holder.
788       
789        holder = the holder of the certificate - an inidividual user or an
790        organisation to which the user belongs who vouches for that user's ID
791       
792        userId = the identifier for the user who is entitled to the roles
793        specified in the Attribute Certificate that is issued.
794                 
795        @type holderX509Cert: string / ndg.security.common.X509.X509Cert type
796        @param holderX509Cert: base64 encoded string containing proxy cert./
797        X.509 cert object corresponding to the ID who will be the HOLDER of
798        the Attribute Certificate that will be issued.  - Normally, using
799        proxy certificates, the holder and user ID are the same but there
800        may be cases where the holder will be an organisation ID.  This is the
801        case for NDG security with the DEWS project
802       
803        @param holderX509CertFilePath: string
804        @param holderX509CertFilePath: file path to proxy/X.509 certificate of
805        candidate holder
806     
807        @type userAttCert: string or AttCert type
808        @param userAttCert: externally provided attribute certificate from
809        another data centre.  This is only necessary if the user is not
810        registered with this attribute authority.
811                       
812        @type userAttCertFilePath: string
813        @param userAttCertFilePath: alternative to userAttCert except pass
814        in as a file path to an attribute certificate instead.
815       
816        @rtype: AttCert
817        @return: new attribute certificate"""
818
819        log.debug("Calling getAttCert ...")
820       
821        # Read candidate Attribute Certificate holder's X.509 certificate
822        try:
823            if holderX509CertFilePath is not None:
824                                   
825                # Certificate input as a file
826                holderX509Cert = X509Cert()
827                holderX509Cert.read(holderX509CertFilePath)
828               
829            elif isinstance(holderX509Cert, basestring):
830
831                # Certificate input as string text
832                holderX509Cert = X509Cert.Parse(holderX509Cert)
833               
834            elif not isinstance(holderX509Cert, (X509Cert, None.__class__)):
835                raise AttributeAuthorityError("Holder X.509 Certificate must "
836                                              "be set to valid type: a file "
837                                              "path, string, X509 object or "
838                                              "None")           
839        except Exception, e:
840            log.error("Holder X.509 certificate: %s" % e)
841            raise
842
843
844        # Check certificate hasn't expired
845        if holderX509Cert:
846            log.debug("Checking candidate holder X.509 certificate ...")
847            try:
848                holderX509Cert.isValidTime(raiseExcep=True)
849               
850            except Exception, e:
851                log.error("User X.509 certificate is invalid: " + e)
852                raise
853
854           
855        # If no user ID is input, set id from holder X.509 certificate DN
856        # instead
857        if not userId:
858            if not holderX509Cert:
859                raise AttributeAuthorityError("If no user ID is set a holder "
860                                              "X.509 certificate must be "
861                                              "present")
862            try:
863                userId = holderX509Cert.dn.serialise(\
864                                         separator=self.dnSeparator) 
865            except Exception, e:
866                log.error("Setting user Id from holder certificate DN: %s" % e)
867                raise
868       
869        # Make a new Attribute Certificate instance passing in certificate
870        # details for later signing
871        attCert = AttCert()
872
873        # First certificate in list contains the public key corresponding to
874        # the private key
875        attCert.certFilePathList = [self.signingCertFilePath] + \
876                                                                self.caCertFilePathList
877             
878        # Check for expiry of each certificate                   
879        for x509Cert in attCert.certFilePathList:
880            X509Cert.Read(x509Cert).isValidTime(raiseExcep=True)
881                                                               
882        attCert.signingKeyFilePath = self.signingPriKeyFilePath
883        attCert.signingKeyPwd = self.signingPriKeyPwd
884       
885       
886        # Set holder's Distinguished Name if a holder X.509 certificate was
887        # input
888        if holderX509Cert:
889            try:
890                attCert['holder'] = holderX509Cert.dn.serialise(
891                                        separator=self.dnSeparator)           
892            except Exception, e:
893                 log.error("Holder X.509 Certificate DN: %s" % e)
894                 raise
895           
896        # Set Issuer details from Attribute Authority
897        issuerDN = self.cert.dn
898        try:
899            attCert['issuer'] = \
900                    issuerDN.serialise(separator=self.dnSeparator)           
901        except Exception, e:
902            log.error("Issuer X.509 Certificate DN: %s" % e)
903            raise 
904           
905        attCert['issuerName'] = self.name
906        attCert['issuerSerialNumber'] = self.issuerSerialNumber
907
908        attCert['userId'] = userId
909       
910        # Set validity time
911        try:
912            attCert.setValidityTime(
913                        lifetime=self.attCertLifetime,
914                        notBeforeOffset=self.attCertNotBeforeOff)
915
916            # Check against the holder X.509 certificate's expiry if set
917            if holderX509Cert:
918                dtHolderCertNotAfter = holderX509Cert.notAfter
919               
920                if attCert.getValidityNotAfter(asDatetime=True) > \
921                   dtHolderCertNotAfter:
922   
923                    # Adjust the attribute certificate's expiry date time
924                    # so that it agrees with that of the certificate
925                    # ... but also make ensure that the not before skew is
926                    # still applied
927                    attCert.setValidityTime(dtNotAfter=dtHolderCertNotAfter,
928                            notBeforeOffset=self.attCertNotBeforeOff)
929           
930        except Exception, e:
931            log.error("Error setting attribute certificate validity time: %s" %
932                      e)
933            raise 
934
935        # Check name is registered with this Attribute Authority - if no
936        # user roles are found, the user is not registered
937        userRoles = self.getRoles(userId)
938        if userRoles:
939            # Set as an Original Certificate
940            #
941            # User roles found - user is registered with this data centre
942            # Add roles for this user for this data centre
943            attCert.addRoles(userRoles)
944
945            # Mark new Attribute Certificate as an original
946            attCert['provenance'] = AttCert.origProvenance
947
948        else:           
949            # Set as a Mapped Certificate
950            #
951            # No roles found - user is not registered with this data centre
952            # Check for an externally provided certificate from another
953            # trusted data centre
954            if userAttCertFilePath:
955               
956                # Read externally provided certificate
957                try:
958                    userAttCert = AttCert.Read(userAttCertFilePath)
959                   
960                except Exception, e:
961                    raise AttributeAuthorityError("Reading external Attribute "
962                                                  "Certificate: %s" % e)                           
963            elif userAttCert:
964                # Allow input as a string but convert to
965                if isinstance(userAttCert, basestring):
966                    userAttCert = AttCert.Parse(userAttCert)
967                   
968                elif not isinstance(userAttCert, AttCert):
969                    raise AttributeAuthorityError(
970                        "Expecting userAttCert as a string or AttCert type")       
971            else:
972                raise AttributeAuthorityAccessDenied('User "%s" is not '
973                    'registered and no external attribute certificate is '
974                    'available to make a mapping.' % userId)
975
976
977            # Check it's an original certificate - mapped certificates can't
978            # be used to make further mappings
979            if userAttCert.isMapped():
980                raise AttributeAuthorityError("External Attribute Certificate "
981                                              "must have an original "
982                                              "provenance in order "
983                                              "to make further mappings.")
984
985
986            # Check it's valid and signed
987            try:
988                # Give path to CA cert to allow check
989                userAttCert.certFilePathList = self.caCertFilePathList
990                userAttCert.isValid(raiseExcep=True)
991               
992            except Exception, e:
993                raise AttributeAuthorityError("Invalid Remote Attribute "
994                                        "Certificate: " + str(e))       
995
996
997            # Check that's it's holder matches the candidate holder
998            # certificate DN
999            if holderX509Cert and userAttCert.holderDN != holderX509Cert.dn:
1000                raise AttributeAuthorityError("User certificate and Attribute "
1001                                        'Certificate DNs don\'t match: "%s"'
1002                                        ' and "%s"' % (holderX509Cert.dn, 
1003                                                       userAttCert.holderDN))
1004           
1005 
1006            # Get roles from external Attribute Certificate
1007            trustedHostRoles = userAttCert.roles
1008
1009
1010            # Map external roles to local ones
1011            localRoles = self.mapRemoteRoles2LocalRoles(
1012                                                    userAttCert['issuerName'],
1013                                                    trustedHostRoles)
1014            if not localRoles:
1015                raise AttributeAuthorityAccessDenied("No local roles mapped "
1016                                               "to the %s roles: %s" % 
1017                                               (userAttCert['issuerName'], 
1018                                                ', '.join(trustedHostRoles)))
1019
1020            attCert.addRoles(localRoles)
1021           
1022           
1023            # Mark new Attribute Certificate as mapped
1024            attCert.provenance = AttCert.mappedProvenance
1025
1026            # Copy the user Id from the external AC
1027            attCert.userId = userAttCert.userId
1028           
1029            # End set mapped certificate block
1030
1031        try:
1032            # Digitally sign certificate using Attribute Authority's
1033            # certificate and private key
1034            attCert.applyEnvelopedSignature()
1035           
1036            # Check the certificate is valid
1037            attCert.isValid(raiseExcep=True)
1038           
1039            # Write out certificate to keep a record of it for auditing
1040            #attCert.write()
1041            self.__attCertLog.info(attCert)
1042           
1043            log.info('Issued an Attribute Certificate to "%s" with roles: '
1044                     '"%s"' % (userId, '", "'.join(attCert.roles)))
1045
1046            # Return the cert to caller
1047            return attCert
1048       
1049        except Exception, e:
1050            raise AttributeAuthorityError('New Attribute Certificate "%s": %s'%
1051                                          (attCert.filePath, e))
1052
1053    def samlAttributeQuery(self, attributeQuery):
1054        """Respond to SAML 2.0 Attribute Query
1055        """
1056        if not isinstance(attributeQuery, AttributeQuery):
1057            raise TypeError('Expecting %r for attribute query; got %r' %
1058                            (AttributeQuery, type(attributeQuery)))
1059           
1060        samlResponse = Response()
1061       
1062        samlResponse.issueInstant = datetime.utcnow()
1063        if self.attCertNotBeforeOff != 0:
1064            samlResponse.issueInstant += timedelta(
1065                                            seconds=self.attCertNotBeforeOff)
1066           
1067        samlResponse.id = str(uuid4())
1068        samlResponse.issuer = Issuer()
1069       
1070        # Initialise to success status but reset on error
1071        samlResponse.status = Status()
1072        samlResponse.status.statusCode = StatusCode()
1073        samlResponse.status.statusMessage = StatusMessage()
1074        samlResponse.status.statusCode.value = StatusCode.SUCCESS_URI
1075       
1076        # Nb. SAML 2.0 spec says issuer format must be omitted
1077        samlResponse.issuer.value = self.issuer
1078       
1079        samlResponse.inResponseTo = attributeQuery.id
1080       
1081        # Attribute Query validation ...
1082        utcNow = datetime.utcnow()
1083        if attributeQuery.issueInstant >= utcNow + self.clockSkew:
1084            msg = ('SAML Attribute Query issueInstant [%s] is at or after '
1085                   'the current clock time [%s]') % \
1086                   (attributeQuery.issueInstant, SAMLDateTime.toString(utcNow))
1087            log.error(msg)
1088                     
1089            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1090            samlResponse.status.statusMessage = StatusMessage()
1091            samlResponse.status.statusMessage.value = msg
1092            return samlResponse
1093           
1094        elif attributeQuery.version < SAMLVersion.VERSION_20:
1095            samlResponse.status.statusCode.value = \
1096                                        StatusCode.REQUEST_VERSION_TOO_LOW_URI
1097            return samlResponse
1098       
1099        elif attributeQuery.version > SAMLVersion.VERSION_20:
1100            samlResponse.status.statusCode.value = \
1101                                        StatusCode.REQUEST_VERSION_TOO_HIGH_URI
1102            return samlResponse
1103       
1104        elif attributeQuery.subject.nameID.format != "urn:esg:openid":
1105            log.error('SAML Attribute Query subject format is %r; expecting '
1106                      '%r' % (attributeQuery.subject.nameID.format,
1107                                "urn:esg:openid"))
1108            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1109            samlResponse.status.statusMessage.value = \
1110                                "Subject Name ID format is not recognised"
1111            return samlResponse
1112       
1113        elif attributeQuery.issuer.format not in Issuer.X509_SUBJECT:
1114            log.error('SAML Attribute Query issuer format is %r; expecting '
1115                      '%r' % (attributeQuery.issuer.format,
1116                                Issuer.X509_SUBJECT))
1117            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1118            samlResponse.status.statusMessage.value = \
1119                                            "Issuer format is not recognised"
1120            return samlResponse
1121       
1122        try:
1123            # Return a dictionary of name, value pairs
1124            self.attributeInterface.getAttributes(attributeQuery, samlResponse)
1125           
1126        except InvalidUserId, e:
1127            log.exception(e)
1128            samlResponse.status.statusCode.value = \
1129                                        StatusCode.UNKNOWN_PRINCIPAL_URI
1130            return samlResponse
1131           
1132        except UserIdNotKnown, e:
1133            log.exception(e)
1134            samlResponse.status.statusCode.value = \
1135                                        StatusCode.UNKNOWN_PRINCIPAL_URI
1136            samlResponse.status.statusMessage.value = str(e)
1137            return samlResponse
1138           
1139        except InvalidRequestorId, e:
1140            log.exception(e)
1141            samlResponse.status.statusCode.value = StatusCode.REQUEST_DENIED_URI
1142            samlResponse.status.statusMessage.value = str(e)
1143            return samlResponse
1144           
1145        except AttributeReleaseDenied, e:
1146            log.exception(e)
1147            samlResponse.status.statusCode.value = \
1148                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
1149            samlResponse.status.statusMessage.value = str(e)
1150            return samlResponse
1151           
1152        except AttributeNotKnownError, e:
1153            log.exception(e)
1154            samlResponse.status.statusCode.value = \
1155                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
1156            samlResponse.status.statusMessage.value = str(e)
1157            return samlResponse
1158           
1159        except Exception, e:
1160            log.exception("Unexpected error calling Attribute Interface "
1161                          "for subject [%s] and query issuer [%s]" %
1162                          (attributeQuery.subject.nameID.value,
1163                           attributeQuery.issuer.value))
1164           
1165            # SAML spec says application server should set a HTTP 500 Internal
1166            # Server error in this case
1167            raise 
1168
1169        return samlResponse
1170   
1171    def readMapConfig(self):
1172        """Parse Map Configuration file.
1173        """
1174        log.debug("Reading map configuration file ...")
1175       
1176        try:
1177            tree = ElementTree.parse(self.mapConfigFilePath)
1178            rootElem = tree.getroot()
1179           
1180        except IOError, e:
1181            raise AttributeAuthorityConfigError('Error parsing Map '
1182                                                'Configuration file "%s": %s' % 
1183                                                (e.filename, e.strerror))         
1184        except Exception, e:
1185            raise AttributeAuthorityConfigError('Error parsing Map '
1186                                                'Configuration file: "%s": %s'% 
1187                                                (self.mapConfigFilePath, e))
1188       
1189        trustedElem = rootElem.findall('trusted')
1190        if not trustedElem: 
1191            # Make an empty list so that for loop block below is skipped
1192            # without an error 
1193            trustedElem = ()
1194
1195        # Dictionaries:
1196        # 1) to hold all the data
1197        self.__mapConfig = {'thisHost': {}, 'trustedHosts': {}}
1198
1199        # ... look-up
1200        # 2) hosts corresponding to a given role and
1201        # 3) roles of external data centre to this data centre
1202        self.__localRole2TrustedHost = {}
1203        self.__localRole2RemoteRole = {}
1204        self.__remoteRole2LocalRole = {}
1205
1206        # Information about this host
1207        try:
1208            thisHostElem = rootElem.findall('thisHost')[0]
1209           
1210        except Exception, e:
1211            raise AttributeAuthorityConfigError('"thisHost" tag not found in '
1212                                                'Map Configuration file "%s"' % 
1213                                                self.mapConfigFilePath)
1214
1215        try:
1216            hostName = thisHostElem.attrib.values()[0]
1217           
1218        except Exception, e:
1219            raise AttributeAuthorityConfigError('"name" attribute of '
1220                                                '"thisHost" element not found '
1221                                                'in Map Configuration file '
1222                                                '"%s"' % 
1223                                                self.mapConfigFilePath)
1224
1225        # hostname is also stored in the AA's config file in the 'name' tag. 
1226        # Check the two match as the latter is copied into Attribute
1227        # Certificates issued by this AA
1228        #
1229        # TODO: would be better to rationalise this so that the hostname is
1230        # stored in one place only.
1231        #
1232        # P J Kershaw 14/06/06
1233        if hostName != self.name:
1234            raise AttributeAuthorityError('"name" attribute of "thisHost" '
1235                                          'element in Map Configuration file '
1236                                          'doesn\'t match "name" element in '
1237                                          'properties file.')
1238       
1239        # Information for THIS Attribute Authority
1240        self.__mapConfig['thisHost'][hostName] = {}
1241
1242        for k, v in AttributeAuthority.mapConfigHostDefaults.items():
1243            val = thisHostElem.findtext(k)
1244            if val is None and v == NotImplemented:
1245                raise AttributeAuthorityConfigError('<thisHost> option <%s> '
1246                                                    'must be set.' % k)
1247            self.__mapConfig['thisHost'][hostName][k] = val     
1248       
1249        # Information about trusted hosts
1250        for elem in trustedElem:
1251            try:
1252                trustedHost = elem.attrib.values()[0]
1253               
1254            except Exception, e:
1255                raise AttributeAuthorityConfigError('Error reading trusted '
1256                                                    'host name: %s' % e)
1257 
1258            # Add signatureFile and list of roles
1259            #
1260            # (Currently Optional) additional tag allows query of the URI
1261            # where a user would normally login at the trusted host.  Added
1262            # this feature to allow users to be forwarded to their home site
1263            # if they are accessing a secure resource and are not
1264            # authenticated
1265            #
1266            # P J Kershaw 25/05/06
1267            self.__mapConfig['trustedHosts'][trustedHost] = {}
1268            for k, v in AttributeAuthority.mapConfigHostDefaults.items():
1269                val = thisHostElem.findtext(k)
1270                if val is None and v == NotImplemented:
1271                    raise AttributeAuthorityConfigError('<trustedHost> option '
1272                                                        '<%s> must be set.'%k)
1273                   
1274                self.__mapConfig['trustedHosts'][trustedHost][k] = \
1275                                                        elem.findtext(k)   
1276
1277            roleElem = elem.findall('role')
1278            if roleElem:
1279                # Role keyword value requires special parsing before
1280                # assignment
1281                self.__mapConfig['trustedHosts'][trustedHost]['role'] = \
1282                                        [dict(i.items()) for i in roleElem]
1283            else:
1284                # It's possible for trust relationships to not contain any
1285                # role mapping.  e.g. a site's login service trusting other
1286                # sites login requests
1287                self.__mapConfig['trustedHosts'][trustedHost]['role'] = []
1288                       
1289            self.__localRole2RemoteRole[trustedHost] = {}
1290            self.__remoteRole2LocalRole[trustedHost] = {}
1291           
1292            for role in self.__mapConfig['trustedHosts'][trustedHost]['role']:
1293                try:
1294                    localRole = role['local']
1295                    remoteRole = role['remote']
1296                except KeyError, e:
1297                    raise AttributeAuthorityError('Reading map configuration '
1298                                                  ' file "%s": no element '
1299                                                  '"%s" for host "%s"' % 
1300                                                (self.mapConfigFilePath, 
1301                                                 e, 
1302                                                 trustedHost))
1303                   
1304                # Role to host look-up
1305                if localRole in self.__localRole2TrustedHost:
1306                   
1307                    if trustedHost not in \
1308                       self.__localRole2TrustedHost[localRole]:
1309                        self.__localRole2TrustedHost[localRole].\
1310                                                        append(trustedHost)                       
1311                else:
1312                    self.__localRole2TrustedHost[localRole] = [trustedHost]
1313
1314
1315                # Trusted Host to local role and trusted host to trusted role
1316                # map look-ups
1317                try:
1318                    self.__remoteRole2LocalRole[trustedHost][remoteRole].\
1319                                                            append(localRole)                 
1320                except KeyError:
1321                    self.__remoteRole2LocalRole[trustedHost][remoteRole] = \
1322                                                                [localRole]
1323                   
1324                try:
1325                    self.__localRole2RemoteRole[trustedHost][localRole].\
1326                                                            append(remoteRole)                 
1327                except KeyError:
1328                    self.__localRole2RemoteRole[trustedHost][localRole] = \
1329                                                                [remoteRole]
1330       
1331        # Store trusted host info look-up for retrieval by getTrustedHostInfo
1332        # method                                                                         
1333        #
1334        # Nb. {}.fromkeys([...]).keys() is a fudge to get unique elements
1335        # from a list i.e. convert the list elements to a dict eliminating
1336        # duplicated elements and convert the keys back into a list.
1337        self._trustedHostInfo = dict(
1338        [
1339            (
1340                k, 
1341                {
1342                    'siteName':             v['siteName'],
1343                    'aaURI':                v['aaURI'], 
1344                    'aaDN':                 v['aaDN'], 
1345                    'loginURI':             v['loginURI'], 
1346                    'loginServerDN':        v['loginServerDN'], 
1347                    'loginRequestServerDN': v['loginRequestServerDN'], 
1348                    'role':                 {}.fromkeys([role['remote'] 
1349                                                         for role in v['role']]
1350                                                       ).keys()
1351                }
1352            ) for k, v in self.__mapConfig['trustedHosts'].items()
1353        ])
1354
1355        log.info('Loaded map configuration file "%s"' % self.mapConfigFilePath)
1356       
1357       
1358    def getRoles(self, userId):
1359        """Get the roles available to the registered user identified userId.
1360
1361        @type dn: string
1362        @param dn: user identifier - could be a X500 Distinguished Name
1363        @return: list of roles for the given user ID"""
1364
1365        log.debug('Calling getRoles for user "%s" ...' % userId)
1366       
1367        # Call to AttributeInterface derived class.  Each Attribute Authority
1368        # should define it's own roles class derived from AttributeInterface to
1369        # define how roles are accessed
1370        try:
1371            return self.__attributeInterface.getRoles(userId)
1372
1373        except Exception, e:
1374            raise AttributeAuthorityError("Getting user roles: %s" % e)
1375       
1376       
1377    def _getHostInfo(self):
1378        """Return the host that this Attribute Authority represents: its ID,
1379        the user login URI and WSDL address.  Call this method via the
1380        'hostInfo' property
1381       
1382        @rtype: dict
1383        @return: dictionary of host information derived from the map
1384        configuration"""
1385       
1386        return self.__mapConfig['thisHost']
1387       
1388    hostInfo = property(fget=_getHostInfo, 
1389                        doc="Return information about this host")
1390       
1391       
1392    def getTrustedHostInfo(self, role=None):
1393        """Return a dictionary of the hosts that have trust relationships
1394        with this AA.  The dictionary is indexed by the trusted host name
1395        and contains AA service, login URIs and the roles that map to the
1396        given input local role.
1397
1398        @type role: string
1399        @param role: if set, return trusted hosts that having a mapping set
1400        for this role.  If no role is input, return all the AA's trusted hosts
1401        with all their possible roles
1402
1403        @rtype: dict
1404        @return: dictionary of the hosts that have trust relationships
1405        with this AA.  It returns an empty dictionary if role isn't
1406        recognised"""
1407               
1408        log.debug('Calling getTrustedHostInfo with role = "%s" ...' % role) 
1409                                 
1410        if not self.__mapConfig or not self.__localRole2RemoteRole:
1411            # This Attribute Authority has no trusted hosts
1412            raise AttributeAuthorityNoTrustedHosts("The %s Attribute "
1413                                                   "Authority has no trusted "
1414                                                   "hosts" % 
1415                                                   self.name)
1416
1417
1418        if role is None:
1419            # No role input - return all trusted hosts with their service URIs
1420            # and the remote roles they map to
1421            return self._trustedHostInfo
1422
1423        else:           
1424            # Get trusted hosts for given input local role       
1425            try:
1426                trustedHosts = self.__localRole2TrustedHost[role]
1427            except:
1428                raise AttributeAuthorityNoMatchingRoleInTrustedHosts(
1429                    'None of the trusted hosts have a mapping to the '
1430                    'input role "%s"' % role)
1431   
1432   
1433            # Get associated Web service URI and roles for the trusted hosts
1434            # identified and return as a dictionary indexed by host name
1435            trustedHostInfo = dict(
1436       [(
1437            host, 
1438            {
1439                'siteName': self.__mapConfig['trustedHosts'][host]['siteName'],
1440                'aaURI':    self.__mapConfig['trustedHosts'][host]['aaURI'],
1441                'aaDN':     self.__mapConfig['trustedHosts'][host]['aaDN'],
1442                'loginURI': self.__mapConfig['trustedHosts'][host]['loginURI'],
1443                'loginServerDN': 
1444                        self.__mapConfig['trustedHosts'][host]['loginServerDN'],
1445                'loginRequestServerDN': 
1446                self.__mapConfig['trustedHosts'][host]['loginRequestServerDN'],
1447                'role':     self.__localRole2RemoteRole[host][role]
1448            }
1449        ) for host in trustedHosts])
1450                         
1451            return trustedHostInfo
1452       
1453       
1454    def mapRemoteRoles2LocalRoles(self, trustedHost, trustedHostRoles):
1455        """Map roles of trusted hosts to roles for this data centre
1456
1457        @type trustedHost: string
1458        @param trustedHost: name of external trusted data centre
1459        @type trustedHostRoles: list
1460        @param trustedHostRoles:   list of external roles to map
1461        @return: list of mapped roles"""
1462
1463        if not self.__remoteRole2LocalRole:
1464            raise AttributeAuthorityError("Roles map is not set - ensure " 
1465                                    "readMapConfig() has been called.")
1466
1467
1468        # Check the host name is a trusted one recorded in the map
1469        # configuration
1470        if not self.__remoteRole2LocalRole.has_key(trustedHost):
1471            return []
1472
1473        # Add local roles, skipping if no mapping is found
1474        localRoles = []
1475        for trustedRole in trustedHostRoles:
1476            if trustedRole in self.__remoteRole2LocalRole[trustedHost]:
1477                localRoles.extend(
1478                        self.__remoteRole2LocalRole[trustedHost][trustedRole])
1479               
1480        return localRoles
1481
1482    def getAttCertFactory(self):
1483        """Factory method to create SAML Attribute Qeury wrapper function
1484        @rtype: function
1485        @return getAttCert method function wrapper
1486        """
1487        def getAttCertWrapper(*arg, **kw):
1488            """
1489            @type *arg: tuple
1490            @param *arg: getAttCert arguments
1491            @type **kw: dict
1492            @param **kw: getAttCert keyword arguments
1493            @rtype: ndg.security.common.AttCert.AttCert
1494            @return: new attribute certificate
1495            """
1496            return self.getAttCert(*arg, **kw)
1497       
1498        return getAttCertWrapper
1499
1500    def samlAttributeQueryFactory(self):
1501        """Factory method to create SAML Attribute Qeury wrapper function
1502        @rtype: function
1503        @return: samlAttributeQuery method function wrapper
1504        """
1505        def samlAttributeQueryWrapper(attributeQuery):
1506            """
1507            @type attributeQuery: saml.saml2.core.AttributeQuery
1508            @param attributeQuery: SAML Attribute Query
1509            @rtype: saml.saml2.core.Response
1510            @return: SAML response
1511            """
1512            return self.samlAttributeQuery(attributeQuery)
1513       
1514        return samlAttributeQueryWrapper
1515   
1516
1517from logging.handlers import RotatingFileHandler
1518
1519# Inherit directly from Logger
1520_loggerClass = logging.getLoggerClass()
1521class AttCertLog(_loggerClass, object):
1522    """Log each Attribute Certificate issued using a rotating file handler
1523    so that the number of files held can be managed"""
1524   
1525    def __init__(self, attCertFilePath, backUpCnt=1024):
1526        """Set up a rotating file handler to log ACs issued.
1527        @type attCertFilePath: string
1528        @param attCertFilePath: set where to store ACs.  Set from
1529        AttributeAuthority properties file.
1530       
1531        @type backUpCnt: int
1532        @param backUpCnt: set the number of files to store before rotating
1533        and overwriting old files."""
1534       
1535        if not isinstance(backUpCnt, int):
1536            raise TypeError('Expecting int type for "backUpCnt" keyword')
1537       
1538        # Inherit from Logger class
1539        super(AttCertLog, self).__init__(name='', level=logging.INFO)
1540                           
1541        # Set a format for messages so that only the content of the AC is
1542        # logged, nothing else.
1543        formatter = logging.Formatter(fmt="", datefmt="")
1544
1545        # maxBytes is set to one so that only one AC will be written before
1546        # rotation to the next file
1547        fileLog = RotatingFileHandler(attCertFilePath, 
1548                                      maxBytes=1, 
1549                                      backupCount=backUpCnt)
1550        fileLog.setFormatter(formatter)           
1551        self.addHandler(fileLog)
1552 
1553                     
1554class AttributeInterfaceError(Exception):
1555    """Exception handling for NDG Attribute Authority User Roles interface
1556    class."""
1557 
1558                     
1559class AttributeInterfaceConfigError(Exception):
1560    """Invalid configuration set for Attribute interface"""
1561 
1562                     
1563class AttributeInterfaceRetrieveError(Exception):
1564    """Error retrieving attributes for Attribute interface class"""
1565
1566                       
1567class AttributeReleaseDenied(AttributeInterfaceError):
1568    """Requestor was denied release of the requested attributes"""
1569
1570                       
1571class AttributeNotKnownError(AttributeInterfaceError):
1572    """Requested attribute names are not known to this authority"""
1573
1574
1575class InvalidRequestorId(AttributeInterfaceError):
1576    """Requestor is not known or not allowed to request attributes"""
1577   
1578
1579class UserIdNotKnown(AttributeInterfaceError): 
1580    """User ID passed to getAttributes is not known to the authority"""
1581   
1582   
1583class InvalidUserId(AttributeInterfaceError):
1584    """User Id passed to getAttributes is invalid"""
1585   
1586     
1587class AttributeInterface(object):
1588    """An abstract base class to define the user roles interface to an
1589    Attribute Authority.
1590
1591    Each NDG data centre should implement a derived class which implements
1592    the way user roles are provided to its representative Attribute Authority.
1593   
1594    Roles are expected to indexed by user Distinguished Name (DN).  They
1595    could be stored in a database or file."""
1596   
1597    # Enable derived classes to use slots if desired
1598    __slots__ = ()
1599   
1600    # User defined class may wish to specify a URI for a database interface or
1601    # path for a user roles configuration file
1602    def __init__(self, **prop):
1603        """User Roles base class - derive from this class to define
1604        roles interface to Attribute Authority
1605       
1606        @type prop: dict
1607        @param prop: custom properties to pass to this class
1608        """
1609
1610    def getRoles(self, userId):
1611        """Virtual method - Derived method should return the roles for the
1612        given user's Id or else raise an exception
1613       
1614        @type userId: string
1615        @param userId: user identity e.g. user Distinguished Name
1616        @rtype: list
1617        @return: list of roles for the given user ID
1618        @raise AttributeInterfaceError: an error occured requesting
1619        attributes
1620        """
1621        raise NotImplementedError(self.getRoles.__doc__)
1622 
1623    def getAttributes(self, attributeQuery, response):
1624        """Virtual method should be implemented in a derived class to enable
1625        AttributeAuthority.samlAttributeQuery - The derived method should
1626        return the attributes requested for the given user's Id or else raise
1627        an exception
1628       
1629        @type attributeQuery: saml.saml2.core.AttributeQuery
1630        @param userId: query containing requested attributes
1631        @type: saml.saml2.core.Response
1632        @param: Response - add an assertion with the list of attributes
1633        for the given subject ID in the query or set an error Status code and
1634        message
1635        @raise AttributeInterfaceError: an error occured requesting
1636        attributes
1637        @raise AttributeReleaseDeniedError: Requestor was denied release of the
1638        requested attributes
1639        @raise AttributeNotKnownError: Requested attribute names are not known
1640        to this authority
1641        """
1642        raise NotImplementedError(self.getAttributes.__doc__)
1643
1644
1645class CSVFileAttributeInterface(AttributeInterface):
1646    """Attribute Interface based on a Comma Separated Variable file containing
1647    user identities and associated attributes.  For test/development purposes
1648    only.  The SAML getAttributes method is NOT implemented here
1649   
1650    The expected file format is:
1651   
1652    <userID>, <role1>, <role2>, ... <roleN>
1653    """
1654    def __init__(self, propertiesFilePath=None):
1655        """
1656        @param propertiesFilePath: file path to Comma Separated file
1657        containing user ids and roles
1658        @type propertiesFilePath: basestring
1659        """
1660        if propertiesFilePath is None:
1661            raise AttributeError("Expecting propertiesFilePath setting")
1662       
1663        propertiesFile = open(propertiesFilePath)
1664        lines = propertiesFile.readlines()
1665       
1666        self.attributeMap = {}
1667        for line in lines:
1668            fields = re.split(',\W*', line.strip())
1669            self.attributeMap[fields[0]] = fields[1:]
1670   
1671    def getRoles(self, userId):
1672        """
1673        @param userId: user identity to key into attributeMap
1674        @type userId: basestring
1675        """ 
1676        log.debug('CSVFileAttributeInterface.getRoles for user "%s" ...', 
1677                  userId)
1678        return self.attributeMap.get(userId, [])
1679
1680
1681# Properties file
1682from ConfigParser import SafeConfigParser, NoOptionError
1683
1684try:
1685    # PostgreSQL interface
1686    from psycopg2 import connect
1687except ImportError:
1688    pass
1689
1690class PostgresAttributeInterface(AttributeInterface):
1691    """User Roles interface to Postgres database
1692   
1693    The SAML getAttributes method is NOT implemented
1694   
1695    The configuration file follows the form,
1696   
1697    [Connection]
1698    # name of database
1699    dbName: user.db
1700   
1701    # database host machine
1702    host: mydbhost.ac.uk
1703   
1704    # database account username
1705    username: mydbaccount
1706   
1707    # Password - comment out to prompt from stdin instead
1708    pwd: mydbpassword
1709   
1710    [getRoles]
1711    query0: select distinct grp from users_table, where user = '%%s'
1712    defaultRoles = publicRole
1713    """
1714
1715    CONNECTION_SECTION_NAME = "Connection"
1716    GETROLES_SECTION_NAME = "getRoles"
1717    HOST_OPTION_NAME = "host"
1718    DBNAME_OPTION_NAME = "dbName"
1719    USERNAME_OPTION_NAME = "username"
1720    PWD_OPTION_NAME = "pwd"
1721    QUERYN_OPTION_NAME = "query%d"
1722    DEFAULT_ROLES_OPTION_NAME = "defaultRoles"
1723   
1724    def __init__(self, propertiesFilePath=None):
1725        """Connect to Postgres database"""
1726        self.__con = None
1727        self.__host = None
1728        self.__dbName = None
1729        self.__username = None
1730        self.__pwd = None
1731
1732        if propertiesFilePath is None:
1733            raise AttributeError("No Configuration file was set")
1734
1735        self.readConfigFile(propertiesFilePath)
1736
1737    def __del__(self):
1738        """Close database connection"""
1739        self.close()
1740
1741    def readConfigFile(self, propertiesFilePath):
1742        """Read the configuration for the database connection
1743
1744        @type propertiesFilePath: string
1745        @param propertiesFilePath: file path to config file"""
1746
1747        if not isinstance(propertiesFilePath, basestring):
1748            raise TypeError("Input Properties file path must be a valid "
1749                            "string; got %r" % type(propertiesFilePath))
1750
1751        cfg = SafeConfigParser()
1752        cfg.read(propertiesFilePath)
1753
1754        self.__host = cfg.get(
1755                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1756                        PostgresAttributeInterface.HOST_OPTION_NAME)
1757        self.__dbName = cfg.get(
1758                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1759                        PostgresAttributeInterface.DBNAME_OPTION_NAME)
1760        self.__username = cfg.get(
1761                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1762                        PostgresAttributeInterface.USERNAME_OPTION_NAME)
1763        self.__pwd = cfg.get(
1764                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1765                        PostgresAttributeInterface.PWD_OPTION_NAME)
1766
1767        try:
1768            self.__getRolesQuery = []
1769            for i in range(10):
1770                queryStr = cfg.get(
1771                        PostgresAttributeInterface.GETROLES_SECTION_NAME, 
1772                        PostgresAttributeInterface.QUERYN_OPTION_NAME % i)
1773                self.__getRolesQuery += [queryStr]
1774        except NoOptionError:
1775             # Continue until no more query<n> items left
1776             pass
1777
1778        # This option may be omitted in the config file
1779        try:
1780            self.__defaultRoles = cfg.get(
1781                PostgresAttributeInterface.GETROLES_SECTION_NAME, 
1782                PostgresAttributeInterface.DEFAULT_ROLES_OPTION_NAME).split()
1783        except NoOptionError:
1784            self.__defaultRoles = []
1785
1786    def connect(self,
1787                username=None,
1788                dbName=None,
1789                host=None,
1790                pwd=None,
1791                prompt="Database password: "):
1792        """Connect to database
1793
1794        Values for keywords omitted are derived from the config file.  If pwd
1795        is not in the config file it will be prompted for from stdin
1796
1797        @type username: string
1798        @keyword username: database account username
1799        @type dbName: string
1800        @keyword dbName: name of database
1801        @type host: string
1802        @keyword host: database host machine
1803        @type pwd: string
1804        @keyword pwd: password for database account.  If omitted and not in
1805        the config file it will be prompted for from stdin
1806        @type prompt: string
1807        @keyword prompt: override default password prompt"""
1808
1809        if not host:
1810            host = self.__host
1811
1812        if not dbName:
1813            dbName = self.__dbName
1814
1815        if not username:
1816            username = self.__username
1817
1818        if not pwd:
1819            pwd = self.__pwd
1820
1821            if not pwd:
1822                import getpass
1823                pwd = getpass.getpass(prompt=prompt)
1824
1825        try:
1826            self.__db = connect("host=%s dbname=%s user=%s password=%s" % \
1827                                (host, dbName, username, pwd))
1828            self.__cursor = self.__db.cursor()
1829
1830        except NameError, e:
1831            raise AttributeInterfaceError("psycopg2 Postgres package not "
1832                                          "installed? %s" % e)
1833        except Exception, e:
1834            raise AttributeInterfaceError("Error connecting to database "
1835                                          "\"%s\": %s" % (dbName, e))
1836
1837    def close(self):
1838        """Close database connection"""
1839        if self.__con:
1840            self.__con.close()
1841
1842    def getRoles(self, userId):
1843        """Return valid roles for the given userId
1844
1845        @type userId: basestring
1846        @param userId: user identity"""
1847
1848        try:
1849            self.connect()
1850
1851            # Process each query in turn appending role names
1852            roles = self.__defaultRoles[:]
1853            for query in self.__getRolesQuery:
1854                try:
1855                    self.__cursor.execute(query % userId)
1856                    queryRes = self.__cursor.fetchall()
1857
1858                except Exception, e:
1859                    raise AttributeInterfaceError("Query for %s: %s" %
1860                                                  (userId, e))
1861
1862                roles += [res[0] for res in queryRes if res[0]]
1863        finally:
1864            self.close()
1865
1866        return roles
1867
1868    def __getCursor(self):
1869        """Return a database cursor instance"""
1870        return self.__cursor
1871
1872    cursor = property(fget=__getCursor, doc="database cursor")
1873
1874
1875from ndg.security.common.utils import TypedList
1876
1877class Saml2DbAttributeMap(object):
1878    """Helper class for SQLAlchemyAttributeInterface.  Map SAML Attribute names
1879    to the SQL queries used to retrieve the values and the values themselves
1880    """
1881    def __init__(self):
1882        self.__name = None
1883        self.__values = TypedList(basestring)
1884        self.__sqlQuery = None
1885
1886    def getName(self):
1887        return self.__name
1888
1889    def setName(self, value):
1890        if not isinstance(value, basestring):
1891            raise TypeError('Expecting string type for "name" attribute; '
1892                            'got %r' % type(value))
1893        self.__name = value
1894       
1895    name = property(getName, setName, 
1896                    doc="SAML attribute name")
1897
1898    def getValues(self):
1899        return self.__values
1900   
1901    values = property(getValues, 
1902                      doc="values returned for the given SQL query")
1903
1904    def getSqlQuery(self):
1905        return self.__sqlQuery
1906
1907    def setSqlQuery(self, value):
1908        if not isinstance(value, basestring):
1909            raise TypeError('Expecting string type for "sqlQuery" attribute; '
1910                            'got %r' % type(value))
1911        self.__sqlQuery = value
1912
1913    sqlQuery = property(getSqlQuery, setSqlQuery, 
1914                        doc="SQL Query related to the given attribute name")
1915
1916
1917import traceback
1918from string import Template
1919try:
1920    from sqlalchemy import create_engine, exc
1921    sqlAlchemyInstalled = True
1922except ImportError:
1923    sqlAlchemyInstalled = False
1924   
1925from ndg.security.common.utils import str2Bool as _str2Bool
1926
1927
1928class SQLAlchemyAttributeInterface(AttributeInterface):
1929    '''SQLAlchemy based Attribute interface enables the Attribute Authority
1930    to interface to any database type supported by it
1931   
1932    @type SQLQUERY_USERID_KEYNAME: basestring
1933    @cvar SQLQUERY_USERID_KEYNAME: key corresponding to string to be
1934    substituted into attribute query for user identifier e.g.
1935   
1936    select attr from user_table where username = $userId
1937    '''
1938   
1939    str2Bool = staticmethod(_str2Bool)
1940   
1941    SQLQUERY_USERID_KEYNAME = 'userId'
1942   
1943    CONNECTION_STRING_OPTNAME = 'connectionString'
1944    ATTRIBUTE_SQLQUERY_OPTNAME = 'attributeSqlQuery'
1945    SAML_SUBJECT_SQLQUERY_OPTNAME = 'samlSubjectSqlQuery'
1946    SAML_ATTRIBUTE_SQLQUERIES_OPTNAME = 'samlAttributeSqlQueries'
1947    SAML_ATTRIBUTE_NAMES_OPTNAME = 'samlAttributeNames'
1948    SAML_VALID_REQUESTOR_DNS_OPTNAME = 'samlValidRequestorDNs'
1949    SAML_ASSERTION_LIFETIME_OPTNAME = 'samlAssertionLifetime'
1950   
1951    __slots__ = (
1952        CONNECTION_STRING_OPTNAME,
1953        ATTRIBUTE_SQLQUERY_OPTNAME,
1954        SAML_SUBJECT_SQLQUERY_OPTNAME,
1955        SAML_ATTRIBUTE_SQLQUERIES_OPTNAME,
1956        SAML_ATTRIBUTE_NAMES_OPTNAME,
1957        SAML_VALID_REQUESTOR_DNS_OPTNAME,
1958        SAML_ASSERTION_LIFETIME_OPTNAME,
1959        'samlAttributeMap'
1960    )
1961    __PRIVATE_ATTR_PREFIX = '_SQLAlchemyAttributeInterface__'
1962    __slots__ += tuple([__PRIVATE_ATTR_PREFIX + i for i in __slots__])
1963   
1964    def __init__(self, **properties):
1965        '''Instantiate object taking in settings from the input
1966        properties'''
1967        log.debug('Initialising SQLAlchemyAttributeInterface instance ...')
1968       
1969        if not sqlAlchemyInstalled:
1970            raise AttributeInterfaceConfigError("SQLAlchemy is not installed")
1971       
1972        self.__connectionString = None
1973        self.__attributeSqlQuery = None
1974        self.__samlSubjectSqlQuery = None
1975        self.__samlAttributeSqlQueries = None
1976        self.__samlAttributeNames = None
1977        self.__samlValidRequestorDNs = None
1978        self.__samlAssertionLifetime = None
1979
1980        self.setProperties(**properties)
1981
1982    def setProperties(self, **properties):
1983        for name, val in properties.items():
1984            setattr(self, name, val)
1985           
1986    def _getSamlAssertionLifetime(self):
1987        return self.__samlAssertionLifetime
1988
1989    def _setSamlAssertionLifetime(self, value):
1990        if isinstance(value, timedelta):
1991            self.__samlAssertionLifetime = value
1992           
1993        if isinstance(value, (float, int, long)):
1994            self.__samlAssertionLifetime = timedelta(seconds=value)
1995           
1996        elif isinstance(value, basestring):
1997            self.__samlAssertionLifetime = timedelta(seconds=float(value))
1998        else:
1999            raise TypeError('Expecting float, int, long, string or timedelta '
2000                'type for "%s"; got %r' % 
2001                (SQLAlchemyAttributeInterface.SAML_ASSERTION_LIFETIME_OPTNAME,
2002                 type(value)))
2003
2004    samlAssertionLifetime = property(_getSamlAssertionLifetime, 
2005                                     _setSamlAssertionLifetime, 
2006                                     doc="Time validity for SAML Assertion "
2007                                         "set in SAML Response returned from "
2008                                         "getAttributes")
2009
2010    def _getSamlSubjectSqlQuery(self):
2011        return self.__samlSubjectSqlQuery
2012
2013    def _setSamlSubjectSqlQuery(self, value):
2014        if not isinstance(value, basestring):
2015            raise TypeError('Expecting string type for "%s" attribute; got %r'%
2016                    (SQLAlchemyAttributeInterface.SAML_SUBJECT_SQLQUERY_OPTNAME,
2017                     type(value)))
2018           
2019        self.__samlSubjectSqlQuery = value
2020
2021    samlSubjectSqlQuery = property(_getSamlSubjectSqlQuery, 
2022                                   _setSamlSubjectSqlQuery, 
2023                                   doc="SAML Subject SQL Query")
2024
2025    def _getSamlAttributeSqlQueries(self):
2026        return self.__samlAttributeSqlQueries
2027
2028    def _setSamlAttributeSqlQueries(self, value):
2029        if isinstance(value, basestring):
2030            # Allow for multiple queries split by a newline.  Empty and
2031            # commented lines are skipped
2032            self.__samlAttributeSqlQueries = [q for q in value.split(os.linesep)
2033                                              if q and not q.startswith('#')]
2034           
2035        elif isinstance(value, (tuple, list)):
2036            for query in value:
2037                if not isinstance(query, basestring):
2038                    raise TypeError('Expecting string type for "%s" '
2039                                    'attribute items; got %r' %
2040                (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE_SQLQUERIES_OPTNAME,
2041                 type(value)))
2042                   
2043            self.__samlAttributeSqlQueries = value                 
2044        else:
2045            raise TypeError('Expecting string, list or tuple type for "%s" '
2046                'attribute; got %r' %
2047                (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE_SQLQUERIES_OPTNAME,
2048                 type(value)))
2049           
2050
2051    samlAttributeSqlQueries = property(_getSamlAttributeSqlQueries, 
2052                                       _setSamlAttributeSqlQueries, 
2053                                       doc="SQL Query or queries to obtain the "
2054                                           "attribute information to respond "
2055                                           "a SAML attribute query.  The "
2056                                           "attributes returned from each "
2057                                           "query concatenated together, must "
2058                                           "exactly match the SAML attribute "
2059                                           "names set in the samlAttributeNames "
2060                                           "property")
2061
2062    def _getSamlAttributeNames(self):
2063        return self.__samlAttributeNames
2064
2065    def _setSamlAttributeNames(self, value):
2066        if isinstance(value, basestring):
2067            self.__samlAttributeNames = [name for name in value.split()]
2068           
2069        elif isinstance(value, (tuple, list)):
2070            for name in value:
2071                if not isinstance(name, basestring):
2072                    raise TypeError('Expecting string type for "%s" '
2073                                    'attribute items; got %r' %
2074                    (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE_NAMES_OPTNAME,
2075                     type(value)))
2076                   
2077            self.__samlAttributeNames = value
2078        else:
2079            raise TypeError('Expecting list/tuple or string type for "%s" '
2080                    'attribute; got %r' %
2081                    (SQLAlchemyAttributeInterface.SAML_ATTRIBUTE_NAMES_OPTNAME,
2082                     type(value)))
2083           
2084    samlAttributeNames = property(_getSamlAttributeNames, 
2085                                  _setSamlAttributeNames, 
2086                                  doc="Attribute names that the Attribute "
2087                                      "Authority supports retrieval of via "
2088                                      "its SAML Attribute query interface.  "
2089                                      "The order of these names is significant:"
2090                                      " the order must map directly to the "
2091                                      "results of the samlAttributeSqlQueries "
2092                                      "as retrieved by the "
2093                                      "_queryDbForSamlAttributes method")
2094
2095    def _getSamlValidRequestorDNs(self):
2096        return self.__samlValidRequestorDNs
2097
2098    def _setSamlValidRequestorDNs(self, value):
2099        if isinstance(value, basestring):
2100            self.__samlValidRequestorDNs = [X500DN.fromString(dn) 
2101                                            for dn in value.split()]
2102        elif isinstance(value, (tuple, list)):
2103            self.__samlValidRequestorDNs = [X500DN.fromString(dn) 
2104                                            for dn in value]
2105        else:
2106            raise TypeError('Expecting list/tuple or basestring type for "%s" '
2107                'attribute; got %r' %
2108                (SQLAlchemyAttributeInterface.SAML_VALID_REQUESTOR_DNS_OPTNAME,
2109                 type(value)))
2110   
2111    samlValidRequestorDNs = property(_getSamlValidRequestorDNs, 
2112                                     _setSamlValidRequestorDNs, 
2113                                     doc="list of certificate Distinguished "
2114                                         "Names referring to the client "
2115                                         "identities permitted to query the "
2116                                         "Attribute Authority via the SAML "
2117                                         "Attribute Query interface")
2118    def _getConnectionString(self):
2119        return self.__connectionString
2120
2121    def _setConnectionString(self, value):
2122        if not isinstance(value, basestring):
2123            raise TypeError('Expecting string type for "%s" attribute; got %r'%
2124                        (SQLAlchemyAttributeInterface.CONNECTION_STRING_OPTNAME,
2125                         type(value)))
2126        self.__connectionString = value
2127
2128    connectionString = property(fget=_getConnectionString, 
2129                                fset=_setConnectionString, 
2130                                doc="Database connection string")
2131
2132    def _getAttributeSqlQuery(self):
2133        return self.__attributeSqlQuery
2134
2135    def _setAttributeSqlQuery(self, value):
2136        if not isinstance(value, basestring):
2137            raise TypeError('Expecting string type for "%s" attribute; got %r'% 
2138                    (SQLAlchemyAttributeInterface.ATTRIBUTE_SQLQUERY_OPTNAME,
2139                     type(value)))
2140        self.__attributeSqlQuery = value
2141
2142    attributeSqlQuery = property(fget=_getAttributeSqlQuery, 
2143                                 fset=_setAttributeSqlQuery, 
2144                                 doc="SQL Query for attribute query")
2145
2146    def getRoles(self, userId):     
2147        """Return valid roles for the given userId
2148
2149        @type userId: basestring
2150        @param userId: user identity
2151        @rtype: list
2152        @return: list of roles for the given user
2153        """
2154
2155        dbEngine = create_engine(self.connectionString)
2156        connection = dbEngine.connect()
2157       
2158        try:
2159            queryInputs = {
2160                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME:
2161                userId
2162            }
2163            query = Template(self.attributeSqlQuery).substitute(queryInputs)
2164            result = connection.execute(query)
2165
2166        except exc.ProgrammingError:
2167            raise AttributeInterfaceRetrieveError("Error with SQL Syntax: %s" %
2168                                                  traceback.format_exc())
2169        finally:
2170            connection.close()
2171
2172        try:
2173            attributes = [attr for attr in result][0][0]
2174       
2175        except (IndexError, TypeError):
2176            raise AttributeInterfaceRetrieveError("Error with result set: %s" %
2177                                                  traceback.format_exc())
2178       
2179        log.debug('Attributes=%r retrieved for user=%r' % (attributes, 
2180                                                           userId))
2181       
2182        return attributes
2183
2184    def getAttributes(self, attributeQuery, response):
2185        """Attribute Authority SAML AttributeQuery
2186       
2187        @type attributeQuery: saml.saml2.core.AttributeQuery
2188        @param userId: query containing requested attributes
2189        @type: saml.saml2.core.Response
2190        @param: Response - add an assertion with the list of attributes
2191        for the given subject ID in the query or set an error Status code and
2192        message
2193        @raise AttributeInterfaceError: an error occured requesting
2194        attributes
2195        @raise AttributeReleaseDeniedError: Requestor was denied release of the
2196        requested attributes
2197        @raise AttributeNotKnownError: Requested attribute names are not known
2198        to this authority
2199        """
2200        userId = attributeQuery.subject.nameID.value
2201        requestedAttributeNames = [attribute.name
2202                                   for attribute in attributeQuery.attributes]
2203       
2204        requestorDN = X500DN.fromString(attributeQuery.issuer.value)
2205
2206        if not self._queryDbForSamlSubject(userId)(userId):
2207            raise UserIdNotKnown('Subject Id "%s" is not known to this '
2208                                 'authority' % userId)
2209
2210        if requestorDN not in self.samlValidRequestorDNs:
2211            raise InvalidRequestorId('Requestor identity "%s" is invalid' %
2212                                     requestorDN)
2213
2214        unknownAttrNames = [attrName for attrName in requestedAttributeNames
2215                            if attrName not in self.samlAttributeNames]
2216
2217        if len(unknownAttrNames) > 0:
2218            raise AttributeNotKnownError("Unknown attributes requested: %r" %
2219                                         unknownAttrNames)
2220
2221        # Query the database for the supported attributes and return them
2222        # mapped to their attribute names as specified by the attributeNames
2223        # property
2224        attributeMap = self._queryDbForSamlAttributes(userId)
2225       
2226        # Create a new assertion to hold the attributes to be returned
2227        assertion = Assertion()
2228
2229        assertion.version = SAMLVersion(SAMLVersion.VERSION_20)
2230        assertion.id = str(uuid4())
2231        assertion.issueInstant = response.issueInstant
2232
2233        assertion.conditions = Conditions()
2234        assertion.conditions.notBefore = assertion.issueInstant
2235        assertion.conditions.notOnOrAfter = (assertion.conditions.notBefore + 
2236                                timedelta(seconds=self.samlAssertionLifetime))
2237
2238        assertion.subject = Subject()
2239        assertion.subject.nameID = NameID()
2240        assertion.subject.nameID.format = attributeQuery.subject.nameID.format
2241        assertion.subject.nameID.value = attributeQuery.subject.nameID.value
2242
2243        attributeStatement = AttributeStatement()
2244
2245        # Add test set of attributes
2246        for requestedAttribute in attributeQuery.attributes:
2247            attribute = Attribute()
2248            attribute.name = requestedAttribute.name
2249            attribute.nameFormat = requestedAttribute.nameFormat
2250
2251            if requestedAttribute.friendlyName is not None:
2252                attribute.friendlyName = requestedAttribute.friendlyName
2253
2254            if attribute.name in self.samlAttributeNames:
2255                attributeVals = attributeMap[attribute.name]
2256#                for val in badcAttributeVals:
2257#                    attribute.attributeValues.append(XSStringAttributeValue())
2258#                    attribute.attributeValues[-1].value = val
2259#
2260#            elif attribute.name == CSVFileAttributeInterface.EMAILADDR_ATTR_NAME:
2261#                attribute.attributeValues.append(XSStringAttributeValue())
2262#                attribute.attributeValues[-1].value = self.attributeMap[
2263#                        userId][CSVFileAttributeInterface.EMAILADDR_ATTR_INDEX]
2264#
2265#            elif attribute.name == CSVFileAttributeInterface.FIRSTNAME_ATTR_NAME:
2266#                attribute.attributeValues.append(XSStringAttributeValue())
2267#                attribute.attributeValues[-1].value = self.attributeMap[
2268#                        userId][CSVFileAttributeInterface.FIRSTNAME_ATTR_INDEX]
2269#
2270#            elif attribute.name == CSVFileAttributeInterface.LASTNAME_ATTR_NAME:
2271#                attribute.attributeValues.append(XSStringAttributeValue())
2272#                attribute.attributeValues[-1].value = self.attributeMap[
2273#                        userId][CSVFileAttributeInterface.LASTNAME_ATTR_INDEX]
2274
2275            attributeStatement.attributes.append(attribute)
2276
2277        assertion.attributeStatements.append(attributeStatement)
2278        response.assertions.append(assertion)
2279       
2280    def _queryDbForSamlSubject(self, userId):     
2281        """Check a given SAML subject (user) is registered in the database.
2282        This method is called from the getAttributes() method
2283
2284        @type userId: basestring
2285        @param userId: user identity
2286        @rtype: bool
2287        @return: True/False is user registered?
2288        """
2289        if self.samlSubjectSqlQuery is None:
2290            log.debug('No "self.samlSubjectSqlQuery" property has been set, '
2291                      'skipping SAML subject query step')
2292            return True
2293       
2294        dbEngine = create_engine(self.connectionString)
2295        connection = dbEngine.connect()
2296       
2297        try:
2298            queryInputs = {
2299                SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME:
2300                userId
2301            }
2302            query = Template(self.samlSubjectSqlQuery).substitute(queryInputs)
2303            result = connection.execute(query)
2304
2305        except exc.ProgrammingError:
2306            raise AttributeInterfaceRetrieveError("Error with SQL Syntax: %s" %
2307                                                  traceback.format_exc())
2308        finally:
2309            connection.close()
2310
2311        try:
2312            found = [entry for entry in result][0][0]
2313       
2314        except (IndexError, TypeError):
2315            raise AttributeInterfaceRetrieveError("Error with result set: %s" %
2316                                                  traceback.format_exc())
2317       
2318        log.debug('user=%r found=%r' % (username, found))
2319       
2320        return found
2321     
2322    def _queryDbForSamlAttributes(self, userId):     
2323        """Query the database in response to a SAML attribute query
2324       
2325        This method is called from the getAttributes() method
2326
2327        @type userId: basestring
2328        @param userId: user identity
2329        @rtype: bool
2330        @return: True/False is user registered?
2331        """
2332
2333        dbEngine = create_engine(self.connectionString)
2334        connection = dbEngine.connect()
2335       
2336        attributes = () 
2337       
2338        for queryTmpl in self.samlAttributeSqlQueries:
2339            try:
2340                queryInputs = {
2341                    SQLAlchemyAttributeInterface.SQLQUERY_USERID_KEYNAME:
2342                    userId
2343                }
2344                query = Template(queryTmpl).substitute(queryInputs)
2345                result = connection.execute(query)
2346   
2347            except exc.ProgrammingError:
2348                raise AttributeInterfaceRetrieveError("Error with SQL Syntax: "
2349                                                      "%s" %
2350                                                      traceback.format_exc())
2351            finally:
2352                connection.close()
2353
2354        try:
2355            found = [entry for entry in result][0][0]
2356       
2357        except (IndexError, TypeError):
2358            raise AttributeInterfaceRetrieveError("Error with result set: %s" %
2359                                                  traceback.format_exc())
2360       
2361        log.debug('user=%r found=%r' % (username, found))
2362       
2363        attributeMap = dict(zip(self.samlAttributeNames, attributes))
2364       
2365        return attributeMap
2366     
2367    def __getstate__(self):
2368        '''Explicit pickling required with __slots__'''
2369        return dict([(attrName, getattr(self, attrName)) \
2370                      for attrName in SQLAlchemyAttributeInterface.__slots__])
2371       
2372    def __setstate__(self, attrDict):
2373        '''Enable pickling for use with beaker.session'''
2374        for attr, val in attrDict.items():
2375            setattr(self, attr, val)           
2376
2377       
2378   
2379       
Note: See TracBrowser for help on using the repository browser.