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

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

New release for deployment as egg version 0.9.1:

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