Skip to content

Commit 946da5d

Browse files
committed
Rework password checks in account renewal to rely on check_hash / check_scramble
calls in the recently introduced early_validation_checks during account request submission where original password is still available. Fix a use of load_account_dict using configuration where logger is expected. Add unit tests to cover a number of early_validation_checks cases. Still more tests pending e.g. to test renewal with legal and illegal password change.
1 parent 446307f commit 946da5d

File tree

3 files changed

+126
-24
lines changed

3 files changed

+126
-24
lines changed

mig/shared/accountreq.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
gdp_distinguished_field
5252
from mig.shared.fileio import delete_file, make_temp_file
5353
from mig.shared.notification import notify_user
54-
from mig.shared.pwcrypto import check_hash
54+
from mig.shared.pwcrypto import check_hash, check_scramble
5555
# Expose some helper variables for functionality backends
5656
from mig.shared.safeinput import name_extras, password_extras, \
5757
password_min_len, password_max_len, valid_password_chars, \
@@ -1231,24 +1231,31 @@ def early_validation_checks(configuration, raw_request, service, username,
12311231

12321232
client_id = get_user_id(configuration, raw_request)
12331233
db_path = default_db_path(configuration)
1234-
user_dict = load_user_dict(configuration, client_id, db_path)
1234+
user_dict = load_user_dict(logger, client_id, db_path)
12351235
if user_dict:
1236-
# Renewal or password change
1236+
# Renewal or password change - check against existing pw and status
1237+
scrambled = user_dict.get('password', None)
1238+
hashed = user_dict.get('password_hash', None)
1239+
account_status = user_dict.get('status', 'active')
12371240
authorized = raw_request.get('authorized', None)
12381241
reset_token = raw_request.get('reset_token', None)
1239-
account_status = raw_request.get('status', 'active')
1240-
if not authorized and not reset_token:
1241-
hashed = user_dict.get('password_hash', None)
1242-
if not check_hash(configuration, service, username, password,
1243-
hashed):
1244-
logger.warning('illegal password change in request from %r' %
1245-
client_id)
1246-
raw_request['invalid'].append(illegal_pw_change)
1247-
elif account_status not in ('temporal', 'active', 'inactive'):
1242+
if hashed:
1243+
raw_request['pw_changed'] = check_hash(configuration, service,
1244+
username, password, hashed)
1245+
elif scrambled:
1246+
raw_request['pw_changed'] = check_scramble(
1247+
configuration, service, username, password, scrambled,
1248+
salt=configuration.site_password_salt)
1249+
1250+
if account_status not in ('temporal', 'active', 'inactive'):
12481251
logger.warning('existing account for %r is %s and not renewable'
12491252
% (client_id, account_status))
12501253
raw_request['invalid'].append(renewal_blocked)
1251-
else:
1254+
if raw_request['pw_changed'] and not authorized and not reset_token:
1255+
logger.warning('illegal password change in request from %r' %
1256+
client_id)
1257+
raw_request['invalid'].append(illegal_pw_change)
1258+
if not raw_request.get('invalid', []):
12521259
logger.debug('account renewal from %r looks alright' % client_id)
12531260
elif existing_user_collision(configuration, raw_request, client_id):
12541261
# TODO: drop and rely solely on live check in janitor to avoid races?

mig/shared/useradm.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -459,10 +459,10 @@ def verify_user_peers(configuration, db_path, client_id, user, now, verify_peer,
459459

460460

461461
def create_user_in_db(configuration, db_path, client_id, user, now, authorized,
462-
reset_token, reset_auth_type, accepted_peer_list, force,
463-
verbose, ask_renew, default_renew, do_lock,
464-
from_edit_user, ask_change_pw, auto_create_db,
465-
create_backup):
462+
reset_token, reset_auth_type, pw_changed,
463+
accepted_peer_list, force, verbose, ask_renew,
464+
default_renew, do_lock, from_edit_user, ask_change_pw,
465+
auto_create_db, create_backup):
466466
"""Handle all the parts of user creation or renewal relating to the user
467467
datatbase.
468468
"""
@@ -569,14 +569,14 @@ def create_user_in_db(configuration, db_path, client_id, user, now, authorized,
569569
# password alone.
570570
# External OpenID users do not provide a password so again any
571571
# existing password should be left alone on renewal.
572-
# The password_hash field is not guaranteed to exist.
572+
# The password_hash field is not guaranteed to exist and it varies
573+
# depending on the current random seed and assigned salt. So we use
574+
# check_password during submit and save result in pw_changed.
573575
if not user['password']:
574576
user['password'] = user['old_password']
575577
if not user.get('password_hash', ''):
576578
user['password_hash'] = user['old_password_hash']
577-
password_changed = (user['old_password'] != user['password'] or
578-
user['old_password_hash'] != user['password_hash'])
579-
if password_changed:
579+
if pw_changed:
580580
# Allow password change if it's directly authorized with login
581581
# or authorized through a simple reset challenge (reset_token).
582582
if not authorized and reset_token:
@@ -1062,6 +1062,11 @@ def create_user(user, conf_path, db_path, force=False, verbose=False,
10621062
reset_auth_type = user.get('auth', ['UNKNOWN'])[-1]
10631063
# Always remove any reset_token fields before DB insert
10641064
del user['reset_token']
1065+
pw_changed = False
1066+
if 'pw_changed' in user:
1067+
pw_changed = user['pw_changed']
1068+
# Always remove any pw_changed fields before DB insert
1069+
del user['pw_changed']
10651070

10661071
_logger.info('trying to create or renew user %r' % client_id)
10671072
if verbose:
@@ -1090,9 +1095,10 @@ def create_user(user, conf_path, db_path, force=False, verbose=False,
10901095

10911096
created = create_user_in_db(configuration, db_path, client_id, user, now,
10921097
authorized, reset_token, reset_auth_type,
1093-
accepted_peer_list, force, verbose, ask_renew,
1094-
default_renew, do_lock, from_edit_user,
1095-
ask_change_pw, auto_create_db, create_backup)
1098+
pw_changed, accepted_peer_list, force, verbose,
1099+
ask_renew, default_renew, do_lock,
1100+
from_edit_user, ask_change_pw, auto_create_db,
1101+
create_backup)
10961102
# Mark user updated for all logins
10971103
update_account_expire_cache(configuration, created)
10981104
update_account_status_cache(configuration, created)

tests/test_mig_shared_accountreq.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,95 @@ def test_peers_prefilter_email_reject(self):
204204
user)
205205
self.assertFalse(check)
206206

207+
def test_early_validation_checks_valid_new_simple(self):
208+
service = 'migoid'
209+
full_name = 'Valid Test Name'
210+
email = 'john@doe.org'
211+
dummy_pw = 'PW74deb6609F109f504d'
212+
dummy_pw_hash = 'DUMMYPWHASH'
213+
user = {'full_name': full_name, 'organization': 'Test Org',
214+
'organizational_unit': '', 'email': email,
215+
'password_hash': dummy_pw_hash, 'comment': ''}
216+
fill_distinguished_name(user)
217+
checked = accountreq.early_validation_checks(self.configuration, user,
218+
service, email, dummy_pw)
219+
print("DEBUG: early checks on valid simple req: %s" % checked)
220+
self.assertEqual(checked['invalid'], [], "early validation failed")
221+
222+
def test_early_validation_checks_valid_new_peers(self):
223+
self.configuration.site_enable_peers = True
224+
self.configuration.site_peers_explicit_fields = ['full_name', 'email']
225+
self.configuration.site_peers_prefilter = [
226+
('peers_email', r'^.+@([a-z0-9]+\.)*ku\.dk$')]
227+
service = 'migoid'
228+
full_name = 'Valid Test Name'
229+
email = 'john@doe.org'
230+
peers_full_name = 'Valid Peer Name'
231+
dummy_pw = 'PW74deb6609F109f504d'
232+
dummy_pw_hash = 'DUMMYPWHASH'
233+
accept = ['john.doe@science.ku.dk', 'abc123@ku.dk',
234+
'john.doe@a.b.c.ku.dk']
235+
self.configuration.site_peers_prefilter = [
236+
('peers_email', r'^.+@([a-z0-9]+\.)*ku\.dk$')]
237+
user = {'full_name': full_name, 'organization': 'Test Org',
238+
'organizational_unit': '', 'email': email,
239+
'password_hash': dummy_pw_hash, 'comment': '',
240+
'peers_full_name': [peers_full_name], 'peers_email': []}
241+
fill_distinguished_name(user)
242+
for addr in accept:
243+
user['peers_email'] = [addr]
244+
username = user['email']
245+
checked = accountreq.early_validation_checks(self.configuration,
246+
user, service,
247+
username, dummy_pw)
248+
print("DEBUG: early checks on valid peers req: %s" % checked)
249+
self.assertEqual(checked['invalid'], [], "early validation failed")
250+
251+
# TODO: add test valid renew
252+
253+
def test_early_validation_checks_invalid_new_simple(self):
254+
service = 'migoid'
255+
full_name = 'InvalidTestName'
256+
email = 'john@doe.org'
257+
dummy_pw = 'PW74deb6609F109f504d'
258+
dummy_pw_hash = 'DUMMYPWHASH'
259+
user = {'full_name': full_name, 'organization': 'Test Org',
260+
'organizational_unit': '', 'email': email,
261+
'password_hash': dummy_pw_hash, 'comment': ''}
262+
fill_distinguished_name(user)
263+
checked = accountreq.early_validation_checks(self.configuration, user,
264+
service, email, dummy_pw)
265+
print("DEBUG: early checks on invalid simple req: %s" % checked)
266+
self.assertTrue(checked['invalid'], "early validation failed")
267+
268+
def test_early_validation_checks_invalid_new_peers(self):
269+
self.configuration.site_enable_peers = True
270+
self.configuration.site_peers_explicit_fields = ['email']
271+
self.configuration.site_peers_prefilter = [
272+
('peers_email', r'^.+@([a-z0-9]+\.)*ku\.dk$')]
273+
service = 'migoid'
274+
full_name = 'Valid Test Name'
275+
email = 'john@doe.org'
276+
peers_full_name = 'Valid Peer Name'
277+
dummy_pw = 'PW74deb6609F109f504d'
278+
dummy_pw_hash = 'DUMMYPWHASH'
279+
user = {'full_name': full_name, 'organization': 'Test Org',
280+
'organizational_unit': '', 'email': email,
281+
'password_hash': dummy_pw_hash, 'comment': '',
282+
'peers_full_name': [peers_full_name], 'peers_email': []}
283+
fill_distinguished_name(user)
284+
reject = ['', 'john@doe.org', 'a@b.c.org', 'a@ku.dk.com',
285+
'a@sci.ku.dk.org']
286+
for addr in reject:
287+
user['peers_email'] = addr
288+
username = user['email']
289+
checked = accountreq.early_validation_checks(self.configuration, user,
290+
service, email, dummy_pw)
291+
print("DEBUG: early checks on invalid peers req: %s" % checked)
292+
self.assertTrue(checked['invalid'], "early validation failed")
293+
294+
# TODO: add test invalid renew
295+
207296

208297
if __name__ == '__main__':
209298
testmain()

0 commit comments

Comments
 (0)