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

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@5497
Revision 5497, 13.7 KB checked in by pjkersha, 12 years ago (diff)

Work on unit testing for IdP Validator implementation + SSL client authN validator

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 import getLocalName
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    @staticmethod
73    def getNamedElem(name): 
74        for e in elem:
75            if e == name:
76                return e
77        return None
78   
79    def getValidators(self, source): 
80        """Retrieve IdP Validator objects from XML file
81        @type source: basestring/file
82        @param source: file path to XML file or file object
83        """
84        validators = None
85
86        root = self.__parseConfigFile(source)
87        if root is not None:
88            validators = self.__extractValidatorConfigs(root)
89       
90        return validators
91   
92    def getAttrProviders(self, source):
93        """
94        @type source: basestring/file
95        @param source: file path to XML file or file object
96        """   
97        attrProviders = None
98
99        root = self.__parseConfigFile(source)
100        if root is not None:
101            attrProviders = self.__extractAttrProviderConfigs(root)
102       
103        return attrProviders
104   
105    def __parseConfigFile(self, source):
106        """Read in the XML configuration file
107        @type source: basestring/file
108        @param source: file path to XML file or file object
109        """
110        elem = ElementTree.parse(source)
111        root = elem.getroot()
112       
113        return root
114
115    def __extractValidatorConfigs(self, root):
116        """Parse Validator configuration from the XML config file
117        @type root: ElementTree.Element
118        @param root: root element of parsed XML config file
119        """
120        validators = []
121       
122        for elem in root:
123            if getLocalName(elem).lower() == "validator":   
124                validatorConfig = IdPValidatorConfig()
125                validatorConfig.className = elem.attrib["name"]
126               
127                for el in elem:
128                    parameters = {}
129                    if getLocalName(el).lower() == "parameter":
130                        if el.attrib["name"] in parameters:
131                            raise XmlConfigReaderError('Duplicate parameter '
132                                                       'name "%s" found' % 
133                                                       el.attrib["name"])
134                           
135                        parameters[el.attrib["name"]] = os.path.expandvars(
136                                                            el.attrib["value"])
137           
138                validatorConfig.parameters = parameters
139                validators.append(validatorConfig)
140       
141        return validators
142   
143    def __extractAttrProviderConfigs(self, root):
144        attrProviders = []
145        validatorConfig = None
146        parameters = {}
147
148        for elem in root:
149            if getLocalName(elem).lower() == "attributeprovider":
150                if validatorConfig is not None:
151                    validatorConfig.parameters = parameters
152                    attrProviders.append(validatorConfig)
153               
154                validatorConfig = AttributeProviderConfig()
155                validatorConfig.className(elem.attrib("name"))
156           
157            elif getLocalName(elem).lower() == "parameter":
158                if elem.attrib["name"] in parameters:
159                    raise XmlConfigReaderError('Duplicate parameter name "%s" '
160                                               'found' % elem.attrib["name"])
161           
162                parameters[elem.attrib["name"]] = elem.attrib["value"]
163           
164        if validatorConfig != None:
165            validatorConfig.parameters = parameters
166            attrProviders.append(validatorConfig)
167       
168        return attrProviders
169
170
171class IdPValidatorException(Exception):
172    """Base class for IdPValidator exceptions"""
173   
174class IdPInvalidException(IdPValidatorException):
175    """Raise from IdPValidator.validate if the IdP is not acceptable"""
176
177class ConfigException(IdPValidatorException):
178    """Problem with configuration for the IdP Validator class"""
179 
180
181class IdPValidator(object):
182    '''Interface class for implementing OpenID Provider validators for a
183    Relying Party to call'''
184   
185    def __init__(self):
186        raise NotImplementedError()
187
188    def initialize(self, **parameters):
189        '''@raise ConfigException:''' 
190        raise NotImplementedError()
191       
192    def validate(self, idpEndpoint, idpIdentity):
193        '''@raise IdPInvalidException:
194        @raise ConfigException:''' 
195        raise NotImplementedError()
196   
197
198class IdPValidationDriver(object):
199    """Parse an XML Validation configuration containing XML Validators and
200    execute these against the Provider (IdP) input"""   
201   
202    def __init__(self):
203        self._idPValidators = None
204       
205    def _get_idPValidators(self):
206        return self._idPValidators
207   
208    def _set_idPValidators(self, idPValidators):
209        self._idPValidators = idPValidators
210       
211    idPValidators = property(fget=_get_idPValidators,
212                             fset=_set_idPValidators,
213                             doc="list of IdP Validators")
214
215    def readConfig(self, idpConfigFilePath=None):
216        validators = []
217       
218        if idpConfigFilePath is None:
219            idpConfigFilePath = os.environ.get("IDP_CONFIG_FILE")
220           
221        if idpConfigFilePath is None:
222            log.warning("IdPValidationDriver.performIdPValidation: No IdP "
223                        "Configuration file was set")
224            return validators
225       
226        configReader = XmlConfigReader()
227        validatorConfigs = configReader.getValidators(idpConfigFilePath)
228
229        for idpConfig in validatorConfigs:
230       
231            className = idpConfig.className
232            parameters = idpConfig.parameters
233
234            try:
235                validator = instantiateClass(className,
236                                             None, 
237                                             objectType=IdPValidator)
238                validator.initialize(**parameters)
239                validators.append(validator)
240           
241            except Exception, e: 
242                log.error("Failed to initialise validator: ", e)
243               
244        return validators
245   
246    def performIdPValidation(self, identifier, discoveries):
247        """Perform all IdPValidation for all configured IdPValidators.  if
248        the setIdPValidator method was used to initialise a list of
249        IdPValidators before this method is called, the configurations
250        are still checked each time this method is called and any valid
251        IdPValidators found are appended to the initial list and run in
252        addition.
253        """
254        validators = self.readConfig()
255
256        if self.idPValidators is not None:
257            validators += self.idPValidators
258
259        log.info("%d IdPValidators initialised", len(validators))
260
261        # validate the discovered end points
262        if len(validators) > 0:
263       
264            newDiscoveries = []
265            for validator in validators:   
266                for discoveryInfo in discoveries:
267                    try:                   
268                        validator.validate(discoveryInfo.getOPEndpoint(), 
269                                           identifier.getIdentifier())
270
271                        log.info("Whitelist Validator %r accepting endpoint: "
272                                 "%s", validator,
273                                 discoveryInfo.getOPEndpoint())
274
275                        newDiscoveries.append(discoveryInfo)
276                   
277                    except Exception, e:       
278                        log.warning("Whitelist Validator %r rejecting "
279                                    "endpoint: %s: %s", validator, 
280                                    discoveryInfo.getOPEndpoint(), e)
281                       
282            if len(newDiscoveries) > 0:
283                discoveries = newDiscoveries
284                log.info("Found %d valid endpoint(s)." % len(discoveries))
285            else:     
286                raise IdPInvalidException("No valid endpoints were found "
287                                          "after validation.")
288        else:
289            log.warning("No IdP validation executed because no validators "
290                        "were set")
291           
292        return discoveries
293
294    __call__ = performIdPValidation
295   
296   
297from openid.fetchers import USER_AGENT, _allowedURL, Urllib2Fetcher
298import urllib2
299from M2Crypto.m2urllib2 import HTTPSHandler
300from M2Crypto import SSL
301from M2Crypto.X509 import X509_Store_Context
302
303class SSLClientAuthNValidator(IdPValidator):
304    parameters = {
305        'preVerifyOK': int, 
306        'x509StoreCtx': M2Crypto.X509_Store_Context
307    }
308    def __init__(self):
309        raise NotImplementedError()
310
311    def initialize(self, **parameters):
312        '''@raise ConfigException:''' 
313        for name, val in parameters.items():
314            if name not in SSLClientAuthNValidator.parameters:
315                raise AttributeError('Invalid parameter name "%s".  Valid '
316                                     'names are %r' % (name,
317                                    SSLClientAuthNValidator.parameters))
318            if not isinstance(val, parameters[name]):
319                raise TypeError()
320       
321    def validate(self, idpEndpoint, idpIdentity):
322        '''@raise IdPInvalidException:
323        @raise ConfigException:''' 
324        raise NotImplementedError()
325
326    def verifyCallback(preVerifyOK, x509StoreCtx):
327        '''callback function used to control the behaviour when the
328        SSL_VERIFY_PEER flag is set
329       
330        http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html
331       
332        @type preVerifyOK: int
333        @param preVerifyOK: If a verification error is found, this parameter
334        will be set to 0
335        @type x509StoreCtx: M2Crypto.X509_Store_Context
336        @param x509StoreCtx: locate the certificate to be verified and perform
337        additional verification steps as needed
338        @rtype: int
339        @return: controls the strategy of the further verification process.
340        - If verify_callback returns 0, the verification process is immediately
341        stopped with "verification failed" state. If SSL_VERIFY_PEER is set,
342        a verification failure alert is sent to the peer and the TLS/SSL
343        handshake is terminated.
344        - If verify_callback returns 1, the verification process is continued.
345        If verify_callback always returns 1, the TLS/SSL handshake will not be
346        terminated with respect to verification failures and the connection
347        will be established. The calling process can however retrieve the error
348        code of the last verification error using SSL_get_verify_result(3) or
349        by maintaining its own error storage managed by verify_callback.
350        '''
351        if preVerifyOK == 0:
352            # Something is wrong with the certificate don't bother proceeding
353            # any further
354            return preVerifyOK
355       
356        x509Cert = x509StoreCtx.get_current_cert()
357        x509Cert.get_subject()
358        x509CertChain = x509StoreCtx.get1_chain()
359        for cert in x509CertChain:
360            subject = cert.get_subject()
361            dn = subject.as_text()
362            print dn
363           
364        # If all is OK preVerifyOK will be 1.  Return this to the caller to
365        # that it's OK to proceed
366        return preVerifyOK
367       
368    ctx = SSL.Context()
369
370    caDirPath = '../ndg.security.test/ndg/security/test/config/ca'
371    ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 
372                   9, 
373                   callback=verifyCallback)
374#    ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 1)
375
376    ctx.load_verify_locations(capath=caDirPath)
377#    ctx.load_cert(certFilePath,
378#                  keyfile=priKeyFilePath,
379#                  callback=lambda *arg, **kw: priKeyPwd)
380
381    from M2Crypto.m2urllib2 import build_opener
382    urllib2.install_opener(build_opener(ssl_context=ctx))
383
384
Note: See TracBrowser for help on using the repository browser.