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

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

Added a Policy Information Point to encapsulate subject attribute retrieval.

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