source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/msi.py @ 5280

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/msi.py@5280
Revision 5280, 21.3 KB checked in by pjkersha, 10 years ago (diff)

Further improvements to the authorization middleware:

  • PEPFilter no longer explicitly calls the PEPResultHandlerMiddleware (This latter class is the WSGI component which handles the access denied response that the server returns). This is not necessary as it can set a 403 response in order to trigger multiHandlerIntercept callback function set in the MultiHandler? instance. This responds to all 403 type status codes by invoking the PEPResultHandlerMiddleware.
  • ndg.security.common.authz.msi: improvements to the PDP, PIP and Response classes.
  • ndg.security.test.integration.dap: added integration test for secured pyDAP service
Line 
1"""NDG Security MSI Resource Policy module
2
3NERC Data Grid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "03/04/09"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__contact__ = "Philip.Kershaw@stfc.ac.uk"
9__license__ = "BSD - see LICENSE file in top-level directory"
10__contact__ = "Philip.Kershaw@stfc.ac.uk"
11__revision__ = "$Id$"
12
13import logging
14log = logging.getLogger(__name__)
15from elementtree import ElementTree
16
17# For parsing: ElementTree helpers
18getNs = lambda elem: elem.tag.split('}')[0][1:]
19getLocalName = lambda elem: elem.tag.rsplit('}', 1)[ - 1]
20
21class PolicyParseError(Exception):
22    """Error reading policy attributes from file"""
23
24class Policy(object):
25    def __init__(self, policyFilePath=None):
26        self.policyFilePath = policyFilePath
27        self.description = None
28        self.targets = []
29       
30    def parse(self):
31        """Parse the policy file set in policyFilePath attribute
32        """
33        elem = ElementTree.parse(self.policyFilePath)
34        root = elem.getroot()
35
36        for elem in root:
37            localName = getLocalName(elem)
38            if localName == "Description":
39                self.description = elem.text.strip()
40               
41            elif localName == "Target":
42                self.targets.append(Target.Parse(elem))
43               
44            else:
45                raise PolicyParseError("Invalid policy attribute: %s" % 
46                                        localName)
47               
48    @classmethod
49    def Parse(cls, policyFilePath):
50        policy = cls(policyFilePath=policyFilePath)
51        policy.parse()
52        return policy
53
54class TargetParseError(Exception):
55    """Error reading resource attributes from file"""
56
57import re
58   
59class Target(object):
60    """Define access behaviour for a resource match a given URI pattern"""
61    def __init__(self):
62        self.uriPattern = None
63        self.attributes = []
64        self.attributeAuthorityURI = None
65       
66    def parse(self, root):
67        for elem in root:
68            localName = getLocalName(elem)
69            if localName == "URIPattern":
70                self.uriPattern = elem.text.strip()
71                self.regEx = re.compile(self.uriPattern)
72               
73            elif localName == "Attributes":
74                for attrElem in elem:
75                    self.attributes.append(attrElem.text.strip())
76                   
77            elif localName == "AttributeAuthority":
78                # Expecting first element to contain the URI
79                self.attributeAuthorityURI = elem[0].text.strip()
80            else:
81                raise ResourceParseError("Invalid resource attribute: %s" % 
82                                         localName)
83   
84    @classmethod
85    def Parse(cls, root):
86        resource = cls()
87        resource.parse(root)
88        return resource
89   
90    def __str__(self):
91        return str(self.uriPattern)
92
93class _AttrDict(dict):
94    """Utility class for holding a constrained list of attributes governed
95    by a namespace list"""
96    namespaces = ()
97    def __init__(self, **attributes):
98        invalidAttributes = [attr for attr in attributes \
99                             if attr not in self.__class__.namespaces]
100        if len(invalidAttributes) > 0:
101            raise TypeError("The following attribute namespace(s) are not "
102                            "recognised: %s" % invalidAttributes)
103           
104        self.update(attributes)
105
106    def __setitem__(self, key, val):
107        if key not in self.__class__.namespaces:
108            raise KeyError('Namespace "%s" not recognised.  Valid namespaces '
109                           'are: %s' % self.__class__.namespaces)
110           
111        dict.__setitem__(self, key, val)
112
113
114    def update(self, d, **kw):       
115        for dictArg in (d, kw):
116            for k in dictArg:
117                if key not in self.__class__.namespaces:
118                    raise KeyError('Namespace "%s" not recognised.  Valid '
119                                   'namespaces are: %s' % 
120                                   self.__class__.namespaces)
121       
122        dict.update(self, d, **kw)
123
124class Subject(_AttrDict):
125    '''Subject designator'''
126    namespaces = (
127        "urn:ndg:security:authz:1.0:attr:subject:userId",
128        "urn:ndg:security:authz:1.0:attr:subject:sessionId",
129        "urn:ndg:security:authz:1.0:attr:subject:sessionManagerURI",
130        "urn:ndg:security:authz:1.0:attr:subject:roles"       
131    )
132    (USERID_NS, SESSIONID_NS, SESSIONMANAGERURI_NS, ROLES_NS) = namespaces
133
134class Resource(_AttrDict):
135    '''Resource designator'''
136    namespaces = (
137        "urn:ndg:security:authz:1.0:attr:resource:uri",
138    )
139    (URI_NS,) = namespaces
140           
141class Request(object):
142    '''Request to send to a PDP'''
143    def __init__(self, subject=Subject(), resource=Resource()):
144        self.subject = subject
145        self.resource = resource
146
147class Response(object):
148    '''Response from a PDP'''
149    decisionValues = range(4)
150    (DECISION_PERMIT,
151    DECISION_DENY,
152    DECISION_INDETERMINATE,
153    DECISION_NOT_APPLICABLE) = decisionValues
154
155    # string versions of the 4 Decision types used for encoding
156    DECISIONS = ("Permit", "Deny", "Indeterminate", "NotApplicable")
157   
158    decisionValue2String = dict(zip(decisionValues, DECISIONS))
159   
160    def __init__(self, status, message=None):
161       
162        self.status = status
163        self.message = message
164
165    def _setStatus(self, status):
166        if status not in Response.decisionValues:
167            raise TypeError("Status %s not recognised" % status)
168       
169        self._status = status
170       
171    def _getStatus(self):
172        return getattr(self, '_status', Response.DECISION_INDETERMINATE)
173   
174    status = property(fget=_getStatus,
175                      fset=_setStatus,
176                      doc="Integer response code; one of %r" % decisionValues)
177       
178from ndg.security.common.AttCert import AttCertInvalidSignature, \
179    AttCertNotBeforeTimeError, AttCertExpired, AttCertError
180     
181from ndg.security.common.sessionmanager import SessionManagerClient, \
182    SessionNotFound, SessionCertTimeError, SessionExpired, InvalidSession, \
183    AttributeRequestDenied
184
185from ndg.security.common.attributeauthority import AttributeAuthorityClient, \
186    NoTrustedHosts, NoMatchingRoleInTrustedHosts, \
187    InvalidAttributeAuthorityClientCtx
188from ndg.security.common.attributeauthority import AttributeRequestDenied as \
189    AA_AttributeRequestDenied
190                   
191from ndg.security.common.authz.pdp import PDPUserNotLoggedIn, \
192    PDPUserAccessDenied
193   
194class SubjectRetrievalError(Exception):
195    """Generic exception class for errors related to information about the
196    subject"""
197   
198class InvalidAttributeCertificate(SubjectRetrievalError):
199    "The certificate containing authorisation roles is invalid"
200    def __init__(self, msg=None):
201        SubjectRetrievalError.__init__(self, msg or 
202                                       InvalidAttributeCertificate.__doc__)
203
204class AttributeCertificateInvalidSignature(SubjectRetrievalError):
205    ("There is a problem with the signature of the certificate containing "
206     "authorisation roles")
207    def __init__(self, msg=None):
208        SubjectRetrievalError.__init__(self, msg or 
209                                AttributeCertificateInvalidSignature.__doc__)
210             
211class AttributeCertificateNotBeforeTimeError(SubjectRetrievalError):
212    ("There is a time issuing error with certificate containing authorisation "
213    "roles")
214    def __init__(self, msg=None):
215        SubjectRetrievalError.__init__(self, msg or 
216                                AttributeCertificateNotBeforeTimeError.__doc__)
217       
218class AttributeCertificateExpired(SubjectRetrievalError):
219    "The certificate containing authorisation roles has expired"
220    def __init__(self, msg=None):
221        SubjectRetrievalError.__init__(self, msg or 
222                                       AttributeCertificateExpired.__doc__)
223           
224class SessionExpiredMsg(SubjectRetrievalError):
225    'Session has expired.  Please re-login at your home organisation'
226    def __init__(self, msg=None):
227        SubjectRetrievalError.__init__(self, msg or SessionExpiredMsg.__doc__)
228
229class SessionNotFoundMsg(SubjectRetrievalError):
230    'No session was found.  Please try re-login with your home organisation'
231    def __init__(self, msg=None):
232        SubjectRetrievalError.__init__(self, msg or 
233                                       SessionNotFoundMsg.__doc__)
234
235class InvalidSessionMsg(SubjectRetrievalError):
236    'Session is invalid.  Please try re-login with your home organisation'
237    def __init__(self, msg=None):
238        SubjectRetrievalError.__init__(self, msg or 
239                                       InvalidSessionMsg.__doc__)
240
241class InitSessionCtxError(SubjectRetrievalError):
242    'A problem occurred initialising a session connection'
243    def __init__(self, msg=None):
244        SubjectRetrievalError.__init__(self, msg or 
245                                       InitSessionCtxError.__doc__)
246
247class AttributeCertificateRequestError(SubjectRetrievalError):
248    'A problem occurred requesting a certificate containing authorisation roles'
249    def __init__(self, msg=None):
250        SubjectRetrievalError.__init__(self, msg or 
251                                    AttributeCertificateRequestError.__doc__)
252
253class PIPAttributeQuery(_AttrDict):
254    '''Policy Information Point Query class.'''
255    namespaces = (
256        "urn:ndg:security:authz:1.0:attr:subject",
257        "urn:ndg:security:authz:1.0:attr:attributeAuthorityURI",
258    ) 
259    (SUBJECT_NS, ATTRIBUTEAUTHORITY_NS) = namespaces   
260
261class PIPAttributeResponse(dict):
262    '''Policy Information Point Response class.'''
263    namespaces = (
264        Subject.ROLES_NS,
265    )
266
267
268from ndg.security.common.wssecurity import WSSecurityConfig
269from ndg.security.common.credentialwallet import CredentialWallet
270
271class PIP(object):
272    """Policy Information Point - this implementation enables the PDP to
273    retrieve attributes about the Subject"""
274    wsseSectionName = 'wssecurity'
275   
276    def __init__(self, prefix='', **cfg):
277        '''Set-up WS-Security and SSL settings for connection to an
278        Attribute Authority
279       
280        @type **cfg: dict
281        @param **cfg: keywords including 'sslCACertFilePathList' used to set a
282        list of CA certificates for an SSL connection to the Attribute
283        Authority if used and also WS-Security settings as used by
284        ndg.security.common.wssecurity.WSSecurityConfig
285        '''
286        self.wssecurityCfg = WSSecurityConfig()
287        wssePrefix = prefix + PIP.wsseSectionName
288        self.wssecurityCfg.update(cfg, prefix=wssePrefix)
289                 
290        # List of CA certificates used to verify peer certificate with SSL
291        # connections to Attribute Authority
292        self.sslCACertFilePathList=cfg.get(prefix+'sslCACertFilePathList', [])
293       
294        # List of CA certificates used to verify the signatures of
295        # Attribute Certificates retrieved
296        self.caCertFilePathList = cfg.get(prefix + 'caCertFilePathList', [])
297
298
299    def attributeQuery(self, attributeQuery):
300        """Query the Attribute Authority specified in the request to retrieve
301        the attributes if any corresponding to the subject
302       
303        @type attributeResponse: PIPAttributeQuery
304        @param attributeResponse:
305        @rtype: PIPAttributeResponse
306        @return: response containing the attributes retrieved from the
307        Attribute Authority"""
308       
309        subject = attributeQuery[PIPAttributeQuery.SUBJECT_NS]
310        username = subject[Subject.USERID_NS]
311        sessionId = subject[Subject.SESSIONID_NS]
312        attributeAuthorityURI = attributeQuery[
313                                    PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS]
314       
315        sessionId = subject[Subject.SESSIONID_NS]
316        attributeCertificate = self._getAttributeCertificate(
317                                        attributeAuthorityURI,
318                                        username,
319                                        sessionId,
320                                        subject[Subject.SESSIONMANAGERURI_NS])
321
322        attributeResponse = PIPAttributeResponse()
323        attributeResponse[Subject.ROLES_NS] = attributeCertificate.roles
324         
325        return attributeResponse
326   
327   
328    def _getAttributeCertificate(self,
329                                 attributeAuthorityURI,
330                                 username=None,
331                                 sessionId=None,
332                                 sessionManagerURI=None):
333        '''Retrieve an Attribute Certificate
334
335        @type attributeAuthorityURI: basestring
336        @param attributeAuthorityURI: URI to Attribute Authority service
337        @type username: basestring
338        @param username: subject user identifier - could be an OpenID       
339        @type sessionId: basestring
340        @param sessionId: Session Manager session handle
341        @type sessionManagerURI: basestring
342        @param sessionManagerURI: URI to remote session manager service
343        @rtype: ndg.security.common.AttCert.AttCert
344        @return: Attribute Certificate containing user roles
345        '''
346
347        if sessionId and sessionManagerURI:
348            attrCert = self._getAttributeCertificateFromSessionManager(
349                                                     attributeAuthorityURI,
350                                                     sessionId,
351                                                     sessionManagerURI)
352        else:
353            attrCert = self._getAttributeCertificateFromAttributeAuthority(
354                                                     attributeAuthorityURI,
355                                                     username)
356       
357        try:
358            attrCert.certFilePathList = self.caCertFilePathList
359            attrCert.isValid(raiseExcep=True)
360       
361        except AttCertInvalidSignature, e:
362            log.exception(e)
363            raise AttributeCertificateInvalidSignature()
364       
365        except AttCertNotBeforeTimeError, e:   
366            log.exception(e)
367            raise AttributeCertificateNotBeforeTimeError()
368       
369        except AttCertExpired, e:   
370            log.exception(e)
371            raise AttributeCertificateExpired()
372
373        except AttCertError, e:
374            log.exception(e)
375            raise InvalidAttributeCertificate()
376       
377        return attrCert
378   
379           
380    def _getAttributeCertificateFromSessionManager(self,
381                                                   attributeAuthorityURI,
382                                                   sessionId,
383                                                   sessionManagerURI):
384        '''Retrieve an Attribute Certificate using the subject's Session
385        Manager
386       
387        @type sessionId: basestring
388        @param sessionId: Session Manager session handle
389        @type sessionManagerURI: basestring
390        @param sessionManagerURI: URI to remote session manager service
391        @type attributeAuthorityURI: basestring
392        @param attributeAuthorityURI: URI to Attribute Authority service
393        @rtype: ndg.security.common.AttCert.AttCert
394        @return: Attribute Certificate containing user roles
395        '''
396       
397        try:
398            # Create Session Manager client - if a file path was set, setting
399            # are read from a separate config file section otherwise, from the
400            # PDP config object
401            smClnt = SessionManagerClient(
402                            uri=sessionManagerURI,
403                            sslCACertFilePathList=self.sslCACertFilePathList,
404                            cfg=self.wssecurityCfg)
405        except Exception, e:
406            log.error("Creating Session Manager client: %s" % e)
407            raise InitSessionCtxError()
408       
409         
410        try:
411            # Make request for attribute certificate
412            return smClnt.getAttCert(
413                                attributeAuthorityURI=attributeAuthorityURI,
414                                sessID=sessionId)
415       
416        except AttributeRequestDenied, e:
417            log.error("Request for attribute certificate denied: %s" % e)
418            raise PDPUserAccessDenied()
419       
420        except SessionNotFound, e:
421            log.error("No session found: %s" % e)
422            raise SessionNotFoundMsg()
423
424        except SessionExpired, e:
425            log.error("Session expired: %s" % e)
426            raise SessionExpiredMsg()
427
428        except SessionCertTimeError, e:
429            log.error("Session cert. time error: %s" % e)
430            raise InvalidSessionMsg()
431           
432        except InvalidSession, e:
433            log.error("Invalid user session: %s" % e)
434            raise InvalidSessionMsg()
435
436        except Exception, e:
437            log.error("Request from Session Manager [%s] to Attribute "
438                      "Authority [%s] for attribute certificate: %s: %s" % 
439                      (sessionManagerURI,
440                       attributeAuthorityURI,
441                       e.__class__, e))
442            raise AttributeCertificateRequestError()
443
444           
445    def _getAttributeCertificateFromAttributeAuthority(self,
446                                                   attributeAuthorityURI,
447                                                   username):
448        '''Retrieve an Attribute Certificate direct from an Attribute
449        Authority.  This method is invoked if no session ID or Session
450        MAnager endpoint where provided
451       
452        @type username: basestring
453        @param username: user identifier - may be an OpenID URI
454        @type attributeAuthorityURI: basestring
455        @param attributeAuthorityURI: URI to Attribute Authority service
456        @rtype: ndg.security.common.AttCert.AttCert
457        @return: Attribute Certificate containing user roles
458        '''
459       
460        try:
461            # Create Attribute Authority client - if a file path was set,
462            # settingare read  from a separate config file section otherwise,
463            # from the PDP config object
464            aaClnt = AttributeAuthorityClient(
465                            uri=attributeAuthorityURI,
466                            sslCACertFilePathList=self.sslCACertFilePathList,
467                            cfg=self.wssecurityCfg)
468        except Exception, e:
469            log.error("Creating Attribute Authority client: %s" % e)
470            raise InitSessionCtxError()
471       
472         
473        try:
474            # Make request for attribute certificate
475            return aaClnt.getAttCert(userId=username)
476       
477       
478        except AA_AttributeRequestDenied, e:
479            log.error("Request for attribute certificate denied: %s" % e)
480            raise PDPUserAccessDenied()
481       
482        # TODO: handle othe specific Exception types here for more fine
483        # grained response info
484
485        except Exception, e:
486            log.error("Request to Attribute Authority [%s] for attribute "
487                      "certificate: %s: %s", attributeAuthorityURI,
488                       e.__class__, e)
489            raise AttributeCertificateRequestError()
490
491           
492           
493class PDP(object):
494    """Policy Decision Point"""
495   
496    def __init__(self, policy, pip):
497        """Read in a file which determines access policy"""
498        self.policy = policy
499        self.pip = pip
500       
501    def evaluate(self, request):
502        '''Make access control decision'''
503       
504        # Look for matching targets to the given resource
505        resourceURI = request.resource[Resource.URI_NS]
506        matchingTargets = [target for target in self.policy.targets
507                           if target.regEx.match(resourceURI) is not None]
508       
509        knownAttributeAuthorityURIs = []
510        request.subject[Subject.ROLES_NS] = []
511        for matchingTarget in matchingTargets:
512           
513            # Make call to the Policy Information Point to pull user
514            # attributes applicable to this resource
515            if matchingTarget.attributeAuthorityURI not in \
516               knownAttributeAuthorityURIs:
517               
518                attributeQuery = PIPAttributeQuery()
519                attributeQuery[PIPAttributeQuery.SUBJECT_NS] = request.subject
520               
521                attributeQuery[PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS] = \
522                                        matchingTarget.attributeAuthorityURI
523               
524                try:
525                    attributeResponse=self.pip.attributeQuery(attributeQuery)
526                   
527                except SubjectRetrievalError, e:
528                    log.exception(e)
529                    return Response(Response.DECISION_INDETERMINATE,
530                                    message=str(e))
531                   
532                except Exception, e:
533                    log.exception(e)
534                    return Response(Response.DECISION_INDETERMINATE,
535                                    message="An internal error occurred")
536                   
537                knownAttributeAuthorityURIs.append(
538                                        matchingTarget.attributeAuthorityURI)
539               
540                request.subject[Subject.ROLES_NS] += attributeResponse[
541                                                            Subject.ROLES_NS]
542               
543        # Match the subject's attributes against the target
544        for attr in matchingTarget.attributes:
545            if attr in request.subject[Subject.ROLES_NS]:
546                return Response(Response.DECISION_PERMIT)
547           
548        return Response(Response.DECISION_DENY,
549                        message="Insufficient privileges to access the "
550                                "resource")
551   
552
553       
Note: See TracBrowser for help on using the repository browser.