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

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

Tested Policy with regex target URIs

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.sessionmanager import SessionManagerClient, \
167    SessionNotFound, SessionCertTimeError, SessionExpired, InvalidSession, \
168    AttributeRequestDenied
169                   
170from ndg.security.common.authz.pdp import PDPUserNotLoggedIn, \
171    PDPUserAccessDenied
172   
173class SubjectRetrievalError(Exception):
174    """Generic exception class for errors related to information about the
175    subject"""
176   
177class InvalidAttributeCertificate(SubjectRetrievalError):
178    "The certificate containing authorisation roles is invalid"
179    def __init__(self, msg=None):
180        SubjectRetrievalError.__init__(self, msg or 
181                                       InvalidAttributeCertificate.__doc__)
182
183class AttributeCertificateInvalidSignature(SubjectRetrievalError):
184    ("There is a problem with the signature of the certificate containing "
185     "authorisation roles")
186    def __init__(self, msg=None):
187        SubjectRetrievalError.__init__(self, msg or 
188                                AttributeCertificateInvalidSignature.__doc__)
189             
190class AttributeCertificateNotBeforeTimeError(SubjectRetrievalError):
191    ("There is a time issuing error with certificate containing authorisation "
192    "roles")
193    def __init__(self, msg=None):
194        SubjectRetrievalError.__init__(self, msg or 
195                                AttributeCertificateNotBeforeTimeError.__doc__)
196       
197class AttributeCertificateExpired(SubjectRetrievalError):
198    "The certificate containing authorisation roles has expired"
199    def __init__(self, msg=None):
200        SubjectRetrievalError.__init__(self, msg or 
201                                       AttributeCertificateExpired.__doc__)
202           
203class SessionExpiredMsg(SubjectRetrievalError):
204    'Session has expired.  Please re-login at your home organisation'
205    def __init__(self, msg=None):
206        SubjectRetrievalError.__init__(self, msg or SessionExpiredMsg.__doc__)
207
208class SessionNotFoundMsg(SubjectRetrievalError):
209    'No session was found.  Please try re-login with your home organisation'
210    def __init__(self, msg=None):
211        SubjectRetrievalError.__init__(self, msg or 
212                                       SessionNotFoundMsg.__doc__)
213
214class InvalidSessionMsg(SubjectRetrievalError):
215    'Session is invalid.  Please try re-login with your home organisation'
216    def __init__(self, msg=None):
217        SubjectRetrievalError.__init__(self, msg or 
218                                       InvalidSessionMsg.__doc__)
219
220class InitSessionCtxError(SubjectRetrievalError):
221    'A problem occurred initialising a session connection'
222    def __init__(self, msg=None):
223        SubjectRetrievalError.__init__(self, msg or 
224                                       InitSessionCtxError.__doc__)
225
226class AttributeCertificateRequestError(SubjectRetrievalError):
227    'A problem occurred requesting a certificate containing authorisation roles'
228    def __init__(self, msg=None):
229        SubjectRetrievalError.__init__(self, msg or 
230                                    AttributeCertificateRequestError.__doc__)
231
232class PIPAttributeQuery(_AttrDict):
233    '''Policy Information Point Query class.'''
234    namespaces = (
235        "urn:ndg:security:authz:1.0:attr:subject",
236        "urn:ndg:security:authz:1.0:attr:attributeAuthorityURI",
237    ) 
238    (SUBJECT_NS, ATTRIBUTEAUTHORITY_NS) = namespaces   
239
240class PIPAttributeResponse(dict):
241    '''Policy Information Point Response class.'''
242    namespaces = (
243        Subject.ROLES_NS,
244    )
245
246
247from ndg.security.common.wssecurity import WSSecurityConfig
248from ndg.security.common.credentialwallet import CredentialWallet
249
250class PIP(object):
251    """Policy Information Point - this implementation enables the PDP to
252    retrieve attributes about the Subject"""
253
254    def __init__(self, prefix='', **cfg):
255        '''Set-up WS-Security and SSL settings for connection to an
256        Attribute Authority
257       
258        @type **cfg: dict
259        @param **cfg: keywords including 'sslCACertFilePathList' used to set a
260        list of CA certificates for an SSL connection to the Attribute
261        Authority if used and also WS-Security settings as used by
262        ndg.security.common.wssecurity.WSSecurityConfig
263        '''
264        self.wssecurityCfg = WSSecurityConfig()
265        wssePrefix = prefix + 'wssecurity'
266        self.wssecurityCfg.update(cfg, prefix=wssePrefix)
267                 
268        # List of CA certificates used to verify peer certificate with SSL
269        # connections to Attribute Authority
270        self.sslCACertFilePathList = cfg.get(prefix + 'sslCACertFilePathList', [])
271       
272        # List of CA certificates used to verify the signatures of
273        # Attribute Certificates retrieved
274        self.caCertFilePathList = cfg.get(prefix + 'caCertFilePathList', [])
275
276    def attributeQuery(self, attributeQuery):
277        """Query the Attribute Authority specified in the request to retrieve
278        the attributes if any corresponding to the subject
279       
280        @type attributeResponse: PIPAttributeQuery
281        @param attributeResponse:
282        @rtype: PIPAttributeResponse
283        @return: response containing the attributes retrieved from the
284        Attribute Authority"""
285       
286        subject = attributeQuery[PIPAttributeQuery.SUBJECT_NS]
287        sessionId = subject[Subject.SESSIONID_NS]
288        attributeAuthorityURI = attributeQuery[
289                                    PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS]
290       
291        sessionId = subject[Subject.SESSIONID_NS]
292        attributeCertificate = self._getAttributeCertificate(
293                                        sessionId,
294                                        subject[Subject.SESSIONMANAGERURI_NS],
295                                        attributeAuthorityURI)
296
297        attributeResponse = PIPAttributeResponse()
298        attributeResponse[Subject.ROLES_NS] = attributeCertificate.roles
299         
300        return attributeResponse
301   
302   
303    def _getAttributeCertificate(self,
304                                 sessionId,
305                                 sessionManagerURI,
306                                 attributeAuthorityURI):
307        '''Retrieve an Attribute Certificate using the subject's Session
308        Manager
309       
310        @type sessionId: basestring
311        @param sessionId: Session Manager session handle
312        @type sessionManagerURI: basestring
313        @param sessionManagerURI: URI to remote session manager service
314        @type attributeAuthorityURI: basestring
315        @param attributeAuthorityURI: URI to Attribute Authority service
316        '''
317       
318        try:
319            # Create Session Manager client - if a file path was set, setting
320            # are read from a separate config file section otherwise, from the
321            # PDP config object
322            smClnt = SessionManagerClient(
323                            uri=sessionManagerURI,
324                            sslCACertFilePathList=self.sslCACertFilePathList,
325                            cfg=self.wssecurityCfg)
326        except Exception, e:
327            log.error("Creating Session Manager client: %s" % e)
328            raise InitSessionCtxError()
329       
330         
331        try:
332            # Make request for attribute certificate
333            attCert = smClnt.getAttCert(
334                                attributeAuthorityURI=attributeAuthorityURI,
335                                sessID=sessionId)
336       
337        except AttributeRequestDenied, e:
338            log.error("Request for attribute certificate denied: %s" % e)
339            raise PDPUserAccessDenied()
340       
341        except SessionNotFound, e:
342            log.error("No session found: %s" % e)
343            raise SessionNotFoundMsg()
344
345        except SessionExpired, e:
346            log.error("Session expired: %s" % e)
347            raise SessionExpiredMsg()
348
349        except SessionCertTimeError, e:
350            log.error("Session cert. time error: %s" % e)
351            raise InvalidSessionMsg()
352           
353        except InvalidSession, e:
354            log.error("Invalid user session: %s" % e)
355            raise InvalidSessionMsg()
356
357        except Exception, e:
358            log.error("Request from Session Manager [%s] to Attribute "
359                      "Authority [%s] for attribute certificate: %s: %s" % 
360                      (sessionManagerURI,
361                       attributeAuthorityURI,
362                       e.__class__, e))
363            raise AttributeCertificateRequestError()
364       
365        try:
366            attCert.certFilePathList = self.caCertFilePathList
367            attCert.isValid(raiseExcep=True)
368       
369        except AttCertInvalidSignature, e:
370            log.exception(e)
371            raise AttributeCertificateInvalidSignature()
372       
373        except AttCertNotBeforeTimeError, e:   
374            log.exception(e)
375            raise AttributeCertificateNotBeforeTimeError()
376       
377        except AttCertExpired, e:   
378            log.exception(e)
379            raise AttributeCertificateExpired()
380
381        except AttCertError, e:
382            log.exception(e)
383            raise InvalidAttributeCertificate()
384           
385        return attCert
386
387           
388           
389class PDP(object):
390    """Policy Decision Point"""
391   
392    def __init__(self, policy, pip):
393        """Read in a file which determines access policy"""
394        self.policy = policy
395        self.pip = pip
396       
397    def evaluate(self, request):
398        '''Make access control decision'''
399       
400        # Look for matching targets to the given resource
401        resourceURI = request.resource[Resource.URI_NS]
402        matchingTargets = [target for target in self.policy.targets
403                           if target.regEx.match(resourceURI) is not None]
404       
405        knownAttributeAuthorityURIs = []
406        for matchingTarget in matchingTargets:
407           
408            # Make call to the Policy Information Point to pull user
409            # attributes applicable to this resource
410            if matchingTarget.attributeAuthorityURI not in \
411               knownAttributeAuthorityURIs:
412               
413                attributeQuery = PIPAttributeQuery()
414                attributeQuery[PIPAttributeQuery.SUBJECT_NS] = request.subject
415               
416                attributeQuery[PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS] = \
417                                        matchingTarget.attributeAuthorityURI
418               
419                try:
420                    attributeResponse = self.pip.attributeQuery(attributeQuery)
421                   
422                except SubjectRetrievalError, e:
423                    log.exception(e)
424                    return Response(Response.DECISION_INDETERMINATE,
425                                    message=str(e))
426                   
427                except Exception, e:
428                    log.exception(e)
429                    return Response(Response.DECISION_INDETERMINATE,
430                                    message="An internal error occurred")
431                   
432                knownAttributeAuthorityURIs.append(
433                                        matchingTarget.attributeAuthorityURI)
434               
435                request.subject[Subject.ROLES_NS] = attributeResponse[
436                                                            Subject.ROLES_NS]
437               
438        # Match the subject's attributes against the target
439        for attr in matchingTarget.attributes:
440            if attr in request.subject[Subject.ROLES_NS]:
441                return Response(Response.DECISION_PERMIT)
442           
443        return Response(Response.DECISION_DENY)
444   
445
446       
Note: See TracBrowser for help on using the repository browser.