From 52d1203d055b66d49ed6b2dd91de8afe42a7e9d0 Mon Sep 17 00:00:00 2001 From: amcrn Date: Wed, 4 Sep 2013 00:50:57 -0700 Subject: [PATCH] User and Database List in Create Not Validated Fix #1: Every database in instance.users.databases should exist in instance.databases, otherwise the guest will attempt to grant privileges to a database that can't possibly exist. Fix #2: The (name, host) of each user in instance.users should be unique, otherwise there is a semantic duplicate. Note: instance.users.databases is already checked for uniqueness via apischema's uniqueItems assertion. Change-Id: Ib0a148a1d7dcf6adf2a899b15db815273e5688a4 Closes-Bug: #1219627 --- trove/common/exception.py | 18 +++ trove/extensions/mysql/common.py | 24 +++- trove/instance/service.py | 4 +- trove/tests/unittests/mysql/test_common.py | 144 +++++++++++++++++++++ 4 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 trove/tests/unittests/mysql/test_common.py diff --git a/trove/common/exception.py b/trove/common/exception.py index 25d96466fd..f0908a87fd 100644 --- a/trove/common/exception.py +++ b/trove/common/exception.py @@ -293,3 +293,21 @@ class BackupFileNotFound(NotFound): class SwiftAuthError(TroveError): message = _("Swift account not accessible for tenant %(tenant_id)s.") + + +class DatabaseForUserNotInDatabaseListError(TroveError): + message = _("The request indicates that user %(user)s should have access " + "to database %(database)s, but database %(database)s is not " + "included in the initial databases list.") + + +class DatabaseInitialDatabaseDuplicateError(TroveError): + message = _("Two or more databases share the same name in the initial " + "databases list. Please correct the names or remove the " + "duplicate entries.") + + +class DatabaseInitialUserDuplicateError(TroveError): + message = _("Two or more users share the same name and host in the " + "initial users list. Please correct the names or remove the " + "duplicate entries.") diff --git a/trove/extensions/mysql/common.py b/trove/extensions/mysql/common.py index 4fee942e1d..6e42cf0386 100644 --- a/trove/extensions/mysql/common.py +++ b/trove/extensions/mysql/common.py @@ -24,9 +24,13 @@ def populate_validated_databases(dbs): """ try: databases = [] + unique_identities = set() for database in dbs: mydb = guest_models.ValidatedMySQLDatabase() mydb.name = database.get('name', '') + if mydb.name in unique_identities: + raise exception.DatabaseInitialDatabaseDuplicateError() + unique_identities.add(mydb.name) mydb.character_set = database.get('character_set', '') mydb.collate = database.get('collate', '') databases.append(mydb.serialize()) @@ -39,18 +43,28 @@ def populate_validated_databases(dbs): raise exception.BadRequest(safe_string) -def populate_users(users): +def populate_users(users, initial_databases=None): """Create a serializable request containing users""" users_data = [] + unique_identities = set() for user in users: u = guest_models.MySQLUser() u.name = user.get('name', '') u.host = user.get('host') + user_identity = (u.name, u.host) + if user_identity in unique_identities: + raise exception.DatabaseInitialUserDuplicateError() + unique_identities.add(user_identity) u.password = user.get('password', '') - dbs = user.get('databases', '') - if dbs: - for db in dbs: - u.databases = db.get('name', '') + user_dbs = user.get('databases', '') + # user_db_names guaranteed unique and non-empty by apischema + user_db_names = [user_db.get('name', '') for user_db in user_dbs] + for user_db_name in user_db_names: + if (initial_databases is not None and user_db_name not in + initial_databases): + raise exception.DatabaseForUserNotInDatabaseListError( + user=u.name, database=user_db_name) + u.databases = user_db_name users_data.append(u.serialize()) return users_data diff --git a/trove/instance/service.py b/trove/instance/service.py index af30dd8802..b668353caa 100644 --- a/trove/instance/service.py +++ b/trove/instance/service.py @@ -188,9 +188,11 @@ class InstanceController(wsgi.Controller): flavor_id = utils.get_id_from_href(flavor_ref) databases = populate_validated_databases( body['instance'].get('databases', [])) + database_names = [database.get('_name', '') for database in databases] users = None try: - users = populate_users(body['instance'].get('users', [])) + users = populate_users(body['instance'].get('users', []), + database_names) except ValueError as ve: raise exception.BadRequest(msg=ve) diff --git a/trove/tests/unittests/mysql/test_common.py b/trove/tests/unittests/mysql/test_common.py new file mode 100644 index 0000000000..65a7fdf21c --- /dev/null +++ b/trove/tests/unittests/mysql/test_common.py @@ -0,0 +1,144 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from testtools import TestCase +from testtools.matchers import Is +from testtools.matchers import Equals +from trove.common.exception import DatabaseInitialDatabaseDuplicateError +from trove.common.exception import DatabaseForUserNotInDatabaseListError +from trove.common.exception import DatabaseInitialUserDuplicateError +from trove.extensions.mysql.common import populate_validated_databases +from trove.extensions.mysql.common import populate_users + + +class MySqlCommonTest(TestCase): + + def setUp(self): + super(MySqlCommonTest, self).setUp() + + def tearDown(self): + super(MySqlCommonTest, self).tearDown() + + def test_initial_databases_none(self): + databases = [] + result = populate_validated_databases(databases) + self.assertThat(len(result), Is(0)) + + def test_initial_databases_single(self): + databases = [{'name': 'one_db'}] + result = populate_validated_databases(databases) + self.assertThat(len(result), Is(1)) + self.assertThat(result[0]['_name'], Equals('one_db')) + + def test_initial_databases_unique(self): + databases = [{'name': 'one_db'}, {'name': 'diff_db'}] + result = populate_validated_databases(databases) + self.assertThat(len(result), Is(2)) + + def test_initial_databases_duplicate(self): + databases = [{'name': 'same_db'}, {'name': 'same_db'}] + self.assertRaises(DatabaseInitialDatabaseDuplicateError, + populate_validated_databases, databases) + + def test_initial_databases_intermingled(self): + databases = [{'name': 'a_db'}, {'name': 'b_db'}, {'name': 'a_db'}] + self.assertRaises(DatabaseInitialDatabaseDuplicateError, + populate_validated_databases, databases) + + def test_populate_users_single(self): + users = [{'name': 'bob', 'password': 'x'}] + result = populate_users(users) + self.assertThat(len(result), Is(1)) + self.assertThat(result[0]['_name'], Equals('bob')) + self.assertThat(result[0]['_password'], Equals('x')) + + def test_populate_users_unique_host(self): + users = [{'name': 'bob', 'password': 'x', 'host': '127.0.0.1'}, + {'name': 'bob', 'password': 'x', 'host': '128.0.0.1'}] + result = populate_users(users) + self.assertThat(len(result), Is(2)) + + def test_populate_users_unique_name(self): + users = [{'name': 'bob', 'password': 'x', 'host': '127.0.0.1'}, + {'name': 'tom', 'password': 'x', 'host': '127.0.0.1'}] + result = populate_users(users) + self.assertThat(len(result), Is(2)) + + def test_populate_users_duplicate(self): + users = [{'name': 'bob', 'password': 'x', 'host': '127.0.0.1'}, + {'name': 'bob', 'password': 'y', 'host': '127.0.0.1'}] + self.assertRaises(DatabaseInitialUserDuplicateError, + populate_users, users) + + def test_populate_users_intermingled(self): + users = [{'name': 'bob', 'password': 'x', 'host': '127.0.0.1'}, + {'name': 'tom', 'password': 'y', 'host': '128.0.0.1'}, + {'name': 'bob', 'password': 'z', 'host': '127.0.0.1'}] + self.assertRaises(DatabaseInitialUserDuplicateError, + populate_users, users) + + def test_populate_users_both_db_list_empty(self): + initial_databases = [] + users = [{"name": "bob", "password": "x"}] + result = populate_users(users, initial_databases) + self.assertThat(len(result), Is(1)) + + def test_populate_users_initial_db_list_empty(self): + initial_databases = [] + users = [{"name": "bob", "password": "x", + "databases": [{"name": "my_db"}]}] + self.assertRaises(DatabaseForUserNotInDatabaseListError, + populate_users, users, initial_databases) + + def test_populate_users_user_db_list_empty(self): + initial_databases = ['my_db'] + users = [{"name": "bob", "password": "x"}] + result = populate_users(users, initial_databases) + self.assertThat(len(result), Is(1)) + + def test_populate_users_db_in_list(self): + initial_databases = ['my_db'] + users = [{"name": "bob", "password": "x", + "databases": [{"name": "my_db"}]}] + result = populate_users(users, initial_databases) + self.assertThat(len(result), Is(1)) + + def test_populate_users_db_multi_in_list(self): + initial_databases = ['a_db', 'b_db', 'c_db', 'd_db'] + users = [{"name": "bob", "password": "x", + "databases": [{"name": "a_db"}]}, + {"name": "tom", "password": "y", + "databases": [{"name": "c_db"}]}, + {"name": "sue", "password": "z", + "databases": [{"name": "c_db"}]}] + result = populate_users(users, initial_databases) + self.assertThat(len(result), Is(3)) + + def test_populate_users_db_not_in_list(self): + initial_databases = ['a_db', 'b_db', 'c_db', 'd_db'] + users = [{"name": "bob", "password": "x", + "databases": [{"name": "fake_db"}]}] + self.assertRaises(DatabaseForUserNotInDatabaseListError, + populate_users, users, initial_databases) + + def test_populate_users_db_multi_not_in_list(self): + initial_databases = ['a_db', 'b_db', 'c_db', 'd_db'] + users = [{"name": "bob", "password": "x", + "databases": [{"name": "a_db"}]}, + {"name": "tom", "password": "y", + "databases": [{"name": "fake_db"}]}, + {"name": "sue", "password": "z", + "databases": [{"name": "d_db"}]}] + self.assertRaises(DatabaseForUserNotInDatabaseListError, + populate_users, users, initial_databases)