Adds optional host parameter to users.

All calls that involve calling a user by name now
also allow for the host to be specified optionally.

Similarly, all calls that respond with a user also include
the host, defaulting to '%', MySQL's shorthand for 'anywhere'.

Hosts with dots in the name are escaped to avoid a problem
with the way our routing parses URLs.

Change-Id: Idc5d514a7d862a723469ca8b49f1c51ae07f741b
This commit is contained in:
Ed Cranford
2013-03-11 10:53:36 -05:00
parent b2cfa3d465
commit 645a9ad417
4 changed files with 72 additions and 26 deletions

View File

@@ -140,6 +140,7 @@ class UserCommands(common.AuthedCommandsBase):
'id',
'database',
'databases',
'hostname',
'name',
'password',
]
@@ -149,19 +150,20 @@ class UserCommands(common.AuthedCommandsBase):
self._require('id', 'name', 'password', 'databases')
self._make_list('databases')
databases = [{'name': dbname} for dbname in self.databases]
users = [{'name': self.name, 'password': self.password,
'databases': databases}]
users = [{'name': self.name, 'host': self.hostname,
'password': self.password, 'databases': databases}]
self.dbaas.users.create(self.id, users)
def delete(self):
"""Delete the specified user"""
self._require('id', 'name')
self.dbaas.users.delete(self.id, self.name)
self.dbaas.users.delete(self.id, self.name, self.hostname)
def get(self):
"""Get a single user."""
self._require('id', 'name')
self._pretty_print(self.dbaas.users.get, self.id, self.name)
self._pretty_print(self.dbaas.users.get, self.id,
self.name, self.hostname)
def list(self):
"""List all the users for an instance"""
@@ -171,25 +173,29 @@ class UserCommands(common.AuthedCommandsBase):
def access(self):
"""Show all databases the user has access to."""
self._require('id', 'name')
self._pretty_list(self.dbaas.users.list_access, self.id, self.name)
self._pretty_list(self.dbaas.users.list_access, self.id,
self.name, self.hostname)
def grant(self):
"""Allow an existing user permissions to access one or more
databases."""
self._require('id', 'name', 'databases')
self._make_list('databases')
self.dbaas.users.grant(self.id, self.name, self.databases)
self.dbaas.users.grant(self.id, self.name, self.databases,
self.hostname)
def revoke(self):
"""Revoke from an existing user access permissions to a database."""
self._require('id', 'name', 'database')
self.dbaas.users.revoke(self.id, self.name, self.database)
self.dbaas.users.revoke(self.id, self.name, self.database,
self.hostname)
def change_password(self):
"""Change the password of a single user."""
self._require('id', 'name', 'password')
users = [{'name': self.name,
'password': self.password}]
'host': self.hostname,
'password': self.password}]
self.dbaas.users.change_passwords(self.id, users)

View File

@@ -23,6 +23,8 @@ from reddwarfclient import client
from reddwarfclient.xml import ReddwarfXmlClient
from reddwarfclient import exceptions
from urllib import quote
def methods_of(obj):
"""Get all callable methods of an object that don't start with underscore
@@ -68,6 +70,15 @@ def limit_url(url, limit=None, marker=None):
return url + query
def quote_user_host(user, host):
quoted = ''
if host:
quoted = quote("%s@%s" % (user, host))
else:
quoted = quote("%s@%%" % user)
return quoted.replace('.', '%2e')
class CliOptions(object):
"""A token object containing the user, apikey and token which
is pickleable."""

View File

@@ -18,9 +18,9 @@ from reddwarfclient import databases
from reddwarfclient.common import check_for_exceptions
from reddwarfclient.common import limit_url
from reddwarfclient.common import Paginated
from reddwarfclient.common import quote_user_host
import exceptions
import urlparse
from urllib import quote
class User(base.Resource):
@@ -46,8 +46,9 @@ class Users(base.ManagerWithFind):
resp, body = self.api.client.post(url, body=body)
check_for_exceptions(resp, body)
def delete(self, instance_id, user):
def delete(self, instance_id, username, hostname=None):
"""Delete an existing user in the specified instance"""
user = quote_user_host(username, hostname)
url = "/instances/%s/users/%s" % (instance_id, user)
resp, body = self.api.client.delete(url)
check_for_exceptions(resp, body)
@@ -78,41 +79,41 @@ class Users(base.ManagerWithFind):
return self._list("/instances/%s/users" % base.getid(instance),
"users", limit, marker)
def get(self, instance_id, user):
def get(self, instance_id, username, hostname=None):
"""
Get a single User from the instance's Database.
:rtype: :class:`User`.
"""
username = quote(user)
url = "/instances/%s/users/%s" % (instance_id, username)
user = quote_user_host(username, hostname)
url = "/instances/%s/users/%s" % (instance_id, user)
return self._get(url, "user")
def list_access(self, instance, user):
def list_access(self, instance, username, hostname=None):
"""Show all databases the given user has access to. """
instance_id = base.getid(instance)
username = quote(user)
url = "/instances/%(instance_id)s/users/%(username)s/databases"
user = quote_user_host(username, hostname)
url = "/instances/%(instance_id)s/users/%(user)s/databases"
resp, body = self.api.client.get(url % locals())
check_for_exceptions(resp, body)
if not body:
raise Exception("Call to %s did not return to a body" % url)
return [databases.Database(self, db) for db in body['databases']]
def grant(self, instance, user, databases):
def grant(self, instance, username, databases, hostname=None):
"""Allow an existing user permissions to access a database."""
instance_id = base.getid(instance)
username = quote(user)
url = "/instances/%(instance_id)s/users/%(username)s/databases"
user = quote_user_host(username, hostname)
url = "/instances/%(instance_id)s/users/%(user)s/databases"
dbs = {'databases': [{'name': db} for db in databases]}
resp, body = self.api.client.put(url % locals(), body=dbs)
check_for_exceptions(resp, body)
def revoke(self, instance, user, database):
def revoke(self, instance, username, database, hostname=None):
"""Revoke from an existing user access permissions to a database."""
instance_id = base.getid(instance)
username = quote(user)
url = ("/instances/%(instance_id)s/users/%(username)s/"
user = quote_user_host(username, hostname)
url = ("/instances/%(instance_id)s/users/%(user)s/"
"databases/%(database)s")
resp, body = self.api.client.delete(url % locals())
check_for_exceptions(resp, body)

View File

@@ -56,20 +56,48 @@ class UsersTest(TestCase):
return Mock(side_effect=side_effect_func)
def _build_fake_user(self, name, hostname=None, password=None,
databases=None):
return {'name': name,
'password': password if password else 'password',
'host': hostname,
'databases': databases if databases else [],
}
def test_create(self):
self.users.api.client.post = self._get_mock_method()
self._resp.status = 200
self.users.create(23, 'user1')
user = self._build_fake_user('user1')
self.users.create(23, [user])
self.assertEqual('/instances/23/users', self._url)
self.assertEqual({"users": 'user1'}, self._body)
self.assertEqual({"users": [user]}, self._body)
# Even if host isn't supplied originally,
# the default is supplied.
del user['host']
self.users.create(23, [user])
self.assertEqual('/instances/23/users', self._url)
user['host'] = '%'
self.assertEqual({"users": [user]}, self._body)
# If host is supplied, of course it's put into the body.
user['host'] = '127.0.0.1'
self.users.create(23, [user])
self.assertEqual({"users": [user]}, self._body)
# Make sure that response of 400 is recognized as an error.
user['host'] = '%'
self._resp.status = 400
self.assertRaises(Exception, self.users.create, 12, ['user1'])
self.assertRaises(Exception, self.users.create, 12, [user])
def test_delete(self):
self.users.api.client.delete = self._get_mock_method()
self._resp.status = 200
self.users.delete(27, 'user1')
self.assertEqual('/instances/27/users/user1', self._url)
# The client appends the host to remove ambiguity.
# urllib.unquote('%40%25') == '@%'
self.assertEqual('/instances/27/users/user1%40%25', self._url)
self._resp.status = 400
self.assertRaises(Exception, self.users.delete, 34, 'user1')