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

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

python/ndg.security.server/ndg/security/server/share/ndg-aa: added option for http_proxy setting

python/ndg.security.server/ndg/security/server/sso/sso/config/ssoServiceMiddleware.py: make layout section optional

python/ndg.security.server/ndg/security/server/sso/sso/lib/openid_util.py: TypeError? check to ensure request object is accessible

python/ndg.security.server/ndg/security/server/SessionMgr/init.py: allow defaults for 'wssRefInclNS' and 'wssSignedInfoInclNS' properties in case they're not set

security/python/Makefile: added target for making SysV init scripts

python/Tests/pylonsAttributeAuthority/ndgsecurity/ndgsecurity/*... more experiments for generic SOAP WSGI Middleware

python/ndg.security.common/ndg/security/common/authz/pdp/browse.py: important fix - permissive policy for schema types that are not recognised.

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 == '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 isinstance(paramVal, list):
236                listVal = expVars(self._cfg.get(section, paramName)).split()
237                setattr(self, paramName, listVal)
238            else:
239                val = expVars(self._cfg.get(section, paramName))
240                setattr(self, paramName, val)           
241
242
243    def accessPermitted(self, resrcHandle, userHandle, accessType=None):
244        """Make an access control decision based on whether the user is
245        authenticated and has the required roles
246       
247        @type resrcHandle: dict
248        @param resrcHandle: dict 'uri' = resource URI, 'doc' =
249        ElementTree type doc
250       
251        @type userHandle: dict
252        @param userHandle: dict with keys 'sid' = user session ID,
253        'h' = Session Manager URI
254       
255        @type accessType: -
256        @param accessType: not implemented - logs a warning if set
257       
258        @rtype: bool
259        @return: True if access permitted; False if denied or else raise
260        an Exception
261       
262        @type uri: string
263        @param uri: URI corresponding to data granule ID
264       
265        @type: ElementTree Element
266        @param securityElement: MOES security constraint containing role and
267        Attribute Authority URI. In xml, could look like:
268        <moles:effect>allow</moles:effect>
269            <moles:simpleCondition>
270            <moles:dgAttributeAuthority>http://dev.badc.rl.ac.uk/AttributeAuthority</moles:dgAttributeAuthority>
271            <moles:attrauthRole>coapec</moles:attrauthRole>
272        </moles:simpleCondition>
273        NB: xmlns:moles="http://ndg.nerc.ac.uk/moles"
274       
275        @type: pylons.session
276        @param userHandle: dict-like session object containing security
277        tokens.  Resets equivalent object attribute."""
278         
279        log.debug("Calling BrowsePDP.accessPermitted ...")
280       
281        if accessType is not None:
282            log.warning("An accessType = [%s] " % accessType + \
283                        "was set Browse assumes all access type is based " + \
284                        "on the role attribute associated with the data")
285               
286        # Check that the user is logged in.  - The User handle contains
287        # 'h' = Session Manager URI and 'sid' user Session ID
288        try:
289            self.smURI = userHandle['h']
290            self.userSessID = userHandle['sid']
291            self.username = userHandle['u']
292           
293        except KeyError, e:
294            log.error("User handle missing key %s" % e)
295            raise PDPUserNotLoggedIn()
296       
297        except TypeError, e:
298            log.error("No User handle set - raising PDPUserNotLoggedIn: %s"%e)
299            raise PDPUserNotLoggedIn()
300           
301        # Resource handle contains URI and ElementTree resource security
302        # element
303        try:
304            self.resrcURI = resrcHandle['uri']
305            self.resrcDoc = resrcHandle['doc'] 
306           
307        except KeyError, e:
308            log.error("Resource handle missing key %s" % e)
309            raise PDPMissingResourceConstraints()
310       
311        except TypeError, e:
312            log.error("Invalid Resource handle: %s" % e)
313            raise PDPMissingResourceConstraints()
314
315        # First query the document for a security constraint
316        role, aaURI = self._getSecurityConstraints()
317        if not role:
318            # No security set
319            log.info("No security role constraint found for [%s]" %\
320                     self.resrcURI.schema + \
321                     " type document [%s]: GRANTING ACCESS for user %s" % \
322                     (self.resrcURI, self.username))
323            return
324       
325        # TODO: OpenID users have no session with the Session Manager
326        if not self.userSessID:
327            log.error("User [%s] has no session ID allocated " % \
328                      self.username + \
329                      "for connection to the Session Manager: raising " + \
330                      "PDPUserInsufficientPrivileges exception...")           
331            raise PDPUserInsufficientPrivileges()
332           
333        # Sanity check on Attribute Authority URI retrieved from the data
334        if aaURI:           
335            # Check Attribute Authority address
336            try:
337                BrowsePDP.urlCanBeOpened(aaURI)
338               
339            except URLCannotBeOpened, e:
340                # Catch situation where either Attribute Authority address in
341                # the data invalid or none was set.  In this situation verify
342                # against the Attribute Authority set in the config   
343                log.warning('security constraint ' + \
344                            'Attribute Authority address is invalid: "%s"' % \
345                            e + \
346                            ' - defaulting to config file setting: [%s]' % \
347                            self.aaURI)
348                aaURI = self.aaURI
349        else:
350            log.warning("Attribute Authority element not " + \
351                        "set in MOLES security constraints - defaulting " + \
352                        "to config file setting: [%s]" % self.aaURI)
353            aaURI = self.aaURI
354   
355        # Retrieve Attribute Certificate from user's session held by
356        # Session Manager
357        attCert = self._pullUserSessionAttCert(aaURI, role)
358       
359        # Check its validity
360        self._checkAttCert(attCert)
361                   
362        log.info('ACCESS GRANTED for user "%s" ' % \
363                 attCert.userId + \
364                 'to "%s" secured with role "%s" ' % \
365                 (self.resrcURI, role) + \
366                 'using attribute certificate:\n\n%s' % attCert)
367           
368       
369    def _pullUserSessionAttCert(self, aaURI, role):
370        """Check to see if the Session Manager can deliver an Attribute
371        Certificate with the required role to gain access to the resource
372        in question
373       
374        @type aaURI: string
375        @param aaURI: address of Attribute Authority that the Session Manager
376        will call in order to request an AC on behalf of the user
377       
378        @type role: string
379        @param role: role controlling access to the secured resource"""
380       
381        if not self.smURI:
382            log.error("No Session Manager URI set.")
383            raise InitSessionCtxError()
384           
385        try:
386            # Create Session Manager client - if a file path was set, setting
387            # are read from a separate config file section otherwise, from the
388            # PDP config object
389            self.smClnt = SessionMgrClient(uri=self.smURI,
390                                        cfg=self.wssCfgFilePath or self._cfg,
391                                        cfgFileSection=self.wssCfgSection,
392                                        **self.wssCfg)
393        except Exception, e:
394            log.error("Creating Session Manager client: %s" % e)
395            raise InitSessionCtxError()
396       
397                 
398        try:
399            # Make request for attribute certificate
400            attCert = self.smClnt.getAttCert(attAuthorityURI=aaURI,
401                                             sessID=self.userSessID,
402                                             reqRole=role)
403            return attCert
404       
405        except AttributeRequestDenied, e:
406            log.info("Request for attribute certificate denied: %s" % e)
407            raise PDPUserAccessDenied()
408       
409        except SessionNotFound, e:
410            log.info("No session found: %s" % e)
411            raise PDPUserNotLoggedIn()
412
413        except SessionExpired, e:
414            log.info("Session expired: %s" % e)
415            raise InvalidSessionMsg()
416
417        except SessionCertTimeError, e:
418            log.info("Session cert. time error: %s" % e)
419            raise InvalidSessionMsg()
420           
421        except InvalidSession, e:
422            log.info("Invalid user session: %s" % e)
423            raise InvalidSessionMsg()
424
425        except Exception, e:
426            log.error("Request for attribute certificate: %s" % e)
427            raise AttributeCertificateRequestError()
428       
429
430    def _checkAttCert(self, attCert):
431        '''Check attribute certificate is valid
432       
433        @type attCert: ndg.security.common.AttCert.AttCert
434        @param attCert: attribute certificate to be check for validity'''
435        attCert.certFilePathList = self.acCACertFilePathList
436        try:
437            attCert.isValid(raiseExcep=True)
438        except Exception, e:
439            log.error("Attribute Certificate: %s" % e)
440            raise InvalidAttributeCertificate() 
441         
442        # Check it's issuer is as expected - Convert to X500DN to do equality
443        # test
444        acIssuerDN = X500DN(self.acIssuer)
445        if attCert.issuerDN != acIssuerDN:
446            log.info('access denied: Attribute Certificate ' + \
447                'issuer DN, "%s" ' % attCert.issuerDN + \
448                'must match this data provider\'s Attribute Authority ' + \
449                'DN: "%s"' % acIssuerDN)
450            raise InvalidAttributeCertificate()
451
452
453    @classmethod
454    def urlCanBeOpened(cls, url, timeout=5, raiseExcep=True):
455       """Check url can be opened - adapted from
456       http://mail.python.org/pipermail/python-list/2004-October/289601.html
457       """
458   
459       found = False
460       defTimeOut = socket.getdefaulttimeout()
461       try:
462           socket.setdefaulttimeout(timeout)
463
464           try:
465               urllib2.urlopen(url)
466           except (urllib2.HTTPError, urllib2.URLError,
467                   socket.error, socket.sslerror, AttributeError), e:
468               if raiseExcep:
469                   raise URLCannotBeOpened(str(e))
470           
471           found = True
472         
473       finally:
474           socket.setdefaulttimeout(defTimeOut)
475           
476       return found
477     
478   
479def makeDecision(resrcHandle, userHandle, accessType=None, **kw):
480    '''One call Wrapper interface to PDP'''
481    return BrowsePDP(**kw)(resrcHandle, userHandle)
482
483 
Note: See TracBrowser for help on using the repository browser.