Skip to content

Commit 322fa04

Browse files
author
Ralf Schlatterbeck
committed
Second patch from issue2550688 -- with some changes:
- password.py now has a second class JournalPassword used for journal storage. We have some backends that directly store serialized python objects. Also when reading from the journal some backends expected the string read to be usable as a parameter to a Password constructor. This now calls a JournalPassword constructor in all these cases. The new JournalPassword just keeps the scheme and has an empty password. - some factoring, move redundant implementation of "history" from rdbms_common and back_anydbm to hyperdb.
1 parent af4da69 commit 322fa04

File tree

6 files changed

+66
-57
lines changed

6 files changed

+66
-57
lines changed

CHANGES.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ Fixed:
7676
(Ralf Schlatterbeck)
7777
- Fixed bug in mailgw refactoring, patch issue2550697 (thanks Hubert
7878
Touvet)
79-
- Fix first part of Password handling security issue2550688 (thanks
80-
Joseph Myers for reporting and Eli Collins for fixing)
79+
- Fix Password handling security issue2550688 (thanks Joseph Myers for
80+
reporting and Eli Collins for fixing) -- this fixes all observations
81+
by Joseph Myers except for auto-migration of existing passwords.
8182

8283
2010-10-08 1.4.16 (r4541)
8384

roundup/backends/back_anydbm.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,8 @@ def set_inner(self, nodeid, **propvalues):
12981298
raise TypeError('new property "%s" not a '
12991299
'Password'%propname)
13001300
propvalues[propname] = value
1301+
journalvalues[propname] = \
1302+
current and password.JournalPassword(current)
13011303

13021304
elif value is not None and isinstance(prop, hyperdb.Date):
13031305
if not isinstance(value, date.Date):
@@ -1423,23 +1425,6 @@ def destroy(self, nodeid):
14231425
raise hyperdb.DatabaseError(_('Database open read-only'))
14241426
self.db.destroynode(self.classname, nodeid)
14251427

1426-
def history(self, nodeid):
1427-
"""Retrieve the journal of edits on a particular node.
1428-
1429-
'nodeid' must be the id of an existing node of this class or an
1430-
IndexError is raised.
1431-
1432-
The returned list contains tuples of the form
1433-
1434-
(nodeid, date, tag, action, params)
1435-
1436-
'date' is a Timestamp object specifying the time of the change and
1437-
'tag' is the journaltag specified when the database was opened.
1438-
"""
1439-
if not self.do_journal:
1440-
raise ValueError('Journalling is disabled for this class')
1441-
return self.db.getjournal(self.classname, nodeid)
1442-
14431428
# Locating nodes:
14441429
def hasnode(self, nodeid):
14451430
"""Determine if the given nodeid actually exists

roundup/backends/rdbms_common.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,7 +1297,7 @@ def getjournal(self, classname, nodeid):
12971297
continue
12981298
cvt = self.to_hyperdb_value(property.__class__)
12991299
if isinstance(property, Password):
1300-
params[param] = cvt(value)
1300+
params[param] = password.JournalPassword(value)
13011301
elif isinstance(property, Date):
13021302
params[param] = cvt(value)
13031303
elif isinstance(property, Interval):
@@ -1864,6 +1864,8 @@ def set_inner(self, nodeid, **propvalues):
18641864
if not isinstance(value, password.Password):
18651865
raise TypeError('new property "%s" not a Password'%propname)
18661866
propvalues[propname] = value
1867+
journalvalues[propname] = \
1868+
current and password.JournalPassword(current)
18671869

18681870
elif value is not None and isinstance(prop, Date):
18691871
if not isinstance(value, date.Date):
@@ -1995,23 +1997,6 @@ def destroy(self, nodeid):
19951997
raise DatabaseError(_('Database open read-only'))
19961998
self.db.destroynode(self.classname, nodeid)
19971999

1998-
def history(self, nodeid):
1999-
"""Retrieve the journal of edits on a particular node.
2000-
2001-
'nodeid' must be the id of an existing node of this class or an
2002-
IndexError is raised.
2003-
2004-
The returned list contains tuples of the form
2005-
2006-
(nodeid, date, tag, action, params)
2007-
2008-
'date' is a Timestamp object specifying the time of the change and
2009-
'tag' is the journaltag specified when the database was opened.
2010-
"""
2011-
if not self.do_journal:
2012-
raise ValueError('Journalling is disabled for this class')
2013-
return self.db.getjournal(self.classname, nodeid)
2014-
20152000
# Locating nodes:
20162001
def hasnode(self, nodeid):
20172002
"""Determine if the given nodeid actually exists

roundup/cgi/templating.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1108,6 +1108,13 @@ def history(self, direction='descending', dre=re.compile('^\d+$'),
11081108
cell[-1] += ' -> %s'%current[k]
11091109
current[k] = val
11101110

1111+
elif isinstance(prop, hyperdb.Password) and args[k] is not None:
1112+
val = args[k].dummystr()
1113+
cell.append('%s: %s'%(self._(k), val))
1114+
if current.has_key(k):
1115+
cell[-1] += ' -> %s'%current[k]
1116+
current[k] = val
1117+
11111118
elif not args[k]:
11121119
if current.has_key(k):
11131120
cell.append('%s: %s'%(self._(k), current[k]))
@@ -1561,7 +1568,10 @@ def plain(self, escape=0):
15611568

15621569
if self._value is None:
15631570
return ''
1564-
return self._('*encrypted*')
1571+
value = self._value.dummystr()
1572+
if escape:
1573+
value = cgi.escape(value)
1574+
return value
15651575

15661576
def field(self, size=30, **kwargs):
15671577
""" Render a form edit field for the property.

roundup/hyperdb.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,9 @@ def history(self, nodeid):
951951
'date' is a Timestamp object specifying the time of the change and
952952
'tag' is the journaltag specified when the database was opened.
953953
"""
954-
raise NotImplementedError
954+
if not self.do_journal:
955+
raise ValueError('Journalling is disabled for this class')
956+
return self.db.getjournal(self.classname, nodeid)
955957

956958
# Locating nodes:
957959
def hasnode(self, nodeid):
@@ -1309,7 +1311,7 @@ def import_journals(self, entries):
13091311
elif isinstance(prop, Interval):
13101312
value = date.Interval(value)
13111313
elif isinstance(prop, Password):
1312-
value = password.Password(encrypted=value)
1314+
value = password.JournalPassword(encrypted=value)
13131315
params[propname] = value
13141316
elif action == 'create' and params:
13151317
# old tracker with data stored in the create!

roundup/password.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,49 @@ def generatePassword(length=8):
168168
chars = string.letters+string.digits
169169
return ''.join([random.choice(chars) for x in range(length)])
170170

171-
class Password:
171+
class JournalPassword:
172+
""" Password dummy instance intended for journal operation.
173+
We do not store passwords in the journal any longer. The dummy
174+
version only reads the encryption scheme from the given
175+
encrypted password.
176+
"""
177+
default_scheme = 'PBKDF2' # new encryptions use this scheme
178+
pwre = re.compile(r'{(\w+)}(.+)')
179+
180+
def __init__ (self, encrypted=''):
181+
if isinstance(encrypted, self.__class__):
182+
self.scheme = encrypted.scheme or self.default_scheme
183+
else:
184+
m = self.pwre.match(encrypted)
185+
if m:
186+
self.scheme = m.group(1)
187+
else:
188+
self.scheme = self.default_scheme
189+
self.password = ''
190+
191+
def dummystr(self):
192+
""" return dummy string to store in journal
193+
- reports scheme, but nothing else
194+
"""
195+
return "{%s}*encrypted*" % (self.scheme,)
196+
197+
__str__ = dummystr
198+
199+
def __cmp__(self, other):
200+
"""Compare this password against another password."""
201+
# check to see if we're comparing instances
202+
if isinstance(other, self.__class__):
203+
if self.scheme != other.scheme:
204+
return cmp(self.scheme, other.scheme)
205+
return cmp(self.password, other.password)
206+
207+
# assume password is plaintext
208+
if self.password is None:
209+
raise ValueError, 'Password not set'
210+
return cmp(self.password, encodePassword(other, self.scheme,
211+
self.password or None))
212+
213+
class Password(JournalPassword):
172214
"""The class encapsulates a Password property type value in the database.
173215
174216
The encoding of the password is one if None, 'SHA', 'MD5' or 'plaintext'.
@@ -192,9 +234,7 @@ class Password:
192234
"""
193235
#TODO: code to migrate from old password schemes.
194236

195-
default_scheme = 'PBKDF2' # new encryptions use this scheme
196237
known_schemes = [ "PBKDF2", "SHA", "MD5", "crypt", "plaintext" ]
197-
pwre = re.compile(r'{(\w+)}(.+)')
198238

199239
def __init__(self, plaintext=None, scheme=None, encrypted=None, strict=False):
200240
"""Call setPassword if plaintext is not None."""
@@ -232,20 +272,6 @@ def setPassword(self, plaintext, scheme=None):
232272
self.password = encodePassword(plaintext, scheme)
233273
self.plaintext = plaintext
234274

235-
def __cmp__(self, other):
236-
"""Compare this password against another password."""
237-
# check to see if we're comparing instances
238-
if isinstance(other, Password):
239-
if self.scheme != other.scheme:
240-
return cmp(self.scheme, other.scheme)
241-
return cmp(self.password, other.password)
242-
243-
# assume password is plaintext
244-
if self.password is None:
245-
raise ValueError, 'Password not set'
246-
return cmp(self.password, encodePassword(other, self.scheme,
247-
self.password))
248-
249275
def __str__(self):
250276
"""Stringify the encrypted password for database storage."""
251277
if self.password is None:

0 commit comments

Comments
 (0)