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

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@5779
Revision 5779, 19.2 KB checked in by pjkersha, 11 years ago (diff)

Integrated automated start-up and shutdown of Paste http servers for unit tests.

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