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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/msi.py@5357
Revision 5357, 24.0 KB checked in by pjkersha, 11 years ago (diff)
  • fix to WS-Security signature handler 4Suite implementation (ndg.security.common.wssecurity.signaturehandler.foursuite) to ensure timestamp is checked correctly
  • refactored ndg.security.common.wssecurity moving encryption handler development code into its own ndg.security.common.wssecurity.encryptionhandler package
  • Fixed copyright on some remaining files that still had NERC/CCLRC
  • further work on SSL CLient AuthN WSGI unit tests ndg.security.test.unit.wsgi.ssl
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       
317        log.debug("PIP: received attribute query: %r", attributeQuery)
318       
319        attributeCertificate = self._getAttributeCertificate(
320                    attributeAuthorityURI,
321                    username=username,
322                    sessionId=sessionId,
323                    sessionManagerURI=subject[Subject.SESSIONMANAGERURI_NS])
324
325        attributeResponse = PIPAttributeResponse()
326        attributeResponse[Subject.ROLES_NS] = attributeCertificate.roles
327         
328        return attributeResponse
329   
330   
331    def _getAttributeCertificate(self,
332                                 attributeAuthorityURI,
333                                 username=None,
334                                 sessionId=None,
335                                 sessionManagerURI=None):
336        '''Retrieve an Attribute Certificate
337
338        @type attributeAuthorityURI: basestring
339        @param attributeAuthorityURI: URI to Attribute Authority service
340        @type username: basestring
341        @param username: subject user identifier - could be an OpenID       
342        @type sessionId: basestring
343        @param sessionId: Session Manager session handle
344        @type sessionManagerURI: basestring
345        @param sessionManagerURI: URI to remote session manager service
346        @rtype: ndg.security.common.AttCert.AttCert
347        @return: Attribute Certificate containing user roles
348        '''
349
350        if sessionId and sessionManagerURI:
351            attrCert = self._getAttributeCertificateFromSessionManager(
352                                                     attributeAuthorityURI,
353                                                     sessionId,
354                                                     sessionManagerURI)
355        else:
356            attrCert = self._getAttributeCertificateFromAttributeAuthority(
357                                                     attributeAuthorityURI,
358                                                     username)
359       
360        try:
361            attrCert.certFilePathList = self.caCertFilePathList
362            attrCert.isValid(raiseExcep=True)
363       
364        except AttCertInvalidSignature, e:
365            log.exception(e)
366            raise AttributeCertificateInvalidSignature()
367       
368        except AttCertNotBeforeTimeError, e:   
369            log.exception(e)
370            raise AttributeCertificateNotBeforeTimeError()
371       
372        except AttCertExpired, e:   
373            log.exception(e)
374            raise AttributeCertificateExpired()
375
376        except AttCertError, e:
377            log.exception(e)
378            raise InvalidAttributeCertificate()
379       
380        return attrCert
381   
382           
383    def _getAttributeCertificateFromSessionManager(self,
384                                                   attributeAuthorityURI,
385                                                   sessionId,
386                                                   sessionManagerURI):
387        '''Retrieve an Attribute Certificate using the subject's Session
388        Manager
389       
390        @type sessionId: basestring
391        @param sessionId: Session Manager session handle
392        @type sessionManagerURI: basestring
393        @param sessionManagerURI: URI to remote session manager service
394        @type attributeAuthorityURI: basestring
395        @param attributeAuthorityURI: URI to Attribute Authority service
396        @rtype: ndg.security.common.AttCert.AttCert
397        @return: Attribute Certificate containing user roles
398        '''
399       
400        log.debug("PIP._getAttributeCertificateFromSessionManager ...")
401       
402        try:
403            # Create Session Manager client - if a file path was set, setting
404            # are read from a separate config file section otherwise, from the
405            # PDP config object
406            smClnt = SessionManagerClient(
407                            uri=sessionManagerURI,
408                            sslCACertFilePathList=self.sslCACertFilePathList,
409                            cfg=self.wssecurityCfg)
410        except Exception, e:
411            log.error("Creating Session Manager client: %s" % e)
412            raise InitSessionCtxError()
413       
414         
415        try:
416            # Make request for attribute certificate
417            return smClnt.getAttCert(
418                                attributeAuthorityURI=attributeAuthorityURI,
419                                sessID=sessionId)
420       
421        except AttributeRequestDenied, e:
422            log.error("Request for attribute certificate denied: %s" % e)
423            raise PDPUserAccessDenied()
424       
425        except SessionNotFound, e:
426            log.error("No session found: %s" % e)
427            raise SessionNotFoundMsg()
428
429        except SessionExpired, e:
430            log.error("Session expired: %s" % e)
431            raise SessionExpiredMsg()
432
433        except SessionCertTimeError, e:
434            log.error("Session cert. time error: %s" % e)
435            raise InvalidSessionMsg()
436           
437        except InvalidSession, e:
438            log.error("Invalid user session: %s" % e)
439            raise InvalidSessionMsg()
440
441        except Exception, e:
442            log.error("Request from Session Manager [%s] to Attribute "
443                      "Authority [%s] for attribute certificate: %s: %s" % 
444                      (sessionManagerURI,
445                       attributeAuthorityURI,
446                       e.__class__, e))
447            raise AttributeCertificateRequestError()
448
449           
450    def _getAttributeCertificateFromAttributeAuthority(self,
451                                                       attributeAuthorityURI,
452                                                       username):
453        '''Retrieve an Attribute Certificate direct from an Attribute
454        Authority.  This method is invoked if no session ID or Session
455        Manager endpoint where provided
456       
457        @type username: basestring
458        @param username: user identifier - may be an OpenID URI
459        @type attributeAuthorityURI: basestring
460        @param attributeAuthorityURI: URI to Attribute Authority service
461        @rtype: ndg.security.common.AttCert.AttCert
462        @return: Attribute Certificate containing user roles
463        '''
464       
465        log.debug("PIP._getAttributeCertificateFromAttributeAuthority ...")
466       
467        try:
468            # Create Attribute Authority client - if a file path was set,
469            # settingare read  from a separate config file section otherwise,
470            # from the PDP config object
471            aaClnt = AttributeAuthorityClient(
472                            uri=attributeAuthorityURI,
473                            sslCACertFilePathList=self.sslCACertFilePathList,
474                            cfg=self.wssecurityCfg)
475        except Exception, e:
476            log.error("Creating Attribute Authority client: %s" % e)
477            raise InitSessionCtxError()
478       
479         
480        try:
481            # Make request for attribute certificate
482            return aaClnt.getAttCert(userId=username)
483       
484       
485        except AA_AttributeRequestDenied, e:
486            log.error("Request for attribute certificate denied: %s" % e)
487            raise PDPUserAccessDenied()
488       
489        # TODO: handle other specific Exception types here for more fine
490        # grained response info
491
492        except Exception, e:
493            log.error("Request to Attribute Authority [%s] for attribute "
494                      "certificate: %s: %s", attributeAuthorityURI,
495                      e.__class__, e)
496            raise AttributeCertificateRequestError()
497
498           
499           
500class PDP(object):
501    """Policy Decision Point"""
502   
503    def __init__(self, policy, pip):
504        """Read in a file which determines access policy"""
505        self.policy = policy
506        self.pip = pip
507
508    def _getPolicy(self):
509        if self._policy is None:
510            raise TypeError("Policy object has not been initialised")
511        return self._policy
512   
513    def _setPolicy(self, policy):
514        if not isinstance(policy, (Policy, None.__class__)):
515            raise TypeError("Expecting %s or None type for PDP policy; got %r"%
516                            (Policy.__class__.__name__, policy))
517        self._policy = policy
518
519    policy = property(fget=_getPolicy,
520                      fset=_setPolicy,
521                      doc="Policy type object used by the PDP to determine "
522                          "access for resources")
523
524    def _getPIP(self):
525        if self._pip is None:
526            raise TypeError("PIP object has not been initialised")
527       
528        return self._pip
529   
530    def _setPIP(self, pip):
531        if not isinstance(pip, (PIP, None.__class__)):
532            raise TypeError("Expecting %s or None type for PDP PIP; got %r"%
533                            (PIP.__class__.__name__, pip))
534        self._pip = pip
535
536    pip = property(fget=_getPIP,
537                   fset=_setPIP,
538                   doc="Policy Information Point - PIP type object used by "
539                       "the PDP to retrieve user attributes")
540   
541    def evaluate(self, request):
542        '''Make access control decision'''
543       
544        # Look for matching targets to the given resource
545        resourceURI = request.resource[Resource.URI_NS]
546        matchingTargets = [target for target in self.policy.targets
547                           if target.regEx.match(resourceURI) is not None]
548        numMatchingTargets = len(matchingTargets)
549        if numMatchingTargets == 0:
550            log.debug("PDP.evaluate: granting access - no targets matched "
551                      "the resource URI path [%s]", 
552                      resourceURI)
553            return Response(status=Response.DECISION_PERMIT)
554       
555        # Iterate through matching targets checking for user access
556        request.subject[Subject.ROLES_NS] = []
557        permitForAllTargets = [Response.DECISION_PERMIT]*numMatchingTargets
558       
559        # Keep a look-up of the decisions for each target
560        status = []
561       
562        for matchingTarget in matchingTargets:
563           
564            # Make call to the Policy Information Point to pull user
565            # attributes applicable to this resource
566            attributeQuery = PIPAttributeQuery()
567            attributeQuery[PIPAttributeQuery.SUBJECT_NS] = request.subject
568           
569            attributeQuery[PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS] = \
570                                    matchingTarget.attributeAuthorityURI
571           
572            # Exit from function returning indeterminate status if a
573            # problem occurs here
574            try:
575                attributeResponse=self.pip.attributeQuery(attributeQuery)
576               
577            except SubjectRetrievalError, e:
578                # i.e. a defined exception within the scope of this
579                # module
580                log.exception(e)
581                return Response(Response.DECISION_INDETERMINATE,
582                                message=str(e))
583               
584            except Exception, e:
585                log.exception(e)
586                return Response(Response.DECISION_INDETERMINATE,
587                                message="An internal error occurred")
588                           
589            # Accumulate attributes retrieved from multiple attribute
590            # authorities
591            request.subject[Subject.ROLES_NS] += attributeResponse[
592                                                        Subject.ROLES_NS]
593               
594            # Match the subject's attributes against the target
595            # One of any rule - at least one of the subject's attributes
596            # must match one of the attributes restricting access to the
597            # resource.
598            status.append(PDP._match(matchingTarget.attributes, 
599                                     request.subject[Subject.ROLES_NS]))
600           
601        # All targets must yield permit status for access to be granted
602        if status == permitForAllTargets:
603            return Response(Response.DECISION_PERMIT)
604        else:   
605            return Response(Response.DECISION_DENY,
606                            message="Insufficient privileges to access the "
607                                    "resource")
608       
609    @staticmethod
610    def _match(resourceAttr, subjectRoleAttr):
611        """Helper method to iterate over user and resource attributes
612        If one at least one match is found, a permit response is returned
613        """
614        for attr in resourceAttr:
615            if attr in subjectRoleAttr:
616                return Response.DECISION_PERMIT
617           
618        return Response.DECISION_DENY
619
620       
Note: See TracBrowser for help on using the repository browser.