source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/gatekeeper.py @ 3085

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/gatekeeper.py@3085
Revision 3085, 22.4 KB checked in by pjkersha, 13 years ago (diff)

ndg.security.common/ndg/security/common/gatekeeperService/TestGatekeeperResrc.py: moved to new gatekeeper unit test package

  • Property svn:keywords set to Id
Line 
1"""NDG Gatekeeper - A PDP (Policy Decision Point) determines whether
2a given Attribute Certificate can access a given resource.
3
4NERC Data Grid Project
5"""
6__author__ = "P J Kershaw"
7__date__ = "15/05/06"
8__copyright__ = "(C) 2007 STFC & NERC"
9__contact__ = "P.J.Kershaw@rl.ac.uk"
10__license__ = \
11"""This software may be distributed under the terms of the Q Public
12License, version 1.0 or later."""
13__contact__ = "P.J.Kershaw@rl.ac.uk"
14__revision__ = "$Id:gatekeeper.py 3079 2007-11-30 09:39:46Z pjkersha $"
15
16import logging
17log = logging.getLogger(__name__)
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# Alter system path for dynamic import of resource interface class
27import sys
28
29# Expand environment vars in paths
30import os
31
32from ndg.security.common.AttCert import *
33
34
35#_____________________________________________________________________________
36class GatekeeperError(Exception):
37    """Exception handling for NDG Gatekeeper class."""
38   
39    def __init__(self, msg):
40        self.__msg = msg
41         
42    def __str__(self):
43        return self.__msg
44
45           
46#_____________________________________________________________________________
47class Gatekeeper(object):
48    """NDG Gatekeeper class - determines whether a given Attribute
49    Certificate can access a given resource."""
50   
51    __validKeys = ( 'resrcID',
52                    'resrcModFilePath',
53                    'resrcModName',
54                    'resrcClassName',
55                    'resrcPropFile',
56                    'caCertFilePath')
57   
58    #_________________________________________________________________________
59    def __init__(self, propFilePath=None, **prop):
60         
61        self.__propFilePath = propFilePath               
62        self.__resrcObj = None
63        self.__prop = {}.fromkeys(self.__validKeys)
64       
65        if propFilePath:
66            self.readProperties(propFilePath)
67           
68        # Any keywords set will override equivalent file property settings
69        if prop:
70            invalidKeys = [key for key in prop if key not in self.__validKeys]
71            if invalidKeys:
72                raise GatekeeperError("Invalid property or properties: " + \
73                                      ", ".join(invalidKeys))
74            self.__prop.update(prop)
75           
76           
77        if max(self.__prop.values()) is not None:
78            # Initialize if all required resource URI class properties are set
79            self.initResrcInterface()
80       
81       
82    #_________________________________________________________________________
83    def initResrcInterface(self):
84        """Set-up Resource URI interface to Gatekeeper"""
85       
86        try:
87            try:
88                # Temporarily extend system path ready for import
89                sysPathBak = sys.path[:]
90                sys.path.append(self.__prop['resrcModFilePath'])
91               
92                # Import module name specified in properties file
93                resrcMod = __import__(self.__prop['resrcModName'],
94                                      globals(),
95                                      locals(),
96                                      [self.__prop['resrcClassName']])
97   
98                resrcClass = eval('resrcMod.' + self.__prop['resrcClassName'])
99               
100            finally:
101                sys.path[:] = sysPathBak
102                               
103        except KeyError, e:
104            raise GatekeeperError(\
105                'Importing Resource URI module, key not recognised: %s' % e)
106                               
107        except Exception, e:
108            raise GatekeeperError('Importing Resource URI module: %s' % e)
109
110
111        # Check class inherits from GatekeeperResrc abstract base class
112        if not issubclass(resrcClass, GatekeeperResrc):
113            raise GatekeeperError, \
114                "Resource interface class %s must be derived from " % \
115                self.__prop['resrcClassName'] + "GatekeeperResrc"
116
117
118        # Instantiate custom class
119        try:
120            self.__resrcObj = resrcClass(\
121                                    resrcID=self.__prop['resrcID'],
122                                    filePath=self.__prop['resrcPropFile'])           
123        except Exception, e:
124            raise GatekeeperError(\
125                "Error instantiating Resource URI interface: " + str(e))
126
127
128    #_________________________________________________________________________
129    def readProperties(self, propFilePath=None):
130
131        """Read the configuration properties for the Attribute Authority
132
133        propFilePath: file path to properties file
134        """
135       
136        if propFilePath is not None:
137            if not isinstance(propFilePath, basestring):
138                raise GatekeeperError("Input Properties file path " + \
139                                        "must be a valid string.")
140           
141            self.__propFilePath = propFilePath
142
143
144        try:
145            elems = ElementTree.parse(self.__propFilePath).getroot()
146           
147        except IOError, ioErr:
148            raise GatekeeperError(\
149                                "Error parsing properties file \"%s\": %s" % \
150                                (ioErr.filename, ioErr.strerror))
151                               
152        # Copy properties from file as dictionary
153        #
154        # Nb. # Allow for environment variables in paths
155        self.__prop.update(dict([(elem.tag, 
156                                  os.path.expandvars(elem.text.strip())) \
157                                 for elem in elems if elem.text is not None]))
158
159
160        # Check for missing properties
161        propKeys = self.__prop.keys()
162        missingKeys = [key for key in Gatekeeper.__validKeys \
163                       if key not in propKeys]
164        if missingKeys != []:
165            raise GatekeeperError("The following properties are " + \
166                                  "missing from the properties file: " + \
167                                  ', '.join(missingKeys))
168
169
170    def __formatInput(self, input):
171        """Convert generic input into a list of roles - use with access
172        routines"""
173       
174        if isinstance(input, list):
175            # Input is list of roles
176            return input
177             
178        elif isinstance(input, basestring):
179            # Input is a role
180            return [input]
181           
182        elif isinstance(input, AttCert):
183            # Input is an Attribute Certificate
184            # Check signature of AttCert
185            try:
186                input.isValid(raiseExcep=True, 
187                              certFilePathList=self.__prop['caCertFilePath'])
188            except Exception, e:
189                raise GatekeeperError, "Access denied for input: %s" % str(e)
190           
191            return input.roles
192        else:
193            raise GatekeeperError("Input must be a role, role list or " + \
194                                  "Attribute Certificate type")
195
196   
197    #_________________________________________________________________________
198    def __call__(self, input):
199        """Get the permissions for the input file, list of roles or
200        Attribute Certificate containing roles.  A Dictionary of permissions
201        are returned indexed by role name.  Permissions are expressed as a
202        tuple containing the relevant permissions flags e.g. ('r', 'w', 'x')
203        for read/write/execute permission or e.g. ('x') for exceute only
204        permission"""
205       
206        roleList = self.__formatInput(input)
207                                     
208        return dict([(role, self.__resrcObj.getPermissions(role)) \
209                     for role in roleList])
210       
211   
212    getPermissions = __call__
213   
214   
215    #_________________________________________________________________________
216    def readAccess(self, input):
217        """Determine read access permitted against the given
218        input role/role list or Attribute Certificate roles
219       
220        Return a dictionary of booleans for access granted/denied keyed
221        by role name"""
222       
223        roleList = self.__formatInput(input)
224       
225        return dict([(role, self.__resrcObj.readAccess(role)) \
226                     for role in roleList])
227   
228   
229    #_________________________________________________________________________
230    def writeAccess(self, input):
231        """Determine write access permitted against the given
232        input role/role list or Attribute Certificate roles
233       
234        Return a dictionary of booleans for access granted/denied keyed
235        by role name"""
236       
237        roleList = self.__formatInput(input)
238       
239        return dict([(role, self.__resrcObj.writeAccess(role)) \
240                     for role in roleList])
241   
242   
243    #_________________________________________________________________________
244    def executeAccess(self, input):
245        """Determine execute access permitted against the given
246        input role/role list or Attribute Certificate roles
247       
248        Return a dictionary of booleans for access granted/denied keyed
249        by role name"""
250       
251        roleList = self.__formatInput(input)
252       
253        return dict([(role, self.__resrcObj.executeAccess(role)) \
254                     for role in roleList])
255                     
256
257#_____________________________________________________________________________
258class GatekeeperResrcError(GatekeeperError):
259    """Exception handling for NDG Gatekeeper Resource interface class
260    class."""
261    pass
262
263
264#_____________________________________________________________________________
265class GatekeeperResrc:
266    """An abstract base class to define the resource -> role interface
267    for the Gatekeeper.
268
269    Each NDG resource should implement a derived class which implements
270    the way a resource roles is served from the given resource."""
271
272    # User defined class may wish to specify a URI or path for a configuration
273    # file
274    def __init__(self, resrcID=None, filePath=None):
275        """Abstract base class - derive from this class to define
276        resource role interface to Gatekeeper"""
277        raise NotImplementedError(\
278            self.__init__.__doc__.replace('\n       ',''))
279   
280
281    def getPermissions(self, role):
282        """Derived method should return the permissions for the given resource
283        role.  Format is a tuple e.g.
284           
285        ('r', 'w', 'x'): read, write and execute permission granted for this
286                         role
287        ():              access denied
288        ('r',):          read only access
289        ('r', 'x'):      read and execute permission granted
290       
291        This method is needed for the interface to the Gatekeeper class"""
292        raise NotImplementedError(
293            self.__getPermissions.__doc__.replace('\n       ',''))
294
295
296    def readAccess(self, role):
297        """Derived method should return the role for read access to the
298        resource - should return boolean for access granted/denied"""
299        raise NotImplementedError(
300            self.readAccess.__doc__.replace('\n       ',''))
301
302
303    def writeAccess(self, role):
304        """Derived method should return the role for write access to the
305        resource - should return boolean for access granted/denied"""
306        raise NotImplementedError(
307            self.writeAccess.__doc__.replace('\n       ',''))
308
309
310    def executeAccess(self, role):
311        """Derived method should return the role for execute access to the
312        resource - should return boolean for access granted/denied"""
313        raise NotImplementedError(
314            self.executeAccess.__doc__.replace('\n       ',''))
315   
316   
317import sys # tracefile config param may be set to e.g. sys.stderr
318import urllib2
319import socket
320
321from ndg.security.common.SessionMgr import SessionMgrClient, SessionNotFound,\
322    SessionCertTimeError, SessionExpired, InvalidSession, \
323    AttributeRequestDenied
324   
325def HandleSecurity(*args):
326    return PullModelHandler(*args)()
327
328class URLCannotBeOpened(Exception):
329    """Raise from canURLBeOpened PullModelHandler class method
330    if URL is invalid - this method is used to check the AA
331    service"""
332
333class PullModelHandler(object):
334    """Make access control decision based on CSML constraint and user security
335    token"""
336   
337    AccessAllowedMsg = "Access Allowed"
338    InvalidAttributeCertificate = \
339            "The certificate containing your authorisation roles is invalid"
340    NotLoggedInMsg = 'Not Logged in'
341    SessionExpiredMsg = 'Session has expired.  Please re-login'
342    InvalidSessionMsg = 'Session is invalid.  Please try re-login'
343    InvalidSecurityCondition = 'Invalid Security Condition'
344
345    def __init__(self, uri, securityElement, securityTokens):
346        """Initialise settings for WS-Security and SSL for SOAP
347        call to Session Manager
348       
349        @type uri: string
350        @param uri: URI corresponding to data granule ID
351       
352        @type securityElement: ElementTree Element
353        @param securityElement: MOLES security constraint containing role and
354        Attribute Authority URI. In xml, could look like:
355        <moles:effect>allow</moles:effect>
356            <moles:simpleCondition>
357            <moles:dgAttributeAuthority>https://glue.badc.rl.ac.uk/AttributeAuthority</moles:dgAttributeAuthority>
358            <moles:attrauthRole>coapec</moles:attrauthRole>
359        </moles:simpleCondition>
360        NB: xmlns:moles="http://ndg.nerc.ac.uk/moles
361       
362        @type: pylons.session
363        @param securityTokens: dict-like session object containing security
364        tokens"""
365       
366        self.uri = uri
367        self.securityElement = securityElement
368        self.securityTokens = securityTokens
369
370
371    def __call__(self, **kw):
372        """Convenience wrapper for checkAccess"""
373        return self.checkAccess(**kw)
374
375
376    def checkAccess(self, 
377                    uri=None, 
378                    securityElement=None, 
379                    securityTokens=None):
380        """Make an access control decision based on whether the user is
381        authenticated and has the required roles
382       
383        @type uri: string
384        @param uri: URI corresponding to data granule ID
385       
386        @type: ElementTree Element
387        @param securityElement: MOES security constraint containing role and
388        Attribute Authority URI. In xml, could look like:
389        <moles:effect>allow</moles:effect>
390            <moles:simpleCondition>
391            <moles:dgAttributeAuthority>https://glue.badc.rl.ac.uk/AttributeAuthority</moles:dgAttributeAuthority>
392            <moles:attrauthRole>coapec</moles:attrauthRole>
393        </moles:simpleCondition>
394        NB: xmlns:moles="http://ndg.nerc.ac.uk/moles"
395       
396        @type: pylons.session
397        @param securityTokens: dict-like session object containing security
398        tokens.  Resets equivalent object attribute."""
399         
400        # tokens and element may be set from __init__ or as args to this
401        # method.  If the latter copy them into self 
402        if uri:
403            self.uri = uri
404           
405        if securityTokens:
406            self.securityTokens = securityTokens
407                           
408        if securityElement:
409            self.securityElement=securityElement
410     
411        # Check self.securityTokens - if not set then the user mustn't be
412        # logged in.  This situation is possible if a user has been denied
413        # access to data and then tried to logout - after log out they are
414        # redirected back to the page where they tried accessing data but this
415        # time they will have no security credential set
416        if not self.securityTokens:
417            # Try to recover and do something sensible
418            #
419            # TODO: this adds insult to injury if the person has just been
420            # denied access to data.  Instead do a redirect back to the
421            # discovery page?
422            # P J Kershaw 10/08/07
423            log.info("Exiting from Gatekeeper: user is not logged in")
424            return False, self.__class__.NotLoggedInMsg
425           
426        xpathr='{http://ndg.nerc.ac.uk/moles}simpleCondition/{http://ndg.nerc.ac.uk/moles}attrauthRole'
427        xpathaa='{http://ndg.nerc.ac.uk/moles}simpleCondition/{http://ndg.nerc.ac.uk/moles}dgAttributeAuthority'
428        roleE,aaE=self.securityElement.find(xpathr),self.securityElement.find(xpathaa)
429        if roleE is None:
430            log.error("Gatekeeper: role not found in dataset element: %s" % \
431                      self.securityElement)
432            return False, self.__class__.InvalidSecurityCondition
433       
434        self.reqRole=roleE.text
435       
436        # Check Attribute Authority address
437        try:
438            PullModelHandler.urlCanBeOpened(aaE.text)
439        except (URLCannotBeOpened, AttributeError):
440            # Catch situation where either Attribute Authority address in the
441            # data invalid or none was set.  In this situation verify
442            # against the Attribute Authority set in the config
443            log.info('Gatekeeper: Attribute Authority address is invalid ' + \
444                     'in data "%s" - defaulting to config file setting' % \
445                     self.securityElement)
446            self.reqAAURI = g.securityCfg.aaURI
447   
448        # Create Session Manager client
449        self.smClnt = SessionMgrClient(uri=self.securityTokens['h'],
450                    sslCACertFilePathList=g.securityCfg.sslCACertFilePathList,
451                    sslPeerCertCN=g.securityCfg.sslPeerCertCN,
452                    signingCertFilePath=g.securityCfg.wssCertFilePath,
453                    signingPriKeyFilePath=g.securityCfg.wssPriKeyFilePath,
454                    signingPriKeyPwd=g.securityCfg.wssPriKeyPwd,
455                    caCertFilePathList=g.securityCfg.wssCACertFilePathList,
456                    tracefile=g.securityCfg.tracefile)       
457       
458        return self.__pullSessionAttCert()
459           
460           
461    def __pullSessionAttCert(self):
462        """Check to see if the Session Manager can deliver an Attribute
463        Certificate with the required role to gain access to the resource
464        in question"""
465           
466        try:
467            # Make request for attribute certificate
468            attCert = self.smClnt.getAttCert(attAuthorityURI=self.reqAAURI,
469                                         sessID=self.securityTokens['sid'],
470                                         reqRole=self.reqRole)
471        except AttributeRequestDenied, e:
472            log.info(\
473                "Gatekeeper - request for attribute certificate denied: %s"%e)
474            return False, str(e)
475       
476        except SessionNotFound, e:
477            log.info("Gatekeeper - no session found: %s" % e)
478            return False, self.__class__.NotLoggedInMsg
479
480        except SessionExpired, e:
481            log.info("Gatekeeper - session expired: %s" % e)
482            return False, self.__class__.SessionExpiredMsg
483
484        except SessionCertTimeError, e:
485            log.info("Gatekeeper - session cert. time error: %s" % e)
486            return False, self.__class__.InvalidSessionMsg
487           
488        except InvalidSession, e:
489            log.info("Gatekeeper - invalid user session: %s" % e)
490            return False, self.__class__.InvalidSessionMsg
491
492        except Exception, e:
493            raise GateKeeperError, "Gatekeeper request for attribute certificate: "+\
494                            str(e)
495                           
496        # Check attribute certificate is valid
497        attCert.certFilePathList = g.securityCfg.acCACertFilePathList
498        attCert.isValid(raiseExcep=True)
499           
500        # Check it's issuer is as expected
501        if attCert.issuer != g.securityCfg.acIssuer:
502            log.info('Gatekeeper - access denied: Attribute Certificate ' + \
503                'issuer DN, "%s" ' % attCert.issuer + \
504                'must match this data provider\'s Attribute Authority ' + \
505                'DN: "%s"' % g.securityCfg.acIssuer)
506            return False, self.__class__.InvalidAttributeCertificate
507       
508        log.info('Gatekeeper - access granted for user "%s" '%attCert.userId+\
509                 'to "%s" secured with role "%s" ' % (self.uri,self.reqRole)+\
510                 'using attribute certificate:\n\n%s' % attCert)
511                     
512        return True, self.__class__.AccessAllowedMsg
513
514    @classmethod
515    def urlCanBeOpened(cls, url, timeout=5, raiseExcep=True):
516       """Check url can be opened - adapted from
517       http://mail.python.org/pipermail/python-list/2004-October/289601.html
518       """
519   
520       found = False
521       defTimeOut = socket.getdefaulttimeout()
522       try:
523           socket.setdefaulttimeout(timeout)
524
525           try:
526               urllib2.urlopen(url)
527           except (urllib2.HTTPError, urllib2.URLError,
528                   socket.error, socket.sslerror):
529               if raiseExcep:
530                   raise URLCannotBeOpened
531           
532           found = True
533         
534       finally:
535           socket.setdefaulttimeout(defTimeOut)
536           
537       return found
538   
539
540class SecurityConfigError(Exception):
541    """Handle errors from parsing security config items"""
542       
543class SecurityConfig(object):
544    """Get Security related parameters from the Pylons NDG config file"""
545   
546    def parse(self, cfg, section='NDG_SECURITY'):
547        '''Get PKI settings for Attribute Authority and Session Manager from
548        the configuration file
549       
550        @type cfg: ConfigParser object
551        @param cfg: reference to configuration file.'''
552       
553        tracefileExpr = cfg.get(section, 'tracefile')
554        if tracefileExpr:
555            self.tracefile = eval(tracefileExpr)
556
557        self.smURI = cfg.get(section, 'sessionMgrURI')       
558        self.aaURI = cfg.get(section, 'attAuthorityURI')
559
560        try:
561            self.wssCACertFilePathList = \
562                cfg.get(section, 'wssCACertFilePathList').split()
563               
564        except AttributeError:
565            raise SecurityConfigError, \
566                                'No "wssCACertFilePathList" security setting'
567
568        # Attribute Certificate Issuer
569        self.acIssuer = cfg.get(section, 'acIssuer')
570       
571        # verification of X.509 cert back to CA
572        try:
573            self.acCACertFilePathList = cfg.get(section, 
574                                            'acCACertFilePathList').split()         
575        except AttributeError:
576            raise SecurityConfigError, \
577                                'No "acCACertFilePathList" security setting'
578
579             
580    def __repr__(self):
581        return '\n'.join(["%s=%s" % (k,v) for k,v in self.__dict__.items() \
582                if k[:2] != "__"])
583     
Note: See TracBrowser for help on using the repository browser.