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

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

1.0.1 rc3

  • Fixed return in msi PIP._getAttributeCertificate
  • improved documentation for ndg.security.server.wsgi.authz
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
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    def __init__(self, status, message=None):
159        if status not in Response.decisionValues:
160            raise TypeError("Status %s not recognised" % status)
161       
162        self.status = status
163        self.message = message
164 
165       
166from ndg.security.common.AttCert import AttCertInvalidSignature, \
167    AttCertNotBeforeTimeError, AttCertExpired, AttCertError
168     
169from ndg.security.common.sessionmanager import SessionManagerClient, \
170    SessionNotFound, SessionCertTimeError, SessionExpired, InvalidSession, \
171    AttributeRequestDenied
172
173from ndg.security.common.attributeauthority import AttributeAuthorityClient, \
174    NoTrustedHosts, NoMatchingRoleInTrustedHosts, \
175    InvalidAttributeAuthorityClientCtx
176from ndg.security.common.attributeauthority import AttributeRequestDenied as \
177    AA_AttributeRequestDenied
178                   
179from ndg.security.common.authz.pdp import PDPUserNotLoggedIn, \
180    PDPUserAccessDenied
181   
182class SubjectRetrievalError(Exception):
183    """Generic exception class for errors related to information about the
184    subject"""
185   
186class InvalidAttributeCertificate(SubjectRetrievalError):
187    "The certificate containing authorisation roles is invalid"
188    def __init__(self, msg=None):
189        SubjectRetrievalError.__init__(self, msg or 
190                                       InvalidAttributeCertificate.__doc__)
191
192class AttributeCertificateInvalidSignature(SubjectRetrievalError):
193    ("There is a problem with the signature of the certificate containing "
194     "authorisation roles")
195    def __init__(self, msg=None):
196        SubjectRetrievalError.__init__(self, msg or 
197                                AttributeCertificateInvalidSignature.__doc__)
198             
199class AttributeCertificateNotBeforeTimeError(SubjectRetrievalError):
200    ("There is a time issuing error with certificate containing authorisation "
201    "roles")
202    def __init__(self, msg=None):
203        SubjectRetrievalError.__init__(self, msg or 
204                                AttributeCertificateNotBeforeTimeError.__doc__)
205       
206class AttributeCertificateExpired(SubjectRetrievalError):
207    "The certificate containing authorisation roles has expired"
208    def __init__(self, msg=None):
209        SubjectRetrievalError.__init__(self, msg or 
210                                       AttributeCertificateExpired.__doc__)
211           
212class SessionExpiredMsg(SubjectRetrievalError):
213    'Session has expired.  Please re-login at your home organisation'
214    def __init__(self, msg=None):
215        SubjectRetrievalError.__init__(self, msg or SessionExpiredMsg.__doc__)
216
217class SessionNotFoundMsg(SubjectRetrievalError):
218    'No session was found.  Please try re-login with your home organisation'
219    def __init__(self, msg=None):
220        SubjectRetrievalError.__init__(self, msg or 
221                                       SessionNotFoundMsg.__doc__)
222
223class InvalidSessionMsg(SubjectRetrievalError):
224    'Session is invalid.  Please try re-login with your home organisation'
225    def __init__(self, msg=None):
226        SubjectRetrievalError.__init__(self, msg or 
227                                       InvalidSessionMsg.__doc__)
228
229class InitSessionCtxError(SubjectRetrievalError):
230    'A problem occurred initialising a session connection'
231    def __init__(self, msg=None):
232        SubjectRetrievalError.__init__(self, msg or 
233                                       InitSessionCtxError.__doc__)
234
235class AttributeCertificateRequestError(SubjectRetrievalError):
236    'A problem occurred requesting a certificate containing authorisation roles'
237    def __init__(self, msg=None):
238        SubjectRetrievalError.__init__(self, msg or 
239                                    AttributeCertificateRequestError.__doc__)
240
241class PIPAttributeQuery(_AttrDict):
242    '''Policy Information Point Query class.'''
243    namespaces = (
244        "urn:ndg:security:authz:1.0:attr:subject",
245        "urn:ndg:security:authz:1.0:attr:attributeAuthorityURI",
246    ) 
247    (SUBJECT_NS, ATTRIBUTEAUTHORITY_NS) = namespaces   
248
249class PIPAttributeResponse(dict):
250    '''Policy Information Point Response class.'''
251    namespaces = (
252        Subject.ROLES_NS,
253    )
254
255
256from ndg.security.common.wssecurity import WSSecurityConfig
257from ndg.security.common.credentialwallet import CredentialWallet
258
259class PIP(object):
260    """Policy Information Point - this implementation enables the PDP to
261    retrieve attributes about the Subject"""
262
263    def __init__(self, prefix='', **cfg):
264        '''Set-up WS-Security and SSL settings for connection to an
265        Attribute Authority
266       
267        @type **cfg: dict
268        @param **cfg: keywords including 'sslCACertFilePathList' used to set a
269        list of CA certificates for an SSL connection to the Attribute
270        Authority if used and also WS-Security settings as used by
271        ndg.security.common.wssecurity.WSSecurityConfig
272        '''
273        self.wssecurityCfg = WSSecurityConfig()
274        wssePrefix = prefix + 'wssecurity'
275        self.wssecurityCfg.update(cfg, prefix=wssePrefix)
276                 
277        # List of CA certificates used to verify peer certificate with SSL
278        # connections to Attribute Authority
279        self.sslCACertFilePathList = cfg.get(prefix + 'sslCACertFilePathList', [])
280       
281        # List of CA certificates used to verify the signatures of
282        # Attribute Certificates retrieved
283        self.caCertFilePathList = cfg.get(prefix + 'caCertFilePathList', [])
284
285
286    def attributeQuery(self, attributeQuery):
287        """Query the Attribute Authority specified in the request to retrieve
288        the attributes if any corresponding to the subject
289       
290        @type attributeResponse: PIPAttributeQuery
291        @param attributeResponse:
292        @rtype: PIPAttributeResponse
293        @return: response containing the attributes retrieved from the
294        Attribute Authority"""
295       
296        subject = attributeQuery[PIPAttributeQuery.SUBJECT_NS]
297        username = subject[Subject.USERID_NS]
298        sessionId = subject[Subject.SESSIONID_NS]
299        attributeAuthorityURI = attributeQuery[
300                                    PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS]
301       
302        sessionId = subject[Subject.SESSIONID_NS]
303        attributeCertificate = self._getAttributeCertificate(
304                                        attributeAuthorityURI,
305                                        username,
306                                        sessionId,
307                                        subject[Subject.SESSIONMANAGERURI_NS])
308
309        attributeResponse = PIPAttributeResponse()
310        attributeResponse[Subject.ROLES_NS] = attributeCertificate.roles
311         
312        return attributeResponse
313   
314   
315    def _getAttributeCertificate(self,
316                                 attributeAuthorityURI,
317                                 username=None,
318                                 sessionId=None,
319                                 sessionManagerURI=None):
320        '''Retrieve an Attribute Certificate
321
322        @type attributeAuthorityURI: basestring
323        @param attributeAuthorityURI: URI to Attribute Authority service
324        @type username: basestring
325        @param username: subject user identifier - could be an OpenID       
326        @type sessionId: basestring
327        @param sessionId: Session Manager session handle
328        @type sessionManagerURI: basestring
329        @param sessionManagerURI: URI to remote session manager service
330        @rtype: ndg.security.common.AttCert.AttCert
331        @return: Attribute Certificate containing user roles
332        '''
333
334        if sessionId and sessionManagerURI:
335            attrCert = self._getAttributeCertificateFromSessionManager(
336                                                     attributeAuthorityURI,
337                                                     sessionId,
338                                                     sessionManagerURI)
339        else:
340            attrCert = self._getAttributeCertificateFromAttributeAuthority(
341                                                     attributeAuthorityURI,
342                                                     username)
343       
344        try:
345            attrCert.certFilePathList = self.caCertFilePathList
346            attrCert.isValid(raiseExcep=True)
347       
348        except AttCertInvalidSignature, e:
349            log.exception(e)
350            raise AttributeCertificateInvalidSignature()
351       
352        except AttCertNotBeforeTimeError, e:   
353            log.exception(e)
354            raise AttributeCertificateNotBeforeTimeError()
355       
356        except AttCertExpired, e:   
357            log.exception(e)
358            raise AttributeCertificateExpired()
359
360        except AttCertError, e:
361            log.exception(e)
362            raise InvalidAttributeCertificate()
363       
364        return attrCert
365   
366           
367    def _getAttributeCertificateFromSessionManager(self,
368                                                   attributeAuthorityURI,
369                                                   sessionId,
370                                                   sessionManagerURI):
371        '''Retrieve an Attribute Certificate using the subject's Session
372        Manager
373       
374        @type sessionId: basestring
375        @param sessionId: Session Manager session handle
376        @type sessionManagerURI: basestring
377        @param sessionManagerURI: URI to remote session manager service
378        @type attributeAuthorityURI: basestring
379        @param attributeAuthorityURI: URI to Attribute Authority service
380        @rtype: ndg.security.common.AttCert.AttCert
381        @return: Attribute Certificate containing user roles
382        '''
383       
384        try:
385            # Create Session Manager client - if a file path was set, setting
386            # are read from a separate config file section otherwise, from the
387            # PDP config object
388            smClnt = SessionManagerClient(
389                            uri=sessionManagerURI,
390                            sslCACertFilePathList=self.sslCACertFilePathList,
391                            cfg=self.wssecurityCfg)
392        except Exception, e:
393            log.error("Creating Session Manager client: %s" % e)
394            raise InitSessionCtxError()
395       
396         
397        try:
398            # Make request for attribute certificate
399            return smClnt.getAttCert(
400                                attributeAuthorityURI=attributeAuthorityURI,
401                                sessID=sessionId)
402       
403        except AttributeRequestDenied, e:
404            log.error("Request for attribute certificate denied: %s" % e)
405            raise PDPUserAccessDenied()
406       
407        except SessionNotFound, e:
408            log.error("No session found: %s" % e)
409            raise SessionNotFoundMsg()
410
411        except SessionExpired, e:
412            log.error("Session expired: %s" % e)
413            raise SessionExpiredMsg()
414
415        except SessionCertTimeError, e:
416            log.error("Session cert. time error: %s" % e)
417            raise InvalidSessionMsg()
418           
419        except InvalidSession, e:
420            log.error("Invalid user session: %s" % e)
421            raise InvalidSessionMsg()
422
423        except Exception, e:
424            log.error("Request from Session Manager [%s] to Attribute "
425                      "Authority [%s] for attribute certificate: %s: %s" % 
426                      (sessionManagerURI,
427                       attributeAuthorityURI,
428                       e.__class__, e))
429            raise AttributeCertificateRequestError()
430
431           
432    def _getAttributeCertificateFromAttributeAuthority(self,
433                                                   attributeAuthorityURI,
434                                                   username):
435        '''Retrieve an Attribute Certificate direct from an Attribute
436        Authority.  This method is invoked if no session ID or Session
437        MAnager endpoint where provided
438       
439        @type username: basestring
440        @param username: user identifier - may be an OpenID URI
441        @type attributeAuthorityURI: basestring
442        @param attributeAuthorityURI: URI to Attribute Authority service
443        @rtype: ndg.security.common.AttCert.AttCert
444        @return: Attribute Certificate containing user roles
445        '''
446       
447        try:
448            # Create Attribute Authority client - if a file path was set,
449            # settingare read  from a separate config file section otherwise,
450            # from the PDP config object
451            aaClnt = AttributeAuthorityClient(
452                            uri=attributeAuthorityURI,
453                            sslCACertFilePathList=self.sslCACertFilePathList,
454                            cfg=self.wssecurityCfg)
455        except Exception, e:
456            log.error("Creating Attribute Authority client: %s" % e)
457            raise InitSessionCtxError()
458       
459         
460        try:
461            # Make request for attribute certificate
462            return aaClnt.getAttCert(userId=username)
463       
464       
465        except AA_AttributeRequestDenied, e:
466            log.error("Request for attribute certificate denied: %s" % e)
467            raise PDPUserAccessDenied()
468       
469        # TODO: handle othe specific Exception types here for more fine
470        # grained response info
471
472        except Exception, e:
473            log.error("Request to Attribute Authority [%s] for attribute "
474                      "certificate: %s: %s", attributeAuthorityURI,
475                       e.__class__, e)
476            raise AttributeCertificateRequestError()
477
478           
479           
480class PDP(object):
481    """Policy Decision Point"""
482   
483    def __init__(self, policy, pip):
484        """Read in a file which determines access policy"""
485        self.policy = policy
486        self.pip = pip
487       
488    def evaluate(self, request):
489        '''Make access control decision'''
490       
491        # Look for matching targets to the given resource
492        resourceURI = request.resource[Resource.URI_NS]
493        matchingTargets = [target for target in self.policy.targets
494                           if target.regEx.match(resourceURI) is not None]
495       
496        knownAttributeAuthorityURIs = []
497        for matchingTarget in matchingTargets:
498           
499            # Make call to the Policy Information Point to pull user
500            # attributes applicable to this resource
501            if matchingTarget.attributeAuthorityURI not in \
502               knownAttributeAuthorityURIs:
503               
504                attributeQuery = PIPAttributeQuery()
505                attributeQuery[PIPAttributeQuery.SUBJECT_NS] = request.subject
506               
507                attributeQuery[PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS] = \
508                                        matchingTarget.attributeAuthorityURI
509               
510                try:
511                    attributeResponse=self.pip.attributeQuery(attributeQuery)
512                   
513                except SubjectRetrievalError, e:
514                    log.exception(e)
515                    return Response(Response.DECISION_INDETERMINATE,
516                                    message=str(e))
517                   
518                except Exception, e:
519                    log.exception(e)
520                    return Response(Response.DECISION_INDETERMINATE,
521                                    message="An internal error occurred")
522                   
523                knownAttributeAuthorityURIs.append(
524                                        matchingTarget.attributeAuthorityURI)
525               
526                request.subject[Subject.ROLES_NS] = attributeResponse[
527                                                            Subject.ROLES_NS]
528               
529        # Match the subject's attributes against the target
530        for attr in matchingTarget.attributes:
531            if attr in request.subject[Subject.ROLES_NS]:
532                return Response(Response.DECISION_PERMIT)
533           
534        return Response(Response.DECISION_DENY)
535   
536
537       
Note: See TracBrowser for help on using the repository browser.