""".strip ())
#
# SECURITY
#
# XXX test all default permissions
def _make_client(self, form, classname='user', nodeid='1',
userid='2', template='item'):
cl = client.Client(self.instance, None, {'PATH_INFO':'/',
'REQUEST_METHOD':'POST'}, makeForm(form))
cl.classname = classname
if nodeid is not None:
cl.nodeid = nodeid
cl.db = self.db
cl.userid = userid
cl.language = ('en',)
cl._error_message = []
cl._ok_message = []
cl.template = template
return cl
def testClassPermission(self):
cl = self._make_client(dict(username='bob'))
self.failUnlessRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl.nodeid = '1'
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
def testCheckAndPropertyPermission(self):
self.db.security.permissions = {}
def own_record(db, userid, itemid):
return userid == itemid
p = self.db.security.addPermission(name='Edit', klass='user',
check=own_record, properties=("password", ))
self.db.security.addPermissionToRole('User', p)
cl = self._make_client(dict(username='bob'))
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'), userid='4', nodeid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'), userid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'))
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
# working example, mary may change her pw
cl = self._make_client({'password':'ob', '@confirm@password':'ob'},
nodeid='4', userid='4')
self.assertRaises(exceptions.Redirect,
actions.EditItemAction(cl).handle)
cl = self._make_client({'password':'bob', '@confirm@password':'bob'})
self.failUnlessRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
def testCreatePermission(self):
# this checks if we properly differentiate between create and
# edit permissions
self.db.security.permissions = {}
self.db.security.addRole(name='UserAdd')
# Don't allow roles
p = self.db.security.addPermission(name='Create', klass='user',
properties=("username", "password", "address",
"alternate_address", "realname", "phone", "organisation",
"timezone"))
self.db.security.addPermissionToRole('UserAdd', p)
# Don't allow roles *and* don't allow username
p = self.db.security.addPermission(name='Edit', klass='user',
properties=("password", "address", "alternate_address",
"realname", "phone", "organisation", "timezone"))
self.db.security.addPermissionToRole('UserAdd', p)
self.db.user.set('4', roles='UserAdd')
# anonymous may not
cl = self._make_client({'username':'new_user', 'password':'secret',
'@confirm@password':'secret', 'address':'new_user@bork.bork',
'roles':'Admin'}, nodeid=None, userid='2')
self.assertRaises(exceptions.Unauthorised,
actions.NewItemAction(cl).handle)
# Don't allow creating new user with roles
cl = self._make_client({'username':'new_user', 'password':'secret',
'@confirm@password':'secret', 'address':'new_user@bork.bork',
'roles':'Admin'}, nodeid=None, userid='4')
self.assertRaises(exceptions.Unauthorised,
actions.NewItemAction(cl).handle)
self.assertEqual(cl._error_message,[])
# this should work
cl = self._make_client({'username':'new_user', 'password':'secret',
'@confirm@password':'secret', 'address':'new_user@bork.bork'},
nodeid=None, userid='4')
self.assertRaises(exceptions.Redirect,
actions.NewItemAction(cl).handle)
self.assertEqual(cl._error_message,[])
# don't allow changing (my own) username (in this example)
cl = self._make_client(dict(username='new_user42'), userid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(username='new_user42'), userid='4',
nodeid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
# don't allow changing (my own) roles
cl = self._make_client(dict(roles='User,Admin'), userid='4',
nodeid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'), userid='4')
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
cl = self._make_client(dict(roles='User,Admin'))
self.assertRaises(exceptions.Unauthorised,
actions.EditItemAction(cl).handle)
def testSearchPermission(self):
# this checks if we properly check for search permissions
self.db.security.permissions = {}
self.db.security.addRole(name='User')
self.db.security.addRole(name='Project')
self.db.security.addPermissionToRole('User', 'Web Access')
self.db.security.addPermissionToRole('Project', 'Web Access')
# Allow viewing department
p = self.db.security.addPermission(name='View', klass='department')
self.db.security.addPermissionToRole('User', p)
# Allow viewing interesting things (but not department) on iss
# But users might only view issues where they are on nosy
# (so in the real world the check method would be better)
p = self.db.security.addPermission(name='View', klass='iss',
properties=("title", "status"), check=lambda x,y,z: True)
self.db.security.addPermissionToRole('User', p)
# Allow all relevant roles access to stat
p = self.db.security.addPermission(name='View', klass='stat')
self.db.security.addPermissionToRole('User', p)
self.db.security.addPermissionToRole('Project', p)
# Allow role "Project" access to whole iss
p = self.db.security.addPermission(name='View', klass='iss')
self.db.security.addPermissionToRole('Project', p)
department = self.instance.backend.Class(self.db, "department",
name=hyperdb.String())
status = self.instance.backend.Class(self.db, "stat",
name=hyperdb.String())
issue = self.instance.backend.Class(self.db, "iss",
title=hyperdb.String(), status=hyperdb.Link('stat'),
department=hyperdb.Link('department'))
d1 = department.create(name='d1')
d2 = department.create(name='d2')
open = status.create(name='open')
closed = status.create(name='closed')
issue.create(title='i1', status=open, department=d2)
issue.create(title='i2', status=open, department=d1)
issue.create(title='i2', status=closed, department=d1)
chef = self.db.user.lookup('Chef')
mary = self.db.user.lookup('mary')
self.db.user.set(chef, roles = 'User, Project')
perm = self.db.security.hasPermission
search = self.db.security.hasSearchPermission
self.assert_(perm('View', chef, 'iss', 'department', '1'))
self.assert_(perm('View', chef, 'iss', 'department', '2'))
self.assert_(perm('View', chef, 'iss', 'department', '3'))
self.assert_(search(chef, 'iss', 'department'))
self.assert_(not perm('View', mary, 'iss', 'department'))
self.assert_(perm('View', mary, 'iss', 'status'))
# Conditionally allow view of whole iss (check is False here,
# this might check for department owner in the real world)
p = self.db.security.addPermission(name='View', klass='iss',
check=lambda x,y,z: False)
self.db.security.addPermissionToRole('User', p)
self.assert_(perm('View', mary, 'iss', 'department'))
self.assert_(not perm('View', mary, 'iss', 'department', '1'))
self.assert_(not search(mary, 'iss', 'department'))
self.assert_(perm('View', mary, 'iss', 'status'))
self.assert_(not search(mary, 'iss', 'status'))
# Allow user to search for iss.status
p = self.db.security.addPermission(name='Search', klass='iss',
properties=("status",))
self.db.security.addPermissionToRole('User', p)
self.assert_(search(mary, 'iss', 'status'))
dep = {'@action':'search','columns':'id','@filter':'department',
'department':'1'}
stat = {'@action':'search','columns':'id','@filter':'status',
'status':'1'}
depsort = {'@action':'search','columns':'id','@sort':'department'}
depgrp = {'@action':'search','columns':'id','@group':'department'}
# Filter on department ignored for role 'User':
cl = self._make_client(dep, classname='iss', nodeid=None, userid=mary,
template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
# Filter on department works for role 'Project':
cl = self._make_client(dep, classname='iss', nodeid=None, userid=chef,
template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['2', '3'])
# Filter on status works for all:
cl = self._make_client(stat, classname='iss', nodeid=None, userid=mary,
template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2'])
cl = self._make_client(stat, classname='iss', nodeid=None, userid=chef,
template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2'])
# Sorting and grouping for class Project works:
cl = self._make_client(depsort, classname='iss', nodeid=None,
userid=chef, template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
self.assertEqual(cl._error_message, []) # test for empty _error_message when sort is valid
self.assertEqual(cl._ok_message, []) # test for empty _ok_message when sort is valid
# Test for correct _error_message for invalid sort/group properties
baddepsort = {'@action':'search','columns':'id','@sort':'dep'}
baddepgrp = {'@action':'search','columns':'id','@group':'dep'}
cl = self._make_client(baddepsort, classname='iss', nodeid=None,
userid=chef, template='index')
h = HTMLRequest(cl)
self.assertEqual(cl._error_message, ['Unknown sort property dep'])
cl = self._make_client(baddepgrp, classname='iss', nodeid=None,
userid=chef, template='index')
h = HTMLRequest(cl)
self.assertEqual(cl._error_message, ['Unknown group property dep'])
cl = self._make_client(depgrp, classname='iss', nodeid=None,
userid=chef, template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['2', '3', '1'])
# Sorting and grouping for class User fails:
cl = self._make_client(depsort, classname='iss', nodeid=None,
userid=mary, template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
cl = self._make_client(depgrp, classname='iss', nodeid=None,
userid=mary, template='index')
h = HTMLRequest(cl)
self.assertEqual([x.id for x in h.batch()],['1', '2', '3'])
def testEditCSV(self):
form = dict(rows='id,name\n1,newkey')
cl = self._make_client(form, userid='1', classname='keyword')
cl._ok_message = []
actions.EditCSVAction(cl).handle()
self.assertEqual(cl._ok_message, ['Items edited OK'])
k = self.db.keyword.getnode('1')
self.assertEqual(k.name, 'newkey')
form = dict(rows=u'id,name\n1,\xe4\xf6\xfc'.encode('utf-8'))
cl = self._make_client(form, userid='1', classname='keyword')
cl._ok_message = []
actions.EditCSVAction(cl).handle()
self.assertEqual(cl._ok_message, ['Items edited OK'])
k = self.db.keyword.getnode('1')
self.assertEqual(k.name, u'\xe4\xf6\xfc'.encode('utf-8'))
def testRoles(self):
cl = self._make_client({})
self.db.user.set('1', roles='aDmin, uSer')
item = HTMLItem(cl, 'user', '1')
self.assert_(item.hasRole('Admin'))
self.assert_(item.hasRole('User'))
self.assert_(item.hasRole('AdmiN'))
self.assert_(item.hasRole('UseR'))
self.assert_(item.hasRole('UseR','Admin'))
self.assert_(item.hasRole('UseR','somethingelse'))
self.assert_(item.hasRole('somethingelse','Admin'))
self.assert_(not item.hasRole('userr'))
self.assert_(not item.hasRole('adminn'))
self.assert_(not item.hasRole(''))
self.assert_(not item.hasRole(' '))
self.db.user.set('1', roles='')
self.assert_(not item.hasRole(''))
def testCSVExport(self):
cl = self._make_client({'@columns': 'id,name'}, nodeid=None,
userid='1')
cl.classname = 'status'
output = StringIO.StringIO()
cl.request = MockNull()
cl.request.wfile = output
actions.ExportCSVAction(cl).handle()
self.assertEquals('id,name\r\n1,unread\r\n2,deferred\r\n3,chatting\r\n'
'4,need-eg\r\n5,in-progress\r\n6,testing\r\n7,done-cbb\r\n'
'8,resolved\r\n',
output.getvalue())
def testCSVExportBadColumnName(self):
cl = self._make_client({'@columns': 'falseid,name'}, nodeid=None,
userid='1')
cl.classname = 'status'
output = StringIO.StringIO()
cl.request = MockNull()
cl.request.wfile = output
self.assertRaises(exceptions.NotFound,
actions.ExportCSVAction(cl).handle)
def testCSVExportFailPermissionBadColumn(self):
cl = self._make_client({'@columns': 'id,email,password'}, nodeid=None,
userid='2')
cl.classname = 'user'
output = StringIO.StringIO()
cl.request = MockNull()
cl.request.wfile = output
# used to be self.assertRaises(exceptions.Unauthorised,
# but not acting like the column name is not found
# see issue2550755 - should this return Unauthorised?
# The unauthorised user should never get to the point where
# they can determine if the column name is valid or not.
self.assertRaises(exceptions.NotFound,
actions.ExportCSVAction(cl).handle)
def testCSVExportFailPermissionValidColumn(self):
cl = self._make_client({'@columns': 'id,address,password'}, nodeid=None,
userid='2')
cl.classname = 'user'
output = StringIO.StringIO()
cl.request = MockNull()
cl.request.wfile = output
# used to be self.assertRaises(exceptions.Unauthorised,
# but not acting like the column name is not found
self.assertRaises(exceptions.Unauthorised,
actions.ExportCSVAction(cl).handle)
class TemplateHtmlRendering(unittest.TestCase):
''' try to test the rendering code for tal '''
def setUp(self):
self.dirname = '_test_template'
# set up and open a tracker
self.instance = db_test_base.setupTracker(self.dirname)
# open the database
self.db = self.instance.open('admin')
self.db.tx_Source = "web"
self.db.user.create(username='Chef', address='chef@bork.bork.bork',
realname='Bork, Chef', roles='User')
self.db.user.create(username='mary', address='mary@test.test',
roles='User', realname='Contrary, Mary')
self.db.post_init()
# create a client instance and hijack write_html
self.client = client.Client(self.instance, "user",
{'PATH_INFO':'/user', 'REQUEST_METHOD':'POST'},
form=makeForm({"@template": "item"}))
self.client._error_message = []
self.client._ok_message = []
self.client.db = self.db
self.client.userid = '1'
self.client.language = ('en',)
self.output = []
# ugly hack to get html_write to return data here.
def html_write(s):
self.output.append(s)
# hijack html_write
self.client.write_html = html_write
self.db.issue.create(title='foo')
def tearDown(self):
self.db.close()
try:
shutil.rmtree(self.dirname)
except OSError, error:
if error.errno not in (errno.ENOENT, errno.ESRCH): raise
def testrenderFrontPage(self):
self.client.renderFrontPage("hello world RaNdOmJunk")
# make sure we can find the "hello world RaNdOmJunk"
# message in the output.
self.assertNotEqual(-1,
self.output[0].index('
hello world RaNdOmJunk
'))
# make sure we can find issue 1 title foo in the output
self.assertNotEqual(-1,
self.output[0].index('foo'))
# make sure we can find the last SHA1 sum line at the end of the
# page
self.assertNotEqual(-1,
self.output[0].index(''))
def testrenderContext(self):
# set up the client;
# run determine_context to set the required client attributes
# run renderContext(); check result for proper page
# this will generate the default home page like
# testrenderFrontPage
self.client.form=makeForm({})
self.client.path = ''
self.client.determine_context()
self.assertEqual((self.client.classname, self.client.template, self.client.nodeid), (None, '', None))
self.assertEqual(self.client._ok_message, [])
result = self.client.renderContext()
self.assertNotEqual(-1,
result.index(''))
# now look at the user index page
self.client.form=makeForm({ "@ok_message": "ok message", "@template": "index"})
self.client.path = 'user'
self.client.determine_context()
self.assertEqual((self.client.classname, self.client.template, self.client.nodeid), ('user', 'index', None))
self.assertEqual(self.client._ok_message, ['ok message'])
result = self.client.renderContext()
self.assertNotEqual(-1, result.index('User listing - Roundup issue tracker'))
self.assertNotEqual(-1, result.index('ok message'))
# print result
def testRenderAltTemplates(self):
# check that right page is returned when rendering
# @template=oktempl|errortmpl
# set up the client;
# run determine_context to set the required client attributes
# run renderContext(); check result for proper page
# Test ok state template that uses user.forgotten.html
self.client.form=makeForm({"@template": "forgotten|item"})
self.client.path = 'user'
self.client.determine_context()
self.assertEqual((self.client.classname, self.client.template, self.client.nodeid), ('user', 'forgotten|item', None))
self.assertEqual(self.client._ok_message, [])
result = self.client.renderContext()
self.assertNotEqual(-1,
result.index(''))
# now set an error in the form to get error template user.item.html
self.client.form=makeForm({"@template": "forgotten|item",
"@error_message": "this is an error"})
self.client.path = 'user'
self.client.determine_context()
self.assertEqual((self.client.classname, self.client.template, self.client.nodeid), ('user', 'forgotten|item', None))
self.assertEqual(self.client._ok_message, [])
self.assertEqual(self.client._error_message, ["this is an error"])
result = self.client.renderContext()
print result
self.assertNotEqual(-1,
result.index(''))
def testexamine_url(self):
''' test the examine_url function '''
def te(url, exception, raises=ValueError):
with self.assertRaises(raises) as cm:
examine_url(url)
self.assertEqual(cm.exception.message, exception)
action = actions.Action(self.client)
examine_url = action.examine_url
# Christmas tree url: test of every component that passes
self.assertEqual(
examine_url("http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue"),
'http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue')
# allow replacing http with https if base is http
self.assertEqual(
examine_url("https://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue"),
'https://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue')
# change base to use https and make sure we don't redirect to http
saved_base = action.base
action.base = "https://tracker.example/cgi-bin/roundup.cgi/bugs/"
te("http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue",
'Base url https://tracker.example/cgi-bin/roundup.cgi/bugs/ requires https. Redirect url http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue uses http.')
action.base = saved_base
# url doesn't have to be valid to roundup, just has to be contained
# inside of roundup. No zoik class is defined
self.assertEqual(examine_url("http://tracker.example/cgi-bin/roundup.cgi/bugs/zoik7;parm=bar?@template=foo&parm=(zot)#issue"), "http://tracker.example/cgi-bin/roundup.cgi/bugs/zoik7;parm=bar?@template=foo&parm=(zot)#issue")
# test with wonky schemes
te("email://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue",
'Unrecognized scheme in email://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue')
te("http%3a//tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Unrecognized scheme in http%3a//tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue')
# test different netloc/path prefix
# assert port
te("http://tracker.example:1025/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue",'Net location in http://tracker.example:1025/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue does not match base: tracker.example')
#assert user
te("http://user@tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Net location in http://user@tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue does not match base: tracker.example')
#assert user:password
te("http://user:pass@tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Net location in http://user:pass@tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue does not match base: tracker.example')
# try localhost http scheme
te("http://localhost/cgi-bin/roundup.cgi/bugs/user3", 'Net location in http://localhost/cgi-bin/roundup.cgi/bugs/user3 does not match base: tracker.example')
# try localhost https scheme
te("https://localhost/cgi-bin/roundup.cgi/bugs/user3", 'Net location in https://localhost/cgi-bin/roundup.cgi/bugs/user3 does not match base: tracker.example')
# try different host
te("http://bad.guys.are.us/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Net location in http://bad.guys.are.us/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#issue does not match base: tracker.example')
# change the base path to .../bug from .../bugs
te("http://tracker.example/cgi-bin/roundup.cgi/bug/user3;parm=bar?@template=foo&parm=(zot)#issue", 'Base path /cgi-bin/roundup.cgi/bugs/ is not a prefix for url http://tracker.example/cgi-bin/roundup.cgi/bug/user3;parm=bar?@template=foo&parm=(zot)#issue')
# change the base path eliminate - in cgi-bin
te("http://tracker.example/cgibin/roundup.cgi/bug/user3;parm=bar?@template=foo&parm=(zot)#issue",'Base path /cgi-bin/roundup.cgi/bugs/ is not a prefix for url http://tracker.example/cgibin/roundup.cgi/bug/user3;parm=bar?@template=foo&parm=(zot)#issue')
# scan for unencoded characters
# we skip schema and net location since unencoded character
# are allowed only by an explicit match to a reference.
#
# break components with unescaped character '<'
# path component
te("http://tracker.example/cgi-bin/roundup.cgi/bugs/&parm=(zot)#issue", 'Query component (@template=&parm=(zot)) in http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=&parm=(zot)#issue is not properly escaped')
# fragment component
te("http://tracker.example/cgi-bin/roundup.cgi/bugs/user3;parm=bar?@template=foo&parm=(zot)#iss