]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: CLI commands: read passwords from file
authorAlfonso Martínez <almartin@redhat.com>
Tue, 15 Dec 2020 08:28:46 +0000 (09:28 +0100)
committerAlfonso Martínez <almartin@redhat.com>
Thu, 17 Dec 2020 07:36:57 +0000 (08:36 +0100)
Fixes: https://tracker.ceph.com/issues/48355
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
Signed-off-by: Juan Miguel Olmo Martínez <jolmomar@redhat.com>
25 files changed:
doc/mgr/dashboard.rst
qa/tasks/mgr/dashboard/helper.py
qa/tasks/mgr/dashboard/test_auth.py
qa/tasks/mgr/dashboard/test_ganesha.py
qa/tasks/mgr/dashboard/test_rgw.py
qa/tasks/mgr/dashboard/test_user.py
qa/workunits/cephadm/test_dashboard_e2e.sh
src/cephadm/cephadm
src/pybind/mgr/cephadm/services/cephadmservice.py
src/pybind/mgr/cephadm/services/iscsi.py
src/pybind/mgr/cephadm/tests/test_cephadm.py
src/pybind/mgr/cephadm/tests/test_services.py
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh
src/pybind/mgr/dashboard/services/access_control.py
src/pybind/mgr/dashboard/services/iscsi_cli.py
src/pybind/mgr/dashboard/settings.py
src/pybind/mgr/dashboard/tests/__init__.py
src/pybind/mgr/dashboard/tests/test_access_control.py
src/pybind/mgr/dashboard/tests/test_iscsi.py
src/pybind/mgr/dashboard/tests/test_settings.py
src/pybind/mgr/mgr_module.py
src/pybind/mgr/tests/__init__.py
src/test/mgr/mgr-dashboard-smoke.sh
src/vstart.sh

index 761bcd505013d6bb728bee749981ddae42081ab0..37f092c5ab1dbb88a588fc4814c634e3f000c9a8 100644 (file)
@@ -331,7 +331,7 @@ section.
 To create a user with the administrator role you can use the following
 commands::
 
-  $ ceph dashboard ac-user-create <username> <password> administrator
+  $ ceph dashboard ac-user-create <username> -i <file-containing-password> administrator
 
 Account Lock-out
 ^^^^^^^^^^^^^^^^
@@ -391,8 +391,8 @@ To obtain the credentials of an existing user via `radosgw-admin`::
 
 Finally, provide the credentials to the dashboard::
 
-  $ ceph dashboard set-rgw-api-access-key <access_key>
-  $ ceph dashboard set-rgw-api-secret-key <secret_key>
+  $ ceph dashboard set-rgw-api-access-key -i <file-containing-access-key>
+  $ ceph dashboard set-rgw-api-secret-key -i <file-containing-secret-key>
 
 In a simple configuration with a single RGW endpoint, this is all you
 have to do to get the Object Gateway management functionality working. The
@@ -454,7 +454,8 @@ To disable API SSL verification run the following command::
 The available iSCSI gateways must be defined using the following commands::
 
   $ ceph dashboard iscsi-gateway-list
-  $ ceph dashboard iscsi-gateway-add <scheme>://<username>:<password>@<host>[:port]
+  $ # Gateway URL format for a new gateway: <scheme>://<username>:<password>@<host>[:port]
+  $ ceph dashboard iscsi-gateway-add -i <file-containing-gateway-url> [<gateway_name>]
   $ ceph dashboard iscsi-gateway-rm <gateway_name>
 
 
@@ -908,7 +909,7 @@ We provide a set of CLI commands to manage user accounts:
 
 - *Create User*::
 
-  $ ceph dashboard ac-user-create [--enabled] [--force-password] [--pwd_update_required] <username> [<password>] [<rolename>] [<name>] [<email>] [<pwd_expiration_date>]
+  $ ceph dashboard ac-user-create [--enabled] [--force-password] [--pwd_update_required] <username> -i <file-containing-password> [<rolename>] [<name>] [<email>] [<pwd_expiration_date>]
 
   To bypass password policy checks use the `force-password` option.
   Add the option `pwd_update_required` so that a newly created user has
@@ -920,11 +921,11 @@ We provide a set of CLI commands to manage user accounts:
 
 - *Change Password*::
 
-  $ ceph dashboard ac-user-set-password [--force-password] <username> <password>
+  $ ceph dashboard ac-user-set-password [--force-password] <username> -i <file-containing-password>
 
 - *Change Password Hash*::
 
-  $ ceph dashboard ac-user-set-password-hash <username> <hash>
+  $ ceph dashboard ac-user-set-password-hash <username> -i <file-containing-password-hash>
 
   The hash must be a bcrypt hash and salt, e.g. ``$2b$12$Pt3Vq/rDt2y9glTPSV.VFegiLkQeIpddtkhoFetNApYmIJOY8gau2``.
   This can be used to import users from an external database.
@@ -1060,7 +1061,7 @@ and has read-only access to other scopes.
 
 1. *Create the user*::
 
-   $ ceph dashboard ac-user-create bob mypassword
+   $ ceph dashboard ac-user-create bob -i <file-containing-password>
 
 2. *Create role and specify scope permissions*::
 
index 02b13f58ce6dc24afeac7724ba746bd26d8dcd54..bbf7eb4d5f45d38caa2b6175d79672c56f5c94cd 100644 (file)
@@ -4,9 +4,12 @@ from __future__ import absolute_import
 
 import json
 import logging
+import random
 import re
+import string
 import time
 from collections import namedtuple
+from typing import List
 
 import requests
 from tasks.mgr.mgr_test_case import MgrTestCase
@@ -112,14 +115,13 @@ class DashboardTestCase(MgrTestCase):
                 raise ex
 
         user_create_args = [
-            'dashboard', 'ac-user-create', username, password
+            'dashboard', 'ac-user-create', username
         ]
         if force_password:
             user_create_args.append('--force-password')
         if cmd_args:
             user_create_args.extend(cmd_args)
-        cls._ceph_cmd(user_create_args)
-
+        cls._ceph_cmd_with_secret(user_create_args, password)
         if roles:
             set_roles_args = ['dashboard', 'ac-user-set-roles', username]
             for idx, role in enumerate(roles):
@@ -493,6 +495,22 @@ class DashboardTestCase(MgrTestCase):
         log.info("command exit status: %d", exitstatus)
         return exitstatus
 
+    @classmethod
+    def _ceph_cmd_with_secret(cls, cmd: List[str], secret: str, return_exit_code: bool = False):
+        cmd.append('-i')
+        cmd.append('{}'.format(cls._ceph_create_tmp_file(secret)))
+        if return_exit_code:
+            return cls._ceph_cmd_result(cmd)
+        return cls._ceph_cmd(cmd)
+
+    @classmethod
+    def _ceph_create_tmp_file(cls, content: str) -> str:
+        """Create a temporary file in the remote cluster"""
+        file_name = ''.join(random.choices(string.ascii_letters + string.digits, k=20))
+        file_path = '/tmp/{}'.format(file_name)
+        cls._cmd(['sh', '-c', 'echo -n {} > {}'.format(content, file_path)])
+        return file_path
+
     def set_config_key(self, key, value):
         self._ceph_cmd(['config-key', 'set', key, value])
 
index 890ea05d3b10228edbc93f983bc100d94d3693d7..9bc195bbbbdf994fe30a5bbc2201c40bc14756c5 100644 (file)
@@ -5,6 +5,8 @@ from __future__ import absolute_import
 import time
 
 import jwt
+from teuthology.orchestra.run import \
+    CommandFailedError  # pylint: disable=import-error
 
 from .helper import DashboardTestCase, JLeaf, JObj
 
@@ -29,6 +31,10 @@ class AuthTest(DashboardTestCase):
             self.assertIn('create', perms)
             self.assertIn('delete', perms)
 
+    def test_login_without_password(self):
+        with self.assertRaises(CommandFailedError):
+            self.create_user('admin2', '', ['administrator'], force_password=True)
+
     def test_a_set_login_credentials(self):
         self.create_user('admin2', 'admin2', ['administrator'])
         self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
@@ -60,17 +66,6 @@ class AuthTest(DashboardTestCase):
             "detail": "Invalid credentials"
         })
 
-    def test_login_without_password(self):
-        self.create_user('admin2', '', ['administrator'])
-        self._post("/api/auth", {'username': 'admin2', 'password': ''})
-        self.assertStatus(400)
-        self.assertJsonBody({
-            "component": "auth",
-            "code": "invalid_credentials",
-            "detail": "Invalid credentials"
-        })
-        self.delete_user('admin2')
-
     def test_lockout_user(self):
         self._ceph_cmd(['dashboard', 'set-account-lockout-attempts', '3'])
         for _ in range(3):
@@ -159,8 +154,9 @@ class AuthTest(DashboardTestCase):
         self._get("/api/host")
         self.assertStatus(200)
         time.sleep(1)
-        self._ceph_cmd(['dashboard', 'ac-user-set-password', '--force-password',
-                        'user', 'user2'])
+        self._ceph_cmd_with_secret(['dashboard', 'ac-user-set-password', '--force-password',
+                                    'user'],
+                                   'user2')
         time.sleep(1)
         self._get("/api/host")
         self.assertStatus(401)
index 368284b8670919cbf79cfe8c64147b59f2def345..6868e0cb324954ff07625a6821454fe99f3fe313 100644 (file)
@@ -28,8 +28,8 @@ class GaneshaTest(DashboardTestCase):
             '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'])
+        cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin')
+        cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin')
 
     @classmethod
     def tearDownClass(cls):
index ade4163cd37822409c3fda8c68237b1585876dac..36227f9d22674578759102f05b92358949380467 100644 (file)
@@ -32,8 +32,8 @@ class RgwTestCase(DashboardTestCase):
         ])
         # Update the dashboard configuration.
         cls._ceph_cmd(['dashboard', 'set-rgw-api-user-id', 'admin'])
-        cls._ceph_cmd(['dashboard', 'set-rgw-api-secret-key', 'admin'])
-        cls._ceph_cmd(['dashboard', 'set-rgw-api-access-key', 'admin'])
+        cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin')
+        cls._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin')
         # Create a test user?
         if cls.create_test_user:
             cls._radosgw_admin_cmd([
@@ -80,13 +80,13 @@ class RgwApiCredentialsTest(RgwTestCase):
         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'])
+        self._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin')
+        self._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin')
         super(RgwApiCredentialsTest, self).setUp()
 
     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', ''])
+        self._ceph_cmd(['dashboard', 'reset-rgw-api-secret-key'])
+        self._ceph_cmd(['dashboard', 'reset-rgw-api-access-key'])
         resp = self._get('/api/rgw/user')
         self.assertStatus(500)
         self.assertIn('detail', resp)
index be2a57d563cdbc94b2ca2f9566863c9a3080daff..73ead662d6bec778ddc0c15aa19edeb429bcf5f7 100644 (file)
@@ -296,39 +296,50 @@ class UserTest(DashboardTestCase):
         self.assertError(code='invalid_credentials', component='auth')
 
     def test_create_user_password_cli(self):
-        exitcode = self._ceph_cmd_result(['dashboard', 'ac-user-create',
-                                          'test1', 'mypassword10#'])
+        exitcode = self._ceph_cmd_with_secret(['dashboard', 'ac-user-create',
+                                               'test1'],
+                                              'mypassword10#',
+                                              return_exit_code=True)
         self.assertEqual(exitcode, 0)
         self.delete_user('test1')
 
     @DashboardTestCase.RunAs('test2', 'foo_bar_10#', force_password=False, login=False)
     def test_change_user_password_cli(self):
-        exitcode = self._ceph_cmd_result(['dashboard', 'ac-user-set-password',
-                                          'test2', 'foo_new-password01#'])
+        exitcode = self._ceph_cmd_with_secret(['dashboard', 'ac-user-set-password',
+                                               'test2'],
+                                              'foo_new-password01#',
+                                              return_exit_code=True)
         self.assertEqual(exitcode, 0)
 
     def test_create_user_password_force_cli(self):
-        exitcode = self._ceph_cmd_result(['dashboard', 'ac-user-create',
-                                          '--force-password', 'test11',
-                                          'bar'])
+        exitcode = self._ceph_cmd_with_secret(['dashboard', 'ac-user-create',
+                                               '--force-password', 'test11'],
+                                              'bar',
+                                              return_exit_code=True)
         self.assertEqual(exitcode, 0)
         self.delete_user('test11')
 
     @DashboardTestCase.RunAs('test22', 'foo_bar_10#', force_password=False, login=False)
     def test_change_user_password_force_cli(self):
-        exitcode = self._ceph_cmd_result(['dashboard', 'ac-user-set-password',
-                                          '--force-password', 'test22',
-                                          'bar'])
+        exitcode = self._ceph_cmd_with_secret(['dashboard', 'ac-user-set-password',
+                                               '--force-password', 'test22'],
+                                              'bar',
+                                              return_exit_code=True)
         self.assertEqual(exitcode, 0)
 
     def test_create_user_password_cli_fail(self):
-        exitcode = self._ceph_cmd_result(['dashboard', 'ac-user-create', 'test3', 'foo'])
+        exitcode = self._ceph_cmd_with_secret(['dashboard', 'ac-user-create',
+                                               'test3'],
+                                              'foo',
+                                              return_exit_code=True)
         self.assertNotEqual(exitcode, 0)
 
     @DashboardTestCase.RunAs('test4', 'x1z_tst+_10#', force_password=False, login=False)
     def test_change_user_password_cli_fail(self):
-        exitcode = self._ceph_cmd_result(['dashboard', 'ac-user-set-password',
-                                          'test4', 'bar'])
+        exitcode = self._ceph_cmd_with_secret(['dashboard', 'ac-user-set-password',
+                                               'test4'],
+                                              'bar',
+                                              return_exit_code=True)
         self.assertNotEqual(exitcode, 0)
 
     def test_create_user_with_pwd_expiration_date(self):
index 9e1da387d4c55dc24482bc191f00f1c62130ad81..5764ecb5d43c25b248babb9bfd25af60a288f597 100755 (executable)
@@ -79,7 +79,9 @@ ceph orch device ls --refresh
 sleep 10  # the previous call is asynchronous
 ceph orch device ls --format=json | tee cypress/fixtures/orchestrator/inventory.json
 
-ceph dashboard ac-user-set-password admin admin --force-password
+DASHBOARD_ADMIN_SECRET_FILE="/tmp/dashboard-admin-secret.txt"
+printf 'admin' > "${DASHBOARD_ADMIN_SECRET_FILE}"
+ceph dashboard ac-user-set-password admin -i "${DASHBOARD_ADMIN_SECRET_FILE}" --force-password
 
 # Run Dashboard e2e tests.
 # These tests are designed with execution order in mind, since orchestrator operations
index 54a87614f8ffae7b814a07ec0eec5026049cd43a..df516f0fe97e7817dec8028cefdeab1866de81a0 100755 (executable)
@@ -3313,10 +3313,11 @@ def command_bootstrap():
 
         logger.info('Creating initial admin user...')
         password = args.initial_dashboard_password or generate_password()
-        cmd = ['dashboard', 'ac-user-create', args.initial_dashboard_user, password, 'administrator', '--force-password']
+        tmp_password_file = write_tmp(password, uid, gid)
+        cmd = ['dashboard', 'ac-user-create', args.initial_dashboard_user, '-i', '/tmp/dashboard.pw', 'administrator', '--force-password']
         if not args.dashboard_password_noupdate:
             cmd.append('--pwd-update-required')
-        cli(cmd)
+        cli(cmd, extra_mounts={pathify(tmp_password_file.name): '/tmp/dashboard.pw:z'})
         logger.info('Fetching dashboard port number...')
         out = cli(['config', 'get', 'mgr', 'mgr/dashboard/ssl_server_port'])
         port = int(out)
index 999f10856fd82fac1eb33bc1b6d780289b72c436..def766782ecd5d20fea4c9aafeb165cdde3bbf2f 100644 (file)
@@ -200,7 +200,8 @@ class CephadmService(metaclass=ABCMeta):
         cmd_dicts = get_set_cmd_dicts(out.strip())
         for cmd_dict in list(cmd_dicts):
             try:
-                _, out, _ = self.mgr.check_mon_command(cmd_dict)
+                inbuf = cmd_dict.pop('inbuf', None)
+                _, out, _ = self.mgr.check_mon_command(cmd_dict, inbuf)
             except MonCommandFailed as e:
                 logger.warning('Failed to set Dashboard config for %s: %s', service_name, e)
 
index 160c75d154f2e128b3c2dbfc7d941fff8591964e..f20bf2fbb3785d27e1acfe866457eb71f2f37745 100644 (file)
@@ -108,7 +108,7 @@ class IscsiService(CephService):
                     logger.info('Adding iSCSI gateway %s to Dashboard', safe_service_url)
                     cmd_dicts.append({
                         'prefix': 'dashboard iscsi-gateway-add',
-                        'service_url': service_url,
+                        'inbuf': service_url,
                         'name': dd.hostname
                     })
             return cmd_dicts
index 7cc61269860b0729101493d4c3cd10279a40d0ab..cec4939ea2648084fe2d9f6feba0c7d3bb53f0ee 100644 (file)
@@ -312,7 +312,8 @@ class TestCephadm(object):
                 with mock.patch("cephadm.module.CephadmOrchestrator.mon_command") as _mon_cmd:
                     CephadmServe(cephadm_module)._check_daemons()
                     _mon_cmd.assert_any_call(
-                        {'prefix': 'dashboard set-grafana-api-url', 'value': 'https://test:3000'})
+                        {'prefix': 'dashboard set-grafana-api-url', 'value': 'https://test:3000'},
+                        None)
 
     @mock.patch("cephadm.module.CephadmOrchestrator._run_cephadm", _run_cephadm('[]'))
     def test_mon_add(self, cephadm_module):
index da53e51014591f7566e1d73ec28098e8585a3351..560ee412862de671aac5b9d5daf79314fff72283 100644 (file)
@@ -18,7 +18,7 @@ class FakeMgr:
         self.config = ''
         self.check_mon_command = MagicMock(side_effect=self._check_mon_command)
 
-    def _check_mon_command(self, cmd_dict):
+    def _check_mon_command(self, cmd_dict, inbuf=None):
         prefix = cmd_dict.get('prefix')
         if prefix == 'get-cmd':
             return 0, self.config, ''
index e3f69664764447eb4167338ffaef3dc3528dfca3..11c54bf0be0f35f172885cad6be43d970d9cf8cf 100644 (file)
@@ -384,7 +384,7 @@ class Module(MgrModule, CherryPyConfig):
 
     def handle_command(self, inbuf, cmd):
         # pylint: disable=too-many-return-statements
-        res = handle_option_command(cmd)
+        res = handle_option_command(cmd, inbuf)
         if res[0] != -errno.ENOSYS:
             return res
         res = handle_sso_command(cmd)
index e6a7cef09e2c8df4ac9e546a7f8ba09bdabadbcc..c375d677812e4e48b8138f90533de8668ef06d61 100755 (executable)
@@ -13,8 +13,12 @@ start_ceph() {
     # Set the user-id
     ./bin/ceph dashboard set-rgw-api-user-id dev
     # Obtain and set access and secret key for the previously created user. $() is safer than backticks `..`
-    ./bin/ceph dashboard set-rgw-api-access-key $(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].access_key)
-    ./bin/ceph dashboard set-rgw-api-secret-key $(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].secret_key)
+    RGW_ACCESS_KEY_FILE="/tmp/rgw-user-access-key.txt"
+    printf "$(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].access_key)" > "${RGW_ACCESS_KEY_FILE}"
+    ./bin/ceph dashboard set-rgw-api-access-key -i "${RGW_ACCESS_KEY_FILE}"
+    RGW_SECRET_KEY_FILE="/tmp/rgw-user-secret-key.txt"
+    printf "$(./bin/radosgw-admin user info --uid=dev | jq -r .keys[0].secret_key)" > "${RGW_SECRET_KEY_FILE}"
+    ./bin/ceph dashboard set-rgw-api-secret-key -i "${RGW_SECRET_KEY_FILE}"
     # Set SSL verify to False
     ./bin/ceph dashboard set-rgw-api-ssl-verify False
 
index 233f749981254ed11db46aaaa5cd6dc6a5648560..fbc3aa747c9ab062bb009ca2ebfffd0f8d3f9a1b 100644 (file)
@@ -13,7 +13,7 @@ from datetime import datetime, timedelta
 from string import ascii_lowercase, ascii_uppercase, digits, punctuation
 
 import bcrypt
-from mgr_module import CLIReadCommand, CLIWriteCommand
+from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
 
 from .. import mgr
 from ..exceptions import PasswordPolicyException, PermissionNotValid, \
@@ -580,10 +580,11 @@ def load_access_control_db():
 # CLI dashboard access control scope commands
 
 @CLIWriteCommand('dashboard set-login-credentials',
-                 'name=username,type=CephString '
-                 'name=password,type=CephString',
-                 'Set the login credentials')
-def set_login_credentials_cmd(_, username, password):
+                 'name=username,type=CephString',
+                 'Set the login credentials. Password read from -i <file>')
+@CLICheckNonemptyFileInput
+def set_login_credentials_cmd(_, username, inbuf):
+    password = inbuf
     try:
         user = mgr.ACCESS_CTRL_DB.get_user(username)
         user.set_password(password)
@@ -713,7 +714,6 @@ def ac_user_show_cmd(_, username=None):
 
 @CLIWriteCommand('dashboard ac-user-create',
                  'name=username,type=CephString '
-                 'name=password,type=CephString,req=false '
                  'name=rolename,type=CephString,req=false '
                  'name=name,type=CephString,req=false '
                  'name=email,type=CephString,req=false '
@@ -721,10 +721,12 @@ def ac_user_show_cmd(_, username=None):
                  'name=force_password,type=CephBool,req=false '
                  'name=pwd_expiration_date,type=CephInt,req=false '
                  'name=pwd_update_required,type=CephBool,req=false',
-                 'Create a user')
-def ac_user_create_cmd(_, username, password=None, rolename=None, name=None,
+                 'Create a user. Password read from -i <file>')
+@CLICheckNonemptyFileInput
+def ac_user_create_cmd(_, username, inbuf, rolename=None, name=None,
                        email=None, enabled=True, force_password=False,
                        pwd_expiration_date=None, pwd_update_required=False):
+    password = inbuf
     try:
         role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None
     except RoleDoesNotExist as ex:
@@ -864,10 +866,11 @@ def ac_user_del_roles_cmd(_, username, roles):
 
 @CLIWriteCommand('dashboard ac-user-set-password',
                  'name=username,type=CephString '
-                 'name=password,type=CephString '
                  'name=force_password,type=CephBool,req=false',
-                 'Set user password')
-def ac_user_set_password(_, username, password, force_password=False):
+                 'Set user password from -i <file>')
+@CLICheckNonemptyFileInput
+def ac_user_set_password(_, username, inbuf, force_password=False):
+    password = inbuf
     try:
         user = mgr.ACCESS_CTRL_DB.get_user(username)
         if not force_password:
@@ -883,10 +886,11 @@ def ac_user_set_password(_, username, password, force_password=False):
 
 
 @CLIWriteCommand('dashboard ac-user-set-password-hash',
-                 'name=username,type=CephString '
-                 'name=hashed_password,type=CephString',
-                 'Set user password bcrypt hash')
-def ac_user_set_password_hash(_, username, hashed_password):
+                 'name=username,type=CephString',
+                 'Set user password bcrypt hash from -i <file>')
+@CLICheckNonemptyFileInput
+def ac_user_set_password_hash(_, username, inbuf):
+    hashed_password = inbuf
     try:
         # make sure the hashed_password is actually a bcrypt hash
         bcrypt.checkpw(b'', hashed_password.encode('utf-8'))
index 2067d1e8d17aeae38797dfc720a1979cd3d4ebfa..fa39db0a225da7c50597fc926832ef9274c21ea9 100644 (file)
@@ -4,7 +4,7 @@ from __future__ import absolute_import
 import errno
 import json
 
-from mgr_module import CLIReadCommand, CLIWriteCommand
+from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
 
 from ..rest_client import RequestException
 from .iscsi_client import IscsiClient
@@ -19,10 +19,11 @@ def list_iscsi_gateways(_):
 
 
 @CLIWriteCommand('dashboard iscsi-gateway-add',
-                 'name=service_url,type=CephString '
                  'name=name,type=CephString,req=false',
-                 'Add iSCSI gateway configuration')
-def add_iscsi_gateway(_, service_url, name=None):
+                 'Add iSCSI gateway configuration. Gateway URL read from -i <file>')
+@CLICheckNonemptyFileInput
+def add_iscsi_gateway(_, inbuf, name=None):
+    service_url = inbuf
     try:
         IscsiGatewaysConfig.validate_service_url(service_url)
         if name is None:
index 45cae2965a80077c5829ac0597fdde41d4bf69f0..d42e6ed5b45593dc3ad2d55d8dc82269eb32c1cb 100644 (file)
@@ -4,6 +4,8 @@ from __future__ import absolute_import
 import errno
 import inspect
 
+from mgr_module import CLICheckNonemptyFileInput
+
 from . import mgr
 
 
@@ -162,12 +164,16 @@ def options_command_list():
                 'perm': 'r'
             })
         elif cmd.startswith('dashboard set'):
-            cmd_list.append({
+            cmd_entry = {
                 'cmd': '{} name=value,type={}'
                        .format(cmd, py2ceph(opt['type'])),
                 'desc': 'Set the {} option value'.format(opt['name']),
                 'perm': 'w'
-            })
+            }
+            if handles_secret(cmd):
+                cmd_entry['cmd'] = cmd
+                cmd_entry['desc'] = '{} read from -i <file>'.format(cmd_entry['desc'])
+            cmd_list.append(cmd_entry)
         elif cmd.startswith('dashboard reset'):
             desc = 'Reset the {} option to its default value'.format(
                 opt['name'])
@@ -194,7 +200,7 @@ def options_schema_list():
     return result
 
 
-def handle_option_command(cmd):
+def handle_option_command(cmd, inbuf):
     if cmd['prefix'] not in _OPTIONS_COMMAND_MAP:
         return -errno.ENOSYS, '', "Command not found '{}'".format(cmd['prefix'])
 
@@ -207,8 +213,23 @@ def handle_option_command(cmd):
     elif cmd['prefix'].startswith('dashboard get'):
         return 0, str(getattr(Settings, opt['name'])), ''
     elif cmd['prefix'].startswith('dashboard set'):
-        value = opt['type'](cmd['value'])
+        if handles_secret(cmd['prefix']):
+            value, stdout, stderr = get_secret(inbuf=inbuf)
+            if stderr:
+                return value, stdout, stderr
+        else:
+            value = cmd['value']
+        value = opt['type'](value)
         if opt['type'] == bool and cmd['value'].lower() == 'false':
             value = False
         setattr(Settings, opt['name'], value)
         return 0, 'Option {} updated'.format(opt['name']), ''
+
+
+def handles_secret(cmd: str) -> bool:
+    return bool([cmd for secret_word in ['password', 'key'] if (secret_word in cmd)])
+
+
+@CLICheckNonemptyFileInput
+def get_secret(inbuf=None):
+    return inbuf, None, None
index 49d28916833d7ebc0ecf43993a8353ce1e84cfb2..9ea4585b25fe27dd5ab1286b8abc46cd0d965ffc 100644 (file)
@@ -34,6 +34,7 @@ class CmdException(Exception):
 
 
 def exec_dashboard_cmd(command_handler, cmd, **kwargs):
+    inbuf = kwargs['inbuf'] if 'inbuf' in kwargs else None
     cmd_dict = {'prefix': 'dashboard {}'.format(cmd)}
     cmd_dict.update(kwargs)
     if cmd_dict['prefix'] not in CLICommand.COMMANDS:
@@ -45,8 +46,7 @@ def exec_dashboard_cmd(command_handler, cmd, **kwargs):
         except ValueError:
             return out
 
-    ret, out, err = CLICommand.COMMANDS[cmd_dict['prefix']].call(mgr, cmd_dict,
-                                                                 None)
+    ret, out, err = CLICommand.COMMANDS[cmd_dict['prefix']].call(mgr, cmd_dict, inbuf)
     if ret < 0:
         raise CmdException(ret, err)
     try:
index 7ace71d6ce66cb0e743f8457e86aed78d7226a86..ba0db6b159015be71e3c51c887b7855dcde70bb7 100644 (file)
@@ -8,6 +8,8 @@ import time
 import unittest
 from datetime import datetime, timedelta
 
+from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE
+
 from .. import mgr
 from ..security import Permission, Scope
 from ..services.access_control import SYSTEM_ROLES, AccessControlDB, \
@@ -277,7 +279,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
     def test_create_user(self, username='admin', rolename=None, enabled=True,
                          pwdExpirationDate=None):
         user = self.exec_cmd('ac-user-create', username=username,
-                             rolename=rolename, password='admin',
+                             rolename=rolename, inbuf='admin',
                              name='{} User'.format(username),
                              email='{}@user.com'.format(username),
                              enabled=enabled, force_password=True,
@@ -326,7 +328,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
 
     def test_create_duplicate_user(self):
         self.test_create_user()
-        ret = self.exec_cmd('ac-user-create', username='admin', password='admin',
+        ret = self.exec_cmd('ac-user-create', username='admin', inbuf='admin',
                             force_password=True)
         self.assertEqual(ret, "User 'admin' already exists")
 
@@ -337,7 +339,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         # create a user with a role that does not exist; expect a failure
         try:
             self.exec_cmd('ac-user-create', username='foo',
-                          rolename='dne_role', password='foopass',
+                          rolename='dne_role', inbuf='foopass',
                           name='foo User', email='foo@user.com',
                           force_password=True)
         except CmdException as e:
@@ -358,7 +360,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         # create a role (this will be 'test_role')
         self.test_create_role()
         self.exec_cmd('ac-user-create', username='bar',
-                      rolename='test_role', password='barpass',
+                      rolename='test_role', inbuf='barpass',
                       name='bar User', email='bar@user.com',
                       force_password=True)
 
@@ -562,7 +564,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
     def test_set_user_password(self):
         user_orig = self.test_create_user()
         user = self.exec_cmd('ac-user-set-password', username='admin',
-                             password='newpass', force_password=True)
+                             inbuf='newpass', force_password=True)
         pass_hash = password_hash('newpass', user['password'])
         self.assertDictEqual(user, {
             'username': 'admin',
@@ -582,16 +584,23 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
     def test_set_user_password_nonexistent_user(self):
         with self.assertRaises(CmdException) as ctx:
             self.exec_cmd('ac-user-set-password', username='admin',
-                          password='newpass', force_password=True)
+                          inbuf='newpass', force_password=True)
 
         self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
         self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
 
+    def test_set_user_password_empty(self):
+        with self.assertRaises(CmdException) as ctx:
+            self.exec_cmd('ac-user-set-password', username='admin', inbuf='\n',
+                          force_password=True)
+
+        self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+        self.assertEqual(str(ctx.exception), ERROR_MSG_EMPTY_INPUT_FILE)
+
     def test_set_user_password_hash(self):
         user_orig = self.test_create_user()
         user = self.exec_cmd('ac-user-set-password-hash', username='admin',
-                             hashed_password='$2b$12$Pt3Vq/rDt2y9glTPSV.'
-                                             'VFegiLkQeIpddtkhoFetNApYmIJOY8gau2')
+                             inbuf='$2b$12$Pt3Vq/rDt2y9glTPSV.VFegiLkQeIpddtkhoFetNApYmIJOY8gau2')
         pass_hash = password_hash('newpass', user['password'])
         self.assertDictEqual(user, {
             'username': 'admin',
@@ -611,8 +620,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
     def test_set_user_password_hash_nonexistent_user(self):
         with self.assertRaises(CmdException) as ctx:
             self.exec_cmd('ac-user-set-password-hash', username='admin',
-                          hashed_password='$2b$12$Pt3Vq/rDt2y9glTPSV.'
-                                          'VFegiLkQeIpddtkhoFetNApYmIJOY8gau2')
+                          inbuf='$2b$12$Pt3Vq/rDt2y9glTPSV.VFegiLkQeIpddtkhoFetNApYmIJOY8gau2')
 
         self.assertEqual(ctx.exception.retcode, -errno.ENOENT)
         self.assertEqual(str(ctx.exception), "User 'admin' does not exist")
@@ -621,14 +629,14 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
         self.test_create_user()
         with self.assertRaises(CmdException) as ctx:
             self.exec_cmd('ac-user-set-password-hash', username='admin',
-                          hashed_password='')
+                          inbuf='1')
 
         self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
         self.assertEqual(str(ctx.exception), 'Invalid password hash')
 
     def test_set_login_credentials(self):
         self.exec_cmd('set-login-credentials', username='admin',
-                      password='admin')
+                      inbuf='admin')
         user = self.exec_cmd('ac-user-show', username='admin')
         pass_hash = password_hash('admin', user['password'])
         self.assertDictEqual(user, {
@@ -648,7 +656,7 @@ class AccessControlTest(unittest.TestCase, CLICommandTestMixin):
     def test_set_login_credentials_for_existing_user(self):
         self.test_add_user_roles('admin', ['read-only'])
         self.exec_cmd('set-login-credentials', username='admin',
-                      password='admin2')
+                      inbuf='admin2')
         user = self.exec_cmd('ac-user-show', username='admin')
         pass_hash = password_hash('admin2', user['password'])
         self.assertDictEqual(user, {
index 223c9e61d4b3d98b5a327133913598023fbec390..994a0b91a43662fa1daa1ac36750d9dd7579a32f 100644 (file)
@@ -10,6 +10,8 @@ try:
 except ImportError:
     import unittest.mock as mock
 
+from mgr_module import ERROR_MSG_NO_INPUT_FILE
+
 from .. import mgr
 from ..controllers.iscsi import Iscsi, IscsiTarget
 from ..rest_client import RequestException
@@ -33,18 +35,26 @@ class IscsiTestCli(unittest.TestCase, CLICommandTestMixin):
     def test_cli_add_gateway_invalid_url(self):
         with self.assertRaises(CmdException) as ctx:
             self.exec_cmd('iscsi-gateway-add', name='node1',
-                          service_url='http:/hello.com')
+                          inbuf='http:/hello.com')
 
         self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
         self.assertEqual(str(ctx.exception),
                          "Invalid service URL 'http:/hello.com'. Valid format: "
                          "'<scheme>://<username>:<password>@<host>[:port]'.")
 
+    def test_cli_add_gateway_empty_url(self):
+        with self.assertRaises(CmdException) as ctx:
+            self.exec_cmd('iscsi-gateway-add', name='node1',
+                          inbuf='')
+
+        self.assertEqual(ctx.exception.retcode, -errno.EINVAL)
+        self.assertEqual(str(ctx.exception), ERROR_MSG_NO_INPUT_FILE)
+
     def test_cli_add_gateway(self):
         self.exec_cmd('iscsi-gateway-add', name='node1',
-                      service_url='https://admin:admin@10.17.5.1:5001')
+                      inbuf='https://admin:admin@10.17.5.1:5001')
         self.exec_cmd('iscsi-gateway-add', name='node2',
-                      service_url='https://admin:admin@10.17.5.2:5001')
+                      inbuf='https://admin:admin@10.17.5.2:5001')
         iscsi_config = json.loads(self.get_key("_iscsi_config"))
         self.assertEqual(iscsi_config['gateways'], {
             'node1': {
index 474e7a17bdd95bd119e8055252e153f328f82efa..240eafee348ab0dcb501a274a7bdc773b7eb13bf 100644 (file)
@@ -4,6 +4,8 @@ from __future__ import absolute_import
 import errno
 import unittest
 
+from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE
+
 from .. import settings
 from ..controllers.settings import Settings as SettingsController
 from ..settings import Settings, handle_option_command
@@ -43,7 +45,9 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin):
 
     def test_get_cmd(self):
         r, out, err = handle_option_command(
-            {'prefix': 'dashboard get-grafana-api-port'})
+            {'prefix': 'dashboard get-grafana-api-port'},
+            None
+        )
         self.assertEqual(r, 0)
         self.assertEqual(out, '3000')
         self.assertEqual(err, '')
@@ -51,14 +55,35 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin):
     def test_set_cmd(self):
         r, out, err = handle_option_command(
             {'prefix': 'dashboard set-grafana-api-port',
-             'value': '4000'})
+             'value': '4000'},
+            None
+        )
         self.assertEqual(r, 0)
         self.assertEqual(out, 'Option GRAFANA_API_PORT updated')
         self.assertEqual(err, '')
 
+    def test_set_secret_empty(self):
+        r, out, err = handle_option_command(
+            {'prefix': 'dashboard set-grafana-api-password'},
+            None
+        )
+        self.assertEqual(r, -errno.EINVAL)
+        self.assertEqual(out, '')
+        self.assertEqual(err, ERROR_MSG_EMPTY_INPUT_FILE)
+
+    def test_set_secret(self):
+        r, out, err = handle_option_command(
+            {'prefix': 'dashboard set-grafana-api-password'},
+            'my-secret'
+        )
+        self.assertEqual(r, 0)
+        self.assertEqual(out, 'Option GRAFANA_API_PASSWORD updated')
+        self.assertEqual(err, '')
+
     def test_reset_cmd(self):
         r, out, err = handle_option_command(
-            {'prefix': 'dashboard reset-grafana-enabled'}
+            {'prefix': 'dashboard reset-grafana-enabled'},
+            None
         )
         self.assertEqual(r, 0)
         self.assertEqual(out, 'Option {} reset to default value "{}"'.format(
@@ -67,7 +92,9 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin):
 
     def test_inv_cmd(self):
         r, out, err = handle_option_command(
-            {'prefix': 'dashboard get-non-existent-option'})
+            {'prefix': 'dashboard get-non-existent-option'},
+            None
+        )
         self.assertEqual(r, -errno.ENOSYS)
         self.assertEqual(out, '')
         self.assertEqual(err, "Command not found "
@@ -76,13 +103,17 @@ class SettingsTest(unittest.TestCase, KVStoreMockMixin):
     def test_sync(self):
         Settings.GRAFANA_API_PORT = 5000
         r, out, err = handle_option_command(
-            {'prefix': 'dashboard get-grafana-api-port'})
+            {'prefix': 'dashboard get-grafana-api-port'},
+            None
+        )
         self.assertEqual(r, 0)
         self.assertEqual(out, '5000')
         self.assertEqual(err, '')
         r, out, err = handle_option_command(
             {'prefix': 'dashboard set-grafana-api-host',
-             'value': 'new-local-host'})
+             'value': 'new-local-host'},
+            None
+        )
         self.assertEqual(r, 0)
         self.assertEqual(out, 'Option GRAFANA_API_HOST updated')
         self.assertEqual(err, '')
index 548255745e2dc2ef45e2d08dbf338337ae8cbef3..66e3f4caf734c44487ff6cbeb5647c8f32b5d354 100644 (file)
@@ -20,6 +20,8 @@ import re
 import time
 from mgr_util import profile_method
 
+ERROR_MSG_EMPTY_INPUT_FILE = 'Empty content: please add a password/secret to the file.'
+ERROR_MSG_NO_INPUT_FILE = 'Please specify the file containing the password/secret with "-i" option.'
 # Full list of strings in "osd_types.cc:pg_state_string()"
 PG_STATES = [
     "active",
@@ -356,6 +358,17 @@ def CLIWriteCommand(prefix, args="", desc=""):
     return CLICommand(prefix, args, desc, "w")
 
 
+def CLICheckNonemptyFileInput(func):
+    def check(*args, **kwargs):
+        if not 'inbuf' in kwargs:
+            return -errno.EINVAL, '', ERROR_MSG_NO_INPUT_FILE
+        if not kwargs['inbuf'] or (isinstance(kwargs['inbuf'], str)
+                                   and not kwargs['inbuf'].strip('\n')):
+            return -errno.EINVAL, '', ERROR_MSG_EMPTY_INPUT_FILE
+        return func(*args, **kwargs)
+    return check
+
+
 def _get_localized_key(prefix, key):
     return '{}/{}'.format(prefix, key)
 
@@ -1123,18 +1136,18 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
         """
         return self._ceph_get_daemon_status(svc_type, svc_id)
 
-    def check_mon_command(self, cmd_dict: dict) -> HandleCommandResult:
+    def check_mon_command(self, cmd_dict: dict, inbuf: Optional[str]=None) -> HandleCommandResult:
         """
         Wrapper around :func:`~mgr_module.MgrModule.mon_command`, but raises,
         if ``retval != 0``.
         """
 
-        r = HandleCommandResult(*self.mon_command(cmd_dict))
+        r = HandleCommandResult(*self.mon_command(cmd_dict, inbuf))
         if r.retval:
             raise MonCommandFailed(f'{cmd_dict["prefix"]} failed: {r.stderr} retval: {r.retval}')
         return r
 
-    def mon_command(self, cmd_dict):
+    def mon_command(self, cmd_dict: dict, inbuf: Optional[str]=None):
         """
         Helper for modules that do simple, synchronous mon command
         execution.
@@ -1146,7 +1159,7 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
 
         t1 = time.time()
         result = CommandResult()
-        self.send_command(result, "mon", "", json.dumps(cmd_dict), "")
+        self.send_command(result, "mon", "", json.dumps(cmd_dict), "", inbuf)
         r = result.wait()
         t2 = time.time()
 
@@ -1156,7 +1169,14 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
 
         return r
 
-    def send_command(self, *args, **kwargs):
+    def send_command(
+            self,
+            result: CommandResult,
+            svc_type: str,
+            svc_id: str,
+            command: str,
+            tag: str,
+            inbuf: Optional[str]=None):
         """
         Called by the plugin to send a command to the mon
         cluster.
@@ -1176,8 +1196,9 @@ class MgrModule(ceph_module.BaseMgrModule, MgrModuleLoggingMixin):
             completes, the ``notify()`` callback on the MgrModule instance is
             triggered, with notify_type set to "command", and notify_id set to
             the tag of the command.
+        :param str inbuf: input buffer for sending additional data.
         """
-        self._ceph_send_command(*args, **kwargs)
+        self._ceph_send_command(result, svc_type, svc_id, command, tag, inbuf)
 
     def set_health_checks(self, checks):
         """
index a5a531a12f5e6598d5cb6727adafb637c5fd9d92..cee20003f4f699488eba7be0c93f7a8d89980e3a 100644 (file)
@@ -98,7 +98,7 @@ if 'UNITTEST' in os.environ:
         def _ceph_get(self, data_name):
             return self.mock_store_get('_ceph_get', data_name, mock.MagicMock())
 
-        def _ceph_send_command(self, res, svc_type, svc_id, command, tag):
+        def _ceph_send_command(self, res, svc_type, svc_id, command, tag, inbuf):
             cmd = json.loads(command)
 
             # Mocking the config store is handy sometimes:
@@ -153,7 +153,7 @@ if 'UNITTEST' in os.environ:
 
 
             if self.__class__.__name__ not in M_classes:
-                # call those only once. 
+                # call those only once.
                 self._register_commands('')
                 self._register_options('')
                 M_classes.add(self.__class__.__name__)
index 5ad7877c8b1145838c83e58fd3093d298e56e136..f50bd99c9f16ba5c842fbad441a6d629d86407af 100755 (executable)
@@ -54,7 +54,10 @@ function TEST_dashboard() {
         tries=$((tries+1))
         sleep 1
     done
-    ceph_adm dashboard set-login-credentials admin admin
+
+    DASHBOARD_ADMIN_SECRET_FILE="/tmp/dashboard-admin-secret.txt"
+    printf 'admin' > "${DASHBOARD_ADMIN_SECRET_FILE}"
+    ceph_adm dashboard ac-user-create admin -i "${DASHBOARD_ADMIN_SECRET_FILE}" --force-password
 
     tries=0
     while [[ $tries < 30 ]] ; do
index b38d6885232f2e03ee62afc9053400521bb4e4cb..1c09bcc39ea79c77ae6ab08ef6d0f1c88c4df551 100755 (executable)
@@ -998,7 +998,10 @@ EOF
                 debug echo 'waiting for mgr dashboard module to start'
                 sleep 1
             done
-            ceph_adm dashboard ac-user-create --force-password admin admin administrator
+            DASHBOARD_ADMIN_SECRET_FILE="${CEPH_CONF_PATH}/dashboard-admin-secret.txt"
+            printf 'admin' > "${DASHBOARD_ADMIN_SECRET_FILE}"
+            ceph_adm dashboard ac-user-create admin -i "${DASHBOARD_ADMIN_SECRET_FILE}" \
+                administrator --force-password
             if [ "$ssl" != "0" ]; then
                 if ! ceph_adm dashboard create-self-signed-cert;  then
                     debug echo dashboard module not working correctly!