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

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

Fix problem with search and replace licence not adding a new line.

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