diff --git a/pillar/cli/operations.py b/pillar/cli/operations.py index 91194e4a..3112b696 100644 --- a/pillar/cli/operations.py +++ b/pillar/cli/operations.py @@ -179,3 +179,29 @@ def index_users_update_settings(): 'unordered(roles)' ] }) + + +@manager_operations.command +def hash_auth_tokens(): + """Hashes all unhashed authentication tokens.""" + + from pymongo.results import UpdateResult + from pillar.api.utils.authentication import hash_auth_token + + tokens_coll = current_app.db('tokens') + query = {'token': {'$exists': True}} + cursor = tokens_coll.find(query, projection={'token': 1, '_id': 1}) + log.info('Updating %d tokens', cursor.count()) + + for token_doc in cursor: + hashed_token = hash_auth_token(token_doc['token']) + token_id = token_doc['_id'] + res: UpdateResult = tokens_coll.update_one( + {'_id': token_id}, + {'$set': {'token_hashed': hashed_token}, + '$unset': {'token': 1}}, + ) + if res.modified_count != 1: + raise ValueError(f'Unable to update token {token_id}!') + + log.info('Done') diff --git a/tests/test_api/test_auth.py b/tests/test_api/test_auth.py index 8f16abb1..5c600cbb 100644 --- a/tests/test_api/test_auth.py +++ b/tests/test_api/test_auth.py @@ -193,6 +193,58 @@ class AuthenticationTests(AbstractPillarTest): self.assertEqual({tokdat_se['token_hashed'], tokdat_ne['token_hashed']}, {item['token_hashed'] for item in token_coll.find()}) + def test_token_hashing_cli(self): + from dateutil.parser import parse + self.enter_app_context() + + user_id1 = self.create_user(24 * 'a') + user_id2 = self.create_user(24 * 'b') + now = datetime.datetime.now(tz_util.utc) + + # Force unhashed tokens into our database. + tokens_coll = self.app.db('tokens') + tokens_coll.insert_one({ + '_id': ObjectId('59c0f8ef98377327e1525cd1'), + '_etag': '3b8fffa5177e87555acd95e49e6764cb81de5d70', + '_created': parse('2017-09-19T13:01:03.000+0200'), + '_updated': parse('2017-09-19T13:01:03.000+0200'), + 'user': user_id1, + 'token': 'unhashed-token', + 'expire_time': now + datetime.timedelta(hours=1), + }) + tokens_coll.insert_one({ + '_id': ObjectId(24 * 'c'), + '_etag': '3b8fffa5177e87555acd95e49e6764cb81de5d70', + '_created': parse('2017-09-20T13:01:03.000+0200'), + '_updated': parse('2017-09-20T13:01:03.000+0200'), + 'user': user_id2, + 'token': 'some-other-token', + 'expire_time': now + datetime.timedelta(hours=1), + }) + + # This token should work. + me1 = self.get('/api/users/me', auth_token='unhashed-token').get_json() + self.assertEqual(str(user_id1), me1['_id']) + me2 = self.get('/api/users/me', auth_token='some-other-token').get_json() + self.assertEqual(str(user_id2), me2['_id']) + + from pillar.cli.operations import hash_auth_tokens + hash_auth_tokens() + + # The same token should still work, but be hashed in the DB. + me1 = self.get('/api/users/me', auth_token='unhashed-token').get_json() + self.assertEqual(str(user_id1), me1['_id']) + me2 = self.get('/api/users/me', auth_token='some-other-token').get_json() + self.assertEqual(str(user_id2), me2['_id']) + + db_token = tokens_coll.find_one({'_id': ObjectId('59c0f8ef98377327e1525cd1')}) + self.assertNotIn('token', db_token) + self.assertIn('token_hashed', db_token) + + db_token = tokens_coll.find_one({'_id': ObjectId('59c0f8ef98377327e1525cd1')}) + self.assertNotIn('token', db_token) + self.assertIn('token_hashed', db_token) + class UserListTests(AbstractPillarTest): """Security-related tests."""