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

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

Refactoring of SSO service to enable use of local AA and SM instances via keys to environ.

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"
10__contact__ = "Philip.Kershaw@stfc.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__ = "Philip.Kershaw@stfc.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.sessionmanager import SessionManagerClient, 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 = SessionManagerClient(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(attributeAuthorityURI=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.