Add support for root-disable

Added the route action for disabling the root user in the extensions.

Modified the resource extension to allow the generation of a DELETE
route on the resource itself.

Implemented root-disable on the mysql guest.  Added not implemented
error messages for all other datastores.

Change-Id: I52519b86c47694c554b624d1d2fbe7a001af55fc
Partially implements: blueprint root-disable
Depends-On: I27831eb361c2b219a9623f152b9def73a2865d67
This commit is contained in:
Duk Loi 2015-06-08 13:42:37 -04:00
parent fac6e76b54
commit 2a5439aad2
20 changed files with 174 additions and 13 deletions

View File

@ -0,0 +1,7 @@
DELETE /v1.0/1234/instances/44b277eb-39be-4921-be31-3d61b43651d7/root HTTP/1.1
User-Agent: python-troveclient
Host: troveapi.org
X-Auth-Token: 87c6033c-9ff6-405f-943e-2deb73f278b7
Accept: application/json
Content-Type: application/json

View File

@ -0,0 +1,5 @@
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 0
Date: Mon, 18 Mar 2013 19:09:17 GMT

View File

@ -535,7 +535,7 @@ mysql_opts = [
deprecated_name='backup_incremental_strategy',
deprecated_group='DEFAULT'),
cfg.StrOpt('root_controller',
default='trove.extensions.common.service.DefaultRootController',
default='trove.extensions.mysql.service.MySQLRootController',
help='Root controller implementation for mysql.'),
cfg.ListOpt('ignore_users', default=['os_admin', 'root'],
help='Users to exclude when listing users.',

View File

@ -76,6 +76,11 @@ class Root(object):
return root_user
@classmethod
def delete(cls, context, instance_id):
load_and_verify(context, instance_id)
create_guest_client(context, instance_id).disable_root()
class RootHistory(object):

View File

@ -49,6 +49,10 @@ class BaseDatastoreRootController(wsgi.Controller):
def root_create(self, req, body, tenant_id, instance_id, is_cluster):
pass
@abc.abstractmethod
def root_delete(self, req, tenant_id, instance_id, is_cluster):
pass
class DefaultRootController(BaseDatastoreRootController):
@ -79,6 +83,22 @@ class DefaultRootController(BaseDatastoreRootController):
user_name, password)
return wsgi.Result(views.RootCreatedView(root).data(), 200)
def root_delete(self, req, tenant_id, instance_id, is_cluster):
if is_cluster:
raise exception.ClusterOperationNotSupported(
operation='disable_root')
LOG.info(_LI("Disabling root for instance '%s'.") % instance_id)
LOG.info(_LI("req : '%s'\n\n") % req)
context = req.environ[wsgi.CONTEXT_KEY]
try:
found_user = self._find_root_user(context, instance_id)
except (ValueError, AttributeError) as e:
raise exception.BadRequest(msg=str(e))
if not found_user:
raise exception.UserNotFound(uuid="root")
models.Root.delete(context, instance_id)
return wsgi.Result(None, 200)
class RootController(wsgi.Controller):
"""Controller for instance functionality."""
@ -102,6 +122,16 @@ class RootController(wsgi.Controller):
else:
raise NoSuchOptError
def delete(self, req, tenant_id, instance_id):
datastore_manager, is_cluster = self._get_datastore(tenant_id,
instance_id)
root_controller = self.load_root_controller(datastore_manager)
if root_controller is not None:
return root_controller.root_delete(req, tenant_id,
instance_id, is_cluster)
else:
raise NoSuchOptError
def _get_datastore(self, tenant_id, instance_or_cluster_id):
"""
Returns datastore manager and a boolean

View File

@ -46,9 +46,12 @@ class User(object):
self.databases = databases
@classmethod
def load(cls, context, instance_id, username, hostname):
def load(cls, context, instance_id, username, hostname, root_user=False):
load_and_verify(context, instance_id)
validate = guest_models.MySQLUser()
if root_user:
validate = guest_models.RootUser()
else:
validate = guest_models.MySQLUser()
validate.name = username
validate.host = hostname
client = create_guest_client(context, instance_id)

View File

@ -26,6 +26,7 @@ from trove.common.i18n import _
from trove.common import pagination
from trove.common.utils import correct_id_with_req
from trove.common import wsgi
from trove.extensions.common.service import DefaultRootController
from trove.extensions.mysql.common import populate_users
from trove.extensions.mysql.common import populate_validated_databases
from trove.extensions.mysql.common import unquote_user_host
@ -294,3 +295,12 @@ class SchemaController(wsgi.Controller):
def show(self, req, tenant_id, instance_id, id):
raise webob.exc.HTTPNotImplemented()
class MySQLRootController(DefaultRootController):
def _find_root_user(self, context, instance_id):
user = guest_models.MySQLRootUser()
return models.User.load(context, instance_id,
user.name, user.host,
root_user=True)

View File

@ -71,7 +71,8 @@ class Mysql(extensions.ExtensionDescriptor):
'root',
common_service.RootController(),
parent={'member_name': 'instance',
'collection_name': '{tenant_id}/instances'})
'collection_name': '{tenant_id}/instances'},
collection_actions={'delete': 'DELETE'})
resources.append(resource)
resource = extensions.ResourceExtension(

View File

@ -15,6 +15,8 @@
from oslo_log import log as logging
from trove.common import cfg
from trove.common import exception
from trove.common.i18n import _LI
from trove.common import wsgi
from trove.extensions.common.service import BaseDatastoreRootController
@ -23,6 +25,8 @@ from trove.extensions.vertica import models
from trove.instance.models import DBInstance
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
MANAGER = CONF.datastore_manager
class VerticaRootController(BaseDatastoreRootController):
@ -74,6 +78,10 @@ class VerticaRootController(BaseDatastoreRootController):
return self.instance_root_create(req, body, master_instance_id,
cluster_instances)
def delete(self, req, tenant_id, instance_id):
raise exception.DatastoreOperationNotSupported(
operation='disable_root', datastore=MANAGER)
def _get_cluster_instance_id(self, tenant_id, cluster_id):
args = {'tenant_id': tenant_id, 'cluster_id': cluster_id}
cluster_instances = DBInstance.find_all(**args).all()

View File

@ -87,3 +87,10 @@ class PgSqlRoot(PgSqlUsers):
)
pgutil.psql(query, timeout=30)
return user
def disable_root(self, context):
"""Generate a new random password for the public superuser account.
Do not disable its access rights. Once enabled the account should
stay that way.
"""
self.enable_root(context)

View File

@ -587,6 +587,11 @@ class Manager(periodic_task.PeriodicTasks):
raise exception.DatastoreOperationNotSupported(
operation='enable_root_with_password', datastore=self.manager)
def disable_root(self, context):
LOG.debug("Disabling root.")
raise exception.DatastoreOperationNotSupported(
operation='disable_root', datastore=self.manager)
def is_root_enabled(self, context):
LOG.debug("Checking if root was ever enabled.")
raise exception.DatastoreOperationNotSupported(

View File

@ -175,6 +175,9 @@ class MySqlManager(manager.Manager):
def is_root_enabled(self, context):
return self.mysql_admin().is_root_enabled()
def disable_root(self, context):
return self.mysql_admin().disable_root()
def _perform_restore(self, backup_info, context, restore_location, app):
LOG.info(_("Restoring database from backup %s.") % backup_info['id'])
try:

View File

@ -416,6 +416,11 @@ class BaseMySqlAdmin(object):
"""
return self.mysql_root_access.enable_root(root_password)
def disable_root(self):
"""Disable the root user global access
"""
return self.mysql_root_access.disable_root()
def list_databases(self, limit=None, marker=None, include_marker=False):
"""List databases the user created on this mysql instance."""
LOG.debug("---Listing Databases---")
@ -1011,10 +1016,7 @@ class BaseMySqlRootAccess(object):
"""Enable the root user global access and/or
reset the root password.
"""
user = models.RootUser()
user.name = "root"
user.host = "%"
user.password = root_password or utils.generate_random_password()
user = models.MySQLRootUser(root_password)
with self.local_sql_client(self.mysql_app.get_engine()) as client:
print(client)
try:
@ -1044,3 +1046,9 @@ class BaseMySqlRootAccess(object):
t = text(str(g))
client.execute(t)
return user.serialize()
def disable_root(self):
"""Disable the root user global access
"""
with self.local_sql_client(self.mysql_app.get_engine()) as client:
client.execute(text(sql_query.REMOVE_ROOT))

View File

@ -22,6 +22,7 @@ import netaddr
from trove.common import cfg
from trove.common import exception
from trove.common.i18n import _
from trove.common import utils
CONF = cfg.CONF
@ -840,6 +841,19 @@ class MySQLUser(Base):
class RootUser(MySQLUser):
"""Overrides _ignore_users from the MySQLUser class."""
# _ignore_users = []
def __init__(self):
self._ignore_users = []
class MySQLRootUser(RootUser):
"""Represents the MySQL root user."""
def __init__(self, password=None):
super(MySQLRootUser, self).__init__()
self._name = "root"
self._host = "%"
if password is None:
self._password = utils.generate_random_password()
else:
self._password = password

View File

@ -101,6 +101,14 @@ class TestRoot(object):
self._verify_root_timestamp(instance_info.id)
@test(depends_on=[test_root_initially_disabled_details])
def test_root_disable_when_root_not_enabled(self):
reh = self.dbaas_admin.management.root_enabled_history
self.root_enabled_timestamp = reh(instance_info.id).enabled
assert_raises(exceptions.NotFound, self.dbaas.root.delete,
instance_info.id)
self._verify_root_timestamp(instance_info.id)
@test(depends_on=[test_root_disable_when_root_not_enabled])
def test_enable_root(self):
self._root()
@ -170,3 +178,11 @@ class TestRoot(object):
"""Even if root was enabled, the user root cannot be deleted."""
assert_raises(exceptions.BadRequest, self.dbaas.users.delete,
instance_info.id, "root")
@test(depends_on=[test_root_still_enabled_details])
def test_root_disable(self):
reh = self.dbaas_admin.management.root_enabled_history
self.root_enabled_timestamp = reh(instance_info.id).enabled
self.dbaas.root.delete(instance_info.id)
assert_equal(200, self.dbaas.last_http_code)
self._verify_root_timestamp(instance_info.id)

View File

@ -116,3 +116,16 @@ class TestRootOnCreate(object):
enabled = self.enabled(self.instance_id).rootEnabled
assert_equal(200, self.dbaas.last_http_code)
assert_true(enabled)
@test(depends_on=[test_root_still_enabled])
def test_root_disable(self):
"""
After root disable ensure the the history enabled flag
is still enabled.
"""
self.dbaas.root.delete(self.instance_id)
assert_equal(200, self.dbaas.last_http_code)
enabled = self.enabled(self.instance_id).rootEnabled
assert_equal(200, self.dbaas.last_http_code)
assert_true(enabled)

View File

@ -497,6 +497,17 @@ class Root(Example):
lambda client: client.root.is_root_enabled(json_instance.id))
assert_equal(results[JSON_INDEX].rootEnabled, True)
@test(depends_on=[get_check_root_access])
def delete_disable_root_access(self):
self.snippet(
"disable_root_user",
"/instances/%s/root" % json_instance.id,
"DELETE", 200, "OK",
lambda client: client.root.delete(json_instance.id))
# restore root for subsequent tests
self.post_enable_root_access()
class ActiveMixin(Example):
"""Adds a method to wait for instance status to become ACTIVE."""

View File

@ -157,6 +157,11 @@ class FakeGuest(object):
"_databases": [],
})
def disable_root(self):
self.delete_user({
"_name": "root",
"_host": "%"})
def delete_user(self, user):
username = user['_name']
self._check_username(username)

View File

@ -42,6 +42,7 @@ from trove.conductor import api as conductor_api
from trove.guestagent.common.configuration import ImportOverrideStrategy
from trove.guestagent.common import operating_system
from trove.guestagent.common.operating_system import FileMode
from trove.guestagent.common import sql_query
from trove.guestagent.datastore.experimental.cassandra import (
service as cass_service)
from trove.guestagent.datastore.experimental.cassandra import (
@ -1636,10 +1637,14 @@ class MySqlRootStatusTest(trove_testtools.TestCase):
mock_execute.assert_any_call(TextClauseMatcher(
'UPDATE mysql.user'))
def test_enable_root_failed(self):
with patch.object(models.MySQLUser, '_is_valid_user_name',
return_value=False):
self.assertRaises(ValueError, MySqlAdmin().enable_root)
def test_root_disable(self):
with patch.object(self.mock_client,
'execute', return_value=None) as mock_execute:
# invocation
MySqlRootAccess().disable_root()
# verification
mock_execute.assert_any_call(TextClauseMatcher(
sql_query.REMOVE_ROOT))
class MockStats:

View File

@ -167,6 +167,11 @@ class GuestAgentManagerTest(trove_testtools.TestCase):
self.assertThat(user_id, Is(enable_root_mock.return_value))
enable_root_mock.assert_any_call()
@patch.object(dbaas.MySqlAdmin, 'disable_root')
def test_disable_root(self, disable_root_mock):
self.manager.disable_root(self.context)
disable_root_mock.assert_any_call()
@patch.object(dbaas.MySqlAdmin, 'is_root_enabled', return_value=True)
def test_is_root_enabled(self, is_root_enabled_mock):
is_enabled = self.manager.is_root_enabled(self.context)