Ignore:
Timestamp:
13/05/08 09:14:41 (12 years ago)
Author:
pjkersha
Message:
  • Big changes enabling modularised security from Discovery/Browse? Pylons code stack. Changes are for login only and don't include the Gatekeeper yet.
  • Updates to OpenID AuthKit? test code to enable kid templates.

ndg.security.server/ndg/security/server/sso/sso/config/ssoServiceMiddleware.py

  • include client in ndg.security.client.cfg class for globals - needed for server/sslServer config settings for SSO Client BaseController?
  • read WS-Security settings using ndg.security.common.wssecurity.WSSecurityConfig

ndg.security.server/ndg/security/server/sso/sso/controllers/login.py,
ndg.security.server/ndg/security/server/sso/sso/controllers/logout.py:

  • Give specific alias for kid templates to enable a separate security templates dir to ows_server

ndg.security.server/ndg/security/server/sso/sso/controllers/wayf.py:

  • ditto to above
  • fix to URL input into base 64 encode - convert from unicode to regular string as otherwise b64 code will fail

ndg.security.server/ndg/security/server/sso/sso/lib/base.py:

  • Provide full path to sso.* imports so that controllers can be imported across into ows_server or any other pylons code stack.
  • LoginServiceQuery? -> SSOServiceQuery

ndg.security.server/ndg/security/server/sso/sso/templates/ndg/security/wayf.kid:

  • got rid of login status info - it's confusing to the user

ndg.security.client/ndg/security/client/ssoclient/ssoClient.cfg:

  • added tracefile option for ZSI SOAP i/o display

ndg.security.client/ndg/security/client/ssoclient/ssoclient/config/ssoClientMiddleware.py:

  • SSOMiddleware interface changed to enable reading direct from an existing config object as well as from file

ndg.security.client/ndg/security/client/ssoclient/ssoclient/controllers/logout.py:

  • fixes for full path import statements + correct g config attr settings

ndg.security.client/ndg/security/client/ssoclient/ssoclient/lib/base.py:

  • enable processing of logout response from a separate SSO Service - logout flag in URL arg tells base controller to delete the security details from the cookie.

ndg.security.client/ndg/security/client/ssoclient/ssoclient/templates/ndg/security/ndgPage.kid: typo fix

Tests/authtest/development.ini,
Tests/authtest/authtest/config/environment.py,
Tests/authtest/authtest/controllers/auth.py:

  • enable kid template for OpenID signin

Tests/authtest/authtest/tests/functional/test_test2.py,
Tests/authtest/authtest/controllers/test2.py: test controller

ndg.security.common/ndg/security/common/wssecurity/init.py:

  • enable initialisation from an existing config file object

ndg.security.common/ndg/security/common/pylons/security_util.py:

ndg.security.common/ndg/security/common/init.py: fix to imports

ndg.security.common/ndg/security/common/wsSecurity.py: fix for altered WSSecurityConfig interface

ndg.security.common/ndg/security/common/m2CryptoSSLUtility.py:

  • fix to HostCheck?.call - check for peerCert is None when peer tries http instead of https
Location:
TI12-security/trunk/python/ndg.security.server/ndg/security/server/sso/sso
Files:
1 added
6 edited

Legend:

Unmodified
Added
Removed
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/sso/sso/config/ssoServiceMiddleware.py

    r3754 r3892  
    44P J Kershaw 18/03/08 
    55''' 
     6from os.path import expandvars as xpdvars 
     7import logging 
     8log = logging.getLogger(__name__) 
    69 
    710class ndg: 
     
    1316            class ssoservice: 
    1417                cfg = None 
     18        class client: 
     19            '''Client class is also needed for BaseController handler to handle 
     20            responses from Single Sign On IdP''' 
     21            class ssoclient: 
     22                class cfg: 
     23                    '''Placeholder for server and sslServer attributes''' 
    1524 
    1625class SSOMiddleware: 
    17      
    18     def __init__(self, app, cfgFilePath, appGlobals): 
     26             
     27    def __init__(self, app, cfg, appGlobals, **kw): 
     28        log.debug("SSOMiddleware.__init__ ...") 
    1929        self.app = app 
    20         ndg.security.server.ssoservice.cfg = SecurityConfig(cfgFilePath) 
    21         ndg.security.server.ssoservice.cfg.read() 
     30        ndg.security.server.ssoservice.cfg = SSOServiceConfig(cfg, **kw) 
     31         
     32        # Copy into client for the benefit of 
     33        # ndg.security.client.ssoclient.ssoclient.lib.base.BaseController 
     34        # used to process responses back from SSO IdP 
     35        ndg.security.client.ssoclient.cfg.server = \ 
     36            ndg.security.server.ssoservice.cfg.server 
     37        ndg.security.client.ssoclient.cfg.sslServer = \ 
     38            ndg.security.server.ssoservice.cfg.sslServer 
     39             
    2240        appGlobals.ndg = ndg 
    2341           
    2442    def __call__(self, environ, start_response): 
    2543         
    26 #        environ['securityConfig'] = self.config 
    2744        return self.app(environ, start_response) 
    2845 
     
    3249from ndg.security.common.wssecurity import WSSecurityConfig 
    3350 
    34 class SecurityConfigError(Exception): 
     51class SSOServiceConfigError(Exception): 
    3552    """Handle errors from parsing security config items""" 
    3653        
    37 class SecurityConfig(object): 
     54class SSOServiceConfig(object): 
    3855    """Get Security related parameters from the Pylons NDG config file""" 
    3956 
    40     def __init__(self, cfgFilePath=None): 
     57    def __init__(self, cfg=None, **parseKw): 
    4158        '''Get PKI settings for Attribute Authority and Session Manager from 
    4259        the configuration file 
    4360         
    44         @type cfgFilePath: pylons config file object 
    45         @param cfgFilePath: reference to NDG configuration file.  If omitted  
    46         defaults to request.environ['ndgConfig']''' 
     61        @type cfg: config file object or string 
     62        @param cfg: reference to NDG configuration file or config file object 
     63        ''' 
    4764         
    48         self.cfgFilePath = cfgFilePath 
    49         self.gk = None 
    5065        self.wss = {} 
    5166         
    52     def read(self): 
     67        if isinstance(cfg, basestring): 
     68            # Assume file path to be read 
     69            self.read(cfg) 
     70        else: 
     71            # Assume existing config type object 
     72            self.cfg = cfg 
     73 
     74        if self.cfg: 
     75            self.parse(**parseKw) 
     76 
     77         
     78    def read(self, cfgFilePath): 
    5379        '''Read content of config file into object''' 
    54         cfg = ConfigParser() 
    55         cfg.read(self.cfgFilePath) 
    56         
    57         tracefileExpr = cfg.get('NDG_SECURITY', 'tracefile') 
    58         if tracefileExpr: 
    59             self.tracefile = eval(tracefileExpr) 
     80        self.cfg = ConfigParser() 
     81        self.cfg.read(cfgFilePath) 
     82  
    6083 
    61         self.smURI = cfg.get('NDG_SECURITY', 'sessionMgrURI')         
    62         self.aaURI = cfg.get('NDG_SECURITY', 'attAuthorityURI') 
     84    def parse(self,  
     85              defSection='DEFAULT',  
     86              layoutSection='layout', 
     87              wssSection='NDG_SECURITY.wssecurity'): 
     88        '''Extract content of config file object into self''' 
     89               
     90        if self.cfg.has_option(defSection, 'tracefile'):         
     91            self.tracefile = eval(self.cfg.get(defSection,'tracefile'))     
     92        else: 
     93            self.tracefile = None 
     94             
     95        self.smURI = self.cfg.get(defSection, 'sessionMgrURI')         
     96        self.aaURI = self.cfg.get(defSection, 'attAuthorityURI') 
    6397         
    6498        # ... for SSL connections to security web services 
    6599        try: 
    66100            self.sslCACertFilePathList = \ 
    67             cfg.get('NDG_SECURITY', 'sslCACertFilePathList').split() 
     101            xpdvars(self.cfg.get(defSection, 'sslCACertFilePathList')).split() 
    68102                 
    69103        except AttributeError: 
    70             raise SecurityConfigError, \ 
     104            raise SSOServiceConfigError, \ 
    71105                        'No "sslCACertFilePathList" security setting' 
    72106 
    73         self.sslPeerCertCN = cfg.get('NDG_SECURITY', 'sslPeerCertCN', None) 
     107        # If no separate WS-Security config file is set then read these params 
     108        # from the current config file 
     109        if self.cfg.has_option(defSection, 'wssCfgFilePath'): 
     110            path = self.cfg.get(defSection,'wssCfgFilePath', None)  
     111            wssCfgFilePath = xpdvars(path) 
     112        else: 
     113            wssCfgFilePath = None 
     114             
     115        wss = WSSecurityConfig(cfg=wssCfgFilePath or self.cfg) 
     116        wss.parse(section=wssSection) 
    74117 
    75         wssCfgFilePath = cfg.get('NDG_SECURITY', 'wssCfgFilePath', None) 
    76         wss = WSSecurityConfig() 
    77         wss.read(wssCfgFilePath) 
    78118         
    79119        # Cast to standard dict because WSSecurityConfig object can't be 
     
    81121        # TODO: check for cleaner solution - dict(wss) 
    82122        self.wss = dict(wss.items()) 
    83          
    84 #        # ...and for WS-Security digital signature 
    85 #        self.wssCertFilePath = cfg.get('NDG_SECURITY', 'wssCertFilePath') 
    86 #        self.wssPriKeyFilePath = cfg.get('NDG_SECURITY', 'wssKeyFilePath') 
    87 #        self.wssPriKeyPwd = cfg.get('NDG_SECURITY', 'wssKeyPwd') 
    88 # 
    89 #        try: 
    90 #            self.wssCACertFilePathList = \ 
    91 #                cfg.get('NDG_SECURITY', 'wssCACertFilePathList').split() 
    92 #                 
    93 #        except AttributeError: 
    94 #            raise SecurityConfigError, \ 
    95 #                                'No "wssCACertFilePathList" security setting' 
    96 # 
    97 #        # Inclusive namespace prefixes for Exclusive C14N 
    98 #        try: 
    99 #            self.wssRefInclNS = cfg.get('NDG_SECURITY', 'wssRefInclNS').split() 
    100 #                 
    101 #        except AttributeError: 
    102 #            raise SecurityConfigError, 'No "wssRefInclNS" security setting' 
    103 # 
    104 #        try: 
    105 #            self.wssSignedInfoInclNS = cfg.get('NDG_SECURITY',  
    106 #                                               'wssSignedInfoInclNS').split()            
    107 #        except AttributeError: 
    108 #            raise SecurityConfigError, \ 
    109 #                                    'No "wssSignedInfoInclNS" security setting' 
    110123 
    111124         
     
    113126         
    114127        # Attribute Certificate Issuer 
    115         self.acIssuer = cfg.get('NDG_SECURITY', 'acIssuer') 
     128        self.acIssuer = self.cfg.get(defSection, 'acIssuer') 
    116129         
    117130        # verification of X.509 cert back to CA 
    118131        try: 
    119             self.acCACertFilePathList = cfg.get('NDG_SECURITY',  
    120                                             'acCACertFilePathList').split()           
     132            self.acCACertFilePathList = xpdvars(self.cfg.get(defSection,  
     133                                            'acCACertFilePathList')).split()           
    121134        except AttributeError: 
    122             raise SecurityConfigError, \ 
    123                                 'No "acCACertFilePathList" security setting' 
     135            raise SSOServiceConfigError( 
     136                                'No "acCACertFilePathList" security setting') 
    124137 
    125138        # Hostname 
    126         self.server=cfg.get('NDG_SECURITY', 'server', '') 
     139        self.server=self.cfg.get(defSection, 'server', '') 
    127140 
    128141        # For secure connections 
    129         self.sslServer = cfg.get('NDG_SECURITY', 'sslServer', '') 
     142        self.sslServer = self.cfg.get(defSection, 'sslServer', '') 
    130143         
    131144        # These URLs are referred from template files 
    132145        self.getCredentials = '%s/getCredentials' % self.sslServer        
    133         self.logout = '%s/logout' % self.server 
     146        self.logoutURI = '%s/logout' % self.server 
    134147                       
    135148        # Where Are You From URI           
    136149        self.wayfuri='%s/wayf' % self.server 
    137150 
    138         self.localLink=cfg.get('layout', 'localLink', None) 
    139         self.localImage=cfg.get('layout', 'localImage', None) 
    140         self.localAlt=cfg.get('layout', 'localAlt', 'Visit Local Site') 
    141         self.ndgLink=cfg.get('layout', 'ndgLink', 'http://ndg.nerc.ac.uk') 
    142         self.ndgImage=cfg.get('layout', 'ndgImage', None) 
    143         self.ndgAlt=cfg.get('layout', 'ndgAlt','Visit NDG') 
    144         self.stfcLink=cfg.get('layout', 'stfcLink') 
    145         self.stfcImage=cfg.get('layout', 'stfcImage') 
    146         self.helpIcon=cfg.get('layout', 'helpIcon') 
    147         self.LeftAlt=cfg.get('layout', 'HdrLeftAlt') 
    148         self.LeftLogo=cfg.get('layout', 'HdrLeftLogo') 
     151        self.localLink=self.cfg.get(layoutSection, 'localLink', None) 
     152        self.localImage=self.cfg.get(layoutSection, 'localImage', None) 
     153        self.localAlt=self.cfg.get(layoutSection, 'localAlt', 'Visit Local Site') 
     154        self.ndgLink=self.cfg.get(layoutSection, 'ndgLink', 'http://ndg.nerc.ac.uk') 
     155        self.ndgImage=self.cfg.get(layoutSection, 'ndgImage', None) 
     156        self.ndgAlt=self.cfg.get(layoutSection, 'ndgAlt','Visit NDG') 
     157        self.stfcLink=self.cfg.get(layoutSection, 'stfcLink') 
     158        self.stfcImage=self.cfg.get(layoutSection, 'stfcImage') 
     159        self.helpIcon=self.cfg.get(layoutSection, 'helpIcon') 
     160        self.LeftAlt=self.cfg.get(layoutSection, 'HdrLeftAlt') 
     161        self.LeftLogo=self.cfg.get(layoutSection, 'HdrLeftLogo') 
    149162        self.pageLogo="bodcHdr" 
    150         self.icons_xml=cfg.get('layout','Xicon') 
    151         self.icons_plot=cfg.get('layout','plot') 
    152         self.icons_prn=cfg.get('layout', 'printer') 
     163        self.icons_xml=self.cfg.get(layoutSection,'Xicon') 
     164        self.icons_plot=self.cfg.get(layoutSection,'plot') 
     165        self.icons_prn=self.cfg.get(layoutSection, 'printer') 
    153166         
    154         self.disclaimer = cfg.get('DEFAULT', 'disclaimer') 
     167        self.disclaimer = self.cfg.get('DEFAULT', 'disclaimer') 
    155168             
    156169             
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/sso/sso/controllers/login.py

    r3754 r3892  
    11import logging 
    22 
    3 from sso.lib.base import * 
    4 from ndg.security.common.pylons.security_util import setSecuritySession, SecuritySession, \ 
    5     LoginServiceQuery 
     3from ndg.security.server.sso.sso.lib.base import * 
     4from ndg.security.common.pylons.security_util import setSecuritySession, \ 
     5    SecuritySession, SSOServiceQuery 
    66from ndg.security.common.AttAuthority import AttAuthorityClient 
    77from ndg.security.common.SessionMgr import SessionMgrClient, SessionExpired, \ 
     
    2727        if 'ndgSec' not in session:  
    2828            log.debug('No security session details found - offering login...') 
    29             return render('ndg.security.login') 
     29            return render('ndg.security.kid', 'ndg.security.login') 
    3030         
    3131        # Session is set in this domain - check it  
     
    4242            SecuritySession.delete() 
    4343            response.status_code = 400 
    44             return render('ndg.security.login') 
     44            return render('ndg.security.kid', 'ndg.security.login') 
    4545         
    4646        # Check session status 
     
    5555            SecuritySession.delete() 
    5656            response.status_code = 400 
    57             return render('ndg.security.login') 
     57            return render('ndg.security.kid', 'ndg.security.login') 
    5858    
    5959        if bSessOK: 
     
    6666                      "from cookie and re-displaying login...") 
    6767            SecuritySession.delete() 
    68             return render('ndg.security.login') 
     68            return render('ndg.security.kid', 'ndg.security.login') 
    6969 
    7070 
     
    7979        if 'username' not in request.params: 
    8080            log.debug("No username set - rendering login...") 
    81             return render('ndg.security.login') 
     81            return render('ndg.security.kid', 'ndg.security.login') 
    8282         
    8383        try:     
     
    9595            log.error("Login: initialising SessionMgrClient: %s" % e) 
    9696            response.status_code = 400 
    97             return render('ndg.security.login') 
     97            return render('ndg.security.kid', 'ndg.security.login') 
    9898         
    9999        # Connect to Session Manager 
     
    107107            log.error("Session Manager connect returned: %s" % e) 
    108108            response.status_code = 401 
    109             return render('ndg.security.login') 
     109            return render('ndg.security.kid', 'ndg.security.login') 
    110110         
    111111        # Cache user attributes in Session Manager 
     
    119119            c.xml = "Session has expired, please re-login" 
    120120            response.status_code = 401 
    121             return render('ndg.security.login') 
     121            return render('ndg.security.kid', 'ndg.security.login') 
    122122             
    123123        except AttributeRequestDenied, e: 
     
    126126                    "account.  Please check with your site administrator." 
    127127            response.status_code = 401 
    128             return render('ndg.security.login') 
     128            return render('ndg.security.kid', 'ndg.security.login') 
    129129             
    130130        except Exception, e: 
     
    133133                    "your site administrator." 
    134134            response.status_code = 400 
    135             return render('ndg.security.login') 
     135            return render('ndg.security.kid', 'ndg.security.login') 
    136136 
    137137        log.debug('Completing login...') 
     
    145145        session.save() 
    146146 
     147        log.debug("session = %s" % session) 
    147148        log.info("user %s logged in with roles %s" % (session['ndgSec']['u'], 
    148149                                                  session['ndgSec']['roles'])) 
     
    177178                # details into the URL query string 
    178179                if '?' in returnToURL: 
    179                     returnToURL += '&%s' % LoginServiceQuery() 
     180                    returnToURL += '&%s' % SSOServiceQuery() 
    180181                else: 
    181                     returnToURL += '?%s' % LoginServiceQuery() 
     182                    returnToURL += '?%s' % SSOServiceQuery() 
    182183             
    183184            # Check return-to address by examining peer cert 
     
    197198            "Attribute Authority [%s] expecting DN for SSL peer one of: %s" % \ 
    198199                (g.ndg.security.server.ssoservice.cfg.aaURI, requestServerDN)) 
    199             hostCheck=HostCheck(acceptedDNs=requestServerDN, 
    200                     caCertFilePathList=g.ndg.security.server.ssoservice.cfg.sslCACertFilePathList)             
     200             
     201            hostCheck = HostCheck(acceptedDNs=requestServerDN, 
     202                                  caCertFilePathList=\ 
     203                    g.ndg.security.server.ssoservice.cfg.sslCACertFilePathList) 
     204             
    201205            testConnection = HTTPSConnection(returnToURLHostname,  
    202206                                             None,  
     
    213217  Please report this to your site administrator.""" % returnToURLHostname 
    214218                    response.status_code = 400 
    215                     return render('ndg.security.login') 
     219                    return render('ndg.security.kid', 'ndg.security.login') 
    216220            finally:     
    217221                testConnection.close() 
     
    225229        "LoginController._redirect: no redirect URL set - render login page") 
    226230            c.xml='Logged in' 
    227             return render('ndg.security.login') 
     231            return render('ndg.security.kid', 'ndg.security.login') 
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/sso/sso/controllers/logout.py

    r3754 r3892  
    1 from sso.lib.base import * 
     1from ndg.security.server.sso.sso.lib.base import * 
    22from ndg.security.common.pylons.security_util import SecuritySession 
    33import logging 
     
    1616    ''' 
    1717 
    18      
    1918    def index(self): 
    2019        '''Logout - remove session from Session Manager tidy up cookie''' 
     
    7675                log.error("logout - decoding return URL: %s" % e)  
    7776                c.xml = "Error carrying out browser redirect following logout" 
    78                 return render('ndg.security.error') 
     77                return render('ndg.security.kid', 'ndg.security.error') 
    7978             
    8079            # Check for 'getCredentials' - avoid in case username/password 
     
    8685                b64decReturnTo = b64decReturnTo[:getCredentialsIdx] + '/login' 
    8786             
     87            # Add flag indicating to caller that logout succeeded.  The caller 
     88            # can use this to remove any security cookie present in their 
     89            # domain - See: 
     90            # ndg.security.client.ssoclient.ssoclient.lib.base.BaseController 
     91            if '?' in b64decReturnTo: 
     92                b64decReturnTo += '&logout=1' 
     93            else: 
     94                b64decReturnTo += '?logout=1' 
     95 
    8896            # and now go back to whence we had come 
    8997            log.debug("LogoutController._redirect: redirect to %s" % \ 
     
    92100        else: 
    93101            log.debug("LogoutController._redirect: no redirect URL set.") 
    94             return render('ndg.security.error') 
     102            return render('ndg.security.kid', 'ndg.security.error') 
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/sso/sso/controllers/wayf.py

    r3754 r3892  
    11import logging 
    22 
    3 from sso.lib.base import * 
     3from ndg.security.server.sso.sso.lib.base import * 
    44from ndg.security.common.AttAuthority import AttAuthorityClient 
    55import base64 
     
    1515        """For each action, get 'r' return to URL argument from current URL  
    1616        query string.  c.b64encReturnTo is used in some of the .kid files""" 
    17         c.b64encReturnTo = request.params.get('r', '')  
     17        c.b64encReturnTo = str(request.params.get('r', '')) 
    1818        log.debug("WayfController.__before__: c.b64encReturnTo = %s" % \ 
    1919                                                              c.b64encReturnTo) 
     
    5555        session.save() 
    5656         
    57         return render('ndg.security.wayf') 
     57        # Use an alias 'ndg.security.kid' to integration with another pylons 
     58        # code stack.  The alias tells render to pick up the template from a 
     59        # separate SSO templates directory to whatever is the default 
     60        return render('ndg.security.kid', 'ndg.security.wayf') 
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/sso/sso/lib/base.py

    r3754 r3892  
    1111from pylons.templating import render 
    1212 
    13 import sso.lib.helpers as h 
    14 import sso.model as model 
     13import ndg.security.server.sso.sso.lib.helpers as h 
     14import ndg.security.server.sso.sso.model as model 
    1515 
    1616import urllib 
     
    1919 
    2020from ndg.security.common.pylons.security_util import setSecuritySession, \ 
    21     LoginServiceQuery 
     21    SSOServiceQuery 
    2222 
    2323import logging 
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/sso/sso/templates/ndg/security/wayf.kid

    r3754 r3892  
    2121        redirected back to the URL: <a href="${c.returnTo}">${c.returnTo}</a></p> 
    2222                </div> 
    23         <div py:replace="footer()"/> 
     23        <div py:replace="footer(showLoginStatus=False)"/> 
    2424    </body> 
    25      
    26         <div py:def="footer(showLoginStatus=False)" id="Footer"> 
    27         <center><table><tbody> 
    28             <tr> 
    29                 <td align="center" width="60%"> 
    30                     <table><tbody> 
    31                     <tr><td><span py:replace="linkimage(g.ndg.security.server.ssoservice.cfg.ndgLink,g.ndg.security.server.ssoservice.cfg.ndgImage,'NDG')"/></td> 
    32                     <td> This portal is a product of the <a href="http://ndg.nerc.ac.uk"> NERC DataGrid</a> 
    33                     Not all functionality is completely implemented, bugs and problems are expected </td> 
    34                     </tr> 
    35                     </tbody></table> 
    36                 </td> 
    37                 <td width="40%" align="center"> 
    38                     <div id="loginStatus"> 
    39                         <!--! now we choose one of the next two (logged in or not) --> 
    40                         <div py:if="'ndgSec' in session"><table><tbody><tr><td> User [${session['ndgSec']['u']}] logged in 
    41                         at [${session['ndgSec']['h']}] with roles [${session['ndgSec']['roles']}]</td><td> 
    42                         &nbsp;<span py:replace="logOut()"/></td></tr></tbody></table></div> 
    43                         <div py:if="'ndgSec' not in session"></div> 
    44                     </div> 
    45                 </td> 
    46                 <td><span py:replace="linkimage(g.ndg.security.server.ssoservice.cfg.stfcLink,g.ndg.security.server.ssoservice.cfg.stfcImage,'Hosted by the STFC CEDA')"/></td> 
    47             </tr> 
    48         </tbody></table></center> 
    49     </div> 
    5025</html> 
Note: See TracChangeset for help on using the changeset viewer.