############################################################################## # # Copyright (c) 2001 Zope Foundation and Contributors. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """CookieCrumbler tests. """ import unittest import Testing from zope.component import eventtesting from zope.interface.verify import verifyClass from zope.testing.cleanup import cleanUp def makerequest(root, stdout, stdin=None): # Customized version of Testing.makerequest.makerequest() from cStringIO import StringIO from ZPublisher.HTTPRequest import HTTPRequest from ZPublisher.HTTPResponse import HTTPResponse resp = HTTPResponse(stdout=stdout) environ = {} environ['SERVER_NAME'] = 'example.com' environ['SERVER_PORT'] = '80' environ['REQUEST_METHOD'] = 'GET' if stdin is None: stdin = StringIO('') # Empty input req = HTTPRequest(stdin, environ, resp) req['PARENTS'] = [root] return req class CookieCrumblerTests(unittest.TestCase): _CC_ID = 'cookie_authentication' def _getTargetClass(self): from Products.CMFCore.CookieCrumbler import CookieCrumbler return CookieCrumbler def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def setUp(self): from zope.component import provideHandler from zope.component.interfaces import IObjectEvent from Products.CMFCore.interfaces import ICookieCrumbler from Products.CMFCore.CookieCrumbler import handleCookieCrumblerEvent self._finally = None eventtesting.setUp() provideHandler(handleCookieCrumblerEvent, adapts=(ICookieCrumbler, IObjectEvent)) def tearDown(self): from AccessControl.SecurityManagement import noSecurityManager if self._finally is not None: self._finally() noSecurityManager() cleanUp() def _makeSite(self): import base64 from cStringIO import StringIO import urllib try: from OFS.userfolder import UserFolder except ImportError: # BBB for Zope < 2.13 from AccessControl.User import UserFolder from OFS.Folder import Folder from OFS.DTMLMethod import DTMLMethod root = Folder() root.isTopLevelPrincipiaApplicationObject = 1 # User folder needs this root.getPhysicalPath = lambda: () # hack root._View_Permission = ('Anonymous',) users = UserFolder() users._setId('acl_users') users._doAddUser('abraham', 'pass-w', ('Patriarch',), ()) users._doAddUser('isaac', 'pass-w', ('Son',), ()) root._setObject(users.id, users) cc = self._makeOne() cc.id = self._CC_ID root._setObject(cc.id, cc) index = DTMLMethod() index.munge('This is the default view') index._setId('index_html') root._setObject(index.getId(), index) login = DTMLMethod() login.munge('Please log in first.') login._setId('login_form') root._setObject(login.getId(), login) protected = DTMLMethod() protected._View_Permission = ('Manager',) protected.munge('This is the protected view') protected._setId('protected') root._setObject(protected.getId(), protected) req = makerequest(root, StringIO()) self._finally = req.close credentials = urllib.quote( base64.encodestring('abraham:pass-w').rstrip()) return root, cc, req, credentials def test_interfaces(self): from Products.CMFCore.interfaces import ICookieCrumbler verifyClass(ICookieCrumbler, self._getTargetClass()) def testNoCookies(self): # verify the cookie crumbler doesn't break when no cookies are given root, cc, req, credentials = self._makeSite() req.traverse('/') self.assertEqual(req['AUTHENTICATED_USER'].getUserName(), 'Anonymous User') def testCookieLogin(self): # verify the user and auth cookie get set root, cc, req, credentials = self._makeSite() req.cookies['__ac_name'] = 'abraham' req.cookies['__ac_password'] = 'pass-w' req.traverse('/') self.assertTrue(req.has_key('AUTHENTICATED_USER')) self.assertEqual(req['AUTHENTICATED_USER'].getUserName(), 'abraham') resp = req.response self.assertTrue(resp.cookies.has_key('__ac')) self.assertEqual(resp.cookies['__ac']['value'], credentials) self.assertEqual(resp.cookies['__ac']['path'], '/') def testCookieResume(self): # verify the cookie crumbler continues the session root, cc, req, credentials = self._makeSite() req.cookies['__ac'] = credentials req.traverse('/') self.assertTrue(req.has_key('AUTHENTICATED_USER')) self.assertEqual(req['AUTHENTICATED_USER'].getUserName(), 'abraham') def testPasswordShredding(self): # verify the password is shredded before the app gets the request root, cc, req, credentials = self._makeSite() req.cookies['__ac_name'] = 'abraham' req.cookies['__ac_password'] = 'pass-w' self.assertTrue(req.has_key('__ac_password')) req.traverse('/') self.assertFalse( req.has_key('__ac_password')) self.assertFalse( req.has_key('__ac')) def testCredentialsNotRevealed(self): # verify the credentials are shredded before the app gets the request root, cc, req, credentials = self._makeSite() req.cookies['__ac'] = credentials self.assertTrue(req.has_key('__ac')) req.traverse('/') self.assertFalse( req.has_key('__ac')) def testAutoLoginRedirection(self): # Redirect unauthorized anonymous users to the login page from Products.CMFCore.CookieCrumbler import Redirect root, cc, req, credentials = self._makeSite() self.assertRaises(Redirect, req.traverse, '/protected') def testDisabledAutoLoginRedirection(self): # When disable_cookie_login__ is set, don't redirect. from zExceptions.unauthorized import Unauthorized root, cc, req, credentials = self._makeSite() req['disable_cookie_login__'] = 1 self.assertRaises(Unauthorized, req.traverse, '/protected') def testNoRedirectAfterAuthenticated(self): # Don't redirect already-authenticated users to the login page, # even when they try to access things they can't get. from zExceptions.unauthorized import Unauthorized root, cc, req, credentials = self._makeSite() req.cookies['__ac'] = credentials self.assertRaises(Unauthorized, req.traverse, '/protected') def testRetryLogin(self): # After a failed login, CookieCrumbler should give the user an # opportunity to try to log in again. from Products.CMFCore.CookieCrumbler import Redirect root, cc, req, credentials = self._makeSite() req.cookies['__ac_name'] = 'israel' req.cookies['__ac_password'] = 'pass-w' try: req.traverse('/protected') except Redirect, s: # Test passed if hasattr(s, 'args'): s = s.args[0] self.assertTrue(s.find('came_from=') >= 0) self.assertTrue(s.find('retry=1') >= 0) self.assertTrue(s.find('disable_cookie_login__=1') >= 0) else: self.fail('Did not redirect') def testLoginRestoresQueryString(self): # When redirecting for login, the came_from form field should # include the submitted URL as well as the query string. import urllib from Products.CMFCore.CookieCrumbler import Redirect root, cc, req, credentials = self._makeSite() req['PATH_INFO'] = '/protected' req['QUERY_STRING'] = 'a:int=1&x:string=y' try: req.traverse('/protected') except Redirect, s: if hasattr(s, 'args'): s = s.args[0] to_find = urllib.quote('/protected?' + req['QUERY_STRING']) self.assertTrue(s.find(to_find) >= 0, s) else: self.fail('Did not redirect') def testCacheHeaderAnonymous(self): # Should not set cache-control root, cc, req, credentials = self._makeSite() req.traverse('/') self.assertEqual( req.response.headers.get('cache-control', ''), '') def testCacheHeaderLoggingIn(self): # Should set cache-control root, cc, req, credentials = self._makeSite() req.cookies['__ac_name'] = 'abraham' req.cookies['__ac_password'] = 'pass-w' req.traverse('/') self.assertEqual( req.response.headers.get('cache-control', ''), 'private') def testCacheHeaderAuthenticated(self): # Should set cache-control root, cc, req, credentials = self._makeSite() req.cookies['__ac'] = credentials req.traverse('/') self.assertEqual( req.response.headers.get('cache-control', ''), 'private') def testCacheHeaderDisabled(self): # Should not set cache-control root, cc, req, credentials = self._makeSite() cc.cache_header_value = '' req.cookies['__ac'] = credentials req.traverse('/') self.assertEqual( req.response.headers.get('cache-control', ''), '') def testDisableLoginDoesNotPreventPasswordShredding(self): # Even if disable_cookie_login__ is set, read the cookies # anyway to avoid revealing the password to the app. # (disable_cookie_login__ does not mean disable cookie # authentication, it only means disable the automatic redirect # to the login page.) root, cc, req, credentials = self._makeSite() req.cookies['__ac_name'] = 'abraham' req.cookies['__ac_password'] = 'pass-w' req['disable_cookie_login__'] = 1 req.traverse('/') self.assertEqual(req['AUTHENTICATED_USER'].getUserName(), 'abraham') # Here is the real test: the password should have been shredded. self.assertFalse( req.has_key('__ac_password')) def testDisableLoginDoesNotPreventPasswordShredding2(self): root, cc, req, credentials = self._makeSite() req.cookies['__ac'] = credentials req['disable_cookie_login__'] = 1 req.traverse('/') self.assertEqual(req['AUTHENTICATED_USER'].getUserName(), 'abraham') self.assertFalse( req.has_key('__ac')) def testMidApplicationAutoLoginRedirection(self): # Redirect anonymous users to login page if Unauthorized # occurs in the middle of the app from zExceptions.unauthorized import Unauthorized root, cc, req, credentials = self._makeSite() req.traverse('/') try: raise Unauthorized except: req.response.exception() self.assertEqual(req.response.status, 302) def testMidApplicationAuthenticationButUnauthorized(self): # Don't redirect already-authenticated users to the login page, # even when Unauthorized happens in the middle of the app. from zExceptions.unauthorized import Unauthorized root, cc, req, credentials = self._makeSite() req.cookies['__ac'] = credentials req.traverse('/') try: raise Unauthorized except: req.response.exception() self.assertEqual(req.response.status, 401) def testRedirectOnUnauthorized(self): # Redirect already-authenticated users to the unauthorized # handler page if that's what the sysadmin really wants. from Products.CMFCore.CookieCrumbler import Redirect root, cc, req, credentials = self._makeSite() cc.unauth_page = 'login_form' req.cookies['__ac'] = credentials self.assertRaises(Redirect, req.traverse, '/protected') def testLoginRatherThanResume(self): # When the user presents both a session resume and new # credentials, choose the new credentials (so that it's # possible to log in without logging out) root, cc, req, credentials = self._makeSite() req.cookies['__ac_name'] = 'isaac' req.cookies['__ac_password'] = 'pass-w' req.cookies['__ac'] = credentials req.traverse('/') self.assertTrue(req.has_key('AUTHENTICATED_USER')) self.assertEqual(req['AUTHENTICATED_USER'].getUserName(), 'isaac') def testCreateForms(self): # Verify the factory creates the login forms. from Products.CMFCore.CookieCrumbler import manage_addCC if 'CMFCore' in self._getTargetClass().__module__: # This test is disabled in CMFCore. return root, cc, req, credentials = self._makeSite() root._delObject('cookie_authentication') manage_addCC(root, 'login', create_forms=1) ids = root.login.objectIds() ids.sort() self.assertEqual(tuple(ids), ( 'index_html', 'logged_in', 'logged_out', 'login_form', 'standard_login_footer', 'standard_login_header')) def test_before_traverse_hooks(self): from OFS.Folder import Folder container = Folder() cc = self._makeOne() cc._setId(self._CC_ID) marker = [] bt_before = getattr(container, '__before_traverse__', marker) self.assertTrue(bt_before is marker) container._setObject(self._CC_ID, cc) bt_added = getattr(container, '__before_traverse__') self.assertEqual(len(bt_added.items()), 1) k, v = bt_added.items()[0] self.assertTrue(k[1].startswith(self._getTargetClass().meta_type)) self.assertEqual(v.name, self._CC_ID) container._delObject(self._CC_ID) bt_removed = getattr(container, '__before_traverse__') self.assertEqual(len(bt_removed.items()), 0) def test_suite(): return unittest.TestSuite(( unittest.makeSuite(CookieCrumblerTests), )) if __name__ == '__main__': unittest.main(defaultTest='test_suite')