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

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

Preparing 1.1.0 release: first with SAML interface 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
29from saml.utils import SAMLDateTime
30from saml.saml2.core import Response, Assertion, Attribute, AttributeValue, \
31    AttributeStatement, SAMLVersion, Subject, NameID, Issuer, AttributeQuery, \
32    XSStringAttributeValue, XSGroupRoleAttributeValue, Conditions, Status, \
33    StatusCode
34   
35from saml.common.xml import SAMLConstants
36from saml.xml.etree import AssertionElementTree, AttributeQueryElementTree, \
37    ResponseElementTree, XSGroupRoleAttributeValueElementTree
38
39from ndg.security.common.utils import TypedList
40from ndg.security.common.utils.classfactory import instantiateClass
41from ndg.security.common.utils.configfileparsers import \
42    CaseSensitiveConfigParser
43   
44# X.509 Certificate handling
45from ndg.security.common.X509 import X509Cert
46
47# NDG Attribute Certificate
48from ndg.security.common.AttCert import AttCert
49
50
51class AttributeAuthorityError(Exception):
52    """Exception handling for NDG Attribute Authority class."""
53    def __init__(self, msg):
54        log.error(msg)
55        Exception.__init__(self, msg)
56
57
58class AttributeAuthorityConfigError(Exception):
59    """NDG Attribute Authority error with configuration. e.g. properties file
60    directory permissions or role mapping file"""
61    def __init__(self, msg):
62        log.error(msg)
63        Exception.__init__(self, msg)
64       
65       
66class AttributeAuthorityAccessDenied(AttributeAuthorityError):
67    """NDG Attribute Authority - access denied exception.
68
69    Raise from getAttCert method where no roles are available for the user
70    but that the request is otherwise valid.  In all other error cases raise
71    AttributeAuthorityError"""   
72   
73   
74class AttributeAuthorityNoTrustedHosts(AttributeAuthorityError):
75    """Raise from getTrustedHosts if there are no trusted hosts defined in
76    the map configuration"""
77
78
79class AttributeAuthorityNoMatchingRoleInTrustedHosts(AttributeAuthorityError):
80    """Raise from getTrustedHosts if there is no mapping to any of the
81    trusted hosts for the given input role name"""
82
83
84class AttributeAuthority(object):
85    """NDG Attribute Authority - service for allocation of user authorization
86    tokens - attribute certificates.
87   
88    @type propertyDefaults: dict
89    @cvar propertyDefaults: valid configuration property keywords
90   
91    @type attributeInterfacePropertyDefaults: dict
92    @cvar attributeInterfacePropertyDefaults: valid configuration property
93    keywords for the Attribute Interface plugin
94   
95    @type mapConfigHostDefaults: dict
96    @cvar mapConfigHostDefaults: valid configuration property
97    keywords for the Map Configuration XML Host element
98   
99    @type DEFAULT_CONFIG_DIRNAME: string
100    @cvar DEFAULT_CONFIG_DIRNAME: configuration directory under $NDGSEC_DIR -
101    default location for properties file
102   
103    @type DEFAULT_PROPERTY_FILENAME: string
104    @cvar DEFAULT_PROPERTY_FILENAME: default file name for properties file
105    under DEFAULT_CONFIG_DIRNAME
106   
107    @type ATTRIBUTE_INTERFACE_KEYNAME: basestring
108    @param ATTRIBUTE_INTERFACE_KEYNAME: attribute interface parameters key
109    name - see initAttributeInterface for details
110    """
111
112    # Code designed from NERC Data Grid Enterprise and Information Viewpoint
113    # documents.
114    #
115    # Also, draws from Neil Bennett's ACServer class used in the Java
116    # implementation of NDG Security
117
118    DEFAULT_CONFIG_DIRNAME = "conf"
119    DEFAULT_PROPERTY_FILENAME = "attributeAuthority.cfg"
120    ATTRIBUTE_INTERFACE_KEYNAME = 'attributeInterface'
121    CONFIG_LIST_SEP_PAT = re.compile(',\W*')
122   
123    attributeInterfacePropertyDefaults = {
124        'modFilePath':  '',
125        'modName':      '',
126        'className':    ''
127    }
128   
129    # valid configuration property keywords with accepted default values. 
130    # Values set to not NotImplemented here denote keys which must be specified
131    # in the config
132    propertyDefaults = { 
133        'name':                 '',
134        'signingCertFilePath':  '',
135        'signingPriKeyFilePath':'',
136        'signingPriKeyPwd':     None,
137        'caCertFilePathList':   [],
138        'attCertLifetime':      -1,
139        'attCertNotBeforeOff':  0.,
140        'attCertFileName':      '',
141        'attCertFileLogCnt':    0,
142        'mapConfigFilePath':    '',
143        'attCertDir':           '',
144        'dnSeparator':          '/',
145        ATTRIBUTE_INTERFACE_KEYNAME: attributeInterfacePropertyDefaults
146    }
147   
148    mapConfigHostDefaults = {
149        'siteName':                 None,
150        'aaURI':                    NotImplemented,
151        'aaDN':                     NotImplemented,
152        'loginURI':                 NotImplemented,
153        'loginServerDN':            NotImplemented,
154        'loginRequestServerDN':     NotImplemented
155    }
156
157    def __init__(self):
158        """Create new Attribute Authority instance"""
159        log.info("Initialising service ...")
160       
161        # Initial config file property based attributes
162        for name, val in AttributeAuthority.propertyDefaults.items():
163            setattr(self, '_AttributeAuthority__%s' % name, val)
164       
165        self.__caCertFilePathList = TypedList(basestring)
166       
167        self.__propFilePath = None       
168        self.__propFileSection = 'DEFAULT'
169        self.__propPrefix = ''
170       
171        # Initialise role mapping look-ups - These are set in readMapConfig()
172        self.__mapConfig = None
173        self.__localRole2RemoteRole = None
174        self.__remoteRole2LocalRole = None
175       
176        self.__cert = None
177       
178        # Issuer details - serialise using the separator string set in the
179        # properties file
180        self.__issuer = None
181        self.__issuerSerialNumber = None
182        self.__attCertLog = None
183        self.__name = None
184       
185        self.__attributeInterfaceCfg = {}
186
187    def _getMapConfig(self):
188        return self.__mapConfig
189
190    def _getCert(self):
191        return self.__cert
192
193    def _getIssuer(self):
194        return self.__issuer
195
196    def _getIssuerSerialNumber(self):
197        return self.__issuerSerialNumber
198
199    def _getAttCertLog(self):
200        return self.__attCertLog
201
202    def _getName(self):
203        return self.__name
204
205    def _getAttCertLifetime(self):
206        return self.__attCertLifetime
207
208    def _getAttCertNotBeforeOff(self):
209        return self.__attCertNotBeforeOff
210
211    def _getAttCertDir(self):
212        return self.__attCertDir
213
214    def _getAttributeInterface(self):
215        return self.__attributeInterface
216
217    def _getMapConfigFile(self):
218        return self.__mapConfigFile
219
220    def _getName(self):
221        return self.__name
222
223    def _getTrustedHostInfo(self):
224        return self.__trustedHostInfo
225
226    def _setCert(self, value):
227        if not isinstance(value, X509Cert):
228            raise TypeError('Expecting %r type for "cert"; got %r' %
229                            (X509Cert, type(value)))
230           
231        self.__cert = value
232
233    def _setIssuer(self, value):
234        self.__issuer = value
235
236    def _setIssuerSerialNumber(self, value):
237        if not isinstance(value, (long, int)):
238            raise TypeError('Expecting long or int type for "name"; got %r' %
239                            type(value))
240        self.__issuerSerialNumber = value
241
242    def _setAttCertLog(self, value):
243        if not isinstance(value, AttCertLog):
244            raise TypeError('Expecting %r type for "attCertLog"; got %r' %
245                            (AttCertLog, type(value)))
246        self.__attCertLog = value
247
248    def _setName(self, value):
249        if not isinstance(value, basestring):
250            raise TypeError('Expecting string type for "name"; got %r' %
251                            type(value))
252        self.__name = value
253
254    def _setAttCertLifetime(self, value):
255        if isinstance(value, float):
256            self.__attCertLifetime = value
257           
258        elif isinstance(value, (basestring, int, long)):
259            self.__attCertLifetime = float(value)
260        else:
261            raise TypeError('Expecting float, int, long or string type for '
262                            '"attCertLifetime"; got %r' % type(value))
263
264    def _setAttCertNotBeforeOff(self, value):
265        if isinstance(value, float):
266            self.__attCertNotBeforeOff = value
267           
268        elif isinstance(value, (basestring, int, long)):
269            self.__attCertNotBeforeOff = float(value)
270        else:
271            raise TypeError('Expecting float, int, long or string type for '
272                            '"attCertNotBeforeOff"; got %r' % type(value))
273
274    def _setAttCertDir(self, value):
275        if not isinstance(value, basestring):
276            raise TypeError('Expecting string type for "attCertDir"; got %r' % 
277                            type(value))
278
279        # Check directory path
280        try:
281            dirList = os.listdir(value)
282
283        except OSError, osError:
284            raise AttributeAuthorityConfigError('Invalid directory path for '
285                                                'Attribute Certificates store '
286                                                '"%s": %s' % 
287                                                (value, osError.strerror))
288        self.__attCertDir = value
289
290    def _setMapConfigFilePath(self, value):
291        self.__mapConfigFilePath = value
292
293    def _setTrustedHostInfo(self, value):
294        self.__trustedHostInfo = value
295
296    def _get_caCertFilePathList(self):
297        return self.__caCertFilePathList
298
299    def _set_caCertFilePathList(self, val):
300        if not isinstance(val, (list, tuple)):
301            raise TypeError('Expecting list or tuple type for '
302                            '"caCertFilePathList"; got %r' % type(val))
303           
304        # Overwrite any original settings
305        self.__caCertFilePathList = TypedList(basestring)
306       
307        # Update with new items
308        self.__caCertFilePathList += val
309   
310    caCertFilePathList = property(fget=_get_caCertFilePathList,
311                                  fset=_set_caCertFilePathList,
312                                  doc="list of file paths for CA certificates "
313                                      "used to validate an Attribute "
314                                      "Certificate")
315   
316    def _get_signingCertFilePath(self):
317        return self.__signingCertFilePath
318   
319    def _set_signingCertFilePath(self, value):
320        if not isinstance(value, basestring):
321            raise TypeError('Expecting string type for "signingCertFilePath"; '
322                            'got %r' % type(value))
323        self.__signingCertFilePath = value
324         
325    signingCertFilePath = property(fget=_get_signingCertFilePath, 
326                                   fset=_set_signingCertFilePath,
327                                   doc="X.509 certificate used for Attribute "
328                                       "certificate signature")
329   
330    def _get_signingPriKeyFilePath(self):
331        return self.__signingPriKeyFilePath
332   
333    def _set_signingPriKeyFilePath(self, value):
334        if not isinstance(value, basestring):
335            raise TypeError('Expecting string type for '
336                            '"signingPriKeyFilePath"; got %r' % type(value))
337        self.__signingPriKeyFilePath = value
338         
339    signingPriKeyFilePath = property(fget=_get_signingPriKeyFilePath, 
340                                     fset=_set_signingPriKeyFilePath,
341                                     doc="File Path for private key used to "
342                                         "sign Attribute certificate")
343   
344    def _get_signingPriKeyPwd(self):
345        return self.__signingPriKeyPwd
346   
347    def _set_signingPriKeyPwd(self, value):
348        if not isinstance(value, (type(None), basestring)):
349            raise TypeError('Expecting string or None type for '
350                            '"signingPriKeyPwd"; got %r' % type(value))
351        self.__signingPriKeyPwd = value
352         
353    signingPriKeyPwd = property(fget=_get_signingPriKeyPwd, 
354                                fset=_set_signingPriKeyPwd,
355                                doc="Password for private key file used to "
356                                    "for Attribute certificate signature")
357
358    def _get_attributeInterfaceCfg(self):
359        return self.__attributeInterfaceCfg
360   
361    attributeInterfaceCfg = property(fget=_get_attributeInterfaceCfg,
362                                     doc="Settings for Attribute Interface "
363                                         "initialisation")
364   
365    def _get_attCertFileName(self):
366        return self.__attCertFileName
367   
368    def _set_attCertFileName(self, value):
369        if not isinstance(value, basestring):
370            raise TypeError('Expecting string type for "attCertFileName"; got '
371                            '%r' % type(value))
372           
373        self.__attCertFileName = value
374         
375    attCertFileName = property(fget=_get_attCertFileName, 
376                                fset=_set_attCertFileName,
377                                doc="Attribute certificate file name for log "
378                                    "initialisation")
379   
380    def _get_attCertFileLogCnt(self):
381        return self.__attCertFileLogCnt
382   
383    def _set_attCertFileLogCnt(self, value):
384        if isinstance(value, int):
385            self.__attCertFileLogCnt = value
386        elif isinstance(value, basestring):
387            self.__attCertFileLogCnt = int(value)
388        else:
389            raise TypeError('Expecting int or string type for '
390                            '"attCertFileLogCnt"; got %r' % type(value))
391         
392    attCertFileLogCnt = property(fget=_get_attCertFileLogCnt, 
393                                 fset=_set_attCertFileLogCnt,
394                                 doc="Counter for Attribute Certificate log "
395                                     "rotating file handler")
396   
397    def _get_dnSeparator(self):
398        return self.__dnSeparator
399   
400    def _set_dnSeparator(self, value):
401        if not isinstance(value, basestring):
402            raise TypeError('Expecting string type for "dnSeparator"; got '
403                            '%r' % type(value))
404        self.__dnSeparator = value
405         
406    dnSeparator = property(fget=_get_dnSeparator, 
407                           fset=_set_dnSeparator,
408                           doc="Distinguished Name separator character used "
409                               "with X.509 Certificate issuer certificate")
410           
411    def _getMapConfigFilePath(self):
412        return self.__mapConfigFilePath
413   
414    def _setMapConfigFilePath(self, val):
415        if not isinstance(val, basestring):
416            raise AttributeAuthorityConfigError("Input Map Configuration "
417                                                "file path must be a "
418                                                "valid string.")
419        self.__mapConfigFilePath = val
420         
421    mapConfigFilePath = property(fget=_getMapConfigFilePath,
422                                 fset=_setMapConfigFilePath,
423                                 doc="File path for Role Mapping configuration") 
424
425    def setPropFilePath(self, val=None):
426        """Set properties file from input or based on environment variable
427        settings
428       
429        @type val: basestring
430        @param val: properties file path"""
431        log.debug("Setting property file path")
432        if not val:
433            if 'NDGSEC_AA_PROPFILEPATH' in os.environ:
434                val = os.environ['NDGSEC_AA_PROPFILEPATH']
435               
436            elif 'NDGSEC_DIR' in os.environ:
437                val = os.path.join(os.environ['NDGSEC_DIR'], 
438                                   AttributeAuthority.DEFAULT_CONFIG_DIRNAME,
439                                   AttributeAuthority.DEFAULT_PROPERTY_FILENAME)
440            else:
441                raise AttributeError('Unable to set default Attribute '
442                                     'Authority properties file path: neither '
443                                     '"NDGSEC_AA_PROPFILEPATH" or "NDGSEC_DIR"'
444                                     ' environment variables are set')
445               
446        if not isinstance(val, basestring):
447            raise AttributeError("Input Properties file path "
448                                 "must be a valid string.")
449     
450        self.__propFilePath = os.path.expandvars(val)
451        log.debug("Path set to: %s" % val)
452       
453    def getPropFilePath(self):
454        '''Get the properties file path
455       
456        @rtype: basestring
457        @return: properties file path'''
458        return self.__propFilePath
459       
460    # Also set up as a property
461    propFilePath = property(fset=setPropFilePath,
462                            fget=getPropFilePath,
463                            doc="path to file containing Attribute Authority "
464                                "configuration parameters.  It defaults to "
465                                "$NDGSEC_AA_PROPFILEPATH or if not set, "
466                                "$NDGSEC_DIR/conf/attributeAuthority.cfg")   
467   
468    def setPropFileSection(self, val=None):
469        """Set section name to read properties from ini file.  This is set from
470        input or based on environment variable setting
471        NDGSEC_AA_PROPFILESECTION
472       
473        @type val: basestring
474        @param val: section name"""
475        log.debug("Setting property file section name")
476        if not val:
477            val = os.environ.get('NDGSEC_AA_PROPFILESECTION', 'DEFAULT')
478               
479        if not isinstance(val, basestring):
480            raise AttributeError("Input Properties file section name "
481                                 "must be a valid string.")
482     
483        self.__propFileSection = val
484        log.debug("Properties file section set to: %s" % val)
485       
486    def getPropFileSection(self):
487        '''Get the section name to extract properties from an ini file -
488        DOES NOT apply to XML file properties
489       
490        @rtype: basestring
491        @return: section name'''
492        return self.__propFileSection
493       
494    # Also set up as a property
495    propFileSection = property(fset=setPropFileSection,
496                               fget=getPropFileSection,
497                               doc="Set the file section name for ini file "
498                                   "properties")   
499   
500    def setPropPrefix(self, val=None):
501        """Set prefix for properties read from ini file.  This is set from
502        input or based on environment variable setting
503        NDGSEC_AA_PROPFILEPREFIX
504       
505        DOES NOT apply to XML file properties
506       
507        @type val: basestring
508        @param val: section name"""
509        log.debug("Setting property file section name")
510        if val is None:
511            val = os.environ.get('NDGSEC_AA_PROPFILEPREFIX', 'DEFAULT')
512               
513        if not isinstance(val, basestring):
514            raise AttributeError("Input Properties file section name "
515                                 "must be a valid string.")
516     
517        self.__propPrefix = val
518        log.debug("Properties file section set to: %s" % val)
519       
520    def getPropPrefix(self):
521        '''Get the prefix name used for properties in an ini file -
522        DOES NOT apply to XML file properties
523       
524        @rtype: basestring
525        @return: section name'''
526        log.debug("Getting property file prefix")
527        return self.__propPrefix
528   
529       
530    # Also set up as a property
531    propPrefix = property(fset=setPropPrefix,
532                          fget=getPropPrefix,
533                          doc="Set a prefix for ini file properties")   
534   
535    mapConfig = property(fget=_getMapConfig, 
536                         doc="MapConfig object")
537
538    cert = property(fget=_getCert, 
539                    fset=_setCert, 
540                    doc="X.509 Issuer Certificate")
541
542    issuer = property(fget=_getIssuer, 
543                      fset=_setIssuer, 
544                      doc="Issuer name")
545
546    issuerSerialNumber = property(fget=_getIssuerSerialNumber, 
547                                  fset=_setIssuerSerialNumber, 
548                                  doc="Issuer Serial Number")
549
550    attCertLog = property(fget=_getAttCertLog,
551                          fset=_setAttCertLog, 
552                          doc="Attribute certificate logging object")
553
554    name = property(fget=_getName, 
555                    fset=_setName, 
556                    doc="Issuer organisation name")
557
558    attCertLifetime = property(fget=_getAttCertLifetime, 
559                               fset=_setAttCertLifetime, 
560                               doc="Attribute certificate lifetime")
561
562    attCertNotBeforeOff = property(fget=_getAttCertNotBeforeOff, 
563                                   fset=_setAttCertNotBeforeOff, 
564                                   doc="Attribute certificate clock skew in "
565                                       "seconds")
566
567    attCertDir = property(fget=_getAttCertDir, 
568                          fset=_setAttCertDir, 
569                          doc="Attribute certificate log directory")
570
571    attributeInterface = property(fget=_getAttributeInterface, 
572                                  doc="Attribute Interface object")
573
574    name = property(fget=_getName, fset=_setName, doc="Organisation Name")
575
576    trustedHostInfo = property(fget=_getTrustedHostInfo, 
577                               fset=_setTrustedHostInfo, 
578                               doc="Dictionary of trusted organisations")
579       
580    @classmethod
581    def fromPropertyFile(cls, propFilePath=None, propFileSection='DEFAULT',
582                         propPrefix='attributeauthority.', 
583                         bReadMapConfig=True):
584        """Create new NDG Attribute Authority instance from the property file
585        settings
586
587        @type propFilePath: string
588        @param propFilePath: path to file containing Attribute Authority
589        configuration parameters.  It defaults to $NDGSEC_AA_PROPFILEPATH or
590        if not set, $NDGSEC_DIR/conf/attributeAuthority.cfg
591        @type propFileSection: basestring
592        @param propFileSection: section of properties file to read from.
593        properties files
594        @type propPrefix: basestring
595        @param propPrefix: set a prefix for filtering attribute authority
596        property names - useful where properties are being parsed from a file
597        section containing parameter names for more than one application
598        @type bReadMapConfig: boolean
599        @param bReadMapConfig: by default the Map Configuration file is
600        read.  Set this flag to False to override.
601        """
602           
603        attributeAuthority = AttributeAuthority()
604        if propFileSection:
605            attributeAuthority.propFileSection = propFileSection
606           
607        if propPrefix:
608            attributeAuthority.propPrefix = propPrefix
609
610        attributeAuthority.propFilePath = propFilePath           
611        attributeAuthority.readProperties()
612        attributeAuthority.initialise(bReadMapConfig=bReadMapConfig)
613   
614        return attributeAuthority
615
616       
617    @classmethod
618    def fromProperties(cls, propPrefix='attributeauthority.', 
619                       bReadMapConfig=True, **prop):
620        """Create new NDG Attribute Authority instance from input property
621        keywords
622
623        @type propPrefix: basestring
624        @param propPrefix: set a prefix for filtering attribute authority
625        property names - useful where properties are being parsed from a file
626        section containing parameter names for more than one application
627        @type bReadMapConfig: boolean
628        @param bReadMapConfig: by default the Map Configuration file is
629        read.  Set this flag to False to override.
630        """
631        attributeAuthority = AttributeAuthority()
632        if propPrefix:
633            attributeAuthority.propPrefix = propPrefix
634               
635        attributeAuthority.setProperties(**prop)
636        attributeAuthority.initialise(bReadMapConfig=bReadMapConfig)
637       
638        return attributeAuthority
639   
640    def initialise(self, bReadMapConfig=True):
641        """Convenience method for set up of Attribute Interface, map
642        configuration and PKI"""
643       
644        # Read the Map Configuration file
645        if bReadMapConfig:
646            self.readMapConfig()
647
648        # Instantiate Certificate object
649        log.debug("Reading and checking Attribute Authority X.509 cert. ...")
650        self.cert = X509Cert.Read(self.signingCertFilePath)
651
652        # Check it's valid
653        try:
654            self.cert.isValidTime(raiseExcep=True)
655           
656        except Exception, e:
657            raise AttributeAuthorityError("Attribute Authority's certificate "
658                                          "is invalid: %s" % e)
659       
660        # Check CA certificate
661        log.debug("Reading and checking X.509 CA certificate ...")
662        for caCertFile in self.caCertFilePathList:
663            caCert = X509Cert(caCertFile)
664            caCert.read()
665           
666            try:
667                caCert.isValidTime(raiseExcep=True)
668               
669            except Exception, e:
670                raise AttributeAuthorityError('CA certificate "%s" is '
671                                              'invalid: %s'% (caCert.dn, e))
672       
673        # Issuer details - serialise using the separator string set in the
674        # properties file
675        self.issuer = self.cert.dn.serialise(separator=self.dnSeparator)
676
677        self.issuerSerialNumber = self.cert.serialNumber
678       
679        # Load user - user attribute look-up plugin
680        self.initAttributeInterface()
681       
682        attCertFilePath = os.path.join(self.attCertDir, self.attCertFileName)
683               
684        # Rotating file handler used for logging attribute certificates
685        # issued.
686        self.attCertLog = AttCertLog(attCertFilePath,
687                                     backUpCnt=self.attCertFileLogCnt)
688
689    def setProperties(self, **prop):
690        """Set configuration from an input property dictionary
691        @type prop: dict
692        @param prop: properties dictionary containing configuration items
693        to be set
694        """
695        lenPropPrefix = len(self.propPrefix)
696       
697        # '+ 1' allows for the dot separator
698        lenAttributeInterfacePrefix = len(
699                            AttributeAuthority.ATTRIBUTE_INTERFACE_KEYNAME) + 1
700       
701        for name, val in prop.items():
702            if name.startswith(self.propPrefix):
703                name = name[lenPropPrefix:]
704           
705            if name.startswith(AttributeAuthority.ATTRIBUTE_INTERFACE_KEYNAME):
706                name = name[lenAttributeInterfacePrefix:]
707                self.attributeInterfaceCfg[name] = val
708                continue
709           
710            if name not in AttributeAuthority.propertyDefaults:
711                raise AttributeError('Invalid attribute name "%s"' % name)
712           
713            if isinstance(val, basestring):
714                val = os.path.expandvars(val)
715           
716            if isinstance(AttributeAuthority.propertyDefaults[name], list):
717                val = AttributeAuthority.CONFIG_LIST_SEP_PAT.split(val)
718               
719            # This makes an implicit call to the appropriate property method
720            try:
721                setattr(self, name, val)
722            except AttributeError:
723                raise AttributeError("Can't set attribute \"%s\"" % name)         
724           
725    def readProperties(self):
726        '''Read the properties files and do some checking/converting of input
727        values
728        '''
729        if not os.path.isfile(self.propFilePath):
730            raise IOError('Error parsing properties file "%s": No such file' % 
731                          self.propFilePath)
732           
733        defaultItems = {'here': os.path.dirname(self.propFilePath)}
734       
735        cfg = CaseSensitiveConfigParser(defaults=defaultItems)
736        cfg.read(self.propFilePath)
737       
738        cfgItems = dict([(name, val) 
739                         for name, val in cfg.items(self.propFileSection)
740                         if name != 'here'])
741        self.setProperties(**cfgItems)
742
743    def initAttributeInterface(self):
744        '''Load host sites custom user roles interface to enable the AA to
745        # assign roles in an attribute certificate on a getAttCert request'''
746        classProperties = {}
747        classProperties.update(self.attributeInterfaceCfg)
748       
749        modName = classProperties.pop('modName')
750        className = classProperties.pop('className') 
751       
752        # file path may be omitted   
753        modFilePath = classProperties.pop('modFilePath', None) 
754                     
755        self.__attributeInterface = instantiateClass(modName,
756                                             className,
757                                             moduleFilePath=modFilePath,
758                                             objectType=AttributeInterface,
759                                             classProperties=classProperties)
760
761    def getAttCert(self,
762                   userId=None,
763                   holderX509Cert=None,
764                   holderX509CertFilePath=None,
765                   userAttCert=None,
766                   userAttCertFilePath=None):
767
768        """Request a new Attribute Certificate for use in authorisation
769
770        getAttCert([userId=uid][holderX509Cert=x509Cert|
771                    holderX509CertFilePath=x509CertFile, ]
772                   [userAttCert=cert|userAttCertFilePath=certFile])
773         
774        @type userId: string
775        @param userId: identifier for the user who is entitled to the roles
776        in the certificate that is issued.  If this keyword is omitted, then
777        the userId will be set to the DN of the holder.
778       
779        holder = the holder of the certificate - an inidividual user or an
780        organisation to which the user belongs who vouches for that user's ID
781       
782        userId = the identifier for the user who is entitled to the roles
783        specified in the Attribute Certificate that is issued.
784                 
785        @type holderX509Cert: string / ndg.security.common.X509.X509Cert type
786        @param holderX509Cert: base64 encoded string containing proxy cert./
787        X.509 cert object corresponding to the ID who will be the HOLDER of
788        the Attribute Certificate that will be issued.  - Normally, using
789        proxy certificates, the holder and user ID are the same but there
790        may be cases where the holder will be an organisation ID.  This is the
791        case for NDG security with the DEWS project
792       
793        @param holderX509CertFilePath: string
794        @param holderX509CertFilePath: file path to proxy/X.509 certificate of
795        candidate holder
796     
797        @type userAttCert: string or AttCert type
798        @param userAttCert: externally provided attribute certificate from
799        another data centre.  This is only necessary if the user is not
800        registered with this attribute authority.
801                       
802        @type userAttCertFilePath: string
803        @param userAttCertFilePath: alternative to userAttCert except pass
804        in as a file path to an attribute certificate instead.
805       
806        @rtype: AttCert
807        @return: new attribute certificate"""
808
809        log.debug("Calling getAttCert ...")
810       
811        # Read candidate Attribute Certificate holder's X.509 certificate
812        try:
813            if holderX509CertFilePath is not None:
814                                   
815                # Certificate input as a file
816                holderX509Cert = X509Cert()
817                holderX509Cert.read(holderX509CertFilePath)
818               
819            elif isinstance(holderX509Cert, basestring):
820
821                # Certificate input as string text
822                holderX509Cert = X509Cert.Parse(holderX509Cert)
823               
824            elif not isinstance(holderX509Cert, (X509Cert, None.__class__)):
825                raise AttributeAuthorityError("Holder X.509 Certificate must "
826                                              "be set to valid type: a file "
827                                              "path, string, X509 object or "
828                                              "None")           
829        except Exception, e:
830            log.error("Holder X.509 certificate: %s" % e)
831            raise
832
833
834        # Check certificate hasn't expired
835        if holderX509Cert:
836            log.debug("Checking candidate holder X.509 certificate ...")
837            try:
838                holderX509Cert.isValidTime(raiseExcep=True)
839               
840            except Exception, e:
841                log.error("User X.509 certificate is invalid: " + e)
842                raise
843
844           
845        # If no user ID is input, set id from holder X.509 certificate DN
846        # instead
847        if not userId:
848            if not holderX509Cert:
849                raise AttributeAuthorityError("If no user ID is set a holder "
850                                              "X.509 certificate must be "
851                                              "present")
852            try:
853                userId = holderX509Cert.dn.serialise(\
854                                         separator=self.dnSeparator) 
855            except Exception, e:
856                log.error("Setting user Id from holder certificate DN: %s" % e)
857                raise
858       
859        # Make a new Attribute Certificate instance passing in certificate
860        # details for later signing
861        attCert = AttCert()
862
863        # First certificate in list contains the public key corresponding to
864        # the private key
865        attCert.certFilePathList = [self.signingCertFilePath] + \
866                                                                self.caCertFilePathList
867             
868        # Check for expiry of each certificate                   
869        for x509Cert in attCert.certFilePathList:
870            X509Cert.Read(x509Cert).isValidTime(raiseExcep=True)
871                                                               
872        attCert.signingKeyFilePath = self.signingPriKeyFilePath
873        attCert.signingKeyPwd = self.signingPriKeyPwd
874       
875       
876        # Set holder's Distinguished Name if a holder X.509 certificate was
877        # input
878        if holderX509Cert:
879            try:
880                attCert['holder'] = holderX509Cert.dn.serialise(
881                                        separator=self.dnSeparator)           
882            except Exception, e:
883                 log.error("Holder X.509 Certificate DN: %s" % e)
884                 raise
885           
886        # Set Issuer details from Attribute Authority
887        issuerDN = self.cert.dn
888        try:
889            attCert['issuer'] = \
890                    issuerDN.serialise(separator=self.dnSeparator)           
891        except Exception, e:
892            log.error("Issuer X.509 Certificate DN: %s" % e)
893            raise 
894           
895        attCert['issuerName'] = self.name
896        attCert['issuerSerialNumber'] = self.issuerSerialNumber
897
898        attCert['userId'] = userId
899       
900        # Set validity time
901        try:
902            attCert.setValidityTime(
903                        lifetime=self.attCertLifetime,
904                        notBeforeOffset=self.attCertNotBeforeOff)
905
906            # Check against the holder X.509 certificate's expiry if set
907            if holderX509Cert:
908                dtHolderCertNotAfter = holderX509Cert.notAfter
909               
910                if attCert.getValidityNotAfter(asDatetime=True) > \
911                   dtHolderCertNotAfter:
912   
913                    # Adjust the attribute certificate's expiry date time
914                    # so that it agrees with that of the certificate
915                    # ... but also make ensure that the not before skew is
916                    # still applied
917                    attCert.setValidityTime(dtNotAfter=dtHolderCertNotAfter,
918                            notBeforeOffset=self.attCertNotBeforeOff)
919           
920        except Exception, e:
921            log.error("Error setting attribute certificate validity time: %s" %
922                      e)
923            raise 
924
925        # Check name is registered with this Attribute Authority - if no
926        # user roles are found, the user is not registered
927        userRoles = self.getRoles(userId)
928        if userRoles:
929            # Set as an Original Certificate
930            #
931            # User roles found - user is registered with this data centre
932            # Add roles for this user for this data centre
933            attCert.addRoles(userRoles)
934
935            # Mark new Attribute Certificate as an original
936            attCert['provenance'] = AttCert.origProvenance
937
938        else:           
939            # Set as a Mapped Certificate
940            #
941            # No roles found - user is not registered with this data centre
942            # Check for an externally provided certificate from another
943            # trusted data centre
944            if userAttCertFilePath:
945               
946                # Read externally provided certificate
947                try:
948                    userAttCert = AttCert.Read(userAttCertFilePath)
949                   
950                except Exception, e:
951                    raise AttributeAuthorityError("Reading external Attribute "
952                                                  "Certificate: %s" % e)                           
953            elif userAttCert:
954                # Allow input as a string but convert to
955                if isinstance(userAttCert, basestring):
956                    userAttCert = AttCert.Parse(userAttCert)
957                   
958                elif not isinstance(userAttCert, AttCert):
959                    raise AttributeAuthorityError(
960                        "Expecting userAttCert as a string or AttCert type")       
961            else:
962                raise AttributeAuthorityAccessDenied('User "%s" is not '
963                    'registered and no external attribute certificate is '
964                    'available to make a mapping.' % userId)
965
966
967            # Check it's an original certificate - mapped certificates can't
968            # be used to make further mappings
969            if userAttCert.isMapped():
970                raise AttributeAuthorityError("External Attribute Certificate "
971                                              "must have an original "
972                                              "provenance in order "
973                                              "to make further mappings.")
974
975
976            # Check it's valid and signed
977            try:
978                # Give path to CA cert to allow check
979                userAttCert.certFilePathList = self.caCertFilePathList
980                userAttCert.isValid(raiseExcep=True)
981               
982            except Exception, e:
983                raise AttributeAuthorityError("Invalid Remote Attribute "
984                                        "Certificate: " + str(e))       
985
986
987            # Check that's it's holder matches the candidate holder
988            # certificate DN
989            if holderX509Cert and userAttCert.holderDN != holderX509Cert.dn:
990                raise AttributeAuthorityError("User certificate and Attribute "
991                                        'Certificate DNs don\'t match: "%s"'
992                                        ' and "%s"' % (holderX509Cert.dn, 
993                                                       userAttCert.holderDN))
994           
995 
996            # Get roles from external Attribute Certificate
997            trustedHostRoles = userAttCert.roles
998
999
1000            # Map external roles to local ones
1001            localRoles = self.mapRemoteRoles2LocalRoles(
1002                                                    userAttCert['issuerName'],
1003                                                    trustedHostRoles)
1004            if not localRoles:
1005                raise AttributeAuthorityAccessDenied("No local roles mapped "
1006                                               "to the %s roles: %s" % 
1007                                               (userAttCert['issuerName'], 
1008                                                ', '.join(trustedHostRoles)))
1009
1010            attCert.addRoles(localRoles)
1011           
1012           
1013            # Mark new Attribute Certificate as mapped
1014            attCert.provenance = AttCert.mappedProvenance
1015
1016            # Copy the user Id from the external AC
1017            attCert.userId = userAttCert.userId
1018           
1019            # End set mapped certificate block
1020
1021        try:
1022            # Digitally sign certificate using Attribute Authority's
1023            # certificate and private key
1024            attCert.applyEnvelopedSignature()
1025           
1026            # Check the certificate is valid
1027            attCert.isValid(raiseExcep=True)
1028           
1029            # Write out certificate to keep a record of it for auditing
1030            #attCert.write()
1031            self.__attCertLog.info(attCert)
1032           
1033            log.info('Issued an Attribute Certificate to "%s" with roles: '
1034                     '"%s"' % (userId, '", "'.join(attCert.roles)))
1035
1036            # Return the cert to caller
1037            return attCert
1038       
1039        except Exception, e:
1040            raise AttributeAuthorityError('New Attribute Certificate "%s": %s'%
1041                                          (attCert.filePath, e))
1042
1043    def samlAttributeQuery(self, attributeQuery):
1044        """Respond to SAML 2.0 Attribute Query
1045        """
1046        if not isinstance(attributeQuery, AttributeQuery):
1047            raise TypeError('Expecting %r for attribute query; got %r' %
1048                            (AttributeQuery, type(attributeQuery)))
1049           
1050        samlResponse = Response()
1051       
1052        samlResponse.issueInstant = datetime.utcnow()
1053        if self.attCertNotBeforeOff != 0:
1054            samlResponse.issueInstant += timedelta(
1055                                            seconds=self.attCertNotBeforeOff)
1056           
1057        samlResponse.id = str(uuid4())
1058        samlResponse.issuer = Issuer()
1059       
1060        # Initialise to success status but reset on error
1061        samlResponse.status = Status()
1062        samlResponse.status.statusCode = StatusCode()
1063        samlResponse.status.statusCode.value = StatusCode.SUCCESS_URI
1064       
1065        # Nb. SAML 2.0 spec says issuer format must be omitted
1066        samlResponse.issuer.value = self.issuer
1067       
1068        samlResponse.inResponseTo = attributeQuery.id
1069       
1070        # Attribute Query validation ...
1071        utcNow = datetime.utcnow()
1072        if attributeQuery.issueInstant >= utcNow:
1073            msg = ('SAML Attribute Query issueInstant [%s] is at or after '
1074                   'the current clock time [%s]') % \
1075                   (attributeQuery.issueInstant, SAMLDateTime.toString(utcNow))
1076            log.error(msg)
1077                     
1078            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1079            samlResponse.status.statusMessage = msg
1080            return samlResponse
1081           
1082        elif attributeQuery.version < SAMLVersion.VERSION_20:
1083            samlResponse.status.statusCode.value = \
1084                                        StatusCode.REQUEST_VERSION_TOO_LOW_URI
1085            return samlResponse
1086       
1087        elif attributeQuery.version > SAMLVersion.VERSION_20:
1088            samlResponse.status.statusCode.value = \
1089                                        StatusCode.REQUEST_VERSION_TOO_HIGH_URI
1090            return samlResponse
1091       
1092        elif attributeQuery.subject.nameID.format != "urn:esg:openid":
1093            log.error('SAML Attribute Query subject format is "%r"; expecting '
1094                      '"%s"' % (attributeQuery.subject.nameID.format,
1095                                "urn:esg:openid"))
1096            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1097            samlResponse.status.statusMessage = \
1098                                "Subject Name ID format is not recognised"
1099            return samlResponse
1100       
1101        elif attributeQuery.issuer.format != "urn:esg:issuer":
1102            log.error('SAML Attribute Query issuer format is "%r"; expecting '
1103                      '"%s"' % (attributeQuery.issuer.format,
1104                                "urn:esg:issuer"))
1105            samlResponse.status.statusCode.value = StatusCode.REQUESTER_URI
1106            samlResponse.status.statusMessage="Issuer format is not recognised"
1107            return samlResponse
1108       
1109        try:
1110            # Return a dictionary of name, value pairs
1111            self.attributeInterface.getAttributes(attributeQuery, samlResponse)
1112           
1113        except InvalidUserId, e:
1114            log.exception(e)
1115            samlResponse.status.statusCode.value = \
1116                                        StatusCode.UNKNOWN_PRINCIPAL_URI
1117            return samlResponse
1118           
1119        except UserIdNotKnown, e:
1120            log.exception(e)
1121            samlResponse.status.statusCode.value = \
1122                                        StatusCode.UNKNOWN_PRINCIPAL_URI
1123            return samlResponse
1124           
1125        except InvalidRequestorId, e:
1126            log.exception(e)
1127            samlResponse.status.statusCode.value = StatusCode.REQUEST_DENIED_URI
1128            return samlResponse
1129           
1130        except AttributeReleaseDenied, e:
1131            log.exception(e)
1132            samlResponse.status.statusCode.value = \
1133                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
1134            return samlResponse
1135           
1136        except AttributeNotKnownError, e:
1137            log.exception(e)
1138            samlResponse.status.statusCode.value = \
1139                                        StatusCode.INVALID_ATTR_NAME_VALUE_URI
1140            return samlResponse
1141           
1142        except Exception, e:
1143            log.exception("Unexpected error calling Attribute Interface "
1144                          "for subject [%s] and query issuer [%s]" %
1145                          (attributeQuery.subject.nameID.value,
1146                           attributeQuery.issuer.value))
1147           
1148            # SAML spec says application server should set a HTTP 500 Internal
1149            # Server error in this case
1150            raise 
1151
1152        return samlResponse
1153   
1154    def readMapConfig(self):
1155        """Parse Map Configuration file.
1156        """
1157       
1158        log.debug("Reading map configuration file ...")
1159       
1160        try:
1161            tree = ElementTree.parse(self.mapConfigFilePath)
1162            rootElem = tree.getroot()
1163           
1164        except IOError, e:
1165            raise AttributeAuthorityConfigError('Error parsing Map '
1166                                                'Configuration file "%s": %s' % 
1167                                                (e.filename, e.strerror))         
1168        except Exception, e:
1169            raise AttributeAuthorityConfigError('Error parsing Map '
1170                                                'Configuration file: "%s": %s'% 
1171                                                (self.mapConfigFilePath, e))
1172       
1173        trustedElem = rootElem.findall('trusted')
1174        if not trustedElem: 
1175            # Make an empty list so that for loop block below is skipped
1176            # without an error 
1177            trustedElem = ()
1178
1179        # Dictionaries:
1180        # 1) to hold all the data
1181        self.__mapConfig = {'thisHost': {}, 'trustedHosts': {}}
1182
1183        # ... look-up
1184        # 2) hosts corresponding to a given role and
1185        # 3) roles of external data centre to this data centre
1186        self.__localRole2TrustedHost = {}
1187        self.__localRole2RemoteRole = {}
1188        self.__remoteRole2LocalRole = {}
1189
1190        # Information about this host
1191        try:
1192            thisHostElem = rootElem.findall('thisHost')[0]
1193           
1194        except Exception, e:
1195            raise AttributeAuthorityConfigError('"thisHost" tag not found in '
1196                                                'Map Configuration file "%s"' % 
1197                                                self.mapConfigFilePath)
1198
1199        try:
1200            hostName = thisHostElem.attrib.values()[0]
1201           
1202        except Exception, e:
1203            raise AttributeAuthorityConfigError('"name" attribute of '
1204                                                '"thisHost" element not found '
1205                                                'in Map Configuration file '
1206                                                '"%s"' % 
1207                                                self.mapConfigFilePath)
1208
1209        # hostname is also stored in the AA's config file in the 'name' tag. 
1210        # Check the two match as the latter is copied into Attribute
1211        # Certificates issued by this AA
1212        #
1213        # TODO: would be better to rationalise this so that the hostname is
1214        # stored in one place only.
1215        #
1216        # P J Kershaw 14/06/06
1217        if hostName != self.name:
1218            raise AttributeAuthorityError('"name" attribute of "thisHost" '
1219                                          'element in Map Configuration file '
1220                                          'doesn\'t match "name" element in '
1221                                          'properties file.')
1222       
1223        # Information for THIS Attribute Authority
1224        self.__mapConfig['thisHost'][hostName] = {}
1225
1226        for k, v in AttributeAuthority.mapConfigHostDefaults.items():
1227            val = thisHostElem.findtext(k)
1228            if val is None and v == NotImplemented:
1229                raise AttributeAuthorityConfigError('<thisHost> option <%s> '
1230                                                    'must be set.' % k)
1231            self.__mapConfig['thisHost'][hostName][k] = val     
1232       
1233        # Information about trusted hosts
1234        for elem in trustedElem:
1235            try:
1236                trustedHost = elem.attrib.values()[0]
1237               
1238            except Exception, e:
1239                raise AttributeAuthorityConfigError('Error reading trusted '
1240                                                    'host name: %s' % e)
1241 
1242            # Add signatureFile and list of roles
1243            #
1244            # (Currently Optional) additional tag allows query of the URI
1245            # where a user would normally login at the trusted host.  Added
1246            # this feature to allow users to be forwarded to their home site
1247            # if they are accessing a secure resource and are not
1248            # authenticated
1249            #
1250            # P J Kershaw 25/05/06
1251            self.__mapConfig['trustedHosts'][trustedHost] = {}
1252            for k, v in AttributeAuthority.mapConfigHostDefaults.items():
1253                val = thisHostElem.findtext(k)
1254                if val is None and v == NotImplemented:
1255                    raise AttributeAuthorityConfigError('<trustedHost> option '
1256                                                        '<%s> must be set.'%k)
1257                   
1258                self.__mapConfig['trustedHosts'][trustedHost][k] = \
1259                                                        elem.findtext(k)   
1260
1261            roleElem = elem.findall('role')
1262            if roleElem:
1263                # Role keyword value requires special parsing before
1264                # assignment
1265                self.__mapConfig['trustedHosts'][trustedHost]['role'] = \
1266                                        [dict(i.items()) for i in roleElem]
1267            else:
1268                # It's possible for trust relationships to not contain any
1269                # role mapping.  e.g. a site's login service trusting other
1270                # sites login requests
1271                self.__mapConfig['trustedHosts'][trustedHost]['role'] = []
1272                       
1273            self.__localRole2RemoteRole[trustedHost] = {}
1274            self.__remoteRole2LocalRole[trustedHost] = {}
1275           
1276            for role in self.__mapConfig['trustedHosts'][trustedHost]['role']:
1277                try:
1278                    localRole = role['local']
1279                    remoteRole = role['remote']
1280                except KeyError, e:
1281                    raise AttributeAuthorityError('Reading map configuration '
1282                                                  ' file "%s": no element '
1283                                                  '"%s" for host "%s"' % 
1284                                                (self.mapConfigFilePath, 
1285                                                 e, 
1286                                                 trustedHost))
1287                   
1288                # Role to host look-up
1289                if localRole in self.__localRole2TrustedHost:
1290                   
1291                    if trustedHost not in \
1292                       self.__localRole2TrustedHost[localRole]:
1293                        self.__localRole2TrustedHost[localRole].\
1294                                                        append(trustedHost)                       
1295                else:
1296                    self.__localRole2TrustedHost[localRole] = [trustedHost]
1297
1298
1299                # Trusted Host to local role and trusted host to trusted role
1300                # map look-ups
1301                try:
1302                    self.__remoteRole2LocalRole[trustedHost][remoteRole].\
1303                                                            append(localRole)                 
1304                except KeyError:
1305                    self.__remoteRole2LocalRole[trustedHost][remoteRole] = \
1306                                                                [localRole]
1307                   
1308                try:
1309                    self.__localRole2RemoteRole[trustedHost][localRole].\
1310                                                            append(remoteRole)                 
1311                except KeyError:
1312                    self.__localRole2RemoteRole[trustedHost][localRole] = \
1313                                                                [remoteRole]
1314       
1315        # Store trusted host info look-up for retrieval by getTrustedHostInfo
1316        # method                                                                         
1317        #
1318        # Nb. {}.fromkeys([...]).keys() is a fudge to get unique elements
1319        # from a list i.e. convert the list elements to a dict eliminating
1320        # duplicated elements and convert the keys back into a list.
1321        self._trustedHostInfo = dict(
1322        [
1323            (
1324                k, 
1325                {
1326                    'siteName':             v['siteName'],
1327                    'aaURI':                v['aaURI'], 
1328                    'aaDN':                 v['aaDN'], 
1329                    'loginURI':             v['loginURI'], 
1330                    'loginServerDN':        v['loginServerDN'], 
1331                    'loginRequestServerDN': v['loginRequestServerDN'], 
1332                    'role':                 {}.fromkeys([role['remote'] 
1333                                                         for role in v['role']]
1334                                                       ).keys()
1335                }
1336            ) for k, v in self.__mapConfig['trustedHosts'].items()
1337        ])
1338
1339        log.info('Loaded map configuration file "%s"' % self.mapConfigFilePath)
1340       
1341       
1342    def getRoles(self, userId):
1343        """Get the roles available to the registered user identified userId.
1344
1345        @type dn: string
1346        @param dn: user identifier - could be a X500 Distinguished Name
1347        @return: list of roles for the given user ID"""
1348
1349        log.debug('Calling getRoles for user "%s" ...' % userId)
1350       
1351        # Call to AttributeInterface derived class.  Each Attribute Authority
1352        # should define it's own roles class derived from AttributeInterface to
1353        # define how roles are accessed
1354        try:
1355            return self.__attributeInterface.getRoles(userId)
1356
1357        except Exception, e:
1358            raise AttributeAuthorityError("Getting user roles: %s" % e)
1359       
1360       
1361    def _getHostInfo(self):
1362        """Return the host that this Attribute Authority represents: its ID,
1363        the user login URI and WSDL address.  Call this method via the
1364        'hostInfo' property
1365       
1366        @rtype: dict
1367        @return: dictionary of host information derived from the map
1368        configuration"""
1369       
1370        return self.__mapConfig['thisHost']
1371       
1372    hostInfo = property(fget=_getHostInfo, 
1373                        doc="Return information about this host")
1374       
1375       
1376    def getTrustedHostInfo(self, role=None):
1377        """Return a dictionary of the hosts that have trust relationships
1378        with this AA.  The dictionary is indexed by the trusted host name
1379        and contains AA service, login URIs and the roles that map to the
1380        given input local role.
1381
1382        @type role: string
1383        @param role: if set, return trusted hosts that having a mapping set
1384        for this role.  If no role is input, return all the AA's trusted hosts
1385        with all their possible roles
1386
1387        @rtype: dict
1388        @return: dictionary of the hosts that have trust relationships
1389        with this AA.  It returns an empty dictionary if role isn't
1390        recognised"""
1391               
1392        log.debug('Calling getTrustedHostInfo with role = "%s" ...' % role) 
1393                                 
1394        if not self.__mapConfig or not self.__localRole2RemoteRole:
1395            # This Attribute Authority has no trusted hosts
1396            raise AttributeAuthorityNoTrustedHosts("The %s Attribute "
1397                                                   "Authority has no trusted "
1398                                                   "hosts" % 
1399                                                   self.name)
1400
1401
1402        if role is None:
1403            # No role input - return all trusted hosts with their service URIs
1404            # and the remote roles they map to
1405            return self._trustedHostInfo
1406
1407        else:           
1408            # Get trusted hosts for given input local role       
1409            try:
1410                trustedHosts = self.__localRole2TrustedHost[role]
1411            except:
1412                raise AttributeAuthorityNoMatchingRoleInTrustedHosts(
1413                    'None of the trusted hosts have a mapping to the '
1414                    'input role "%s"' % role)
1415   
1416   
1417            # Get associated Web service URI and roles for the trusted hosts
1418            # identified and return as a dictionary indexed by host name
1419            trustedHostInfo = dict(
1420       [(
1421            host, 
1422            {
1423                'siteName': self.__mapConfig['trustedHosts'][host]['siteName'],
1424                'aaURI':    self.__mapConfig['trustedHosts'][host]['aaURI'],
1425                'aaDN':     self.__mapConfig['trustedHosts'][host]['aaDN'],
1426                'loginURI': self.__mapConfig['trustedHosts'][host]['loginURI'],
1427                'loginServerDN': 
1428                        self.__mapConfig['trustedHosts'][host]['loginServerDN'],
1429                'loginRequestServerDN': 
1430                self.__mapConfig['trustedHosts'][host]['loginRequestServerDN'],
1431                'role':     self.__localRole2RemoteRole[host][role]
1432            }
1433        ) for host in trustedHosts])
1434                         
1435            return trustedHostInfo
1436       
1437       
1438    def mapRemoteRoles2LocalRoles(self, trustedHost, trustedHostRoles):
1439        """Map roles of trusted hosts to roles for this data centre
1440
1441        @type trustedHost: string
1442        @param trustedHost: name of external trusted data centre
1443        @type trustedHostRoles: list
1444        @param trustedHostRoles:   list of external roles to map
1445        @return: list of mapped roles"""
1446
1447        if not self.__remoteRole2LocalRole:
1448            raise AttributeAuthorityError("Roles map is not set - ensure " 
1449                                    "readMapConfig() has been called.")
1450
1451
1452        # Check the host name is a trusted one recorded in the map
1453        # configuration
1454        if not self.__remoteRole2LocalRole.has_key(trustedHost):
1455            return []
1456
1457        # Add local roles, skipping if no mapping is found
1458        localRoles = []
1459        for trustedRole in trustedHostRoles:
1460            if trustedRole in self.__remoteRole2LocalRole[trustedHost]:
1461                localRoles.extend(
1462                        self.__remoteRole2LocalRole[trustedHost][trustedRole])
1463               
1464        return localRoles
1465
1466    def getAttCertFactory(self):
1467        """Factory method to create SAML Attribute Qeury wrapper function
1468        @rtype: function
1469        @return getAttCert method function wrapper
1470        """
1471        def getAttCertWrapper(*arg, **kw):
1472            """
1473            @type *arg: tuple
1474            @param *arg: getAttCert arguments
1475            @type **kw: dict
1476            @param **kw: getAttCert keyword arguments
1477            @rtype: ndg.security.common.AttCert.AttCert
1478            @return: new attribute certificate
1479            """
1480            return self.getAttCert(*arg, **kw)
1481       
1482        return getAttCertWrapper
1483
1484    def samlAttributeQueryFactory(self):
1485        """Factory method to create SAML Attribute Qeury wrapper function
1486        @rtype: function
1487        @return: samlAttributeQuery method function wrapper
1488        """
1489        def samlAttributeQueryWrapper(attributeQuery):
1490            """
1491            @type attributeQuery: saml.saml2.core.AttributeQuery
1492            @param attributeQuery: SAML Attribute Query
1493            @rtype: saml.saml2.core.Response
1494            @return: SAML response
1495            """
1496            return self.samlAttributeQuery(attributeQuery)
1497       
1498        return samlAttributeQueryWrapper
1499   
1500
1501from logging.handlers import RotatingFileHandler
1502
1503# Inherit directly from Logger
1504_loggerClass = logging.getLoggerClass()
1505class AttCertLog(_loggerClass, object):
1506    """Log each Attribute Certificate issued using a rotating file handler
1507    so that the number of files held can be managed"""
1508   
1509    def __init__(self, attCertFilePath, backUpCnt=1024):
1510        """Set up a rotating file handler to log ACs issued.
1511        @type attCertFilePath: string
1512        @param attCertFilePath: set where to store ACs.  Set from
1513        AttributeAuthority properties file.
1514       
1515        @type backUpCnt: int
1516        @param backUpCnt: set the number of files to store before rotating
1517        and overwriting old files."""
1518       
1519        if not isinstance(backUpCnt, int):
1520            raise TypeError('Expecting int type for "backUpCnt" keyword')
1521       
1522        # Inherit from Logger class
1523        super(AttCertLog, self).__init__(name='', level=logging.INFO)
1524                           
1525        # Set a format for messages so that only the content of the AC is
1526        # logged, nothing else.
1527        formatter = logging.Formatter(fmt="", datefmt="")
1528
1529        # maxBytes is set to one so that only one AC will be written before
1530        # rotation to the next file
1531        fileLog = RotatingFileHandler(attCertFilePath, 
1532                                      maxBytes=1, 
1533                                      backupCount=backUpCnt)
1534        fileLog.setFormatter(formatter)           
1535        self.addHandler(fileLog)
1536 
1537                     
1538class AttributeInterfaceError(Exception):
1539    """Exception handling for NDG Attribute Authority User Roles interface
1540    class."""
1541
1542                       
1543class AttributeReleaseDenied(AttributeInterfaceError):
1544    """Requestor was denied release of the requested attributes"""
1545
1546                       
1547class AttributeNotKnownError(AttributeInterfaceError):
1548    """Requested attribute names are not known to this authority"""
1549
1550
1551class InvalidRequestorId(AttributeInterfaceError):
1552    """Requestor is not known or not allowed to request attributes"""
1553   
1554
1555class UserIdNotKnown(AttributeInterfaceError): 
1556    """User ID passed to getAttributes is not known to the authority"""
1557   
1558   
1559class InvalidUserId(AttributeInterfaceError):
1560    """User Id passed to getAttributes is invalid"""
1561   
1562     
1563class AttributeInterface(object):
1564    """An abstract base class to define the user roles interface to an
1565    Attribute Authority.
1566
1567    Each NDG data centre should implement a derived class which implements
1568    the way user roles are provided to its representative Attribute Authority.
1569   
1570    Roles are expected to indexed by user Distinguished Name (DN).  They
1571    could be stored in a database or file."""
1572
1573    # User defined class may wish to specify a URI for a database interface or
1574    # path for a user roles configuration file
1575    def __init__(self, **prop):
1576        """User Roles base class - derive from this class to define
1577        roles interface to Attribute Authority
1578       
1579        @type prop: dict
1580        @param prop: custom properties to pass to this class
1581        """
1582
1583    def getRoles(self, userId):
1584        """Virtual method - Derived method should return the roles for the
1585        given user's Id or else raise an exception
1586       
1587        @type userId: string
1588        @param userId: user identity e.g. user Distinguished Name
1589        @rtype: list
1590        @return: list of roles for the given user ID
1591        @raise AttributeInterfaceError: an error occured requesting
1592        attributes
1593        """
1594        raise NotImplementedError(self.getRoles.__doc__)
1595 
1596    def getAttributes(self, attributeQuery, response):
1597        """Virtual method should be implemented in a derived class to enable
1598        AttributeAuthority.samlAttributeQuery - The derived method should
1599        return the attributes requested for the given user's Id or else raise
1600        an exception
1601       
1602        @type attributeQuery: saml.saml2.core.AttributeQuery
1603        @param userId: query containing requested attributes
1604        @type: saml.saml2.core.Response
1605        @param: Response - add an assertion with the list of attributes
1606        for the given subject ID in the query or set an error Status code and
1607        message
1608        @raise AttributeInterfaceError: an error occured requesting
1609        attributes
1610        @raise AttributeReleaseDeniedError: Requestor was denied release of the
1611        requested attributes
1612        @raise AttributeNotKnownError: Requested attribute names are not known
1613        to this authority
1614        """
1615        raise NotImplementedError(self.getAttributes.__doc__)
1616
1617
1618class CSVFileAttributeInterface(AttributeInterface):
1619    """Attribute Interface based on a Comma Separated Variable file containing
1620    user identities and associated attributes.  For test/development purposes
1621    only.  The SAML getAttributes method is NOT implemented here
1622   
1623    The expected file format is:
1624   
1625    <userID>, <role1>, <role2>, ... <roleN>
1626    """
1627    def __init__(self, propertiesFilePath=None):
1628        """
1629        @param propertiesFilePath: file path to Comma Separated file
1630        containing user ids and roles
1631        @type propertiesFilePath: basestring
1632        """
1633        if propertiesFilePath is None:
1634            raise AttributeError("Expecting propertiesFilePath setting")
1635       
1636        propertiesFile = open(propertiesFilePath)
1637        lines = propertiesFile.readlines()
1638       
1639        self.attributeMap = {}
1640        for line in lines:
1641            fields = re.split(',\W*', line.strip())
1642            self.attributeMap[fields[0]] = fields[1:]
1643   
1644    def getRoles(self, userId):
1645        """
1646        @param userId: user identity to key into attributeMap
1647        @type userId: basestring
1648        """ 
1649        log.debug('CSVFileAttributeInterface.getRoles for user "%s" ...', 
1650                  userId)
1651        return self.attributeMap.get(userId, [])
1652
1653
1654# Properties file
1655from ConfigParser import SafeConfigParser, NoOptionError
1656
1657try:
1658    # PostgreSQL interface
1659    from psycopg2 import connect
1660except ImportError:
1661    pass
1662
1663class PostgresAttributeInterface(AttributeInterface):
1664    """User Roles interface to Postgres database
1665   
1666    The SAML getAttributes method is NOT implemented"""
1667
1668    CONNECTION_SECTION_NAME = "Connection"
1669    GETROLES_SECTION_NAME = "getRoles"
1670    HOST_OPTION_NAME = "host"
1671    DBNAME_OPTION_NAME = "dbName"
1672    USERNAME_OPTION_NAME = "username"
1673    PWD_OPTION_NAME = "pwd"
1674    QUERYN_OPTION_NAME = "query%d"
1675    DEFAULT_ROLES_OPTION_NAME = "defaultRoles"
1676   
1677    def __init__(self, propertiesFilePath=None):
1678        """Connect to Postgres database"""
1679        self.__con = None
1680        self.__host = None
1681        self.__dbName = None
1682        self.__username = None
1683        self.__pwd = None
1684
1685        if propertiesFilePath is None:
1686            raise AttributeError("No Configuration file was set")
1687
1688        self.readConfigFile(propertiesFilePath)
1689
1690    def __del__(self):
1691        """Close database connection"""
1692        self.close()
1693
1694    def readConfigFile(self, propertiesFilePath):
1695        """Read the configuration for the database connection
1696
1697        @type propertiesFilePath: string
1698        @param propertiesFilePath: file path to config file"""
1699
1700        if not isinstance(propertiesFilePath, basestring):
1701            raise TypeError("Input Properties file path must be a valid "
1702                            "string; got %r" % type(propertiesFilePath))
1703
1704        cfg = SafeConfigParser()
1705        cfg.read(propertiesFilePath)
1706
1707        self.__host = cfg.get(
1708                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1709                        PostgresAttributeInterface.HOST_OPTION_NAME)
1710        self.__dbName = cfg.get(
1711                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1712                        PostgresAttributeInterface.DBNAME_OPTION_NAME)
1713        self.__username = cfg.get(
1714                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1715                        PostgresAttributeInterface.USERNAME_OPTION_NAME)
1716        self.__pwd = cfg.get(
1717                        PostgresAttributeInterface.CONNECTION_SECTION_NAME, 
1718                        PostgresAttributeInterface.PWD_OPTION_NAME)
1719
1720        try:
1721            self.__getRolesQuery = []
1722            for i in range(10):
1723                queryStr = cfg.get(
1724                        PostgresAttributeInterface.GETROLES_SECTION_NAME, 
1725                        PostgresAttributeInterface.QUERYN_OPTION_NAME % i)
1726                self.__getRolesQuery += [queryStr]
1727        except NoOptionError:
1728             # Continue until no more query<n> items left
1729             pass
1730
1731        # This option may be omitted in the config file
1732        try:
1733            self.__defaultRoles = cfg.get(
1734                PostgresAttributeInterface.GETROLES_SECTION_NAME, 
1735                PostgresAttributeInterface.DEFAULT_ROLES_OPTION_NAME).split()
1736        except NoOptionError:
1737            self.__defaultRoles = []
1738
1739    def connect(self,
1740                username=None,
1741                dbName=None,
1742                host=None,
1743                pwd=None,
1744                prompt="Database password: "):
1745        """Connect to database
1746
1747        Values for keywords omitted are derived from the config file.  If pwd
1748        is not in the config file it will be prompted for from stdin
1749
1750        @type username: string
1751        @keyword username: database account username
1752        @type dbName: string
1753        @keyword dbName: name of database
1754        @type host: string
1755        @keyword host: database host machine
1756        @type pwd: string
1757        @keyword pwd: password for database account.  If omitted and not in
1758        the config file it will be prompted for from stdin
1759        @type prompt: string
1760        @keyword prompt: override default password prompt"""
1761
1762        if not host:
1763            host = self.__host
1764
1765        if not dbName:
1766            dbName = self.__dbName
1767
1768        if not username:
1769            username = self.__username
1770
1771        if not pwd:
1772            pwd = self.__pwd
1773
1774            if not pwd:
1775                import getpass
1776                pwd = getpass.getpass(prompt=prompt)
1777
1778        try:
1779            self.__db = connect("host=%s dbname=%s user=%s password=%s" % \
1780                                (host, dbName, username, pwd))
1781            self.__cursor = self.__db.cursor()
1782
1783        except NameError, e:
1784            raise AttributeInterfaceError("psycopg2 Postgres package not "
1785                                          "installed? %s" % e)
1786        except Exception, e:
1787            raise AttributeInterfaceError("Error connecting to database "
1788                                          "\"%s\": %s" % (dbName, e))
1789
1790
1791    def close(self):
1792        """Close database connection"""
1793        if self.__con:
1794            self.__con.close()
1795
1796    def getRoles(self, userId):
1797        """Return valid roles for the given userId
1798
1799        @type userId: basestring
1800        @param userId: user identity"""
1801
1802        try:
1803            self.connect()
1804
1805            # Process each query in turn appending role names
1806            roles = self.__defaultRoles[:]
1807            for query in self.__getRolesQuery:
1808                try:
1809                    self.__cursor.execute(query % userId)
1810                    queryRes = self.__cursor.fetchall()
1811
1812                except Exception, e:
1813                    raise AttributeInterfaceError("Query for %s: %s" %
1814                                                  (userId, e))
1815
1816                roles += [res[0] for res in queryRes if res[0]]
1817        finally:
1818            self.close()
1819
1820        return roles
1821
1822    def __getCursor(self):
1823        """Return a database cursor instance"""
1824        return self.__cursor
1825
1826    cursor = property(fget=__getCursor, doc="database cursor")
Note: See TracBrowser for help on using the repository browser.