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:
parent
fac6e76b54
commit
2a5439aad2
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -535,7 +535,7 @@ mysql_opts = [
|
|||||||
deprecated_name='backup_incremental_strategy',
|
deprecated_name='backup_incremental_strategy',
|
||||||
deprecated_group='DEFAULT'),
|
deprecated_group='DEFAULT'),
|
||||||
cfg.StrOpt('root_controller',
|
cfg.StrOpt('root_controller',
|
||||||
default='trove.extensions.common.service.DefaultRootController',
|
default='trove.extensions.mysql.service.MySQLRootController',
|
||||||
help='Root controller implementation for mysql.'),
|
help='Root controller implementation for mysql.'),
|
||||||
cfg.ListOpt('ignore_users', default=['os_admin', 'root'],
|
cfg.ListOpt('ignore_users', default=['os_admin', 'root'],
|
||||||
help='Users to exclude when listing users.',
|
help='Users to exclude when listing users.',
|
||||||
|
@ -76,6 +76,11 @@ class Root(object):
|
|||||||
|
|
||||||
return root_user
|
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):
|
class RootHistory(object):
|
||||||
|
|
||||||
|
@ -49,6 +49,10 @@ class BaseDatastoreRootController(wsgi.Controller):
|
|||||||
def root_create(self, req, body, tenant_id, instance_id, is_cluster):
|
def root_create(self, req, body, tenant_id, instance_id, is_cluster):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def root_delete(self, req, tenant_id, instance_id, is_cluster):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DefaultRootController(BaseDatastoreRootController):
|
class DefaultRootController(BaseDatastoreRootController):
|
||||||
|
|
||||||
@ -79,6 +83,22 @@ class DefaultRootController(BaseDatastoreRootController):
|
|||||||
user_name, password)
|
user_name, password)
|
||||||
return wsgi.Result(views.RootCreatedView(root).data(), 200)
|
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):
|
class RootController(wsgi.Controller):
|
||||||
"""Controller for instance functionality."""
|
"""Controller for instance functionality."""
|
||||||
@ -102,6 +122,16 @@ class RootController(wsgi.Controller):
|
|||||||
else:
|
else:
|
||||||
raise NoSuchOptError
|
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):
|
def _get_datastore(self, tenant_id, instance_or_cluster_id):
|
||||||
"""
|
"""
|
||||||
Returns datastore manager and a boolean
|
Returns datastore manager and a boolean
|
||||||
|
@ -46,9 +46,12 @@ class User(object):
|
|||||||
self.databases = databases
|
self.databases = databases
|
||||||
|
|
||||||
@classmethod
|
@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)
|
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.name = username
|
||||||
validate.host = hostname
|
validate.host = hostname
|
||||||
client = create_guest_client(context, instance_id)
|
client = create_guest_client(context, instance_id)
|
||||||
|
@ -26,6 +26,7 @@ from trove.common.i18n import _
|
|||||||
from trove.common import pagination
|
from trove.common import pagination
|
||||||
from trove.common.utils import correct_id_with_req
|
from trove.common.utils import correct_id_with_req
|
||||||
from trove.common import wsgi
|
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_users
|
||||||
from trove.extensions.mysql.common import populate_validated_databases
|
from trove.extensions.mysql.common import populate_validated_databases
|
||||||
from trove.extensions.mysql.common import unquote_user_host
|
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):
|
def show(self, req, tenant_id, instance_id, id):
|
||||||
raise webob.exc.HTTPNotImplemented()
|
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)
|
||||||
|
@ -71,7 +71,8 @@ class Mysql(extensions.ExtensionDescriptor):
|
|||||||
'root',
|
'root',
|
||||||
common_service.RootController(),
|
common_service.RootController(),
|
||||||
parent={'member_name': 'instance',
|
parent={'member_name': 'instance',
|
||||||
'collection_name': '{tenant_id}/instances'})
|
'collection_name': '{tenant_id}/instances'},
|
||||||
|
collection_actions={'delete': 'DELETE'})
|
||||||
resources.append(resource)
|
resources.append(resource)
|
||||||
|
|
||||||
resource = extensions.ResourceExtension(
|
resource = extensions.ResourceExtension(
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
from oslo_log import log as logging
|
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.i18n import _LI
|
||||||
from trove.common import wsgi
|
from trove.common import wsgi
|
||||||
from trove.extensions.common.service import BaseDatastoreRootController
|
from trove.extensions.common.service import BaseDatastoreRootController
|
||||||
@ -23,6 +25,8 @@ from trove.extensions.vertica import models
|
|||||||
from trove.instance.models import DBInstance
|
from trove.instance.models import DBInstance
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
CONF = cfg.CONF
|
||||||
|
MANAGER = CONF.datastore_manager
|
||||||
|
|
||||||
|
|
||||||
class VerticaRootController(BaseDatastoreRootController):
|
class VerticaRootController(BaseDatastoreRootController):
|
||||||
@ -74,6 +78,10 @@ class VerticaRootController(BaseDatastoreRootController):
|
|||||||
return self.instance_root_create(req, body, master_instance_id,
|
return self.instance_root_create(req, body, master_instance_id,
|
||||||
cluster_instances)
|
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):
|
def _get_cluster_instance_id(self, tenant_id, cluster_id):
|
||||||
args = {'tenant_id': tenant_id, 'cluster_id': cluster_id}
|
args = {'tenant_id': tenant_id, 'cluster_id': cluster_id}
|
||||||
cluster_instances = DBInstance.find_all(**args).all()
|
cluster_instances = DBInstance.find_all(**args).all()
|
||||||
|
@ -87,3 +87,10 @@ class PgSqlRoot(PgSqlUsers):
|
|||||||
)
|
)
|
||||||
pgutil.psql(query, timeout=30)
|
pgutil.psql(query, timeout=30)
|
||||||
return user
|
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)
|
||||||
|
@ -587,6 +587,11 @@ class Manager(periodic_task.PeriodicTasks):
|
|||||||
raise exception.DatastoreOperationNotSupported(
|
raise exception.DatastoreOperationNotSupported(
|
||||||
operation='enable_root_with_password', datastore=self.manager)
|
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):
|
def is_root_enabled(self, context):
|
||||||
LOG.debug("Checking if root was ever enabled.")
|
LOG.debug("Checking if root was ever enabled.")
|
||||||
raise exception.DatastoreOperationNotSupported(
|
raise exception.DatastoreOperationNotSupported(
|
||||||
|
@ -175,6 +175,9 @@ class MySqlManager(manager.Manager):
|
|||||||
def is_root_enabled(self, context):
|
def is_root_enabled(self, context):
|
||||||
return self.mysql_admin().is_root_enabled()
|
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):
|
def _perform_restore(self, backup_info, context, restore_location, app):
|
||||||
LOG.info(_("Restoring database from backup %s.") % backup_info['id'])
|
LOG.info(_("Restoring database from backup %s.") % backup_info['id'])
|
||||||
try:
|
try:
|
||||||
|
@ -416,6 +416,11 @@ class BaseMySqlAdmin(object):
|
|||||||
"""
|
"""
|
||||||
return self.mysql_root_access.enable_root(root_password)
|
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):
|
def list_databases(self, limit=None, marker=None, include_marker=False):
|
||||||
"""List databases the user created on this mysql instance."""
|
"""List databases the user created on this mysql instance."""
|
||||||
LOG.debug("---Listing Databases---")
|
LOG.debug("---Listing Databases---")
|
||||||
@ -1011,10 +1016,7 @@ class BaseMySqlRootAccess(object):
|
|||||||
"""Enable the root user global access and/or
|
"""Enable the root user global access and/or
|
||||||
reset the root password.
|
reset the root password.
|
||||||
"""
|
"""
|
||||||
user = models.RootUser()
|
user = models.MySQLRootUser(root_password)
|
||||||
user.name = "root"
|
|
||||||
user.host = "%"
|
|
||||||
user.password = root_password or utils.generate_random_password()
|
|
||||||
with self.local_sql_client(self.mysql_app.get_engine()) as client:
|
with self.local_sql_client(self.mysql_app.get_engine()) as client:
|
||||||
print(client)
|
print(client)
|
||||||
try:
|
try:
|
||||||
@ -1044,3 +1046,9 @@ class BaseMySqlRootAccess(object):
|
|||||||
t = text(str(g))
|
t = text(str(g))
|
||||||
client.execute(t)
|
client.execute(t)
|
||||||
return user.serialize()
|
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))
|
||||||
|
@ -22,6 +22,7 @@ import netaddr
|
|||||||
from trove.common import cfg
|
from trove.common import cfg
|
||||||
from trove.common import exception
|
from trove.common import exception
|
||||||
from trove.common.i18n import _
|
from trove.common.i18n import _
|
||||||
|
from trove.common import utils
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
@ -840,6 +841,19 @@ class MySQLUser(Base):
|
|||||||
|
|
||||||
class RootUser(MySQLUser):
|
class RootUser(MySQLUser):
|
||||||
"""Overrides _ignore_users from the MySQLUser class."""
|
"""Overrides _ignore_users from the MySQLUser class."""
|
||||||
|
# _ignore_users = []
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._ignore_users = []
|
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
|
||||||
|
@ -101,6 +101,14 @@ class TestRoot(object):
|
|||||||
self._verify_root_timestamp(instance_info.id)
|
self._verify_root_timestamp(instance_info.id)
|
||||||
|
|
||||||
@test(depends_on=[test_root_initially_disabled_details])
|
@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):
|
def test_enable_root(self):
|
||||||
self._root()
|
self._root()
|
||||||
|
|
||||||
@ -170,3 +178,11 @@ class TestRoot(object):
|
|||||||
"""Even if root was enabled, the user root cannot be deleted."""
|
"""Even if root was enabled, the user root cannot be deleted."""
|
||||||
assert_raises(exceptions.BadRequest, self.dbaas.users.delete,
|
assert_raises(exceptions.BadRequest, self.dbaas.users.delete,
|
||||||
instance_info.id, "root")
|
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)
|
||||||
|
@ -116,3 +116,16 @@ class TestRootOnCreate(object):
|
|||||||
enabled = self.enabled(self.instance_id).rootEnabled
|
enabled = self.enabled(self.instance_id).rootEnabled
|
||||||
assert_equal(200, self.dbaas.last_http_code)
|
assert_equal(200, self.dbaas.last_http_code)
|
||||||
assert_true(enabled)
|
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)
|
||||||
|
@ -497,6 +497,17 @@ class Root(Example):
|
|||||||
lambda client: client.root.is_root_enabled(json_instance.id))
|
lambda client: client.root.is_root_enabled(json_instance.id))
|
||||||
assert_equal(results[JSON_INDEX].rootEnabled, True)
|
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):
|
class ActiveMixin(Example):
|
||||||
"""Adds a method to wait for instance status to become ACTIVE."""
|
"""Adds a method to wait for instance status to become ACTIVE."""
|
||||||
|
@ -157,6 +157,11 @@ class FakeGuest(object):
|
|||||||
"_databases": [],
|
"_databases": [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def disable_root(self):
|
||||||
|
self.delete_user({
|
||||||
|
"_name": "root",
|
||||||
|
"_host": "%"})
|
||||||
|
|
||||||
def delete_user(self, user):
|
def delete_user(self, user):
|
||||||
username = user['_name']
|
username = user['_name']
|
||||||
self._check_username(username)
|
self._check_username(username)
|
||||||
|
@ -42,6 +42,7 @@ from trove.conductor import api as conductor_api
|
|||||||
from trove.guestagent.common.configuration import ImportOverrideStrategy
|
from trove.guestagent.common.configuration import ImportOverrideStrategy
|
||||||
from trove.guestagent.common import operating_system
|
from trove.guestagent.common import operating_system
|
||||||
from trove.guestagent.common.operating_system import FileMode
|
from trove.guestagent.common.operating_system import FileMode
|
||||||
|
from trove.guestagent.common import sql_query
|
||||||
from trove.guestagent.datastore.experimental.cassandra import (
|
from trove.guestagent.datastore.experimental.cassandra import (
|
||||||
service as cass_service)
|
service as cass_service)
|
||||||
from trove.guestagent.datastore.experimental.cassandra import (
|
from trove.guestagent.datastore.experimental.cassandra import (
|
||||||
@ -1636,10 +1637,14 @@ class MySqlRootStatusTest(trove_testtools.TestCase):
|
|||||||
mock_execute.assert_any_call(TextClauseMatcher(
|
mock_execute.assert_any_call(TextClauseMatcher(
|
||||||
'UPDATE mysql.user'))
|
'UPDATE mysql.user'))
|
||||||
|
|
||||||
def test_enable_root_failed(self):
|
def test_root_disable(self):
|
||||||
with patch.object(models.MySQLUser, '_is_valid_user_name',
|
with patch.object(self.mock_client,
|
||||||
return_value=False):
|
'execute', return_value=None) as mock_execute:
|
||||||
self.assertRaises(ValueError, MySqlAdmin().enable_root)
|
# invocation
|
||||||
|
MySqlRootAccess().disable_root()
|
||||||
|
# verification
|
||||||
|
mock_execute.assert_any_call(TextClauseMatcher(
|
||||||
|
sql_query.REMOVE_ROOT))
|
||||||
|
|
||||||
|
|
||||||
class MockStats:
|
class MockStats:
|
||||||
|
@ -167,6 +167,11 @@ class GuestAgentManagerTest(trove_testtools.TestCase):
|
|||||||
self.assertThat(user_id, Is(enable_root_mock.return_value))
|
self.assertThat(user_id, Is(enable_root_mock.return_value))
|
||||||
enable_root_mock.assert_any_call()
|
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)
|
@patch.object(dbaas.MySqlAdmin, 'is_root_enabled', return_value=True)
|
||||||
def test_is_root_enabled(self, is_root_enabled_mock):
|
def test_is_root_enabled(self, is_root_enabled_mock):
|
||||||
is_enabled = self.manager.is_root_enabled(self.context)
|
is_enabled = self.manager.is_root_enabled(self.context)
|
||||||
|
Loading…
Reference in New Issue
Block a user