Skip to content

Commit 0012e75

Browse files
author
Richard Jones
committed
Session storage in the hyperdb was horribly, horribly inefficient.
We use a simple anydbm wrapper now - which could be overridden by the metakit backend or RDB backend if necessary. Much, much better.
1 parent 1e44c72 commit 0012e75

File tree

4 files changed

+165
-49
lines changed

4 files changed

+165
-49
lines changed

roundup/backends/back_anydbm.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
#$Id: back_anydbm.py,v 1.54 2002-07-26 08:26:59 richard Exp $
18+
#$Id: back_anydbm.py,v 1.55 2002-07-30 08:22:38 richard Exp $
1919
'''
2020
This module defines a backend that saves the hyperdatabase in a database
2121
chosen by anydbm. It is guaranteed to always be available in python
@@ -26,6 +26,7 @@
2626
import whichdb, anydbm, os, marshal, re, weakref, string, copy
2727
from roundup import hyperdb, date, password, roundupdb, security
2828
from blobfiles import FileStorage
29+
from sessions import Sessions
2930
from roundup.indexer import Indexer
3031
from locking import acquire_lock, release_lock
3132
from roundup.hyperdb import String, Password, Date, Interval, Link, \
@@ -66,6 +67,7 @@ def __init__(self, config, journaltag=None):
6667
self.destroyednodes = {}# keep track of the destroyed nodes by class
6768
self.transactions = []
6869
self.indexer = Indexer(self.dir)
70+
self.sessions = Sessions(self.config)
6971
self.security = security.Security(self)
7072
# ensure files are group readable and writable
7173
os.umask(0002)
@@ -1776,6 +1778,11 @@ def __init__(self, db, classname, **properties):
17761778

17771779
#
17781780
#$Log: not supported by cvs2svn $
1781+
#Revision 1.54 2002/07/26 08:26:59 richard
1782+
#Very close now. The cgi and mailgw now use the new security API. The two
1783+
#templates have been migrated to that setup. Lots of unit tests. Still some
1784+
#issue in the web form for editing Roles assigned to users.
1785+
#
17791786
#Revision 1.53 2002/07/25 07:14:06 richard
17801787
#Bugger it. Here's the current shape of the new security implementation.
17811788
#Still to do:

roundup/backends/back_metakit.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from roundup import hyperdb, date, password, roundupdb
1+
from roundup import hyperdb, date, password, roundupdb, security
22
import metakit
3+
from sessions import Sessions
34
import re, marshal, os, sys, weakref, time, calendar
45
from roundup import indexer
56

@@ -12,13 +13,15 @@ def __init__(self, config, journaltag=None):
1213
self.dirty = 0
1314
self._db = self.__open()
1415
self.indexer = Indexer(self.config.DATABASE, self._db)
16+
self.sessions = Sessions(self.config)
17+
self.security = security.Security(self)
18+
1519
os.umask(0002)
1620
def post_init(self):
1721
if self.indexer.should_reindex():
1822
self.reindex()
1923

2024
def reindex(self):
21-
print "Reindexing!!!"
2225
for klass in self.classes.values():
2326
for nodeid in klass.list():
2427
klass.index(nodeid)

roundup/backends/sessions.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#$Id: sessions.py,v 1.1 2002-07-30 08:22:38 richard Exp $
2+
'''
3+
This module defines a very basic store that's used by the CGI interface
4+
to store session information.
5+
'''
6+
7+
import anydbm, whichdb, os, marshal
8+
9+
class Sessions:
10+
''' Back onto an anydbm store.
11+
12+
Keys are session id strings, values are marshalled data.
13+
'''
14+
def __init__(self, config):
15+
self.config = config
16+
self.dir = config.DATABASE
17+
# ensure files are group readable and writable
18+
os.umask(0002)
19+
20+
def clear(self):
21+
path = os.path.join(self.dir, 'sessions')
22+
if os.path.exists(path):
23+
os.remove(path)
24+
elif os.path.exists(path+'.db'): # dbm appends .db
25+
os.remove(path+'.db')
26+
27+
def determine_db_type(self, path):
28+
''' determine which DB wrote the class file
29+
'''
30+
db_type = ''
31+
if os.path.exists(path):
32+
db_type = whichdb.whichdb(path)
33+
if not db_type:
34+
raise hyperdb.DatabaseError, "Couldn't identify database type"
35+
elif os.path.exists(path+'.db'):
36+
# if the path ends in '.db', it's a dbm database, whether
37+
# anydbm says it's dbhash or not!
38+
db_type = 'dbm'
39+
return db_type
40+
41+
def get(self, sessionid, value):
42+
db = self.opendb('c')
43+
try:
44+
if db.has_key(sessionid):
45+
values = marshal.loads(db[sessionid])
46+
else:
47+
return None
48+
return values.get(value, None)
49+
finally:
50+
db.close()
51+
52+
def set(self, sessionid, **newvalues):
53+
db = self.opendb('c')
54+
try:
55+
if db.has_key(sessionid):
56+
values = marshal.loads(db[sessionid])
57+
else:
58+
values = {}
59+
values.update(newvalues)
60+
db[sessionid] = marshal.dumps(values)
61+
finally:
62+
db.close()
63+
64+
def list(self):
65+
db = self.opendb('r')
66+
try:
67+
return db.keys()
68+
finally:
69+
db.close()
70+
71+
def destroy(self, sessionid):
72+
db = self.opendb('c')
73+
try:
74+
if db.has_key(sessionid):
75+
del db[sessionid]
76+
finally:
77+
db.close()
78+
79+
def opendb(self, mode):
80+
'''Low-level database opener that gets around anydbm/dbm
81+
eccentricities.
82+
'''
83+
# figure the class db type
84+
path = os.path.join(os.getcwd(), self.dir, 'sessions')
85+
db_type = self.determine_db_type(path)
86+
87+
# new database? let anydbm pick the best dbm
88+
if not db_type:
89+
return anydbm.open(path, 'n')
90+
91+
# open the database with the correct module
92+
dbm = __import__(db_type)
93+
return dbm.open(path, mode)
94+
95+
def commit(self):
96+
pass
97+
98+
#
99+
#$Log: not supported by cvs2svn $
100+
#
101+
#

roundup/cgi_client.py

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717
#
18-
# $Id: cgi_client.py,v 1.146 2002-07-30 05:27:30 richard Exp $
18+
# $Id: cgi_client.py,v 1.147 2002-07-30 08:22:38 richard Exp $
1919

2020
__doc__ = """
2121
WWW request handler (also used in the stand-alone server).
@@ -1197,6 +1197,31 @@ def classes(self, message=None):
11971197
self.write('</table>')
11981198
self.pagefoot()
11991199

1200+
def unauthorised(self, message):
1201+
''' The user is not authorised to do something. If they're
1202+
anonymous, throw up a login box. If not, just tell them they
1203+
can't do whatever it was they were trying to do.
1204+
1205+
Bot cases print up the message, which is most likely the
1206+
argument to the Unauthorised exception.
1207+
'''
1208+
self.header(response=403)
1209+
if self.desired_action is None or self.desired_action == 'login':
1210+
if not message:
1211+
message=_("You do not have permission.")
1212+
action = 'index'
1213+
else:
1214+
if not message:
1215+
message=_("You do not have permission to access"\
1216+
" %(action)s.")%{'action': self.desired_action}
1217+
action = self.desired_action
1218+
if self.user == 'anonymous':
1219+
self.login(action=action, message=message)
1220+
else:
1221+
self.pagehead(_('Not Authorised'))
1222+
self.write('<p class="system-msg">%s</p>'%message)
1223+
self.pagefoot()
1224+
12001225
def login(self, message=None, newuser_form=None, action='index'):
12011226
'''Display a login page.
12021227
'''
@@ -1331,19 +1356,17 @@ def newuser_action(self, message=None):
13311356

13321357
def set_cookie(self, user, password):
13331358
# TODO generate a much, much stronger session key ;)
1334-
session = binascii.b2a_base64(repr(time.time())).strip()
1359+
self.session = binascii.b2a_base64(repr(time.time())).strip()
13351360

13361361
# clean up the base64
1337-
if session[-1] == '=':
1338-
if session[-2] == '=':
1339-
session = session[:-2]
1340-
else:
1341-
session = session[:-1]
1362+
if self.session[-1] == '=':
1363+
if self.session[-2] == '=':
1364+
self.session = self.session[:-2]
1365+
else:
1366+
self.session = self.session[:-1]
13421367

13431368
# insert the session in the sessiondb
1344-
sessions = self.db.getclass('__sessions')
1345-
self.session = sessions.create(sessid=session, user=user,
1346-
last_use=date.Date())
1369+
self.db.sessions.set(self.session, user=user, last_use=time.time())
13471370

13481371
# and commit immediately
13491372
self.db.commit()
@@ -1355,10 +1378,10 @@ def set_cookie(self, user, password):
13551378
path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
13561379
''))
13571380
self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;'%(
1358-
session, expire, path)})
1381+
self.session, expire, path)})
13591382

13601383
def make_user_anonymous(self):
1361-
''' Make use anonymous
1384+
''' Make us anonymous
13621385
13631386
This method used to handle non-existence of the 'anonymous'
13641387
user, but that user is mandatory now.
@@ -1367,7 +1390,10 @@ def make_user_anonymous(self):
13671390
self.user = 'anonymous'
13681391

13691392
def logout(self, message=None):
1393+
''' Make us really anonymous - nuke the cookie too
1394+
'''
13701395
self.make_user_anonymous()
1396+
13711397
# construct the logout cookie
13721398
now = Cookie._getdate()
13731399
path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
@@ -1383,37 +1409,14 @@ def opendb(self, user):
13831409
# open the db
13841410
self.db = self.instance.open(user)
13851411

1386-
# make sure we have the session Class
1387-
try:
1388-
sessions = self.db.getclass('__sessions')
1389-
except:
1390-
# add the sessions Class - use a non-journalling Class
1391-
# TODO: not happy with how we're getting the Class here :(
1392-
sessions = self.instance.dbinit.Class(self.db, '__sessions',
1393-
sessid=hyperdb.String(), user=hyperdb.String(),
1394-
last_use=hyperdb.Date())
1395-
sessions.setkey('sessid')
1396-
# make sure session db isn't journalled
1397-
sessions.disableJournalling()
1398-
13991412
def main(self):
14001413
''' Wrap the request and handle unauthorised requests
14011414
'''
14021415
self.desired_action = None
14031416
try:
14041417
self.main_action()
14051418
except Unauthorised, message:
1406-
self.header(response=403)
1407-
if self.desired_action is None or self.desired_action == 'login':
1408-
if not message:
1409-
message=_("You do not have permission.")
1410-
# go to the index after login
1411-
self.login(message=message)
1412-
else:
1413-
if not message:
1414-
message=_("You do not have permission to access"\
1415-
" %(action)s.")%{'action': self.desired_action}
1416-
self.login(action=self.desired_action, message=message)
1419+
self.unauthorised(message)
14171420

14181421
def main_action(self):
14191422
'''Wrap the database accesses so we can close the database cleanly
@@ -1422,12 +1425,12 @@ def main_action(self):
14221425
self.opendb('admin')
14231426

14241427
# make sure we have the session Class
1425-
sessions = self.db.getclass('__sessions')
1428+
sessions = self.db.sessions
14261429

14271430
# age sessions, remove when they haven't been used for a week
14281431
# TODO: this shouldn't be done every access
1429-
week = date.Interval('7d')
1430-
now = date.Date()
1432+
week = 60*60*24*7
1433+
now = time.time()
14311434
for sessid in sessions.list():
14321435
interval = now - sessions.get(sessid, 'last_use')
14331436
if interval > week:
@@ -1436,22 +1439,21 @@ def main_action(self):
14361439
# look up the user session cookie
14371440
cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
14381441
user = 'anonymous'
1442+
14391443
if (cookie.has_key('roundup_user') and
14401444
cookie['roundup_user'].value != 'deleted'):
14411445

14421446
# get the session key from the cookie
1443-
session = cookie['roundup_user'].value
1447+
self.session = cookie['roundup_user'].value
14441448

14451449
# get the user from the session
14461450
try:
1447-
self.session = sessions.lookup(session)
1451+
# update the lifetime datestamp
1452+
sessions.set(self.session, last_use=time.time())
1453+
sessions.commit()
1454+
user = sessions.get(self.session, 'user')
14481455
except KeyError:
14491456
user = 'anonymous'
1450-
else:
1451-
# update the lifetime datestamp
1452-
sessions.set(self.session, last_use=date.Date())
1453-
self.db.commit()
1454-
user = sessions.get(sessid, 'user')
14551457

14561458
# sanity check on the user still being valid
14571459
try:
@@ -1689,6 +1691,9 @@ def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
16891691

16901692
#
16911693
# $Log: not supported by cvs2svn $
1694+
# Revision 1.146 2002/07/30 05:27:30 richard
1695+
# nicer error messages, and a bugfix
1696+
#
16921697
# Revision 1.145 2002/07/26 08:26:59 richard
16931698
# Very close now. The cgi and mailgw now use the new security API. The two
16941699
# templates have been migrated to that setup. Lots of unit tests. Still some

0 commit comments

Comments
 (0)