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

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

Working version with independent Policy Enforcement Point (Gatekeeper) + Polciy Decision Point for Pylons Browse code stack

python/ndg.security.server/ndg/security/server/sso/sso/controllers/login.py: extra help info in message for login error

python/ndg.security.test/ndg/security/test/wsSecurity/server/echoServer.py: mod to SignatureHandler? init due to change in WSSecurityConfig interface

python/Tests/authtest/authtest/controllers/test2.py,
python/Tests/authtest/authtest/lib/template.py: missed out on last check in

python/ndg.security.common/ndg/security/common/authz/pdp/proftp.py: udpate to init following change to PDPInterface class for browse code

python/ndg.security.common/ndg/security/common/authz/pdp/init.py: PDPInterface takes cfg keyword which can be file path or a ConfigParser? object

python/ndg.security.common/ndg/security/common/authz/pdp/browse.py:

  • fixes to XPath queries.
  • BrowsePDP now does some more of the work done previously by ows_server.models.ndgInterface.GateKeep and queries directly for role and AA values direct from the doc root
  • made fix to WS-Security settings - may be picked up from the same config file as the PDP settings

python/ndg.security.common/ndg/security/common/authz/pep.py,
python/ndg.security.common/ndg/security/common/wsSecurity.py: allow generic cfg keyword for file path / config obj input

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
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       
129        # Set from config file
130        if isinstance(cfg, basestring):
131            self._readConfig(cfg)
132        else:
133            self._cfg = cfg
134       
135        # Parse settings
136        if cfg:
137            self._parseConfig(cfgSection)
138           
139               
140        # Separate keywords into PDP and WS-Security specific items
141        paramNames = cfgKw.keys()
142        for paramName in paramNames:
143            if paramName in BrowsePDP.defParam:
144                # Keywords are deleted as they are set
145                setattr(self, paramName, cfgKw.pop('paramName'))
146               
147        # Remaining keys must be for WS-Security config
148        self.wssCfg = cfgKw   
149
150       
151    def _getSecurityConstraints(self):
152        '''Query the input document for a security role and Attribute Authority
153        URI constraints.  The query structure is dependent on the schema of the
154        document
155       
156        @rtype: tuple
157        @return: required role and the URI for the Attribute Authority to
158        query.  If role is None, no security is set'''
159       
160        if self.resrcURI.schema == 'DIF':
161            log.info('BrowsePDP: DIF record found - no security applied')
162            return None, None # no access control
163       
164        elif self.resrcURI.schema == 'NDG-B0':
165            log.info(\
166            'BrowsePDP: Checking for constraints for MOLES B0 document ...')
167
168            roleXPth = '%s/{%s}%s' % (BrowsePDP.b0SimpleConditionXPth, 
169                                      BrowsePDP.molesXMLNS, 
170                                      BrowsePDP.roleElemName)
171           
172            aaXPth = '%s/{%s}%s' % (BrowsePDP.b0SimpleConditionXPth, 
173                                    BrowsePDP.molesXMLNS, 
174                                    BrowsePDP.aaElemName)
175       
176        elif self.resrcURI.schema == 'NDG-B1':
177            # MOLES B1 is dynamically generated from B0 and has no schema
178            log.info(\
179            'BrowsePDP: Checking for constraints for MOLES B1 document ...')
180
181            roleXPth = '%s/%s' % (BrowsePDP.b1SimpleConditionXPth,
182                                  BrowsePDP.roleElemName)
183           
184            aaXPth = '%s/%s' % (BrowsePDP.b1SimpleConditionXPth,
185                                BrowsePDP.aaElemName)
186       
187        elif self.resrcURI.schema == 'NDG-A0':
188            log.info(\
189                'BrowsePDP: Checking for constraints for CSML document ...')
190       
191            roleXPth = '%s/{%s}%s' % (BrowsePDP.a0SimpleConditionXPth, 
192                                      BrowsePDP.molesXMLNS, 
193                                      BrowsePDP.roleElemName)
194           
195            aaXPth = '%s/{%s}%s' % (BrowsePDP.a0SimpleConditionXPth, 
196                                    BrowsePDP.molesXMLNS, 
197                                    BrowsePDP.aaElemName)           
198        else:
199            log.error('BrowsePDP: unknown schema type "%s"' % \
200                      self.resrcURI.schema)
201            raise PDPUnknownResourceType()
202
203        # Execute queries for role and Attribute Authority elements and extract
204        # the text.  Default to None if not found
205        roleElem = self.resrcDoc.tree.find(roleXPth)       
206        if roleElem is not None:
207            role = roleElem.text
208        else:
209            role = None
210           
211        aaURIElem = self.resrcDoc.tree.find(aaXPth)
212        if aaURIElem is not None:
213            aaURI = aaURIElem.text
214        else:
215            aaURI = None
216
217        return role, aaURI
218
219 
220    def _readConfig(self, cfgFilePath):
221        '''Read PDP configuration file'''
222        self._cfg.read(cfgFilePath)
223
224
225    def _parseConfig(self, section='DEFAULT'):
226        '''Extract parameters from _cfg config object'''
227        log.debug("BrowsePDP._parseConfig ...")
228       
229        # Copy directly into attribute of this object
230        for paramName, paramVal in BrowsePDP.defParam.items():
231            if not self._cfg.has_option(section, paramName): 
232                # Set default if parameter is missing
233                log.debug("Setting default %s = %s" % (paramName, paramVal))
234                setattr(self, paramName, paramVal)
235                continue
236             
237            if isinstance(paramVal, list):
238                listVal = expVars(self._cfg.get(section, paramName)).split()
239                setattr(self, paramName, listVal)
240            else:
241                val = expVars(self._cfg.get(section, paramName))
242                setattr(self, paramName, val)           
243
244
245    def accessPermitted(self, resrcHandle, userHandle, accessType=None):
246        """Make an access control decision based on whether the user is
247        authenticated and has the required roles
248       
249        @type resrcHandle: dict
250        @param resrcHandle: dict 'uri' = resource URI, 'doc' =
251        ElementTree type doc
252       
253        @type userHandle: dict
254        @param userHandle: dict with keys 'sid' = user session ID,
255        'h' = Session Manager URI
256       
257        @type accessType: -
258        @param accessType: not implemented - logs a warning if set
259       
260        @rtype: bool
261        @return: True if access permitted; False if denied or else raise
262        an Exception
263       
264        @type uri: string
265        @param uri: URI corresponding to data granule ID
266       
267        @type: ElementTree Element
268        @param securityElement: MOES security constraint containing role and
269        Attribute Authority URI. In xml, could look like:
270        <moles:effect>allow</moles:effect>
271            <moles:simpleCondition>
272            <moles:dgAttributeAuthority>http://dev.badc.rl.ac.uk/AttributeAuthority</moles:dgAttributeAuthority>
273            <moles:attrauthRole>coapec</moles:attrauthRole>
274        </moles:simpleCondition>
275        NB: xmlns:moles="http://ndg.nerc.ac.uk/moles"
276       
277        @type: pylons.session
278        @param userHandle: dict-like session object containing security
279        tokens.  Resets equivalent object attribute."""
280         
281        log.debug("Calling BrowsePDP.accessPermitted ...")
282       
283        if accessType is not None:
284            log.warning("BrowsePDP an accessType = [%s] " % accessType + \
285                        "was set Browse assumes all access type is based " + \
286                        "on the role attribute associated with the data")
287           
288        # Resource handle contains URI and ElementTree resource security
289        # element
290        try:
291            self.resrcURI = resrcHandle['uri']
292            self.resrcDoc = resrcHandle['doc'] 
293        except KeyError, e:
294            log.error("Resource handle missing key %s" % e)
295            raise PDPMissingResourceConstraints()
296
297        # First query the document for a security constraint
298        role, aaURI = self._getSecurityConstraints()
299        if not role:
300            # No security set
301            log.info("BrowsePDP: no security role constraint found for [%s]" %\
302                     self.resrcURI.schema + \
303                     " type document [%s]: GRANTING ANONYMOUS ACCESS" % \
304                     self.resrcURI)
305            return
306               
307        # Check that the user is logged in.  - The User handle contains
308        # 'h' = Session Manager URI and 'sid' user Session ID
309        try:
310            self.smURI = userHandle['h']
311            self.userSessID = userHandle['sid']
312        except KeyError, e:
313            log.error("User handle missing key %s" % e)
314            raise PDPUserNotLoggedIn()
315       
316        # Sanity check on Attribute Authority URI retrieved from the data
317        if aaURI:           
318            # Check Attribute Authority address
319            try:
320                BrowsePDP.urlCanBeOpened(aaURI)
321               
322            except URLCannotBeOpened, e:
323                # Catch situation where either Attribute Authority address in
324                # the data invalid or none was set.  In this situation verify
325                # against the Attribute Authority set in the config   
326                log.warning('BrowsePDP: security constraint ' + \
327                            'Attribute Authority address is invalid - ' + \
328                            'defaulting to config file setting: %s; ' % \
329                            self.aaURI + \
330                            'error message is: %s' % e)
331                aaURI = self.aaURI
332        else:
333            log.warning("BrowsePDP: Attribute Authority element not " + \
334                        "set in MOLES security constraints - defaulting " + \
335                        "to config file setting: %s" % self.aaURI)
336            aaURI = self.aaURI
337   
338        # Retrieve Attirbute Certificate from user's session held by
339        # Session Manager
340        attCert = self._pullUserSessionAttCert(aaURI, role)
341       
342        # Check its validity
343        self._checkAttCert(attCert)
344                   
345        log.info('BrowsePDP: ACCESS GRANTED for user "%s" ' % \
346                 attCert.userId + \
347                 'to "%s" secured with role "%s" ' % \
348                 (self.resrcURI, role) + \
349                 'using attribute certificate:\n\n%s' % attCert)
350           
351       
352    def _pullUserSessionAttCert(self, aaURI, role):
353        """Check to see if the Session Manager can deliver an Attribute
354        Certificate with the required role to gain access to the resource
355        in question
356       
357        @type aaURI: string
358        @param aaURI: address of Attribute Authority that the Session Manager
359        will call in order to request an AC on behalf of the user
360       
361        @type role: string
362        @param role: role controlling access to the secured resource"""
363       
364        try:
365            # Create Session Manager client - if a file path was set, setting
366            # are read from a separate config file section otherwise, from the
367            # PDP config object
368            self.smClnt = SessionMgrClient(uri=self.smURI,
369                                    cfg=self.wssCfgFilePath or self._cfg,
370                                    cfgFileSection=self.wssCfgSection,
371                                    **self.wssCfg)
372        except Exception, e:
373            log.error("BrowsePDP: creating Session Manager client: %s" % e)
374            raise InitSessionCtxError()
375       
376                 
377        try:
378            # Make request for attribute certificate
379            attCert = self.smClnt.getAttCert(attAuthorityURI=aaURI,
380                                             sessID=self.userSessID,
381                                             reqRole=role)
382            return attCert
383       
384        except AttributeRequestDenied, e:
385            log.info(\
386            "PDP -request for attribute certificate denied: %s" % e)
387            raise PDPUserAccessDenied()
388       
389        except SessionNotFound, e:
390            log.info("PDP -no session found: %s" % e)
391            raise PDPUserNotLoggedIn()
392
393        except SessionExpired, e:
394            log.info("PDP -session expired: %s" % e)
395            raise InvalidSessionMsg()
396
397        except SessionCertTimeError, e:
398            log.info("PDP -session cert. time error: %s" % e)
399            raise InvalidSessionMsg()
400           
401        except InvalidSession, e:
402            log.info("PDP -invalid user session: %s" % e)
403            raise InvalidSessionMsg()
404
405        except Exception, e:
406            log.error("PDP request for attribute certificate: %s" % e)
407            raise AttributeCertificateRequestError()
408       
409
410    def _checkAttCert(self, attCert):
411        '''Check attribute certificate is valid
412       
413        @type attCert: ndg.security.common.AttCert.AttCert
414        @param attCert: attribute certificate to be check for validity'''
415        attCert.certFilePathList = self.acCACertFilePathList
416        try:
417            attCert.isValid(raiseExcep=True)
418        except Exception, e:
419            log.error("Attribute Certificate: %s" % e)
420            raise InvalidAttributeCertificate() 
421         
422        # Check it's issuer is as expected - Convert to X500DN to do equality
423        # test
424        acIssuerDN = X500DN(self.acIssuer)
425        if attCert.issuerDN != acIssuerDN:
426            log.info('PDP -access denied: Attribute Certificate ' + \
427                'issuer DN, "%s" ' % attCert.issuerDN + \
428                'must match this data provider\'s Attribute Authority ' + \
429                'DN: "%s"' % acIssuerDN)
430            raise InvalidAttributeCertificate()
431
432
433    @classmethod
434    def urlCanBeOpened(cls, url, timeout=5, raiseExcep=True):
435       """Check url can be opened - adapted from
436       http://mail.python.org/pipermail/python-list/2004-October/289601.html
437       """
438   
439       found = False
440       defTimeOut = socket.getdefaulttimeout()
441       try:
442           socket.setdefaulttimeout(timeout)
443
444           try:
445               urllib2.urlopen(url)
446           except (urllib2.HTTPError, urllib2.URLError,
447                   socket.error, socket.sslerror, AttributeError):
448               if raiseExcep:
449                   raise URLCannotBeOpened()
450           
451           found = True
452         
453       finally:
454           socket.setdefaulttimeout(defTimeOut)
455           
456       return found
457     
458   
459def makeDecision(resrcHandle, userHandle, accessType=None, **kw):
460    '''One call Wrapper interface to PDP'''
461    return BrowsePDP(**kw)(resrcHandle, userHandle)
462
463 
Note: See TracBrowser for help on using the repository browser.