diff --git a/qa/tasks/mgr/dashboard/test_rgw.py b/qa/tasks/mgr/dashboard/test_rgw.py
index eeaf4c761dc..b913b22aa3d 100644
--- a/qa/tasks/mgr/dashboard/test_rgw.py
+++ b/qa/tasks/mgr/dashboard/test_rgw.py
@@ -7,51 +7,90 @@ logger = logging.getLogger(__name__)
 from .helper import DashboardTestCase, authenticate
 
 
-class RgwControllerTest(DashboardTestCase):
-    @authenticate
-    def test_rgw_daemon_list(self):
-        data = self._get('/api/rgw/daemon')
-        self.assertStatus(200)
+class RgwTestCase(DashboardTestCase):
 
-        self.assertEqual(len(data), 1)
-        data = data[0]
-        self.assertIn('id', data)
-        self.assertIn('version', data)
-        self.assertIn('server_hostname', data)
+    maxDiff = None
+    create_test_user = False
 
-    @authenticate
-    def test_rgw_daemon_get(self):
-        data = self._get('/api/rgw/daemon')
-        self.assertStatus(200)
-        data = self._get('/api/rgw/daemon/{}'.format(data[0]['id']))
-        self.assertStatus(200)
-
-        self.assertIn('rgw_metadata', data)
-        self.assertIn('rgw_id', data)
-        self.assertIn('rgw_status', data)
-        self.assertTrue(data['rgw_metadata'])
-
-
-class RgwApiUserTest(DashboardTestCase):
     @classmethod
     def setUpClass(cls):
-        super(RgwApiUserTest, cls).setUpClass()
+        super(RgwTestCase, cls).setUpClass()
+        # Create the administrator account.
         cls._radosgw_admin_cmd([
-            'user', 'create', '--uid=admin', '--display-name=admin',
-            '--system', '--access-key=admin', '--secret=admin'
+            'user', 'create', '--uid', 'admin', '--display-name', 'admin',
+            '--system', '--access-key', 'admin', '--secret', 'admin'
         ])
+        # Update the dashboard configuration.
         cls._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin'])
         cls._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin'])
+        # Create a test user?
+        if cls.create_test_user:
+            cls._radosgw_admin_cmd([
+                'user', 'create', '--uid', 'teuth-test-user',
+                '--display-name', 'teuth-test-user'
+            ])
+            cls._radosgw_admin_cmd([
+                'caps', 'add', '--uid', 'teuth-test-user',
+                '--caps', 'metadata=write'
+            ])
+            cls._radosgw_admin_cmd([
+                'subuser', 'create', '--uid', 'teuth-test-user',
+                '--subuser', 'teuth-test-subuser', '--access',
+                'full', '--key-type', 's3', '--access-key',
+                'xyz123'
+            ])
+            cls._radosgw_admin_cmd([
+                'subuser', 'create', '--uid', 'teuth-test-user',
+                '--subuser', 'teuth-test-subuser2', '--access',
+                'full', '--key-type', 'swift'
+            ])
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls.create_test_user:
+            cls._radosgw_admin_cmd(['user', 'rm', '--uid=teuth-test-user'])
+        super(DashboardTestCase, cls).tearDownClass()
+
+    def get_rgw_user(self, uid):
+        return self._get('/api/rgw/user/{}'.format(uid))
+
+    def find_in_list(self, key, value, data):
+        """
+        Helper function to find an object with the specified key/value
+        in a list.
+        :param key: The name of the key.
+        :param value: The value to search for.
+        :param data: The list to process.
+        :return: Returns the found object or None.
+        """
+        return next(iter(filter(lambda x: x[key] == value, data)), None)
+
+
+class RgwApiCredentialsTest(RgwTestCase):
 
     def setUp(self):
         # Restart the Dashboard module to ensure that the connection to the
         # RGW Admin Ops API is re-established with the new credentials.
         self._ceph_cmd(['mgr', 'module', 'disable', 'dashboard'])
         self._ceph_cmd(['mgr', 'module', 'enable', 'dashboard', '--force'])
+        # Set the default credentials.
+        self._ceph_cmd(['dashboard', 'set-rgw-api-user-id', ''])
+        self._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin'])
+        self._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin'])
+
+    @authenticate
+    def test_no_access_secret_key(self):
+        self._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', ''])
+        self._ceph_cmd(['dashboard', 'set-rgw-api-access-key', ''])
+        resp = self._get('/api/rgw/user')
+        self.assertStatus(500)
+        self.assertIn('detail', resp)
+        self.assertIn('component', resp)
+        self.assertIn('No RGW credentials found', resp['detail'])
+        self.assertEquals(resp['component'], 'rgw')
 
     @authenticate
     def test_success(self):
-        self._ceph_cmd(['dashboard', 'set-rgw-api-user-id', ''])
         data = self._get('/api/rgw/status')
         self.assertStatus(200)
         self.assertIn('available', data)
@@ -59,7 +98,7 @@ class RgwApiUserTest(DashboardTestCase):
         self.assertTrue(data['available'])
 
     @authenticate
-    def test_failure(self):
+    def test_invalid_user_id(self):
         self._ceph_cmd(['dashboard', 'set-rgw-api-user-id', 'xyz'])
         data = self._get('/api/rgw/status')
         self.assertStatus(200)
@@ -70,32 +109,115 @@ class RgwApiUserTest(DashboardTestCase):
                       data['message'])
 
 
-class RgwProxyExceptionsTest(DashboardTestCase):
+class RgwBucketTest(RgwTestCase):
 
     @classmethod
     def setUpClass(cls):
-        super(RgwProxyExceptionsTest, cls).setUpClass()
-
-        cls._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', ''])
-        cls._ceph_cmd(['dashboard', 'set-rgw-api-access-key', ''])
+        cls.create_test_user = True
+        super(RgwBucketTest, cls).setUpClass()
 
     @authenticate
-    def test_no_credentials_exception(self):
-        resp = self._get('/api/rgw/proxy/status')
-        self.assertStatus(500)
-        self.assertIn('detail', resp)
+    def test_all(self):
+        # Create a new bucket.
+        self._post(
+            '/api/rgw/bucket',
+            params={
+                'bucket': 'teuth-test-bucket',
+                'uid': 'admin'
+            })
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self.assertIn('bucket_info', data)
+        data = data['bucket_info']
+        self.assertIn('bucket', data)
+        self.assertIn('quota', data)
+        self.assertIn('creation_time', data)
+        self.assertIn('name', data['bucket'])
+        self.assertIn('bucket_id', data['bucket'])
+        self.assertEquals(data['bucket']['name'], 'teuth-test-bucket')
+
+        # List all buckets.
+        data = self._get('/api/rgw/bucket')
+        self.assertStatus(200)
+        self.assertEqual(len(data), 1)
+        self.assertIn('teuth-test-bucket', data)
+
+        # Get the bucket.
+        data = self._get('/api/rgw/bucket/teuth-test-bucket')
+        self.assertStatus(200)
+        self.assertIn('id', data)
+        self.assertIn('bucket', data)
+        self.assertIn('bucket_quota', data)
+        self.assertIn('owner', data)
+        self.assertEquals(data['bucket'], 'teuth-test-bucket')
+        self.assertEquals(data['owner'], 'admin')
+
+        # Update the bucket.
+        self._put(
+            '/api/rgw/bucket/teuth-test-bucket',
+            params={
+                'bucket_id': data['id'],
+                'uid': 'teuth-test-user'
+            })
+        self.assertStatus(200)
+        data = self._get('/api/rgw/bucket/teuth-test-bucket')
+        self.assertStatus(200)
+        self.assertEquals(data['owner'], 'teuth-test-user')
+
+        # Delete the bucket.
+        self._delete('/api/rgw/bucket/teuth-test-bucket')
+        self.assertStatus(204)
+        data = self._get('/api/rgw/bucket')
+        self.assertStatus(200)
+        self.assertEqual(len(data), 0)
 
 
-class RgwProxyTest(DashboardTestCase):
-    @classmethod
-    def setUpClass(cls):
-        super(RgwProxyTest, cls).setUpClass()
-        cls._radosgw_admin_cmd([
+class RgwDaemonTest(DashboardTestCase):
+
+    @authenticate
+    def test_list(self):
+        data = self._get('/api/rgw/daemon')
+        self.assertStatus(200)
+        self.assertEqual(len(data), 1)
+        data = data[0]
+        self.assertIn('id', data)
+        self.assertIn('version', data)
+        self.assertIn('server_hostname', data)
+
+    @authenticate
+    def test_get(self):
+        data = self._get('/api/rgw/daemon')
+        self.assertStatus(200)
+
+        data = self._get('/api/rgw/daemon/{}'.format(data[0]['id']))
+        self.assertStatus(200)
+        self.assertIn('rgw_metadata', data)
+        self.assertIn('rgw_id', data)
+        self.assertIn('rgw_status', data)
+        self.assertTrue(data['rgw_metadata'])
+
+    @authenticate
+    def test_status(self):
+        self._radosgw_admin_cmd([
             'user', 'create', '--uid=admin', '--display-name=admin',
             '--system', '--access-key=admin', '--secret=admin'
         ])
-        cls._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin'])
-        cls._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin'])
+        self._ceph_cmd(['dashboard', 'set-rgw-api-user-id', 'admin'])
+        self._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin'])
+        self._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin'])
+
+        data = self._get('/api/rgw/status')
+        self.assertStatus(200)
+        self.assertIn('available', data)
+        self.assertIn('message', data)
+        self.assertTrue(data['available'])
+
+
+class RgwUserTest(RgwTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super(RgwUserTest, cls).setUpClass()
 
     def _assert_user_data(self, data):
         self.assertIn('caps', data)
@@ -110,71 +232,308 @@ class RgwProxyTest(DashboardTestCase):
         self.assertIn('tenant', data)
         self.assertIn('user_id', data)
 
-    def _test_put(self):
-        self._put(
-            '/api/rgw/proxy/user',
-            params={
-                'uid': 'teuth-test-user',
-                'display-name': 'display name',
-            })
-        data = self._resp.json()
-
+    @authenticate
+    def test_get(self):
+        data = self.get_rgw_user('admin')
+        self.assertStatus(200)
         self._assert_user_data(data)
+        self.assertEquals(data['user_id'], 'admin')
+
+    @authenticate
+    def test_list(self):
+        data = self._get('/api/rgw/user')
         self.assertStatus(200)
+        self.assertGreaterEqual(len(data), 1)
+        self.assertIn('admin', data)
 
-        data = self._get(
-            '/api/rgw/proxy/user', params={'uid': 'teuth-test-user'})
-
-        self.assertStatus(200)
-        self.assertEqual(data['user_id'], 'teuth-test-user')
-
-    def _test_get(self):
-        data = self._get(
-            '/api/rgw/proxy/user', params={'uid': 'teuth-test-user'})
-
+    @authenticate
+    def test_create_update_delete(self):
+        # Create a new user.
+        self._post('/api/rgw/user', params={
+            'uid': 'teuth-test-user',
+            'display_name': 'display name'
+        })
+        self.assertStatus(201)
+        data = self.jsonBody()
         self._assert_user_data(data)
+        self.assertEquals(data['user_id'], 'teuth-test-user')
+        self.assertEquals(data['display_name'], 'display name')
+
+        # Get the user.
+        data = self.get_rgw_user('teuth-test-user')
         self.assertStatus(200)
+        self._assert_user_data(data)
         self.assertEquals(data['user_id'], 'teuth-test-user')
 
-    def _test_post(self):
-        """Updates the user"""
-        self._post(
-            '/api/rgw/proxy/user',
+        # Update the user.
+        self._put(
+            '/api/rgw/user/teuth-test-user',
             params={
-                'uid': 'teuth-test-user',
-                'display-name': 'new name'
+                'display_name': 'new name'
             })
-
         self.assertStatus(200)
-        self._assert_user_data(self._resp.json())
-        self.assertEqual(self._resp.json()['display_name'], 'new name')
+        data = self.jsonBody()
+        self._assert_user_data(data)
+        self.assertEqual(data['display_name'], 'new name')
 
-    def _test_delete(self):
-        self._delete('/api/rgw/proxy/user', params={'uid': 'teuth-test-user'})
-        self.assertStatus(200)
-
-        self._delete('/api/rgw/proxy/user', params={'uid': 'teuth-test-user'})
+        # Delete the user.
+        self._delete('/api/rgw/user/teuth-test-user')
+        self.assertStatus(204)
+        self.get_rgw_user('teuth-test-user')
         self.assertStatus(500)
-        resp = self._resp.json()
+        resp = self.jsonBody()
         self.assertIn('detail', resp)
         self.assertIn('failed request with status code 404', resp['detail'])
         self.assertIn('"Code":"NoSuchUser"', resp['detail'])
         self.assertIn('"HostId"', resp['detail'])
         self.assertIn('"RequestId"', resp['detail'])
 
+
+class RgwUserCapabilityTest(RgwTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.create_test_user = True
+        super(RgwUserCapabilityTest, cls).setUpClass()
+
     @authenticate
-    def test_rgw_proxy(self):
-        """Test basic request types"""
-        self.maxDiff = None
+    def test_set(self):
+        self._post(
+            '/api/rgw/user/teuth-test-user/capability',
+            params={
+                'type': 'usage',
+                'perm': 'read'
+            })
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self.assertEqual(len(data), 1)
+        data = data[0]
+        self.assertEqual(data['type'], 'usage')
+        self.assertEqual(data['perm'], 'read')
 
-        # PUT - Create a user
-        self._test_put()
+        # Get the user data to validate the capabilities.
+        data = self.get_rgw_user('teuth-test-user')
+        self.assertStatus(200)
+        self.assertGreaterEqual(len(data['caps']), 1)
+        self.assertEqual(data['caps'][0]['type'], 'usage')
+        self.assertEqual(data['caps'][0]['perm'], 'read')
 
-        # GET - Get the user details
-        self._test_get()
+    @authenticate
+    def test_delete(self):
+        self._delete(
+            '/api/rgw/user/teuth-test-user/capability',
+            params={
+                'type': 'metadata',
+                'perm': 'write'
+            })
+        self.assertStatus(204)
 
-        # POST - Update the user details
-        self._test_post()
+        # Get the user data to validate the capabilities.
+        data = self.get_rgw_user('teuth-test-user')
+        self.assertStatus(200)
+        self.assertEqual(len(data['caps']), 0)
 
-        # DELETE - Delete the user
-        self._test_delete()
+
+class RgwUserKeyTest(RgwTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.create_test_user = True
+        super(RgwUserKeyTest, cls).setUpClass()
+
+    @authenticate
+    def test_create_s3(self):
+        self._post(
+            '/api/rgw/user/teuth-test-user/key',
+            params={
+                'key_type': 's3',
+                'generate_key': 'false',
+                'access_key': 'abc987',
+                'secret_key': 'aaabbbccc'
+            })
+        data = self.jsonBody()
+        self.assertStatus(201)
+        self.assertGreaterEqual(len(data), 3)
+        key = self.find_in_list('access_key', 'abc987', data)
+        self.assertIsInstance(key, object)
+        self.assertEqual(key['secret_key'], 'aaabbbccc')
+
+    @authenticate
+    def test_create_swift(self):
+        self._post(
+            '/api/rgw/user/teuth-test-user/key',
+            params={
+                'key_type': 'swift',
+                'subuser': 'teuth-test-subuser',
+                'generate_key': 'false',
+                'secret_key': 'xxxyyyzzz'
+            })
+        data = self.jsonBody()
+        self.assertStatus(201)
+        self.assertGreaterEqual(len(data), 2)
+        key = self.find_in_list('secret_key', 'xxxyyyzzz', data)
+        self.assertIsInstance(key, object)
+
+    @authenticate
+    def test_delete_s3(self):
+        self._delete(
+            '/api/rgw/user/teuth-test-user/key',
+            params={
+                'key_type': 's3',
+                'access_key': 'xyz123'
+            })
+        self.assertStatus(204)
+
+    @authenticate
+    def test_delete_swift(self):
+        self._delete(
+            '/api/rgw/user/teuth-test-user/key',
+            params={
+                'key_type': 'swift',
+                'subuser': 'teuth-test-user:teuth-test-subuser2'
+            })
+        self.assertStatus(204)
+
+
+class RgwUserQuotaTest(RgwTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.create_test_user = True
+        super(RgwUserQuotaTest, cls).setUpClass()
+
+    def _assert_quota(self, data):
+        self.assertIn('user_quota', data)
+        self.assertIn('max_objects', data['user_quota'])
+        self.assertIn('enabled', data['user_quota'])
+        self.assertIn('max_size_kb', data['user_quota'])
+        self.assertIn('max_size', data['user_quota'])
+        self.assertIn('bucket_quota', data)
+        self.assertIn('max_objects', data['bucket_quota'])
+        self.assertIn('enabled', data['bucket_quota'])
+        self.assertIn('max_size_kb', data['bucket_quota'])
+        self.assertIn('max_size', data['bucket_quota'])
+
+    @authenticate
+    def test_get_quota(self):
+        data = self._get('/api/rgw/user/teuth-test-user/quota')
+        self.assertStatus(200)
+        self._assert_quota(data)
+
+    @authenticate
+    def test_set_user_quota(self):
+        self._put(
+            '/api/rgw/user/teuth-test-user/quota',
+            params={
+                'quota_type': 'user',
+                'enabled': 'true',
+                'max_size_kb': 2048,
+                'max_objects': 101
+            })
+        self.assertStatus(200)
+
+        data = self._get('/api/rgw/user/teuth-test-user/quota')
+        self.assertStatus(200)
+        self._assert_quota(data)
+        self.assertEqual(data['user_quota']['max_objects'], 101)
+        self.assertTrue(data['user_quota']['enabled'])
+        self.assertEqual(data['user_quota']['max_size_kb'], 2048)
+
+    @authenticate
+    def test_set_bucket_quota(self):
+        self._put(
+            '/api/rgw/user/teuth-test-user/quota',
+            params={
+                'quota_type': 'bucket',
+                'enabled': 'false',
+                'max_size_kb': 4096,
+                'max_objects': 2000
+            })
+        self.assertStatus(200)
+
+        data = self._get('/api/rgw/user/teuth-test-user/quota')
+        self.assertStatus(200)
+        self._assert_quota(data)
+        self.assertEqual(data['bucket_quota']['max_objects'], 2000)
+        self.assertFalse(data['bucket_quota']['enabled'])
+        self.assertEqual(data['bucket_quota']['max_size_kb'], 4096)
+
+
+class RgwUserSubuserTest(RgwTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.create_test_user = True
+        super(RgwUserSubuserTest, cls).setUpClass()
+
+    @authenticate
+    def test_create_swift(self):
+        self._post(
+            '/api/rgw/user/teuth-test-user/subuser',
+            params={
+                'subuser': 'tux',
+                'access': 'readwrite',
+                'key_type': 'swift'
+            })
+        self.assertStatus(201)
+        data = self.jsonBody()
+        subuser = self.find_in_list('id', 'teuth-test-user:tux', data)
+        self.assertIsInstance(subuser, object)
+        self.assertEqual(subuser['permissions'], 'read-write')
+
+        # Get the user data to validate the keys.
+        data = self.get_rgw_user('teuth-test-user')
+        self.assertStatus(200)
+        key = self.find_in_list('user', 'teuth-test-user:tux', data['swift_keys'])
+        self.assertIsInstance(key, object)
+
+    @authenticate
+    def test_create_s3(self):
+        self._post(
+            '/api/rgw/user/teuth-test-user/subuser',
+            params={
+                'subuser': 'hugo',
+                'access': 'write',
+                'generate_secret': 'false',
+                'access_key': 'yyy',
+                'secret_key': 'xxx'
+            })
+        self.assertStatus(201)
+        data = self.jsonBody()
+        subuser = self.find_in_list('id', 'teuth-test-user:hugo', data)
+        self.assertIsInstance(subuser, object)
+        self.assertEqual(subuser['permissions'], 'write')
+
+        # Get the user data to validate the keys.
+        data = self.get_rgw_user('teuth-test-user')
+        self.assertStatus(200)
+        key = self.find_in_list('user', 'teuth-test-user:hugo', data['keys'])
+        self.assertIsInstance(key, object)
+        self.assertEqual(key['secret_key'], 'xxx')
+
+    @authenticate
+    def test_delete_w_purge(self):
+        self._delete(
+            '/api/rgw/user/teuth-test-user/subuser/teuth-test-subuser2')
+        self.assertStatus(204)
+
+        # Get the user data to check that the keys don't exist anymore.
+        data = self.get_rgw_user('teuth-test-user')
+        self.assertStatus(200)
+        key = self.find_in_list('user', 'teuth-test-user:teuth-test-subuser2',
+                                data['swift_keys'])
+        self.assertIsNone(key)
+
+    @authenticate
+    def test_delete_wo_purge(self):
+        self._delete(
+            '/api/rgw/user/teuth-test-user/subuser/teuth-test-subuser',
+            params={'purge_keys': 'false'})
+        self.assertStatus(204)
+
+        # Get the user data to check whether they keys still exist.
+        data = self.get_rgw_user('teuth-test-user')
+        self.assertStatus(200)
+        key = self.find_in_list('user', 'teuth-test-user:teuth-test-subuser',
+                                data['keys'])
+        self.assertIsInstance(key, object)
diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py
index 4d00178be7a..a86c753cff5 100644
--- a/src/pybind/mgr/dashboard/controllers/__init__.py
+++ b/src/pybind/mgr/dashboard/controllers/__init__.py
@@ -509,19 +509,19 @@ class BaseController(object):
                 content_length = int(cherrypy.request.headers['Content-Length'])
                 body = cherrypy.request.body.read(content_length)
                 if not body:
-                    return func(*args, **kwargs)
-
-                try:
-                    data = json.loads(body.decode('utf-8'))
-                except Exception as e:
-                    raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
-                                             .format(str(e)))
-                kwargs.update(data.items())
-                ret = func(*args, **kwargs)
+                    ret = func(*args, **kwargs)
+                else:
+                    try:
+                        data = json.loads(body.decode('utf-8'))
+                    except Exception as e:
+                        raise cherrypy.HTTPError(400, 'Failed to decode JSON: {}'
+                                                 .format(str(e)))
+                    kwargs.update(data.items())
+                    ret = func(*args, **kwargs)
 
             if json_response:
                 cherrypy.response.headers['Content-Type'] = 'application/json'
-                return json.dumps(ret).encode('utf8')
+                ret = json.dumps(ret).encode('utf8')
             return ret
         return inner
 
diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py
index 26f4dce5045..e1b551aafe6 100644
--- a/src/pybind/mgr/dashboard/controllers/rgw.py
+++ b/src/pybind/mgr/dashboard/controllers/rgw.py
@@ -4,12 +4,12 @@ from __future__ import absolute_import
 import json
 import cherrypy
 
-from . import ApiController, BaseController, RESTController, AuthRequired, \
-              Endpoint, Proxy
+from . import ApiController, BaseController, RESTController, AuthRequired, Endpoint
 from .. import logger
 from ..services.ceph_service import CephService
 from ..services.rgw_client import RgwClient
 from ..rest_client import RequestException
+from ..exceptions import DashboardException
 
 
 @ApiController('/rgw')
@@ -73,7 +73,7 @@ class RgwDaemon(RESTController):
             try:
                 status = json.loads(status['json'])
             except ValueError:
-                logger.warning("%s had invalid status json", service['id'])
+                logger.warning('%s had invalid status json', service['id'])
                 status = {}
         else:
             logger.warning('%s has no key "json" in status', service['id'])
@@ -83,40 +83,179 @@ class RgwDaemon(RESTController):
         return daemon
 
 
-@ApiController('/rgw/proxy')
-@AuthRequired()
-class RgwProxy(BaseController):
+class RgwRESTController(RESTController):
 
-    @Proxy()
-    def __call__(self, path, **params):
+    def proxy(self, method, path, params=None, json_response=True):
         try:
-            rgw_client = RgwClient.admin_instance()
-
-            method = cherrypy.request.method
-            data = None
-
-            if cherrypy.request.body.length:
-                data = cherrypy.request.body.read()
-
-            return rgw_client.proxy(method, path, params, data)
-        except RequestException as e:
-            # Always use status code 500 and NOT the status that may delivered
-            # by the exception. That's because we do not want to forward e.g.
-            # 401 or 404 that may trigger unwanted actions in the UI.
-            cherrypy.response.headers['Content-Type'] = 'application/json'
-            cherrypy.response.status = 500
-            return json.dumps({'detail': str(e)}).encode('utf-8')
+            instance = RgwClient.admin_instance()
+            result = instance.proxy(method, path, params, None)
+            if json_response and result != '':
+                result = json.loads(result.decode('utf-8'))
+            return result
+        except (DashboardException, RequestException) as e:
+            raise DashboardException(e, http_status_code=500, component='rgw')
 
 
 @ApiController('/rgw/bucket')
 @AuthRequired()
-class RgwBucket(RESTController):
+class RgwBucket(RgwRESTController):
+
+    def list(self):
+        return self.proxy('GET', 'bucket')
+
+    def get(self, bucket):
+        return self.proxy('GET', 'bucket', {'bucket': bucket})
 
     def create(self, bucket, uid):
         try:
             rgw_client = RgwClient.instance(uid)
             return rgw_client.create_bucket(bucket)
         except RequestException as e:
-            cherrypy.response.headers['Content-Type'] = 'application/json'
-            cherrypy.response.status = 500
-            return {'detail': str(e)}
+            raise DashboardException(e, http_status_code=500, component='rgw')
+
+    def set(self, bucket, bucket_id, uid):
+        return self.proxy('PUT', 'bucket', {
+            'bucket': bucket,
+            'bucket-id': bucket_id,
+            'uid': uid
+        }, json_response=False)
+
+    def delete(self, bucket, purge_objects='true'):
+        return self.proxy('DELETE', 'bucket', {
+            'bucket': bucket,
+            'purge-objects': purge_objects
+        }, json_response=False)
+
+
+@ApiController('/rgw/user')
+@AuthRequired()
+class RgwUser(RgwRESTController):
+
+    def list(self):
+        return self.proxy('GET', 'metadata/user')
+
+    def get(self, uid):
+        return self.proxy('GET', 'user', {'uid': uid})
+
+    def create(self, uid, display_name, email=None, max_buckets=None,
+               suspended=None, generate_key=None, access_key=None,
+               secret_key=None):
+        params = {'uid': uid}
+        if display_name is not None:
+            params['display-name'] = display_name
+        if email is not None:
+            params['email'] = email
+        if max_buckets is not None:
+            params['max-buckets'] = max_buckets
+        if suspended is not None:
+            params['suspended'] = suspended
+        if generate_key is not None:
+            params['generate-key'] = generate_key
+        if access_key is not None:
+            params['access-key'] = access_key
+        if secret_key is not None:
+            params['secret-key'] = secret_key
+        return self.proxy('PUT', 'user', params)
+
+    def set(self, uid, display_name=None, email=None, max_buckets=None,
+            suspended=None):
+        params = {'uid': uid}
+        if display_name is not None:
+            params['display-name'] = display_name
+        if email is not None:
+            params['email'] = email
+        if max_buckets is not None:
+            params['max-buckets'] = max_buckets
+        if suspended is not None:
+            params['suspended'] = suspended
+        return self.proxy('POST', 'user', params)
+
+    def delete(self, uid):
+        try:
+            instance = RgwClient.admin_instance()
+            # Ensure the user is not configured to access the RGW Object Gateway.
+            if instance.userid == uid:
+                raise DashboardException(msg='Unable to delete "{}" - this user '
+                                             'account is required for managing the '
+                                             'Object Gateway'.format(uid))
+            # Finally redirect request to the RGW proxy.
+            return self.proxy('DELETE', 'user', {'uid': uid}, json_response=False)
+        except (DashboardException, RequestException) as e:
+            raise DashboardException(e, component='rgw')
+
+    # pylint: disable=redefined-builtin
+    @RESTController.Resource(method='POST', path='/capability', status=201)
+    def create_cap(self, uid, type, perm):
+        return self.proxy('PUT', 'user?caps', {
+            'uid': uid,
+            'user-caps': '{}={}'.format(type, perm)
+        })
+
+    # pylint: disable=redefined-builtin
+    @RESTController.Resource(method='DELETE', path='/capability', status=204)
+    def delete_cap(self, uid, type, perm):
+        return self.proxy('DELETE', 'user?caps', {
+            'uid': uid,
+            'user-caps': '{}={}'.format(type, perm)
+        })
+
+    @RESTController.Resource(method='POST', path='/key', status=201)
+    def create_key(self, uid, key_type='s3', subuser=None, generate_key='true',
+                   access_key=None, secret_key=None):
+        params = {'uid': uid, 'key-type': key_type, 'generate-key': generate_key}
+        if subuser is not None:
+            params['subuser'] = subuser
+        if access_key is not None:
+            params['access-key'] = access_key
+        if secret_key is not None:
+            params['secret-key'] = secret_key
+        return self.proxy('PUT', 'user?key', params)
+
+    @RESTController.Resource(method='DELETE', path='/key', status=204)
+    def delete_key(self, uid, key_type='s3', subuser=None, access_key=None):
+        params = {'uid': uid, 'key-type': key_type}
+        if subuser is not None:
+            params['subuser'] = subuser
+        if access_key is not None:
+            params['access-key'] = access_key
+        return self.proxy('DELETE', 'user?key', params, json_response=False)
+
+    @RESTController.Resource(method='GET', path='/quota')
+    def get_quota(self, uid):
+        return self.proxy('GET', 'user?quota', {'uid': uid})
+
+    @RESTController.Resource(method='PUT', path='/quota')
+    def set_quota(self, uid, quota_type, enabled, max_size_kb, max_objects):
+        return self.proxy('PUT', 'user?quota', {
+            'uid': uid,
+            'quota-type': quota_type,
+            'enabled': enabled,
+            'max-size-kb': max_size_kb,
+            'max-objects': max_objects
+        }, json_response=False)
+
+    @RESTController.Resource(method='POST', path='/subuser', status=201)
+    def create_subuser(self, uid, subuser, access, key_type='s3',
+                       generate_secret='true', access_key=None,
+                       secret_key=None):
+        return self.proxy('PUT', 'user', {
+            'uid': uid,
+            'subuser': subuser,
+            'key-type': key_type,
+            'access': access,
+            'generate-secret': generate_secret,
+            'access-key': access_key,
+            'secret-key': secret_key
+        })
+
+    @RESTController.Resource(method='DELETE', path='/subuser/{subuser}', status=204)
+    def delete_subuser(self, uid, subuser, purge_keys='true'):
+        """
+        :param purge_keys: Set to False to do not purge the keys.
+                           Note, this only works for s3 subusers.
+        """
+        return self.proxy('DELETE', 'user', {
+            'uid': uid,
+            'subuser': subuser,
+            'purge-keys': purge_keys
+        }, json_response=False)
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
index b12eae02ca0..6710cb1c4ab 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
@@ -91,7 +91,7 @@ export class RgwBucketFormComponent implements OnInit {
     if (this.editing) {
       // Edit
       const idCtl = this.bucketForm.get('id');
-      this.rgwBucketService.update(idCtl.value, bucketCtl.value, ownerCtl.value).subscribe(
+      this.rgwBucketService.update(bucketCtl.value, idCtl.value, ownerCtl.value).subscribe(
         () => {
           this.goToListView();
         },
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
index 93ba8f44605..98aba62679d 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
@@ -117,12 +117,18 @@
           <label class="control-label col-sm-3"
                  for="max_buckets"
                  i18n>Max. buckets
+            <span class="required"></span>
           </label>
           <div class="col-sm-9">
             <input id="max_buckets"
                    class="form-control"
                    type="number"
                    formControlName="max_buckets">
+            <span class="help-block"
+                  *ngIf="(frm.submitted || userForm.controls.max_buckets.dirty) && userForm.controls.max_buckets.hasError('required')"
+                  i18n>
+              This field is required.
+            </span>
             <span class="help-block"
                   *ngIf="(frm.submitted || userForm.controls.max_buckets.dirty) && userForm.controls.max_buckets.hasError('min')"
                   i18n>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts
index f58ca81fedb..bca02882ecc 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.spec.ts
@@ -9,6 +9,7 @@ import { of as observableOf } from 'rxjs';
 import { RgwUserService } from '../../../shared/api/rgw-user.service';
 import { SharedModule } from '../../../shared/shared.module';
 import { configureTestBed } from '../../../shared/unit-test-helper';
+import { RgwUserS3Key } from '../models/rgw-user-s3-key';
 import { RgwUserFormComponent } from './rgw-user-form.component';
 
 describe('RgwUserFormComponent', () => {
@@ -46,6 +47,53 @@ describe('RgwUserFormComponent', () => {
     expect(component).toBeTruthy();
   });
 
+  describe('s3 key management', () => {
+    let rgwUserService: RgwUserService;
+
+    beforeEach(() => {
+      rgwUserService = TestBed.get(RgwUserService);
+      spyOn(rgwUserService, 'addS3Key').and.stub();
+    });
+
+    it('should not update key', () => {
+      component.setS3Key(new RgwUserS3Key(), 3);
+      expect(component.s3Keys.length).toBe(0);
+      expect(rgwUserService.addS3Key).not.toHaveBeenCalled();
+    });
+
+    it('should set key', () => {
+      const key = new RgwUserS3Key();
+      key.user = 'test1:subuser2';
+      component.setS3Key(key);
+      expect(component.s3Keys.length).toBe(1);
+      expect(component.s3Keys[0].user).toBe('test1:subuser2');
+      expect(rgwUserService.addS3Key).toHaveBeenCalledWith(
+        'test1', {
+          subuser: 'subuser2',
+          generate_key: 'false',
+          access_key: undefined,
+          secret_key: undefined
+        }
+      );
+    });
+
+    it('should set key w/o subuser', () => {
+      const key = new RgwUserS3Key();
+      key.user = 'test1';
+      component.setS3Key(key);
+      expect(component.s3Keys.length).toBe(1);
+      expect(component.s3Keys[0].user).toBe('test1');
+      expect(rgwUserService.addS3Key).toHaveBeenCalledWith(
+        'test1', {
+          subuser: '',
+          generate_key: 'false',
+          access_key: undefined,
+          secret_key: undefined
+        }
+      );
+    });
+  });
+
   describe('quotaMaxSizeValidator', () => {
     it('should validate max size (1/7)', () => {
       const resp = component.quotaMaxSizeValidator(new FormControl(''));
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
index 66a44720680..01073a59e7f 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.ts
@@ -59,7 +59,7 @@ export class RgwUserFormComponent implements OnInit {
       user_id: [null, [Validators.required], [this.userIdValidator()]],
       display_name: [null, [Validators.required]],
       email: [null, [CdValidators.email]],
-      max_buckets: [null, [Validators.min(0)]],
+      max_buckets: [1000, [Validators.required, Validators.min(0)]],
       suspended: [false],
       // S3 key
       generate_key: [true],
@@ -181,8 +181,8 @@ export class RgwUserFormComponent implements OnInit {
               value[type + '_quota_max_size'] = quota.max_size;
             }
             if (quota.max_objects < 0) {
-              value[type + '_quota_max_size_unlimited'] = true;
-              value[type + '_quota_max_size'] = null;
+              value[type + '_quota_max_objects_unlimited'] = true;
+              value[type + '_quota_max_objects'] = null;
             } else {
               value[type + '_quota_max_objects_unlimited'] = false;
               value[type + '_quota_max_objects'] = quota.max_objects;
@@ -225,26 +225,27 @@ export class RgwUserFormComponent implements OnInit {
     if (this.userForm.pristine) {
       this.goToListView();
     }
+    const uid = this.userForm.get('user_id').value;
     if (this.editing) {
       // Edit
       if (this._isGeneralDirty()) {
-        const args = this._getApiPostArgs();
-        this.submitObservables.push(this.rgwUserService.post(args));
+        const args = this._getUpdateArgs();
+        this.submitObservables.push(this.rgwUserService.update(uid, args));
       }
     } else {
       // Add
-      const args = this._getApiPutArgs();
-      this.submitObservables.push(this.rgwUserService.put(args));
+      const args = this._getCreateArgs();
+      this.submitObservables.push(this.rgwUserService.create(args));
     }
     // Check if user quota has been modified.
     if (this._isUserQuotaDirty()) {
-      const userQuotaArgs = this._getApiUserQuotaArgs();
-      this.submitObservables.push(this.rgwUserService.putQuota(userQuotaArgs));
+      const userQuotaArgs = this._getUserQuotaArgs();
+      this.submitObservables.push(this.rgwUserService.updateQuota(uid, userQuotaArgs));
     }
     // Check if bucket quota has been modified.
     if (this._isBucketQuotaDirty()) {
-      const bucketQuotaArgs = this._getApiBucketQuotaArgs();
-      this.submitObservables.push(this.rgwUserService.putQuota(bucketQuotaArgs));
+      const bucketQuotaArgs = this._getBucketQuotaArgs();
+      this.submitObservables.push(this.rgwUserService.updateQuota(uid, bucketQuotaArgs));
     }
     // Finally execute all observables.
     observableForkJoin(this.submitObservables).subscribe(
@@ -304,31 +305,29 @@ export class RgwUserFormComponent implements OnInit {
    * Add/Update a subuser.
    */
   setSubuser(subuser: RgwUserSubuser, index?: number) {
+    const mapPermissions = {
+      'full-control': 'full',
+      'read-write': 'readwrite'
+    };
+    const uid = this.userForm.get('user_id').value;
+    const args = {
+      subuser: subuser.id,
+      access:
+        subuser.permissions in mapPermissions
+          ? mapPermissions[subuser.permissions]
+          : subuser.permissions,
+      key_type: 'swift',
+      secret_key: subuser.secret_key,
+      generate_secret: subuser.generate_secret ? 'true' : 'false'
+    };
+    this.submitObservables.push(this.rgwUserService.createSubuser(uid, args));
     if (_.isNumber(index)) {
       // Modify
       // Create an observable to modify the subuser when the form is submitted.
-      this.submitObservables.push(
-        this.rgwUserService.addSubuser(
-          this.userForm.get('user_id').value,
-          subuser.id,
-          subuser.permissions,
-          subuser.secret_key,
-          subuser.generate_secret
-        )
-      );
       this.subusers[index] = subuser;
     } else {
       // Add
       // Create an observable to add the subuser when the form is submitted.
-      this.submitObservables.push(
-        this.rgwUserService.addSubuser(
-          this.userForm.get('user_id').value,
-          subuser.id,
-          subuser.permissions,
-          subuser.secret_key,
-          subuser.generate_secret
-        )
-      );
       this.subusers.push(subuser);
       // Add a Swift key. If the secret key is auto-generated, then visualize
       // this to the user by displaying a notification instead of the key.
@@ -418,16 +417,17 @@ export class RgwUserFormComponent implements OnInit {
       // Nothing to do here at the moment.
     } else {
       // Add
+      // Split the key's user name into its user and subuser parts.
+      const userMatches = key.user.match(/([^:]+)(:(.+))?/);
       // Create an observable to add the S3 key when the form is submitted.
-      this.submitObservables.push(
-        this.rgwUserService.addS3Key(
-          this.userForm.get('user_id').value,
-          key.user,
-          key.access_key,
-          key.secret_key,
-          key.generate_key
-        )
-      );
+      const uid = userMatches[1];
+      const args = {
+        subuser: userMatches[2] ? userMatches[3] : '',
+        generate_key: key.generate_key ? 'true' : 'false',
+        access_key: key.access_key,
+        secret_key: key.secret_key
+      };
+      this.submitObservables.push(this.rgwUserService.addS3Key(uid, args));
       // If the access and the secret key are auto-generated, then visualize
       // this to the user by displaying a notification instead of the key.
       this.s3Keys.push({
@@ -578,31 +578,28 @@ export class RgwUserFormComponent implements OnInit {
    * Helper function to get the arguments of the API request when a new
    * user is created.
    */
-  private _getApiPutArgs() {
+  private _getCreateArgs() {
     const result = {
       uid: this.userForm.get('user_id').value,
-      'display-name': this.userForm.get('display_name').value
+      display_name: this.userForm.get('display_name').value,
+      suspended: this.userForm.get('suspended').value,
+      email: '',
+      max_buckets: this.userForm.get('max_buckets').value,
+      generate_key: this.userForm.get('generate_key').value,
+      access_key: '',
+      secret_key: ''
     };
-    const suspendedCtl = this.userForm.get('suspended');
-    if (suspendedCtl.value) {
-      _.extend(result, { suspended: suspendedCtl.value });
-    }
     const emailCtl = this.userForm.get('email');
     if (_.isString(emailCtl.value) && emailCtl.value.length > 0) {
-      _.extend(result, { email: emailCtl.value });
-    }
-    const maxBucketsCtl = this.userForm.get('max_buckets');
-    if (maxBucketsCtl.value > 0) {
-      _.extend(result, { 'max-buckets': maxBucketsCtl.value });
+      _.merge(result, { email: emailCtl.value });
     }
     const generateKeyCtl = this.userForm.get('generate_key');
     if (!generateKeyCtl.value) {
-      _.extend(result, {
-        'access-key': this.userForm.get('access_key').value,
-        'secret-key': this.userForm.get('secret_key').value
+      _.merge(result, {
+        generate_key: false,
+        access_key: this.userForm.get('access_key').value,
+        secret_key: this.userForm.get('secret_key').value
       });
-    } else {
-      _.extend(result, { 'generate-key': true });
     }
     return result;
   }
@@ -611,21 +608,12 @@ export class RgwUserFormComponent implements OnInit {
    * Helper function to get the arguments for the API request when the user
    * configuration has been modified.
    */
-  private _getApiPostArgs() {
-    const result = {
-      uid: this.userForm.get('user_id').value
-    };
-    const argsMap = {
-      'display-name': 'display_name',
-      email: 'email',
-      'max-buckets': 'max_buckets',
-      suspended: 'suspended'
-    };
-    for (const key of Object.keys(argsMap)) {
-      const ctl = this.userForm.get(argsMap[key]);
-      if (ctl.dirty) {
-        result[key] = ctl.value;
-      }
+  private _getUpdateArgs() {
+    const result = {};
+    const keys = ['display_name', 'email', 'max_buckets', 'suspended'];
+    for (const key of keys) {
+      const ctl = this.userForm.get(key);
+      result[key] = ctl.value;
     }
     return result;
   }
@@ -634,22 +622,21 @@ export class RgwUserFormComponent implements OnInit {
    * Helper function to get the arguments for the API request when the user
    * quota configuration has been modified.
    */
-  private _getApiUserQuotaArgs(): object {
+  private _getUserQuotaArgs(): object {
     const result = {
-      uid: this.userForm.get('user_id').value,
-      'quota-type': 'user',
+      quota_type: 'user',
       enabled: this.userForm.get('user_quota_enabled').value,
-      'max-size-kb': -1,
-      'max-objects': -1
+      max_size_kb: -1,
+      max_objects: -1
     };
     if (!this.userForm.get('user_quota_max_size_unlimited').value) {
       // Convert the given value to bytes.
       const bytes = new FormatterService().toBytes(this.userForm.get('user_quota_max_size').value);
       // Finally convert the value to KiB.
-      result['max-size-kb'] = (bytes / 1024).toFixed(0) as any;
+      result['max_size_kb'] = (bytes / 1024).toFixed(0) as any;
     }
     if (!this.userForm.get('user_quota_max_objects_unlimited').value) {
-      result['max-objects'] = this.userForm.get('user_quota_max_objects').value;
+      result['max_objects'] = this.userForm.get('user_quota_max_objects').value;
     }
     return result;
   }
@@ -658,13 +645,12 @@ export class RgwUserFormComponent implements OnInit {
    * Helper function to get the arguments for the API request when the bucket
    * quota configuration has been modified.
    */
-  private _getApiBucketQuotaArgs(): object {
+  private _getBucketQuotaArgs(): object {
     const result = {
-      uid: this.userForm.get('user_id').value,
-      'quota-type': 'bucket',
+      quota_type: 'bucket',
       enabled: this.userForm.get('bucket_quota_enabled').value,
-      'max-size-kb': -1,
-      'max-objects': -1
+      max_size_kb: -1,
+      max_objects: -1
     };
     if (!this.userForm.get('bucket_quota_max_size_unlimited').value) {
       // Convert the given value to bytes.
@@ -672,10 +658,10 @@ export class RgwUserFormComponent implements OnInit {
         this.userForm.get('bucket_quota_max_size').value
       );
       // Finally convert the value to KiB.
-      result['max-size-kb'] = (bytes / 1024).toFixed(0) as any;
+      result['max_size_kb'] = (bytes / 1024).toFixed(0) as any;
     }
     if (!this.userForm.get('bucket_quota_max_objects_unlimited').value) {
-      result['max-objects'] = this.userForm.get('bucket_quota_max_objects').value;
+      result['max_objects'] = this.userForm.get('bucket_quota_max_objects').value;
     }
     return result;
   }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
index 1a7c7c73173..a57b1be688d 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
@@ -31,7 +31,7 @@ describe('RgwBucketService', () => {
     service.list().subscribe((resp) => {
       result = resp;
     });
-    const req = httpTesting.expectOne('/api/rgw/proxy/bucket');
+    const req = httpTesting.expectOne('/api/rgw/bucket');
     req.flush([]);
     expect(req.request.method).toBe('GET');
     expect(result).toEqual([]);
@@ -42,13 +42,13 @@ describe('RgwBucketService', () => {
     service.list().subscribe((resp) => {
       result = resp;
     });
-    let req = httpTesting.expectOne('/api/rgw/proxy/bucket');
+    let req = httpTesting.expectOne('/api/rgw/bucket');
     req.flush(['foo', 'bar']);
 
-    req = httpTesting.expectOne('/api/rgw/proxy/bucket?bucket=foo');
+    req = httpTesting.expectOne('/api/rgw/bucket/foo');
     req.flush({ name: 'foo' });
 
-    req = httpTesting.expectOne('/api/rgw/proxy/bucket?bucket=bar');
+    req = httpTesting.expectOne('/api/rgw/bucket/bar');
     req.flush({ name: 'bar' });
 
     expect(req.request.method).toBe('GET');
@@ -57,35 +57,31 @@ describe('RgwBucketService', () => {
 
   it('should call get', () => {
     service.get('foo').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/bucket?bucket=foo');
+    const req = httpTesting.expectOne('/api/rgw/bucket/foo');
     expect(req.request.method).toBe('GET');
   });
 
   it('should call create', () => {
     service.create('foo', 'bar').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/bucket');
+    const req = httpTesting.expectOne('/api/rgw/bucket?bucket=foo&uid=bar');
     expect(req.request.method).toBe('POST');
-    expect(req.request.body).toEqual({
-      bucket: 'foo',
-      uid: 'bar'
-    });
   });
 
   it('should call update', () => {
     service.update('foo', 'bar', 'baz').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/bucket?bucket=bar&bucket-id=foo&uid=baz');
+    const req = httpTesting.expectOne('/api/rgw/bucket/foo?bucket_id=bar&uid=baz');
     expect(req.request.method).toBe('PUT');
   });
 
   it('should call delete, with purgeObjects = true', () => {
     service.delete('foo').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/bucket?bucket=foo&purge-objects=true');
+    const req = httpTesting.expectOne('/api/rgw/bucket/foo?purge_objects=true');
     expect(req.request.method).toBe('DELETE');
   });
 
   it('should call delete, with purgeObjects = false', () => {
     service.delete('foo', false).subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/bucket?bucket=foo&purge-objects=false');
+    const req = httpTesting.expectOne('/api/rgw/bucket/foo?purge_objects=false');
     expect(req.request.method).toBe('DELETE');
   });
 
@@ -94,7 +90,7 @@ describe('RgwBucketService', () => {
     service.exists('foo').subscribe((resp) => {
       result = resp;
     });
-    const req = httpTesting.expectOne('/api/rgw/proxy/bucket');
+    const req = httpTesting.expectOne('/api/rgw/bucket');
     expect(req.request.method).toBe('GET');
     req.flush(['foo', 'bar']);
     expect(result).toBe(true);
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
index e6b4062a5a1..baa609218b8 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
@@ -11,7 +11,7 @@ import { ApiModule } from './api.module';
   providedIn: ApiModule
 })
 export class RgwBucketService {
-  private url = '/api/rgw/proxy/bucket';
+  private url = '/api/rgw/bucket';
 
   constructor(private http: HttpClient) {}
 
@@ -26,10 +26,12 @@ export class RgwBucketService {
           return observableForkJoin(
             buckets.map((bucket: string) => {
               return this.get(bucket);
-            }));
+            })
+          );
         }
         return observableOf([]);
-      }));
+      })
+    );
   }
 
   /**
@@ -41,32 +43,27 @@ export class RgwBucketService {
   }
 
   get(bucket: string) {
-    let params = new HttpParams();
-    params = params.append('bucket', bucket);
-    return this.http.get(this.url, { params: params });
+    return this.http.get(`${this.url}/${bucket}`);
   }
 
   create(bucket: string, uid: string) {
-    const body = {
-      bucket: bucket,
-      uid: uid
-    };
-    return this.http.post('/api/rgw/bucket', body);
-  }
-
-  update(bucketId: string, bucket: string, uid: string) {
     let params = new HttpParams();
     params = params.append('bucket', bucket);
-    params = params.append('bucket-id', bucketId as string);
     params = params.append('uid', uid);
-    return this.http.put(this.url, null, { params: params });
+    return this.http.post(this.url, null, { params: params });
+  }
+
+  update(bucket: string, bucketId: string, uid: string) {
+    let params = new HttpParams();
+    params = params.append('bucket_id', bucketId);
+    params = params.append('uid', uid);
+    return this.http.put(`${this.url}/${bucket}`, null, { params: params});
   }
 
   delete(bucket: string, purgeObjects = true) {
     let params = new HttpParams();
-    params = params.append('bucket', bucket);
-    params = params.append('purge-objects', purgeObjects ? 'true' : 'false');
-    return this.http.delete(this.url, { params: params });
+    params = params.append('purge_objects', purgeObjects ? 'true' : 'false');
+    return this.http.delete(`${this.url}/${bucket}`, { params: params });
   }
 
   /**
@@ -79,6 +76,7 @@ export class RgwBucketService {
       mergeMap((resp: string[]) => {
         const index = _.indexOf(resp, bucket);
         return observableOf(-1 !== index);
-      }));
+      })
+    );
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts
index e9569ef19ec..b9acad14d48 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.spec.ts
@@ -33,7 +33,7 @@ describe('RgwUserService', () => {
     service.list().subscribe((resp) => {
       result = resp;
     });
-    const req = httpTesting.expectOne('/api/rgw/proxy/metadata/user');
+    const req = httpTesting.expectOne('/api/rgw/user');
     expect(req.request.method).toBe('GET');
     req.flush([]);
     expect(result).toEqual([]);
@@ -45,15 +45,15 @@ describe('RgwUserService', () => {
       result = resp;
     });
 
-    let req = httpTesting.expectOne('/api/rgw/proxy/metadata/user');
+    let req = httpTesting.expectOne('/api/rgw/user');
     expect(req.request.method).toBe('GET');
     req.flush(['foo', 'bar']);
 
-    req = httpTesting.expectOne('/api/rgw/proxy/user?uid=foo');
+    req = httpTesting.expectOne('/api/rgw/user/foo');
     expect(req.request.method).toBe('GET');
     req.flush({ name: 'foo' });
 
-    req = httpTesting.expectOne('/api/rgw/proxy/user?uid=bar');
+    req = httpTesting.expectOne('/api/rgw/user/bar');
     expect(req.request.method).toBe('GET');
     req.flush({ name: 'bar' });
 
@@ -62,100 +62,79 @@ describe('RgwUserService', () => {
 
   it('should call enumerate', () => {
     service.enumerate().subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/metadata/user');
+    const req = httpTesting.expectOne('/api/rgw/user');
     expect(req.request.method).toBe('GET');
   });
 
   it('should call get', () => {
     service.get('foo').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?uid=foo');
+    const req = httpTesting.expectOne('/api/rgw/user/foo');
     expect(req.request.method).toBe('GET');
   });
 
   it('should call getQuota', () => {
     service.getQuota('foo').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?quota&uid=foo');
+    const req = httpTesting.expectOne('/api/rgw/user/foo/quota');
     expect(req.request.method).toBe('GET');
   });
 
-  it('should call put', () => {
-    service.put({ foo: 'bar' }).subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?foo=bar');
+  it('should call update', () => {
+    service.update('foo', { xxx: 'yyy' }).subscribe();
+    const req = httpTesting.expectOne('/api/rgw/user/foo?xxx=yyy');
     expect(req.request.method).toBe('PUT');
   });
 
-  it('should call putQuota', () => {
-    service.putQuota({ foo: 'bar' }).subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?quota&foo=bar');
+  it('should call updateQuota', () => {
+    service.updateQuota('foo', { xxx: 'yyy' }).subscribe();
+    const req = httpTesting.expectOne('/api/rgw/user/foo/quota?xxx=yyy');
     expect(req.request.method).toBe('PUT');
   });
 
-  it('should call post', () => {
-    service.post({ foo: 'bar' }).subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?foo=bar');
+  it('should call create', () => {
+    service.create({ foo: 'bar' }).subscribe();
+    const req = httpTesting.expectOne('/api/rgw/user?foo=bar');
     expect(req.request.method).toBe('POST');
   });
 
   it('should call delete', () => {
     service.delete('foo').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?uid=foo');
+    const req = httpTesting.expectOne('/api/rgw/user/foo');
     expect(req.request.method).toBe('DELETE');
   });
 
-  it('should call addSubuser with unrecognized permission', () => {
-    service.addSubuser('foo', 'bar', 'baz', null, true).subscribe();
-    const req = httpTesting.expectOne(
-      '/api/rgw/proxy/user?uid=foo&subuser=bar&key-type=swift&access=baz&generate-secret=true'
-    );
-    expect(req.request.method).toBe('PUT');
-  });
-
-  it('should call addSubuser with mapped permission', () => {
-    service.addSubuser('foo', 'bar', 'full-control', 'baz', false).subscribe();
-    const req = httpTesting.expectOne(
-      '/api/rgw/proxy/user?uid=foo&subuser=bar&key-type=swift&access=full&secret-key=baz'
-    );
-    expect(req.request.method).toBe('PUT');
+  it('should call createSubuser', () => {
+    service.createSubuser('foo', { xxx: 'yyy' }).subscribe();
+    const req = httpTesting.expectOne('/api/rgw/user/foo/subuser?xxx=yyy');
+    expect(req.request.method).toBe('POST');
   });
 
   it('should call deleteSubuser', () => {
     service.deleteSubuser('foo', 'bar').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?uid=foo&subuser=bar&purge-keys=true');
+    const req = httpTesting.expectOne('/api/rgw/user/foo/subuser/bar');
     expect(req.request.method).toBe('DELETE');
   });
 
   it('should call addCapability', () => {
     service.addCapability('foo', 'bar', 'baz').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?caps&uid=foo&user-caps=bar=baz');
-    expect(req.request.method).toBe('PUT');
+    const req = httpTesting.expectOne('/api/rgw/user/foo/capability?type=bar&perm=baz');
+    expect(req.request.method).toBe('POST');
   });
 
   it('should call deleteCapability', () => {
     service.deleteCapability('foo', 'bar', 'baz').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?caps&uid=foo&user-caps=bar=baz');
+    const req = httpTesting.expectOne('/api/rgw/user/foo/capability?type=bar&perm=baz');
     expect(req.request.method).toBe('DELETE');
   });
 
-  it('should call addS3Key, with generateKey true', () => {
-    service.addS3Key('1', 'foo', 'bar', 'baz', true).subscribe();
-    const req = httpTesting.expectOne(
-      '/api/rgw/proxy/user?key&uid=1&key-type=s3&generate-key=true&subuser=foo'
-    );
-    expect(req.request.method).toBe('PUT');
-  });
-
-  it('should call addS3Key, with generateKey false', () => {
-    service.addS3Key('1', 'foo', 'bar', 'baz', false).subscribe();
-    const req = httpTesting.expectOne(
-      '/api/rgw/proxy/user?key' +
-        '&uid=1&key-type=s3&generate-key=false&access-key=bar&secret-key=baz&subuser=foo'
-    );
-    expect(req.request.method).toBe('PUT');
+  it('should call addS3Key', () => {
+    service.addS3Key('foo', { xxx: 'yyy' }).subscribe();
+    const req = httpTesting.expectOne('/api/rgw/user/foo/key?key_type=s3&xxx=yyy');
+    expect(req.request.method).toBe('POST');
   });
 
   it('should call deleteS3Key', () => {
     service.deleteS3Key('foo', 'bar').subscribe();
-    const req = httpTesting.expectOne('/api/rgw/proxy/user?key&uid=foo&key-type=s3&access-key=bar');
+    const req = httpTesting.expectOne('/api/rgw/user/foo/key?key_type=s3&access_key=bar');
     expect(req.request.method).toBe('DELETE');
   });
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
index f1b78f12baf..e93e2102d3f 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-user.service.ts
@@ -2,7 +2,7 @@ import { HttpClient, HttpParams } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
 import * as _ from 'lodash';
-import {forkJoin as observableForkJoin, of as observableOf } from 'rxjs';
+import { forkJoin as observableForkJoin, of as observableOf } from 'rxjs';
 import { mergeMap } from 'rxjs/operators';
 
 import { ApiModule } from './api.module';
@@ -11,7 +11,7 @@ import { ApiModule } from './api.module';
   providedIn: ApiModule
 })
 export class RgwUserService {
-  private url = '/api/rgw/proxy/user';
+  private url = '/api/rgw/user';
 
   constructor(private http: HttpClient) {}
 
@@ -26,10 +26,12 @@ export class RgwUserService {
           return observableForkJoin(
             uids.map((uid: string) => {
               return this.get(uid);
-            }));
+            })
+          );
         }
         return observableOf([]);
-      }));
+      })
+    );
   }
 
   /**
@@ -37,38 +39,18 @@ export class RgwUserService {
    * @return {Observable<string[]>}
    */
   enumerate() {
-    return this.http.get('/api/rgw/proxy/metadata/user');
+    return this.http.get(this.url);
   }
 
   get(uid: string) {
-    let params = new HttpParams();
-    params = params.append('uid', uid);
-    return this.http.get(this.url, { params: params });
+    return this.http.get(`${this.url}/${uid}`);
   }
 
   getQuota(uid: string) {
-    let params = new HttpParams();
-    params = params.append('uid', uid);
-    return this.http.get(`${this.url}?quota`, { params: params });
+    return this.http.get(`${this.url}/${uid}/quota`);
   }
 
-  put(args: object) {
-    let params = new HttpParams();
-    _.keys(args).forEach((key) => {
-      params = params.append(key, args[key]);
-    });
-    return this.http.put(this.url, null, { params: params });
-  }
-
-  putQuota(args: object) {
-    let params = new HttpParams();
-    _.keys(args).forEach((key) => {
-      params = params.append(key, args[key]);
-    });
-    return this.http.put(`${this.url}?quota`, null, { params: params });
-  }
-
-  post(args: object) {
+  create(args: object) {
     let params = new HttpParams();
     _.keys(args).forEach((key) => {
       params = params.append(key, args[key]);
@@ -76,86 +58,66 @@ export class RgwUserService {
     return this.http.post(this.url, null, { params: params });
   }
 
-  delete(uid: string) {
+  update(uid: string, args: object) {
     let params = new HttpParams();
-    params = params.append('uid', uid);
-    return this.http.delete(this.url, { params: params });
+    _.keys(args).forEach((key) => {
+      params = params.append(key, args[key]);
+    });
+    return this.http.put(`${this.url}/${uid}`, null, { params: params });
   }
 
-  addSubuser(
-    uid: string,
-    subuser: string,
-    permissions: string,
-    secretKey: string,
-    generateSecret: boolean
-  ) {
-    const mapPermissions = {
-      'full-control': 'full',
-      'read-write': 'readwrite'
-    };
+  updateQuota(uid: string, args: object) {
     let params = new HttpParams();
-    params = params.append('uid', uid);
-    params = params.append('subuser', subuser);
-    params = params.append('key-type', 'swift');
-    params = params.append(
-      'access',
-      permissions in mapPermissions ? mapPermissions[permissions] : permissions
-    );
-    if (generateSecret) {
-      params = params.append('generate-secret', 'true');
-    } else {
-      params = params.append('secret-key', secretKey);
-    }
-    return this.http.put(this.url, null, { params: params });
+    _.keys(args).forEach((key) => {
+      params = params.append(key, args[key]);
+    });
+    return this.http.put(`${this.url}/${uid}/quota`, null, { params: params });
+  }
+
+  delete(uid: string) {
+    return this.http.delete(`${this.url}/${uid}`);
+  }
+
+  createSubuser(uid: string, args: object) {
+    let params = new HttpParams();
+    _.keys(args).forEach((key) => {
+      params = params.append(key, args[key]);
+    });
+    return this.http.post(`${this.url}/${uid}/subuser`, null, { params: params });
   }
 
   deleteSubuser(uid: string, subuser: string) {
-    let params = new HttpParams();
-    params = params.append('uid', uid);
-    params = params.append('subuser', subuser);
-    params = params.append('purge-keys', 'true');
-    return this.http.delete(this.url, { params: params });
+    return this.http.delete(`${this.url}/${uid}/subuser/${subuser}`);
   }
 
   addCapability(uid: string, type: string, perm: string) {
     let params = new HttpParams();
-    params = params.append('uid', uid);
-    params = params.append('user-caps', `${type}=${perm}`);
-    return this.http.put(`${this.url}?caps`, null, { params: params });
+    params = params.append('type', type);
+    params = params.append('perm', perm);
+    return this.http.post(`${this.url}/${uid}/capability`, null, { params: params });
   }
 
   deleteCapability(uid: string, type: string, perm: string) {
     let params = new HttpParams();
-    params = params.append('uid', uid);
-    params = params.append('user-caps', `${type}=${perm}`);
-    return this.http.delete(`${this.url}?caps`, { params: params });
+    params = params.append('type', type);
+    params = params.append('perm', perm);
+    return this.http.delete(`${this.url}/${uid}/capability`, { params: params });
   }
 
-  addS3Key(
-    uid: string,
-    subuser: string,
-    accessKey: string,
-    secretKey: string,
-    generateKey: boolean
-  ) {
+  addS3Key(uid: string, args: object) {
     let params = new HttpParams();
-    params = params.append('uid', uid);
-    params = params.append('key-type', 's3');
-    params = params.append('generate-key', generateKey ? 'true' : 'false');
-    if (!generateKey) {
-      params = params.append('access-key', accessKey);
-      params = params.append('secret-key', secretKey);
-    }
-    params = params.append('subuser', subuser);
-    return this.http.put(`${this.url}?key`, null, { params: params });
+    params = params.append('key_type', 's3');
+    _.keys(args).forEach((key) => {
+      params = params.append(key, args[key]);
+    });
+    return this.http.post(`${this.url}/${uid}/key`, null, { params: params });
   }
 
   deleteS3Key(uid: string, accessKey: string) {
     let params = new HttpParams();
-    params = params.append('uid', uid);
-    params = params.append('key-type', 's3');
-    params = params.append('access-key', accessKey);
-    return this.http.delete(`${this.url}?key`, { params: params });
+    params = params.append('key_type', 's3');
+    params = params.append('access_key', accessKey);
+    return this.http.delete(`${this.url}/${uid}/key`, { params: params });
   }
 
   /**
@@ -168,6 +130,7 @@ export class RgwUserService {
       mergeMap((resp: string[]) => {
         const index = _.indexOf(resp, uid);
         return observableOf(-1 !== index);
-      }));
+      })
+    );
   }
 }