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

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

Updates for ndg.security.common.authz.pep and ndg.security.common.authz.pdp for BrowsePDP Gatekeeper code for ows_server.

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    # MOLES B1 is dynamically generated from B0 and has no schema   
80    b1dgSecurityConditionXPth = 'dgMetadataSecurity/dgSecurityCondition'
81   
82    # Add schemae prefixes for B0 query
83    b0dgSecurityConditionXPth = '{%s}%s'%(molesXMLNS,b1dgSecurityConditionXPth)
84   
85    csmlDGSecurityConditionXPth = \
86        '{%s}AccessControlPolicy/{%s}dgSecurityCondition' % ((csmlXMLNS, )*2)
87
88    molesXPathQueryPfx = \
89'{http://ndg.nerc.ac.uk/moles}simpleCondition/{http://ndg.nerc.ac.uk/moles}'
90    roleXPathQuery = molesXPathQueryPfx + 'attrauthRole'
91    roleXPathQuery = molesXPathQueryPfx + 'dgAttributeAuthority'
92
93    defParam = {'aaURI': '',
94                'sslCACertFilePathList': [],
95                'tracefile': '',
96                'acCACertFilePathList': [], 
97                'acIssuer': ''}
98           
99   
100    def __init__(self,
101                 cfg=None, 
102                 cfgSection='DEFAULT',
103                 **cfgKw):
104        """Initialise based on settings from a config file, config file object
105        or keywords:
106       
107        @type cfg: string / ConfigParser object
108        @param cfg: if a string type, this is interpreted as the file path to
109        a configuration file, otherwise it will be treated as a ConfigParser
110        object
111        @type cfgSection: string
112        @param cfgSection: sets the section name to retrieve config params
113        from
114        @type cfgKw: dict
115        @param cfgKw: set parameters as key value pairs."""
116       
117        self._cfg = cfg or SafeConfigParser()
118       
119        self.resrcURI = None
120        self.securityElement = None
121        self.userHandle = None
122       
123        # Set from config file
124        if isinstance(cfg, basestring):
125            self._readConfig(cfgFilePath)
126        else:
127            self._cfg = cfg
128       
129        # Parse settings
130        if cfg:
131            self._parseConfig(cfgSection)
132           
133               
134        # Separate keywords into PDP and WS-Security specific items
135        paramNames = cfgKw.keys()
136        for paramName in paramNames:
137            if paramName in BrowsePDP.defParam:
138                # Keywords are deleted as they are set
139                setattr(self, paramName, cfgKw.pop('paramName'))
140               
141        # Remaining keys must be for WS-Security config
142        self.wssCfg = cfgKw   
143
144       
145    def _getSecurityElem(self):
146        '''Query the input document for a security constraint element.       
147        The query type is dependent on the schema of the document'''
148       
149        if self.resrcURI.schema == 'DIF':
150            log.info('BrowsePDP: DIF record found - no security applied')
151            return None # no access control
152       
153        elif self.resrcURI.schema == 'NDG-B0':
154            log.info(\
155            'BrowsePDP: Checking for constraints for MOLES B0 document ...')
156            return self.resrcDoc.tree.find(BrowsePDP.b1dgSecurityConditionXPth)
157       
158        elif self.resrcURI.schema == 'NDG-B1':
159            # MOLES B1 is dynamically generated from B0 and has no schema
160            log.info(\
161            'BrowsePDP: Checking for constraints for MOLES B1 document ...')
162            return self.resrcDoc.tree.find(BrowsePDP.b1dgSecurityConditionXPth)
163         
164        elif self.resrcURI.schema == 'NDG-A0':
165            log.info(\
166            'BrowsePDP: Checking for constraints for CSML document ...')
167            return \
168                self.resrcDoc.tree.find(BrowsePDP.csmlDGSecurityConditionXPth)
169        else:
170            log.error('BrowsePDP._getSecurityElem: unknown schema type "%s"'%\
171                      self.resrcURI.schema)
172            raise PDPUnknownResourceType()
173
174 
175    def _readConfig(self):
176        '''Read PDP configuration file'''
177        self._cfg.read(self.cfgFilePath)
178
179
180    def _parseConfig(self, section='DEFAULT'):
181        '''Extract parameters from _cfg config object'''
182        # Copy directly into attribute of this object
183        for paramName, paramVal in BrowsePDP.defParam.items():
184            if isinstance(paramVal, list):
185                listVal = expVars(self._cfg.get(section, paramName)).split()
186                setattr(self, paramName, listVal)
187            else:
188                val = expVars(self._cfg.get(section, paramName))
189                setattr(self, paramName, val)           
190
191
192    def accessPermitted(self, resrcHandle, userHandle, accessType=None):
193        """Make an access control decision based on whether the user is
194        authenticated and has the required roles
195       
196        @type resrcHandle: dict
197        @param resrcHandle: dict 'uri' = resource URI, 'doc' =
198        ElementTree type doc
199       
200        @type userHandle: dict
201        @param userHandle: dict with keys 'sid' = user session ID,
202        'h' = Session Manager URI
203       
204        @type accessType: -
205        @param accessType: not implemented - logs a warning if set
206       
207        @rtype: bool
208        @return: True if access permitted; False if denied or else raise
209        an Exception
210       
211        @type uri: string
212        @param uri: URI corresponding to data granule ID
213       
214        @type: ElementTree Element
215        @param securityElement: MOES security constraint containing role and
216        Attribute Authority URI. In xml, could look like:
217        <moles:effect>allow</moles:effect>
218            <moles:simpleCondition>
219            <moles:dgAttributeAuthority>https://glue.badc.rl.ac.uk/AttributeAuthority</moles:dgAttributeAuthority>
220            <moles:attrauthRole>coapec</moles:attrauthRole>
221        </moles:simpleCondition>
222        NB: xmlns:moles="http://ndg.nerc.ac.uk/moles"
223       
224        @type: pylons.session
225        @param userHandle: dict-like session object containing security
226        tokens.  Resets equivalent object attribute."""
227         
228        log.debug("BrowsePDP.accessPermitted ...")
229       
230        # Resource handle contains URI and ElementTree resource security
231        # element
232        try:
233            self.resrcURI = resrcHandle['uri']
234            self.resrcDoc = resrcHandle['doc'] 
235        except KeyError, e:
236            log.error("Resource handle missing key %s" % e)
237            raise PDPMissingResourceConstraints()
238
239        # First query the document for a security constraint
240        self.securityElement = self._getSecurityElem()
241        if not self.securityElement:
242            # No security set
243            log.info("BrowsePDP: no security constraints found for [%s]" % \
244                     self.resrcURI.schema + \
245                     " type document [%s]" % self.resrcURI)
246            return
247       
248       
249        # User handle contains 'h' = Session Manager URI and 'sid' user
250        # Session ID
251        try:
252            self.smURI = userHandle['h']
253            self.userSessID = userHandle['sid']
254        except KeyError, e:
255            log.error("User handle missing key %s" % e)
256            raise PDPUserNotLoggedIn()
257
258           
259        roleElem = self.securityElement.find(BrowsePDP.roleXPathQuery)
260        if roleElem is None or not roleElem.text:
261            log.error("PDP: role not set in MOLES security " + \
262                      "constraints")
263            raise PDPMissingResourceConstraints()
264       
265        self.reqRole = roleElem.text
266
267        aaElem = self.securityElement.find(BrowsePDP.aaXPathQuery)
268       
269        # Sanity check on Attribute Authority URI
270        if aaElem and aaElem.text:
271            aaURI = aaElem.text
272           
273            # Check Attribute Authority address
274            try:
275                BrowsePDP.urlCanBeOpened(aaURI)
276            except URLCannotBeOpened, e:
277                # Catch situation where either Attribute Authority address in the
278                # data invalid or none was set.  In this situation verify
279                # against the Attribute Authority set in the config
280   
281                log.warning('PDP: MOLES security constraint ' + \
282                            'Attribute Authority address is invalid - ' + \
283                            'defaulting to config file setting: %s; ' % \
284                            self.aaURI + \
285                            'error message is: %s' % e)
286                aaURI = self.aaURI
287        else:
288            log.warning("PDP: Attribute Authority element not " + \
289                        "set in MOLES security constraints - defaulting " + \
290                        "to config file setting: %s" % self.aaURI)
291            aaURI = self.aaURI
292   
293        # Retrieve Attirbute Certificate from user's session held by
294        # Session Manager
295        attCert = self._pullUserSessionAttCert(aaURI)
296       
297        # Check its validity
298        self._checkAttCert(attCert)
299                   
300        log.info('PDP - access granted for user "%s" ' % \
301                 attCert.userId + \
302                 'to "%s" secured with role "%s" ' % \
303                 (self.resrcURI, self.reqRole) + \
304                 'using attribute certificate:\n\n%s' % attCert)
305           
306       
307    def _pullUserSessionAttCert(self, aaURI):
308        """Check to see if the Session Manager can deliver an Attribute
309        Certificate with the required role to gain access to the resource
310        in question
311       
312        @type aaURI: string
313        @param aaURI: address of Attribute Authority that the Session Manager
314        will call in order to request an AC on behalf of the user"""
315       
316        try:
317            # Create Session Manager client
318            self.smClnt = SessionMgrClient(uri=self.smURI,
319                            cfgFilePath=self.cfgFilePath,
320                            cfgFileSection='WS-Security',
321                            sslCACertFilePathList=self.sslCACertFilePathList,
322                            tracefile=self.tracefile,
323                            **self.wssCfg) 
324        except Exception, e:
325            log.error("PDP: creating Session Manager client: %s"%e)
326            raise InitSessionCtxError()
327       
328                 
329        try:
330            # Make request for attribute certificate
331            attCert = self.smClnt.getAttCert(attAuthorityURI=aaURI,
332                                             sessID=self.userSessID,
333                                             reqRole=self.reqRole)
334            return attCert
335       
336        except AttributeRequestDenied, e:
337            log.info(\
338            "PDP -request for attribute certificate denied: %s" % e)
339            raise PDPUserAccessDenied()
340       
341        except SessionNotFound, e:
342            log.info("PDP -no session found: %s" % e)
343            raise PDPUserNotLoggedIn()
344
345        except SessionExpired, e:
346            log.info("PDP -session expired: %s" % e)
347            raise InvalidSessionMsg()
348
349        except SessionCertTimeError, e:
350            log.info("PDP -session cert. time error: %s" % e)
351            raise InvalidSessionMsg()
352           
353        except InvalidSession, e:
354            log.info("PDP -invalid user session: %s" % e)
355            raise InvalidSessionMsg()
356
357        except Exception, e:
358            log.error("PDP request for attribute certificate: %s" % e)
359            raise AttributeCertificateRequestError()
360       
361
362    def _checkAttCert(self, attCert):
363        '''Check attribute certificate is valid
364       
365        @type attCert: ndg.security.common.AttCert.AttCert
366        @param attCert: attribute certificate to be check for validity'''
367        attCert.certFilePathList = self.acCACertFilePathList
368        try:
369            attCert.isValid(raiseExcep=True)
370        except Exception, e:
371            log.error("Attribute Certificate: %s" % e)
372            raise InvalidAttributeCertificate() 
373         
374        # Check it's issuer is as expected - Convert to X500DN to do equality
375        # test
376        acIssuerDN = X500DN(self.acIssuer)
377        if attCert.issuerDN != acIssuerDN:
378            log.info('PDP -access denied: Attribute Certificate ' + \
379                'issuer DN, "%s" ' % attCert.issuerDN + \
380                'must match this data provider\'s Attribute Authority ' + \
381                'DN: "%s"' % acIssuerDN)
382            raise InvalidAttributeCertificate()
383
384
385    @classmethod
386    def urlCanBeOpened(cls, url, timeout=5, raiseExcep=True):
387       """Check url can be opened - adapted from
388       http://mail.python.org/pipermail/python-list/2004-October/289601.html
389       """
390   
391       found = False
392       defTimeOut = socket.getdefaulttimeout()
393       try:
394           socket.setdefaulttimeout(timeout)
395
396           try:
397               urllib2.urlopen(url)
398           except (urllib2.HTTPError, urllib2.URLError,
399                   socket.error, socket.sslerror, AttributeError):
400               if raiseExcep:
401                   raise URLCannotBeOpened()
402           
403           found = True
404         
405       finally:
406           socket.setdefaulttimeout(defTimeOut)
407           
408       return found
409     
410   
411def makeDecision(resrcHandle, userHandle, accessType=None, **kw):
412    '''One call Wrapper interface to PDP'''
413    return BrowsePDP(**kw)(resrcHandle, userHandle)
414
415 
Note: See TracBrowser for help on using the repository browser.