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_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.',
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user