source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/openid/relyingparty/validation.py @ 5705

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/openid/relyingparty/validation.py@5705
Revision 5705, 18.6 KB checked in by pjkersha, 11 years ago (diff)

Updated Session Manager service test configuration and nosetests.

Line 
1"""NDG Security OpenID Relying Party Provider Validation module
2
3Based on the Earth System Grid IdPValidator interface for restricting
4OpenID Providers that a Relying Party may connect to
5
6An Identity Provider (IdP) is equivalent to an OpenID Provider
7
8NERC DataGrid Project
9"""
10__author__ = "P J Kershaw"
11__date__ = "09/06/2009"
12__copyright__ = "(C) 2009 Science and Technology Facilities Council"
13__license__ = "BSD - see top-level directory for LICENSE file"
14__contact__ = "Philip.Kershaw@stfc.ac.uk"
15__revision__ = "$Id$"
16import logging
17log = logging.getLogger(__name__)
18import os
19
20from elementtree import ElementTree
21from ndg.security.common.utils.etree import QName
22from ndg.security.common.utils.classfactory import instantiateClass
23   
24class _ConfigBase(object):
25    """Base class for IdP Validator and Attribute Provider configuration
26    """
27   
28    def __init__(self):
29        self._className = None
30        self._configFile = None
31        self._parameters = {}
32   
33    def _set_className(self, className):
34        self._className = className
35   
36    def _get_className(self):
37        return self._className
38   
39    className = property(fget=_get_className,
40                         fset=_set_className)
41
42    def _get_configFile(self):
43        return self._configFile
44   
45    def _set_configFile(self, configFile):
46        self._configFile = configFile
47
48    configFile = property(fget=_get_configFile,
49                          fset=_set_configFile)
50   
51    def _get_parameters(self):
52        return self._parameters
53   
54    def _set_parameters(self, parameters):   
55        self._parameters = parameters
56   
57    parameters = property(fget=_get_parameters,
58                          fset=_set_parameters)
59
60class IdPValidatorConfig(_ConfigBase):
61    """Container for IdP validator configuration"""
62   
63class AttributeProviderConfig(_ConfigBase):
64    """Container for Attribute Provider configuration"""
65   
66class XmlConfigReaderError(Exception):
67    """Raise from XmlConfigReader"""
68     
69class XmlConfigReader(object):
70    """Parser for IdP and Attribute Provider config
71    """
72   
73    def getValidators(self, source): 
74        """Retrieve IdP Validator objects from XML file
75        @type source: basestring/file
76        @param source: file path to XML file or file object
77        """
78        validators = None
79
80        root = self.__parseConfigFile(source)
81        if root is not None:
82            validators = self.__extractValidatorConfigs(root)
83       
84        return validators
85   
86    def getAttrProviders(self, source):
87        """
88        @type source: basestring/file
89        @param source: file path to XML file or file object
90        """   
91        attrProviders = None
92
93        root = self.__parseConfigFile(source)
94        if root is not None:
95            attrProviders = self.__extractAttrProviderConfigs(root)
96       
97        return attrProviders
98   
99    def __parseConfigFile(self, source):
100        """Read in the XML configuration file
101        @type source: basestring/file
102        @param source: file path to XML file or file object
103        """
104        elem = ElementTree.parse(source)
105        root = elem.getroot()
106       
107        return root
108
109    def __extractValidatorConfigs(self, root):
110        """Parse Validator configuration from the XML config file
111        @type root: ElementTree.Element
112        @param root: root element of parsed XML config file
113        """
114        validators = []
115       
116        for elem in root:
117            if QName.getLocalPart(elem.tag).lower() == "validator":   
118                validatorConfig = IdPValidatorConfig()
119                validatorConfig.className = elem.attrib["name"]
120               
121                parameters = {}
122                for el in elem:
123                    if QName.getLocalPart(el.tag).lower() == "parameter":
124                        if el.attrib["name"] in parameters:
125                            raise XmlConfigReaderError('Duplicate parameter '
126                                                       'name "%s" found' % 
127                                                       el.attrib["name"])
128                           
129                        parameters[el.attrib["name"]] = os.path.expandvars(
130                                                            el.attrib["value"])
131           
132                validatorConfig.parameters = parameters
133                validators.append(validatorConfig)
134       
135        return validators
136   
137    def __extractAttrProviderConfigs(self, root):
138        attrProviders = []
139        validatorConfig = None
140        parameters = {}
141
142        for elem in root:
143            if QName.getLocalPart(elem.tag).lower() == "attributeprovider":
144                if validatorConfig is not None:
145                    validatorConfig.parameters = parameters
146                    attrProviders.append(validatorConfig)
147               
148                validatorConfig = AttributeProviderConfig()
149                validatorConfig.className(elem.attrib("name"))
150           
151            elif QName.getLocalPart(elem.tag).lower() == "parameter":
152                if elem.attrib["name"] in parameters:
153                    raise XmlConfigReaderError('Duplicate parameter name "%s" '
154                                               'found' % elem.attrib["name"])
155           
156                parameters[elem.attrib["name"]] = elem.attrib["value"]
157           
158        if validatorConfig != None:
159            validatorConfig.parameters = parameters
160            attrProviders.append(validatorConfig)
161       
162        return attrProviders
163
164
165class IdPValidatorException(Exception):
166    """Base class for IdPValidator exceptions"""
167   
168class IdPInvalidException(IdPValidatorException):
169    """Raise from IdPValidator.validate if the IdP is not acceptable"""
170
171class ConfigException(IdPValidatorException):
172    """Problem with configuration for the IdP Validator class"""
173 
174
175class IdPValidator(object):
176    '''Interface class for implementing OpenID Provider validators for a
177    Relying Party to call'''
178   
179    def __init__(self):
180        raise NotImplementedError()
181
182    def initialize(self, **parameters):
183        '''@raise ConfigException:''' 
184        raise NotImplementedError()
185       
186    def validate(self, idpEndpoint, idpIdentity):
187        '''@raise IdPInvalidException:
188        @raise ConfigException:''' 
189        raise NotImplementedError()
190 
191
192class SSLIdPValidator(object):
193    '''Interface class for implementing OpenID Provider validators for a
194    Relying Party to call'''
195   
196    def __init__(self):
197        raise NotImplementedError()
198
199    def initialize(self, **parameters):
200        '''@raise ConfigException:''' 
201        raise NotImplementedError()
202       
203    def validate(self, x509CertCtx):
204        '''@type x509StoreCtx: M2Crypto.X509_Store_Context
205        @param x509StoreCtx: context object containing peer certificate and
206        certificate chain for verification
207 
208        @raise IdPInvalidException:
209        @raise ConfigException:''' 
210        raise NotImplementedError()
211   
212   
213import urllib2
214from M2Crypto import SSL
215from M2Crypto.m2urllib2 import build_opener
216from ndg.security.common.X509 import X500DN
217
218class SSLClientAuthNValidator(SSLIdPValidator):
219    parameters = {
220        'configFilePath': basestring,
221        'caCertDirPath': basestring,
222        'certFilePath': basestring,
223        'priKeyFilePath': basestring,
224        'priKeyPwd': basestring
225    }
226   
227    def __init__(self):
228        """Set-up default SSL context for HTTPS requests"""
229        for p in SSLClientAuthNValidator.parameters:
230            setattr(self, p, None)
231           
232    def initialize(self, ctx, **parameters):
233        '''@raise ConfigException:''' 
234        for name, val in parameters.items():
235            if name not in SSLClientAuthNValidator.parameters:
236                raise AttributeError('Invalid parameter name "%s".  Valid '
237                                     'names are %r' % (name,
238                                    SSLClientAuthNValidator.parameters.keys()))
239               
240            if not isinstance(val, SSLClientAuthNValidator.parameters[name]):
241                raise TypeError('Invalid type %r for parameter "%s" expecting '
242                                '%r ' % 
243                                (val.__class__, 
244                                 name, 
245                                 SSLClientAuthNValidator.parameters[name]))
246       
247            setattr(self, name, os.path.expandvars(val))
248             
249        ctx.load_verify_locations(capath=self.caCertDirPath)
250        if self.certFilePath is not None and self.priKeyFilePath is not None:
251            ctx.load_cert(self.certFilePath, 
252                          keyfile=self.priKeyFilePath, 
253                          callback=lambda *arg, **kw: self.priKeyPwd)
254           
255        if self.configFilePath is not None:
256            # Simple file format - one IdP server name per line
257            cfgFile = open(self.configFilePath)
258            self.validIdPNames = [l.strip() for l in cfgFile.readlines()
259                                  if not l.startswith('#')]
260                         
261    def validate(self, x509StoreCtx):
262        '''callback function used to control the behaviour when the
263        SSL_VERIFY_PEER flag is set
264       
265        @type x509StoreCtx: M2Crypto.X509_Store_Context
266        @param x509StoreCtx: locate the certificate to be verified and perform
267        additional verification steps as needed
268        @rtype: int
269        @return: controls the strategy of the further verification process.
270        - If verify_callback returns 0, the verification process is immediately
271        stopped with "verification failed" state. If SSL_VERIFY_PEER is set,
272        a verification failure alert is sent to the peer and the TLS/SSL
273        handshake is terminated.
274        - If verify_callback returns 1, the verification process is continued.
275        If verify_callback always returns 1, the TLS/SSL handshake will not be
276        terminated with respect to verification failures and the connection
277        will be established. The calling process can however retrieve the error
278        code of the last verification error using SSL_get_verify_result(3) or
279        by maintaining its own error storage managed by verify_callback.
280        '''
281       
282        x509Cert = x509StoreCtx.get_current_cert()
283        dnTxt = x509Cert.get_subject().as_text()
284        commonName = X500DN(dn=dnTxt)['CN']
285
286        # If all is OK preVerifyOK will be 1.  Return this to the caller to
287        # that it's OK to proceed
288        if commonName not in self.validIdPNames:
289            raise IdPInvalidException("Peer certificate CN=%s is not in list "
290                                      "of valid OpenID Providers" % commonName)
291
292class IdPValidationDriver(object):
293    """Parse an XML Validation configuration containing XML Validators and
294    execute these against the Provider (IdP) input"""   
295    idPValidatorBaseClass = IdPValidator
296   
297    def __init__(self):
298        self._idPValidators = []
299       
300    def _get_idPValidators(self):
301        return self._idPValidators
302   
303    def _set_idPValidators(self, idPValidators):
304        badValidators = [i for i in idPValidators
305                         if not isinstance(i, 
306                                        self.__class__.idPValidatorBaseClass)]
307        if len(badValidators):
308            raise TypeError("Input validators must be of IdPValidator derived "
309                            "type")
310               
311        self._idPValidators = idPValidators
312       
313    idPValidators = property(fget=_get_idPValidators,
314                             fset=_set_idPValidators,
315                             doc="list of IdP Validators")
316
317    def readConfig(self, idpConfigFilePath=None):
318        validators = []
319       
320        if idpConfigFilePath is None:
321            idpConfigFilePath = os.environ.get("IDP_CONFIG_FILE")
322           
323        if idpConfigFilePath is None:
324            log.warning("IdPValidationDriver.readConfig: No IdP "
325                        "Configuration file was set")
326            return validators
327       
328        configReader = XmlConfigReader()
329        validatorConfigs = configReader.getValidators(idpConfigFilePath)
330
331        for validatorConfig in validatorConfigs:
332            try:
333                validator = instantiateClass(validatorConfig.className,
334                                             None, 
335                                             objectType=IdPValidator)
336                validator.initialize(**validatorConfig.parameters)
337                validators.append(validator)
338           
339            except Exception, e: 
340                log.error("Failed to initialise validator %s: %s", 
341                          validatorConfig.className,
342                          e)
343               
344        return validators
345   
346    def performIdPValidation(self, identifier, discoveries):
347        """Perform all IdPValidation for all configured IdPValidators.  if
348        the setIdPValidator method was used to initialise a list of
349        IdPValidators before this method is called, the configurations
350        are still checked each time this method is called and any valid
351        IdPValidators found are appended to the initial list and run in
352        addition.
353        """
354        validators = self.readConfig()
355
356        if self.idPValidators is not None:
357            validators += self.idPValidators
358
359        log.info("%d IdPValidators initialised", len(validators))
360
361        # validate the discovered end points
362        if len(validators) > 0:
363       
364            newDiscoveries = []
365            for validator in validators:   
366                for discoveryInfo in discoveries:
367                    try:                   
368                        validator.validate(discoveryInfo.getOPEndpoint(), 
369                                           identifier.getIdentifier())
370
371                        log.info("Whitelist Validator %r accepting endpoint: "
372                                 "%s", validator,
373                                 discoveryInfo.getOPEndpoint())
374
375                        newDiscoveries.append(discoveryInfo)
376                   
377                    except Exception, e:       
378                        log.warning("Whitelist Validator %r rejecting "
379                                    "endpoint: %s: %s", validator, 
380                                    discoveryInfo.getOPEndpoint(), e)
381                       
382            if len(newDiscoveries) > 0:
383                discoveries = newDiscoveries
384                log.info("Found %d valid endpoint(s)." % len(discoveries))
385            else:     
386                raise IdPInvalidException("No valid endpoints were found "
387                                          "after validation.")
388        else:
389            log.warning("No IdP validation executed because no validators "
390                        "were set")
391           
392        return discoveries
393
394
395class SSLIdPValidationDriver(IdPValidationDriver):
396    '''Validate an IdP using the certificate it returns from an SSL based
397    request'''
398    idPValidatorBaseClass = SSLIdPValidator
399   
400    def __init__(self, idpConfigFilePath=None):
401        super(SSLIdPValidationDriver, self).__init__()
402       
403        # Context object determines what validation is applied against the
404        # peer's certificate in the SSL connection
405        self.ctx = SSL.Context()
406       
407        # Enforce peer cert checking via this classes' __call__ method
408        self.ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 
409                            9, 
410                            callback=self)
411
412        urllib2.install_opener(build_opener(ssl_context=self.ctx))
413       
414        if idpConfigFilePath is not None:
415            self.idPValidators += \
416                self.readConfig(idpConfigFilePath=idpConfigFilePath)
417           
418        log.info("%d IdPValidator(s) initialised", len(self.idPValidators))
419       
420    def readConfig(self, idpConfigFilePath):
421        """Read and initialise validators set in a config file""" 
422        validators = []
423       
424        if idpConfigFilePath is None:
425            idpConfigFilePath = os.environ('SSL_IDP_VALIDATION_CONFIG_FILE')
426           
427        if idpConfigFilePath is None:
428            log.warning("SSLIdPValidationDriver.readConfig: No IdP "
429                        "Configuration file was set")
430            return validators
431       
432        configReader = XmlConfigReader()
433        validatorConfigs = configReader.getValidators(idpConfigFilePath)
434
435        for validatorConfig in validatorConfigs:
436            try:
437                validator = instantiateClass(validatorConfig.className,
438                                             None, 
439                                             objectType=SSLIdPValidator)
440               
441                # Validator has access to the SSL context object in addition
442                # to custom settings set in parameters
443                validator.initialize(self.ctx, **validatorConfig.parameters)
444                validators.append(validator)
445           
446            except Exception, e: 
447                raise ConfigException("Validator class %r initialisation "
448                                      "failed with %s exception: %s" %
449                                      (validatorConfig.className, 
450                                       e.__class__.__name__,
451                                       e))
452               
453        return validators
454           
455    def __call__(self, preVerifyOK, x509StoreCtx):
456        '''@type preVerifyOK: int
457        @param preVerifyOK: If a verification error is found, this parameter
458        will be set to 0
459        @type x509StoreCtx: M2Crypto.X509_Store_Context
460        @param x509StoreCtx: context object containing peer certificate and
461        certificate chain for verification
462        '''
463        if preVerifyOK == 0:
464            # Something is wrong with the certificate don't bother proceeding
465            # any further
466            log.info("No custom Validation executed: a previous verification "
467                     "error has occurred")
468            return preVerifyOK
469
470        # validate the discovered end points
471        for validator in self.idPValidators:   
472            try:
473                validator.validate(x509StoreCtx)
474                log.info("Whitelist Validator %s succeeded", 
475                         validator.__class__.__name__)
476           
477            except Exception, e:
478                log.error("Whitelist Validator %r caught %s exception with "
479                          "peer certificate context: %s", 
480                          validator.__class__.__name__, 
481                          e.__class__.__name__,
482                          e)       
483                return 0
484           
485        if len(self.idPValidators) == 0:
486            log.warning("No IdP validation executed because no validators "
487                        "were set")
488           
489        return 1
490       
Note: See TracBrowser for help on using the repository browser.