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); - })); + }) + ); } }