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

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

Integrated PDP, PIP and Policy objects into Authkit.authenticate.MultiHandler? checker wrapper class.

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
90class _AttrDict(dict):
91    """Utility class for holding a constrained list of attributes governed
92    by a namespace list"""
93    namespaces = ()
94    def __init__(self, **attributes):
95        invalidAttributes = [attr for attr in attributes \
96                             if attr not in self.__class__.namespaces]
97        if len(invalidAttributes) > 0:
98            raise TypeError("The following attribute namespace(s) are not "
99                            "recognised: %s" % invalidAttributes)
100           
101        self.update(attributes)
102
103    def __setitem__(self, key, val):
104        if key not in self.__class__.namespaces:
105            raise KeyError('Namespace "%s" not recognised.  Valid namespaces '
106                           'are: %s' % self.__class__.namespaces)
107           
108        dict.__setitem__(self, key, val)
109
110
111    def update(self, d, **kw):       
112        for dictArg in (d, kw):
113            for k in dictArg:
114                if key not in self.__class__.namespaces:
115                    raise KeyError('Namespace "%s" not recognised.  Valid '
116                                   'namespaces are: %s' % 
117                                   self.__class__.namespaces)
118       
119        dict.update(self, d, **kw)
120
121class Subject(_AttrDict):
122    '''Subject designator'''
123    namespaces = (
124        "urn:ndg:security:authz:1.0:attr:subject:userId",
125        "urn:ndg:security:authz:1.0:attr:subject:sessionId",
126        "urn:ndg:security:authz:1.0:attr:subject:sessionManagerURI",
127        "urn:ndg:security:authz:1.0:attr:subject:roles"       
128    )
129    (USERID_NS, SESSIONID_NS, SESSIONMANAGERURI_NS, ROLES_NS) = namespaces
130
131class Resource(_AttrDict):
132    '''Resource designator'''
133    namespaces = (
134        "urn:ndg:security:authz:1.0:attr:resource:uri",
135    )
136    (URI_NS,) = namespaces
137           
138class Request(object):
139    '''Request to send to a PDP'''
140    def __init__(self, subject=Subject(), resource=Resource()):
141        self.subject = subject
142        self.resource = resource
143
144class Response(object):
145
146    decisionValues = range(4)
147    (DECISION_PERMIT,
148    DECISION_DENY,
149    DECISION_INDETERMINATE,
150    DECISION_NOT_APPLICABLE) = decisionValues
151
152    # string versions of the 4 Decision types used for encoding
153    DECISIONS = ("Permit", "Deny", "Indeterminate", "NotApplicable")
154   
155    def __init__(self, status, message=None):
156        if status not in Response.decisionValues:
157            raise TypeError("Status %s not recognised" % status)
158       
159        self.status = status
160        self.message = message
161       
162       
163from ndg.security.common.sessionmanager import SessionManagerClient, \
164    SessionNotFound, SessionCertTimeError, SessionExpired, InvalidSession, \
165    AttributeRequestDenied
166                   
167from ndg.security.common.authz.pdp import PDPUserNotLoggedIn, \
168    PDPUserAccessDenied
169   
170class SubjectRetrievalError(Exception):
171    """Generic exception class for errors related to information about the
172    subject"""
173   
174class InvalidAttributeCertificate(SubjectRetrievalError):
175    "The certificate containing authorisation roles is invalid"
176    def __init__(self, msg=None):
177        SubjectRetrievalError.__init__(self, msg or 
178                                       InvalidAttributeCertificate.__doc__)
179
180class AttributeCertificateInvalidSignature(SubjectRetrievalError):
181    ("There is a problem with the signature of the certificate containing "
182     "authorisation roles")
183    def __init__(self, msg=None):
184        SubjectRetrievalError.__init__(self, msg or 
185                                AttributeCertificateInvalidSignature.__doc__)
186             
187class AttributeCertificateNotBeforeTimeError(SubjectRetrievalError):
188    ("There is a time issuing error with certificate containing authorisation "
189    "roles")
190    def __init__(self, msg=None):
191        SubjectRetrievalError.__init__(self, msg or 
192                                AttributeCertificateNotBeforeTimeError.__doc__)
193       
194class AttributeCertificateExpired(SubjectRetrievalError):
195    "The certificate containing authorisation roles has expired"
196    def __init__(self, msg=None):
197        SubjectRetrievalError.__init__(self, msg or 
198                                       AttributeCertificateExpired.__doc__)
199           
200class SessionExpiredMsg(SubjectRetrievalError):
201    'Session has expired.  Please re-login at your home organisation'
202    def __init__(self, msg=None):
203        SubjectRetrievalError.__init__(self, msg or SessionExpiredMsg.__doc__)
204
205class SessionNotFoundMsg(SubjectRetrievalError):
206    'No session was found.  Please try re-login with your home organisation'
207    def __init__(self, msg=None):
208        SubjectRetrievalError.__init__(self, msg or 
209                                       SessionNotFoundMsg.__doc__)
210
211class InvalidSessionMsg(SubjectRetrievalError):
212    'Session is invalid.  Please try re-login with your home organisation'
213    def __init__(self, msg=None):
214        SubjectRetrievalError.__init__(self, msg or 
215                                       InvalidSessionMsg.__doc__)
216
217class InitSessionCtxError(SubjectRetrievalError):
218    'A problem occurred initialising a session connection'
219    def __init__(self, msg=None):
220        SubjectRetrievalError.__init__(self, msg or 
221                                       InitSessionCtxError.__doc__)
222
223class AttributeCertificateRequestError(SubjectRetrievalError):
224    'A problem occurred requesting a certificate containing authorisation roles'
225    def __init__(self, msg=None):
226        SubjectRetrievalError.__init__(self, msg or 
227                                    AttributeCertificateRequestError.__doc__)
228
229class PIPAttributeQuery(_AttrDict):
230    '''Policy Information Point Query class.'''
231    namespaces = (
232        "urn:ndg:security:authz:1.0:attr:subject",
233        "urn:ndg:security:authz:1.0:attr:attributeAuthorityURI",
234    ) 
235    (SUBJECT_NS, ATTRIBUTEAUTHORITY_NS) = namespaces   
236
237class PIPAttributeResponse(dict):
238    '''Policy Information Point Response class.'''
239    namespaces = (
240        Subject.ROLES_NS,
241    )
242
243
244from ndg.security.common.wssecurity import WSSecurityConfig
245from ndg.security.common.credentialwallet import CredentialWallet
246
247class PIP(object):
248    """Policy Information Point - this implementation enables the PDP to
249    retrieve attributes about the Subject"""
250
251    def __init__(self, prefix='', **cfg):
252        '''Set-up WS-Security and SSL settings for connection to an
253        Attribute Authority
254       
255        @type **cfg: dict
256        @param **cfg: keywords including 'sslCACertFilePathList' used to set a
257        list of CA certificates for an SSL connection to the Attribute
258        Authority if used and also WS-Security settings as used by
259        ndg.security.common.wssecurity.WSSecurityConfig
260        '''
261        self.wssecurityCfg = WSSecurityConfig()
262        wssePrefix = prefix + 'wssecurity'
263        self.wssecurityCfg.update(cfg, prefix=wssePrefix)
264                 
265        # List of CA certificates used to verify peer certificate with SSL
266        # connections to Attribute Authority
267        self.sslCACertFilePathList = cfg.get(prefix + 'sslCACertFilePathList', [])
268       
269        # List of CA certificates used to verify the signatures of
270        # Attribute Certificates retrieved
271        self.caCertFilePathList = cfg.get(prefix + 'caCertFilePathList', [])
272
273    def attributeQuery(self, attributeQuery):
274        """Query the Attribute Authority specified in the request to retrieve
275        the attributes if any corresponding to the subject
276       
277        @type attributeResponse: PIPAttributeQuery
278        @param attributeResponse:
279        @rtype: PIPAttributeResponse
280        @return: response containing the attributes retrieved from the
281        Attribute Authority"""
282       
283        subject = attributeQuery[PIPAttributeQuery.SUBJECT_NS]
284        sessionId = subject[Subject.SESSIONID_NS]
285        attributeAuthorityURI = attributeQuery[
286                                    PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS]
287       
288        sessionId = subject[Subject.SESSIONID_NS]
289        attributeCertificate = self._getAttributeCertificate(
290                                        sessionId,
291                                        subject[Subject.SESSIONMANAGERURI_NS],
292                                        attributeAuthorityURI)
293
294        attributeResponse = PIPAttributeResponse()
295        attributeResponse[Subject.ROLES_NS] = attributeCertificate.roles
296         
297        return attributeResponse
298   
299   
300    def _getAttributeCertificate(self,
301                                 sessionId,
302                                 sessionManagerURI,
303                                 attributeAuthorityURI):
304        '''Retrieve an Attribute Certificate using the subject's Session
305        Manager
306       
307        @type sessionId: basestring
308        @param sessionId: Session Manager session handle
309        @type sessionManagerURI: basestring
310        @param sessionManagerURI: URI to remote session manager service
311        @type attributeAuthorityURI: basestring
312        @param attributeAuthorityURI: URI to Attribute Authority service
313        '''
314       
315        try:
316            # Create Session Manager client - if a file path was set, setting
317            # are read from a separate config file section otherwise, from the
318            # PDP config object
319            smClnt = SessionManagerClient(
320                            uri=sessionManagerURI,
321                            sslCACertFilePathList=self.sslCACertFilePathList,
322                            cfg=self.wssecurityCfg)
323        except Exception, e:
324            log.error("Creating Session Manager client: %s" % e)
325            raise InitSessionCtxError()
326       
327         
328        try:
329            # Make request for attribute certificate
330            attCert = smClnt.getAttCert(
331                                attributeAuthorityURI=attributeAuthorityURI,
332                                sessID=sessionId)
333       
334        except AttributeRequestDenied, e:
335            log.error("Request for attribute certificate denied: %s" % e)
336            raise PDPUserAccessDenied()
337       
338        except SessionNotFound, e:
339            log.error("No session found: %s" % e)
340            raise SessionNotFoundMsg()
341
342        except SessionExpired, e:
343            log.error("Session expired: %s" % e)
344            raise SessionExpiredMsg()
345
346        except SessionCertTimeError, e:
347            log.error("Session cert. time error: %s" % e)
348            raise InvalidSessionMsg()
349           
350        except InvalidSession, e:
351            log.error("Invalid user session: %s" % e)
352            raise InvalidSessionMsg()
353
354        except Exception, e:
355            log.error("Request from Session Manager [%s] to Attribute "
356                      "Authority [%s] for attribute certificate: %s: %s" % 
357                      (sessionManagerURI,
358                       attributeAuthorityURI,
359                       e.__class__, e))
360            raise AttributeCertificateRequestError()
361       
362        try:
363            attCert.certFilePathList = self.caCertFilePathList
364            attCert.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 attCert
383
384           
385           
386class PDP(object):
387    """Policy Decision Point"""
388   
389    def __init__(self, policy, pip):
390        """Read in a file which determines access policy"""
391        self.policy = policy
392        self.pip = pip
393       
394    def evaluate(self, request):
395        '''Make access control decision'''
396       
397        # Look for matching targets to the given resource
398        resourceURI = request.resource[Resource.URI_NS]
399        matchingTargets = [target for target in self.policy.targets
400                           if target.regEx.match(resourceURI) is not None]
401       
402        knownAttributeAuthorityURIs = []
403        for matchingTarget in matchingTargets:
404           
405            # Make call to the Policy Information Point to pull user
406            # attributes applicable to this resource
407            if matchingTarget.attributeAuthorityURI not in \
408               knownAttributeAuthorityURIs:
409               
410                attributeQuery = PIPAttributeQuery()
411                attributeQuery[PIPAttributeQuery.SUBJECT_NS] = request.subject
412               
413                attributeQuery[PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS] = \
414                                        matchingTarget.attributeAuthorityURI
415               
416                try:
417                    attributeResponse = self.pip.attributeQuery(attributeQuery)
418                   
419                except SubjectRetrievalError, e:
420                    log.exception(e)
421                    return Response(Response.DECISION_INDETERMINATE,
422                                    message=str(e))
423                   
424                except Exception, e:
425                    log.exception(e)
426                    return Response(Response.DECISION_INDETERMINATE,
427                                    message="An internal error occurred")
428                   
429                knownAttributeAuthorityURIs.append(
430                                        matchingTarget.attributeAuthorityURI)
431               
432                request.subject[Subject.ROLES_NS] = attributeResponse[
433                                                            Subject.ROLES_NS]
434               
435        # Match the subject's attributes against the target
436        for attr in matchingTarget.attributes:
437            if attr in request.subject[Subject.ROLES_NS]:
438                return Response(Response.DECISION_PERMIT)
439           
440        return Response(Response.DECISION_DENY)
441   
442
443       
Note: See TracBrowser for help on using the repository browser.