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

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

ndg.security.common.authz.msi: added additional logging for PDP and PIP.
ndg.security.common.authz.xacml: added SetFunction? and ConditionSetFunction? implementations to enable support got at-least-one-member of rules.

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