source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/pdp/browse.py @ 4067

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/pdp/browse.py@4067
Revision 4067, 18.7 KB checked in by pjkersha, 11 years ago (diff)

Fixes to ndg.security.common.authz.pdp.proftp - Pro-FTP based Polciy Decision Point. Tested on zonda.

Line 
1"""NDG Policy Decision Point for NDG Browse - access constraints for a
2resource are determined from MOLES access constraints in the data.  Nb. the
3access control portions of the schema are used for CSML also.
4
5NERC Data Grid Project
6"""
7__author__ = "P J Kershaw"
8__date__ = "04/04/08"
9__copyright__ = "(C) 2008 STFC & NERC"
10__contact__ = "P.J.Kershaw@rl.ac.uk"
11__license__ = \
12"""This software may be distributed under the terms of the Q Public
13License, version 1.0 or later."""
14__contact__ = "P.J.Kershaw@rl.ac.uk"
15__revision__ = "$Id$"
16
17import logging
18log = logging.getLogger(__name__)
19
20import sys # tracefile config param may be set to e.g. sys.stderr
21import urllib2
22import socket
23from ConfigParser import SafeConfigParser
24
25# For parsing of properties file
26from os.path import expandvars as expVars
27
28from ndg.security.common.authz.pdp import PDPInterface, PDPError, \
29    PDPUserAccessDenied, PDPUserNotLoggedIn, PDPMissingResourceConstraints, \
30    PDPUnknownResourceType, PDPUserInsufficientPrivileges, \
31    PDPMissingUserHandleAttr
32   
33from ndg.security.common.SessionMgr import SessionMgrClient, SessionNotFound,\
34    SessionCertTimeError, SessionExpired, InvalidSession, \
35    AttributeRequestDenied                   
36   
37from ndg.security.common.X509 import X500DN               
38
39class InvalidAttributeCertificate(PDPError):
40    "The certificate containing authorisation roles is invalid"
41    def __init__(self, msg=None):
42        PDPError.__init__(self, msg or InvalidAttributeCertificate.__doc__)
43   
44class SessionExpiredMsg(PDPError):
45    'Session has expired.  Please re-login'
46    def __init__(self, msg=None):
47        PDPError.__init__(self, msg or SessionExpiredMsg.__doc__)
48
49class InvalidSessionMsg(PDPError):
50    'Session is invalid.  Please try re-login'
51    def __init__(self, msg=None):
52        PDPError.__init__(self, msg or InvalidSessionMsg.__doc__)
53
54class InitSessionCtxError(PDPError):
55    'A problem occured initialising a session connection'
56    def __init__(self, msg=None):
57        PDPError.__init__(self, msg or InitSessionCtxError.__doc__)
58
59class AttributeCertificateRequestError(PDPError):
60    'A problem occured requesting a certificate containing authorisation roles'
61    def __init__(self, msg=None):
62        PDPError.__init__(self,msg or AttributeCertificateRequestError.__doc__)
63
64class URLCannotBeOpened(PDPError):
65    """Raise from canURLBeOpened PullModelHandler class method
66    if URL is invalid - this method is used to check the AA
67    service"""
68
69
70class BrowsePDP(PDPInterface):
71    """Make access control decision based on a MOLES access constraint
72    (applies to CSML too) and user security token
73   
74    This class conforms to the PDPInterface and so can be set-up from a PEP
75    (Policy Enforcement Point) object"""
76   
77    molesXMLNS = 'http://ndg.nerc.ac.uk/moles'
78    csmlXMLNS = 'http://ndg.nerc.ac.uk/csml'
79   
80    roleElemName = 'attrauthRole'
81    aaElemName = 'dgAttributeAuthority'
82   
83    molesSimpleConditionPth = \
84        '%sdgMetadataSecurity/%sdgSecurityCondition/%ssimpleCondition'
85   
86    # MOLES B0 query
87    b0SimpleConditionXPth = molesSimpleConditionPth % (('{'+molesXMLNS+'}',)*3)
88
89    # MOLES B1 is dynamically generated from B0 and has no schema   
90    b1SimpleConditionXPth = molesSimpleConditionPth % (('', )*3)
91   
92    # CSML Query
93    a0SimpleConditionXPth = \
94        '{%s}AccessControlPolicy/{%s}dgSecurityCondition/{%s}simpleCondition'%\
95        ((csmlXMLNS, )*2 + (molesXMLNS,))
96
97    defParam = {'aaURI': '',
98                'sslCACertFilePathList': [],
99                'tracefile': None,
100                'acCACertFilePathList': [], 
101                'acIssuer': '',
102                'wssCfgFilePath': None,
103                'wssCfgSection': 'DEFAULT'}
104           
105   
106    def __init__(self,
107                 cfg=None, 
108                 cfgSection='DEFAULT',
109                 **cfgKw):
110        """Initialise based on settings from a config file, config file object
111        or keywords:
112       
113        @type cfg: string / ConfigParser object
114        @param cfg: if a string type, this is interpreted as the file path to
115        a configuration file, otherwise it will be treated as a ConfigParser
116        object
117        @type cfgSection: string
118        @param cfgSection: sets the section name to retrieve config params
119        from
120        @type cfgKw: dict
121        @param cfgKw: set parameters as key value pairs."""
122       
123        self.resrcURI = None
124        self.resrcDoc = None
125        self.smURI = None
126        self.userSessID = None
127        self.username = None
128       
129        # Set from config file
130        if isinstance(cfg, basestring):
131            self._cfg = SafeConfigParser()
132            self._readConfig(cfg)
133        else:
134            self._cfg = cfg
135       
136        # Parse settings
137        if cfg:
138            self._parseConfig(cfgSection)
139           
140               
141        # Separate keywords into PDP and WS-Security specific items
142        paramNames = cfgKw.keys()
143        for paramName in paramNames:
144            if paramName in BrowsePDP.defParam:
145                # Keywords are deleted as they are set
146                setattr(self, paramName, cfgKw.pop('paramName'))
147               
148        # Remaining keys must be for WS-Security config
149        self.wssCfg = cfgKw   
150
151       
152    def _getSecurityConstraints(self):
153        '''Query the input document for a security role and Attribute Authority
154        URI constraints.  The query structure is dependent on the schema of the
155        document
156       
157        @rtype: tuple
158        @return: required role and the URI for the Attribute Authority to
159        query.  If role is None, no security is set'''
160       
161        if self.resrcURI.schema == 'NDG-B0':
162            log.info(\
163            'Checking for constraints for MOLES B0 document ...')
164
165            roleXPth = '%s/{%s}%s' % (BrowsePDP.b0SimpleConditionXPth, 
166                                      BrowsePDP.molesXMLNS, 
167                                      BrowsePDP.roleElemName)
168           
169            aaXPth = '%s/{%s}%s' % (BrowsePDP.b0SimpleConditionXPth, 
170                                    BrowsePDP.molesXMLNS, 
171                                    BrowsePDP.aaElemName)
172       
173        elif self.resrcURI.schema == 'NDG-B1':
174            # MOLES B1 is dynamically generated from B0 and has no schema
175            log.info(\
176            'Checking for constraints for MOLES B1 document ...')
177
178            roleXPth = '%s/%s' % (BrowsePDP.b1SimpleConditionXPth,
179                                  BrowsePDP.roleElemName)
180           
181            aaXPth = '%s/%s' % (BrowsePDP.b1SimpleConditionXPth,
182                                BrowsePDP.aaElemName)
183       
184        elif self.resrcURI.schema == 'NDG-A0':
185            log.info(\
186                'Checking for constraints for CSML document ...')
187       
188            roleXPth = '%s/{%s}%s' % (BrowsePDP.a0SimpleConditionXPth, 
189                                      BrowsePDP.molesXMLNS, 
190                                      BrowsePDP.roleElemName)
191           
192            aaXPth = '%s/{%s}%s' % (BrowsePDP.a0SimpleConditionXPth, 
193                                    BrowsePDP.molesXMLNS, 
194                                    BrowsePDP.aaElemName)           
195        else:
196            log.warning('No access control set for schema type: "%s"' % \
197                        self.resrcURI.schema)
198            return None, None # no access control
199       
200
201        # Execute queries for role and Attribute Authority elements and extract
202        # the text.  Default to None if not found
203        roleElem = self.resrcDoc.tree.find(roleXPth)       
204        if roleElem is not None:
205            role = roleElem.text
206        else:
207            role = None
208           
209        aaURIElem = self.resrcDoc.tree.find(aaXPth)
210        if aaURIElem is not None:
211            aaURI = aaURIElem.text
212        else:
213            aaURI = None
214
215        return role, aaURI
216
217 
218    def _readConfig(self, cfgFilePath):
219        '''Read PDP configuration file'''
220        self._cfg.read(cfgFilePath)
221
222
223    def _parseConfig(self, section='DEFAULT'):
224        '''Extract parameters from _cfg config object'''
225        log.debug("BrowsePDP._parseConfig ...")
226       
227        # Copy directly into attribute of this object
228        for paramName, paramVal in BrowsePDP.defParam.items():
229            if not self._cfg.has_option(section, paramName): 
230                # Set default if parameter is missing
231                log.debug("Setting default %s = %s" % (paramName, paramVal))
232                setattr(self, paramName, paramVal)
233                continue
234             
235            if paramName.lower() == 'tracefile':
236                val = self._cfg.get(section, paramName)
237                if val:
238                    setattr(self, paramName, eval(val))
239                else:
240                    setattr(self, paramName, None)
241                       
242            elif isinstance(paramVal, list):
243                listVal = expVars(self._cfg.get(section, paramName)).split()
244                setattr(self, paramName, listVal)
245            else:
246                val = expVars(self._cfg.get(section, paramName))
247                setattr(self, paramName, val)           
248
249
250    def accessPermitted(self, resrcHandle, userHandle, accessType=None):
251        """Make an access control decision based on whether the user is
252        authenticated and has the required roles
253       
254        @type resrcHandle: dict
255        @param resrcHandle: dict 'uri' = resource URI, 'doc' =
256        ElementTree type doc
257       
258        @type userHandle: dict
259        @param userHandle: dict with keys 'sid' = user session ID,
260        'h' = Session Manager URI
261       
262        @type accessType: -
263        @param accessType: not implemented - logs a warning if set
264       
265        @rtype: bool
266        @return: True if access permitted; False if denied or else raise
267        an Exception
268       
269        @type uri: string
270        @param uri: URI corresponding to data granule ID
271       
272        @type: ElementTree Element
273        @param securityElement: MOES security constraint containing role and
274        Attribute Authority URI. In xml, could look like:
275        <moles:effect>allow</moles:effect>
276            <moles:simpleCondition>
277            <moles:dgAttributeAuthority>http://dev.badc.rl.ac.uk/AttributeAuthority</moles:dgAttributeAuthority>
278            <moles:attrauthRole>coapec</moles:attrauthRole>
279        </moles:simpleCondition>
280        NB: xmlns:moles="http://ndg.nerc.ac.uk/moles"
281       
282        @type: pylons.session
283        @param userHandle: dict-like session object containing security
284        tokens.  Resets equivalent object attribute."""
285         
286        log.debug("Calling BrowsePDP.accessPermitted ...")
287       
288        if accessType is not None:
289            log.warning("An accessType = [%s] " % accessType + \
290                        "was set Browse assumes all access type is based " + \
291                        "on the role attribute associated with the data")
292               
293        # Check that the user is logged in.  - The User handle contains
294        # 'h' = Session Manager URI and 'sid' user Session ID
295        try:
296            self.smURI = userHandle['h']
297            self.userSessID = userHandle['sid']
298            self.username = userHandle['u']
299           
300        except KeyError, e:
301            log.error("User handle missing key %s" % e)
302            raise PDPMissingUserHandleAttr()
303       
304        except TypeError, e:
305            log.warning("No User handle set - user is not logged in: %s" % e)
306           
307        # Resource handle contains URI and ElementTree resource security
308        # element
309        try:
310            self.resrcURI = resrcHandle['uri']
311            self.resrcDoc = resrcHandle['doc'] 
312           
313        except KeyError, e:
314            log.error("Resource handle missing key %s" % e)
315            raise PDPMissingResourceConstraints()
316       
317        except TypeError, e:
318            log.error("Invalid Resource handle: %s" % e)
319            raise PDPMissingResourceConstraints()
320
321        # First query the document for a security constraint
322        role, aaURI = self._getSecurityConstraints()
323        if not role:
324            # No security set
325            log.info("No security role constraint found for [%s]" %\
326                     self.resrcURI.schema + \
327                     " type document [%s]: GRANTING ACCESS for user %s" % \
328                     (self.resrcURI, self.username))
329            return
330
331        # TODO: OpenID users have no session with the Session Manager
332        if not self.userSessID:
333            log.error("User [%s] has no session ID allocated " % \
334                      self.username + \
335                      "for connection to the Session Manager: raising " + \
336                      "PDPUserInsufficientPrivileges exception...")           
337            raise PDPUserInsufficientPrivileges()
338           
339        # Sanity check on Attribute Authority URI retrieved from the data
340        if aaURI:           
341            # Check Attribute Authority address
342            try:
343                BrowsePDP.urlCanBeOpened(aaURI)
344               
345            except URLCannotBeOpened, e:
346                # Catch situation where either Attribute Authority address in
347                # the data invalid or none was set.  In this situation verify
348                # against the Attribute Authority set in the config   
349                log.warning('security constraint ' + \
350                            'Attribute Authority address is invalid: "%s"' % \
351                            e + \
352                            ' - defaulting to config file setting: [%s]' % \
353                            self.aaURI)
354                aaURI = self.aaURI
355        else:
356            log.warning("Attribute Authority element not " + \
357                        "set in MOLES security constraints - defaulting " + \
358                        "to config file setting: [%s]" % self.aaURI)
359            aaURI = self.aaURI
360   
361        # Retrieve Attribute Certificate from user's session held by
362        # Session Manager
363        attCert = self._pullUserSessionAttCert(aaURI, role)
364       
365        # Check its validity
366        self._checkAttCert(attCert)
367                   
368        log.info('ACCESS GRANTED for user "%s" ' % \
369                 attCert.userId + \
370                 'to "%s" secured with role "%s" ' % \
371                 (self.resrcURI, role) + \
372                 'using attribute certificate:\n\n%s' % attCert)
373           
374       
375    def _pullUserSessionAttCert(self, aaURI, role):
376        """Check to see if the Session Manager can deliver an Attribute
377        Certificate with the required role to gain access to the resource
378        in question
379       
380        @type aaURI: string
381        @param aaURI: address of Attribute Authority that the Session Manager
382        will call in order to request an AC on behalf of the user
383       
384        @type role: string
385        @param role: role controlling access to the secured resource"""
386       
387        if not self.smURI:
388            log.error("No Session Manager URI set.")
389            raise InitSessionCtxError()
390           
391        try:
392            # Create Session Manager client - if a file path was set, setting
393            # are read from a separate config file section otherwise, from the
394            # PDP config object
395            self.smClnt = SessionMgrClient(uri=self.smURI,
396                            sslCACertFilePathList=self.sslCACertFilePathList,
397                            tracefile=self.tracefile,
398                            cfg=self.wssCfgFilePath or self._cfg,
399                            cfgFileSection=self.wssCfgSection,
400                            **self.wssCfg)
401        except Exception, e:
402            log.error("Creating Session Manager client: %s" % e)
403            raise InitSessionCtxError()
404       
405                 
406        try:
407            # Make request for attribute certificate
408            attCert = self.smClnt.getAttCert(attAuthorityURI=aaURI,
409                                             sessID=self.userSessID,
410                                             reqRole=role)
411            return attCert
412       
413        except AttributeRequestDenied, e:
414            log.info("Request for attribute certificate denied: %s" % e)
415            raise PDPUserAccessDenied()
416       
417        except SessionNotFound, e:
418            log.info("No session found: %s" % e)
419            raise PDPUserNotLoggedIn()
420
421        except SessionExpired, e:
422            log.info("Session expired: %s" % e)
423            raise InvalidSessionMsg()
424
425        except SessionCertTimeError, e:
426            log.info("Session cert. time error: %s" % e)
427            raise InvalidSessionMsg()
428           
429        except InvalidSession, e:
430            log.info("Invalid user session: %s" % e)
431            raise InvalidSessionMsg()
432
433        except Exception, e:
434            log.error("Request from Session Manager [%s] " % self.smURI + \
435                      "to Attribute Authority [%s] for " % aaURI + \
436                      "attribute certificate: %s: %s" % (e.__class__, e))
437            raise AttributeCertificateRequestError()
438       
439
440    def _checkAttCert(self, attCert):
441        '''Check attribute certificate is valid
442       
443        @type attCert: ndg.security.common.AttCert.AttCert
444        @param attCert: attribute certificate to be check for validity'''
445        attCert.certFilePathList = self.acCACertFilePathList
446        try:
447            attCert.isValid(raiseExcep=True)
448        except Exception, e:
449            log.error("Attribute Certificate: %s" % e)
450            raise InvalidAttributeCertificate() 
451         
452        # Check it's issuer is as expected - Convert to X500DN to do equality
453        # test
454        acIssuerDN = X500DN(self.acIssuer)
455        if attCert.issuerDN != acIssuerDN:
456            log.error('access denied: Attribute Certificate ' + \
457                'issuer DN, "%s" ' % attCert.issuerDN + \
458                'must match this data provider\'s Attribute Authority ' + \
459                'DN: "%s"' % acIssuerDN)
460            raise InvalidAttributeCertificate()
461
462
463    @classmethod
464    def urlCanBeOpened(cls, url, timeout=5, raiseExcep=True):
465       """Check url can be opened - adapted from
466       http://mail.python.org/pipermail/python-list/2004-October/289601.html
467       """
468   
469       found = False
470       defTimeOut = socket.getdefaulttimeout()
471       try:
472           socket.setdefaulttimeout(timeout)
473
474           try:
475               urllib2.urlopen(url)
476           except (urllib2.HTTPError, urllib2.URLError,
477                   socket.error, socket.sslerror, AttributeError), e:
478               if raiseExcep:
479                   raise URLCannotBeOpened(str(e))
480           
481           found = True
482         
483       finally:
484           socket.setdefaulttimeout(defTimeOut)
485           
486       return found
487     
488   
489def makeDecision(resrcHandle, userHandle, accessType=None, **kw):
490    '''One call Wrapper interface to PDP'''
491    return BrowsePDP(**kw)(resrcHandle, userHandle)
492
493 
Note: See TracBrowser for help on using the repository browser.