Add better input checking for MongoDB
A user-create on the Trove admin user "admin.os_admin" can change the password of that user, rendering the guest agent useless (the password it tries to use no longer being valid). A user-create can also be used to change the password of existing users. The guest does no validation of inputs for various user-* commands. None of the above should be allowed. The fix is to skip any names on the ignored user's list and add more checking for existing users in codepaths. Tests are also updated as needed. Change-Id: Icae98f57f7beda23e52b7e59a1a386c03ff20cf8 Closes-bug: 1517622
This commit is contained in:
parent
e5c5ac70f0
commit
0e87bb5ecf
|
@ -114,6 +114,14 @@ class Manager(manager.Manager):
|
|||
# TODO(peterstac) - why is this hard-coded?
|
||||
return dbaas.get_filesystem_volume_stats(system.MONGODB_MOUNT_POINT)
|
||||
|
||||
def change_passwords(self, context, users):
|
||||
LOG.debug("Changing password.")
|
||||
return service.MongoDBAdmin().change_passwords(users)
|
||||
|
||||
def update_attributes(self, context, username, hostname, user_attrs):
|
||||
LOG.debug("Updating database attributes.")
|
||||
return service.MongoDBAdmin().update_attributes(username, user_attrs)
|
||||
|
||||
def create_database(self, context, databases):
|
||||
LOG.debug("Creating database(s).")
|
||||
return service.MongoDBAdmin().create_database(databases)
|
||||
|
|
|
@ -39,6 +39,7 @@ from trove.guestagent.db import models
|
|||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
CONFIG_FILE = operating_system.file_discovery(system.CONFIG_CANDIDATES)
|
||||
MANAGER = CONF.datastore_manager if CONF.datastore_manager else 'mongodb'
|
||||
|
||||
# Configuration group for clustering-related settings.
|
||||
CNF_CLUSTER = 'clustering'
|
||||
|
@ -456,7 +457,7 @@ class MongoDBApp(object):
|
|||
# the driver engine is already cached, but we need to change it it
|
||||
with MongoDBClient(None, host='localhost',
|
||||
port=MONGODB_PORT) as client:
|
||||
MongoDBAdmin().create_user(user, client=client)
|
||||
MongoDBAdmin().create_validated_user(user, client=client)
|
||||
# now revert to the normal engine
|
||||
self.status.set_host(host=netutils.get_my_ipv4(),
|
||||
port=MONGODB_PORT)
|
||||
|
@ -555,6 +556,12 @@ class MongoDBAdmin(object):
|
|||
type(self).admin_user = user
|
||||
return type(self).admin_user
|
||||
|
||||
def _is_modifiable_user(self, name):
|
||||
if ((name in cfg.get_ignored_users(manager=MANAGER)) or
|
||||
name == system.MONGO_ADMIN_NAME):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def cmd_admin_auth_params(self):
|
||||
"""Returns a list of strings that constitute MongoDB command line
|
||||
|
@ -571,8 +578,11 @@ class MongoDBAdmin(object):
|
|||
user.username, password=user.password, roles=user.roles
|
||||
)
|
||||
|
||||
def create_user(self, user, client=None):
|
||||
"""Creates a user on their database."""
|
||||
def create_validated_user(self, user, client=None):
|
||||
"""Creates a user on their database. The caller should ensure that
|
||||
this action is valid.
|
||||
:param user: a MongoDBUser object
|
||||
"""
|
||||
LOG.debug('Creating user %s on database %s with roles %s.'
|
||||
% (user.username, user.database.name, str(user.roles)))
|
||||
|
||||
|
@ -586,24 +596,50 @@ class MongoDBAdmin(object):
|
|||
self._create_user_with_client(user, admin_client)
|
||||
|
||||
def create_users(self, users):
|
||||
"""Create the given user(s)."""
|
||||
"""Create the given user(s).
|
||||
:param users: list of serialized user objects
|
||||
"""
|
||||
with MongoDBClient(self._admin_user()) as client:
|
||||
for user in users:
|
||||
self.create_user(models.MongoDBUser.deserialize_user(user),
|
||||
client)
|
||||
for item in users:
|
||||
user = models.MongoDBUser.deserialize_user(item)
|
||||
if not self._is_modifiable_user(user.name):
|
||||
LOG.warning('Skipping creation of user with reserved '
|
||||
'name %(user)s' % {'user': user.name})
|
||||
elif self._get_user_record(user.name):
|
||||
LOG.warning('Skipping creation of user with pre-existing '
|
||||
'name %(user)s' % {'user': user.name})
|
||||
else:
|
||||
self.create_validated_user(user, client)
|
||||
|
||||
def delete_validated_user(self, user):
|
||||
"""Deletes a user from their database. The caller should ensure that
|
||||
this action is valid.
|
||||
:param user: a MongoDBUser object
|
||||
"""
|
||||
LOG.debug('Deleting user %s from database %s.'
|
||||
% (user.username, user.database.name))
|
||||
with MongoDBClient(self._admin_user()) as admin_client:
|
||||
admin_client[user.database.name].remove_user(user.username)
|
||||
|
||||
def delete_user(self, user):
|
||||
"""Delete the given user."""
|
||||
"""Delete the given user.
|
||||
:param user: a serialized user object
|
||||
"""
|
||||
user = models.MongoDBUser.deserialize_user(user)
|
||||
username = user.username
|
||||
db_name = user.database.name
|
||||
LOG.debug('Deleting user %s from database %s.' % (username, db_name))
|
||||
with MongoDBClient(self._admin_user()) as admin_client:
|
||||
admin_client[db_name].remove_user(username)
|
||||
if not self._is_modifiable_user(user.name):
|
||||
raise exception.BadRequest(_(
|
||||
'Cannot delete user with reserved name %(user)s')
|
||||
% {'user': user.name})
|
||||
else:
|
||||
self.delete_validated_user(user)
|
||||
|
||||
def _get_user_record(self, name):
|
||||
"""Get the user's record."""
|
||||
user = models.MongoDBUser(name)
|
||||
if not self._is_modifiable_user(user.name):
|
||||
LOG.warning('Skipping retrieval of user with reserved '
|
||||
'name %(user)s' % {'user': user.name})
|
||||
return None
|
||||
with MongoDBClient(self._admin_user()) as admin_client:
|
||||
user_info = admin_client.admin.system.users.find_one(
|
||||
{'user': user.username, 'db': user.database.name})
|
||||
|
@ -615,7 +651,10 @@ class MongoDBAdmin(object):
|
|||
def get_user(self, name):
|
||||
"""Get information for the given user."""
|
||||
LOG.debug('Getting user %s.' % name)
|
||||
return self._get_user_record(name).serialize()
|
||||
user = self._get_user_record(name)
|
||||
if not user:
|
||||
return None
|
||||
return user.serialize()
|
||||
|
||||
def list_users(self, limit=None, marker=None, include_marker=False):
|
||||
"""Get a list of all users."""
|
||||
|
@ -624,12 +663,41 @@ class MongoDBAdmin(object):
|
|||
for user_info in admin_client.admin.system.users.find():
|
||||
user = models.MongoDBUser(name=user_info['_id'])
|
||||
user.roles = user_info['roles']
|
||||
if user.name not in cfg.get_ignored_users(manager='mongodb'):
|
||||
if self._is_modifiable_user(user.name):
|
||||
users.append(user.serialize())
|
||||
LOG.debug('users = ' + str(users))
|
||||
return pagination.paginate_list(users, limit, marker,
|
||||
include_marker)
|
||||
|
||||
def change_passwords(self, users):
|
||||
with MongoDBClient(self._admin_user()) as admin_client:
|
||||
for item in users:
|
||||
user = models.MongoDBUser.deserialize_user(item)
|
||||
if not self._is_modifiable_user(user.name):
|
||||
LOG.warning('Skipping password change for user with '
|
||||
'reserved name %(user)s.'
|
||||
% {'user': user.name})
|
||||
return None
|
||||
LOG.debug('Changing password for user %(user)s'
|
||||
% {'user': user.name})
|
||||
self._create_user_with_client(user, admin_client)
|
||||
|
||||
def update_attributes(self, name, user_attrs):
|
||||
"""Update user attributes."""
|
||||
user = self._get_user_record(name)
|
||||
if not user:
|
||||
raise exception.BadRequest(_(
|
||||
'Cannot update attributes for user %(user)s as it either does '
|
||||
'not exist or is a reserved user.') % {'user': name})
|
||||
password = user_attrs.get('password')
|
||||
if password:
|
||||
user.password = password
|
||||
self.change_passwords([user.serialize()])
|
||||
if user_attrs.get('name'):
|
||||
LOG.warning('Changing user name is not supported.')
|
||||
if user_attrs.get('host'):
|
||||
LOG.warning('Changing user host is not supported.')
|
||||
|
||||
def enable_root(self, password=None):
|
||||
"""Create a user 'root' with role 'root'."""
|
||||
if not password:
|
||||
|
@ -637,7 +705,7 @@ class MongoDBAdmin(object):
|
|||
password = utils.generate_random_password()
|
||||
root_user = models.MongoDBUser(name='admin.root', password=password)
|
||||
root_user.roles = {'db': 'admin', 'role': 'root'}
|
||||
self.create_user(root_user)
|
||||
self.create_validated_user(root_user)
|
||||
return root_user.serialize()
|
||||
|
||||
def is_root_enabled(self):
|
||||
|
@ -656,6 +724,10 @@ class MongoDBAdmin(object):
|
|||
def grant_access(self, username, databases):
|
||||
"""Adds the RW role to the user for each specified database."""
|
||||
user = self._get_user_record(username)
|
||||
if not user:
|
||||
raise exception.BadRequest(_(
|
||||
'Cannot grant access for reserved or non-existant user '
|
||||
'%(user)s') % {'user': username})
|
||||
for db_name in databases:
|
||||
# verify the database name
|
||||
models.MongoDBSchema(db_name)
|
||||
|
@ -673,6 +745,10 @@ class MongoDBAdmin(object):
|
|||
def revoke_access(self, username, database):
|
||||
"""Removes the RW role from the user for the specified database."""
|
||||
user = self._get_user_record(username)
|
||||
if not user:
|
||||
raise exception.BadRequest(_(
|
||||
'Cannot revoke access for reserved or non-existant user '
|
||||
'%(user)s') % {'user': username})
|
||||
# verify the database name
|
||||
models.MongoDBSchema(database)
|
||||
role = {'db': database, 'role': 'readWrite'}
|
||||
|
@ -686,6 +762,10 @@ class MongoDBAdmin(object):
|
|||
"""Returns a list of all databases for which the user has the RW role.
|
||||
"""
|
||||
user = self._get_user_record(username)
|
||||
if not user:
|
||||
raise exception.BadRequest(_(
|
||||
'Cannot list access for reserved or non-existant user '
|
||||
'%(user)s') % {'user': username})
|
||||
return user.databases
|
||||
|
||||
def create_database(self, databases):
|
||||
|
@ -716,7 +796,7 @@ class MongoDBAdmin(object):
|
|||
def list_databases(self, limit=None, marker=None, include_marker=False):
|
||||
"""Lists the databases."""
|
||||
db_names = self.list_database_names()
|
||||
for hidden in cfg.get_ignored_dbs(manager='mongodb'):
|
||||
for hidden in cfg.get_ignored_dbs(manager=MANAGER):
|
||||
if hidden in db_names:
|
||||
db_names.remove(hidden)
|
||||
databases = [models.MongoDBSchema(db_name).serialize()
|
||||
|
|
|
@ -164,12 +164,15 @@ class GuestAgentMongoDBManagerTest(trove_testtools.TestCase):
|
|||
|
||||
@mock.patch.object(service, 'MongoDBClient')
|
||||
@mock.patch.object(service.MongoDBAdmin, '_admin_user')
|
||||
def test_create_user(self, mocked_admin_user, mocked_client):
|
||||
@mock.patch.object(service.MongoDBAdmin, '_get_user_record')
|
||||
def test_create_user(self, mocked_get_user, mocked_admin_user,
|
||||
mocked_client):
|
||||
user = self._serialized_user.copy()
|
||||
user['_password'] = 'testpassword'
|
||||
users = [user]
|
||||
|
||||
client = mocked_client().__enter__()['testdb']
|
||||
mocked_get_user.return_value = None
|
||||
|
||||
self.manager.create_user(self.context, users)
|
||||
|
||||
|
@ -241,7 +244,7 @@ class GuestAgentMongoDBManagerTest(trove_testtools.TestCase):
|
|||
self.assertIsNone(next_marker)
|
||||
self.assertEqual(sorted([user1, user2]), users)
|
||||
|
||||
@mock.patch.object(service.MongoDBAdmin, 'create_user')
|
||||
@mock.patch.object(service.MongoDBAdmin, 'create_validated_user')
|
||||
@mock.patch.object(utils, 'generate_random_password',
|
||||
return_value='password')
|
||||
def test_enable_root(self, mock_gen_rand_pwd, mock_create_user):
|
||||
|
|
Loading…
Reference in New Issue