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

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@5786
Revision 5786, 19.1 KB checked in by pjkersha, 10 years ago (diff)

Updated OpenID AX (Attribute Exchange) interface. Attributes passed over this interface are now stored in the authentication session at the Relying Party.

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