From: Alfonso Martínez Date: Tue, 15 Dec 2020 08:28:46 +0000 (+0100) Subject: mgr/dashboard: CLI commands: read passwords from file X-Git-Tag: v17.0.0~251^2~1 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=5d7ee7c1f0ad971fd0079f917e2b44cdef1d6f9f;p=ceph.git mgr/dashboard: CLI commands: read passwords from file Fixes: https://tracker.ceph.com/issues/48355 Signed-off-by: Alfonso Martínez Signed-off-by: Juan Miguel Olmo Martínez --- diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index 761bcd505013d..37f092c5ab1db 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -331,7 +331,7 @@ section. To create a user with the administrator role you can use the following commands:: - $ ceph dashboard ac-user-create administrator + $ ceph dashboard ac-user-create -i 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 - $ ceph dashboard set-rgw-api-secret-key + $ ceph dashboard set-rgw-api-access-key -i + $ ceph dashboard set-rgw-api-secret-key -i 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 ://:@[:port] + $ # Gateway URL format for a new gateway: ://:@[:port] + $ ceph dashboard iscsi-gateway-add -i [] $ ceph dashboard iscsi-gateway-rm @@ -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] [] [] [] [] [] + $ ceph dashboard ac-user-create [--enabled] [--force-password] [--pwd_update_required] -i [] [] [] [] 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] + $ ceph dashboard ac-user-set-password [--force-password] -i - *Change Password Hash*:: - $ ceph dashboard ac-user-set-password-hash + $ ceph dashboard ac-user-set-password-hash -i 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 2. *Create role and specify scope permissions*:: diff --git a/qa/tasks/mgr/dashboard/helper.py b/qa/tasks/mgr/dashboard/helper.py index 02b13f58ce6dc..bbf7eb4d5f45d 100644 --- a/qa/tasks/mgr/dashboard/helper.py +++ b/qa/tasks/mgr/dashboard/helper.py @@ -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]) diff --git a/qa/tasks/mgr/dashboard/test_auth.py b/qa/tasks/mgr/dashboard/test_auth.py index 890ea05d3b102..9bc195bbbbdf9 100644 --- a/qa/tasks/mgr/dashboard/test_auth.py +++ b/qa/tasks/mgr/dashboard/test_auth.py @@ -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) diff --git a/qa/tasks/mgr/dashboard/test_ganesha.py b/qa/tasks/mgr/dashboard/test_ganesha.py index 368284b867091..6868e0cb32495 100644 --- a/qa/tasks/mgr/dashboard/test_ganesha.py +++ b/qa/tasks/mgr/dashboard/test_ganesha.py @@ -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): diff --git a/qa/tasks/mgr/dashboard/test_rgw.py b/qa/tasks/mgr/dashboard/test_rgw.py index ade4163cd3782..36227f9d22674 100644 --- a/qa/tasks/mgr/dashboard/test_rgw.py +++ b/qa/tasks/mgr/dashboard/test_rgw.py @@ -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) diff --git a/qa/tasks/mgr/dashboard/test_user.py b/qa/tasks/mgr/dashboard/test_user.py index be2a57d563cdb..73ead662d6bec 100644 --- a/qa/tasks/mgr/dashboard/test_user.py +++ b/qa/tasks/mgr/dashboard/test_user.py @@ -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): diff --git a/qa/workunits/cephadm/test_dashboard_e2e.sh b/qa/workunits/cephadm/test_dashboard_e2e.sh index 9e1da387d4c55..5764ecb5d43c2 100755 --- a/qa/workunits/cephadm/test_dashboard_e2e.sh +++ b/qa/workunits/cephadm/test_dashboard_e2e.sh @@ -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 diff --git a/src/cephadm/cephadm b/src/cephadm/cephadm index 54a87614f8ffa..df516f0fe97e7 100755 --- a/src/cephadm/cephadm +++ b/src/cephadm/cephadm @@ -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) diff --git a/src/pybind/mgr/cephadm/services/cephadmservice.py b/src/pybind/mgr/cephadm/services/cephadmservice.py index 999f10856fd82..def766782ecd5 100644 --- a/src/pybind/mgr/cephadm/services/cephadmservice.py +++ b/src/pybind/mgr/cephadm/services/cephadmservice.py @@ -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) diff --git a/src/pybind/mgr/cephadm/services/iscsi.py b/src/pybind/mgr/cephadm/services/iscsi.py index 160c75d154f2e..f20bf2fbb3785 100644 --- a/src/pybind/mgr/cephadm/services/iscsi.py +++ b/src/pybind/mgr/cephadm/services/iscsi.py @@ -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 diff --git a/src/pybind/mgr/cephadm/tests/test_cephadm.py b/src/pybind/mgr/cephadm/tests/test_cephadm.py index 7cc61269860b0..cec4939ea2648 100644 --- a/src/pybind/mgr/cephadm/tests/test_cephadm.py +++ b/src/pybind/mgr/cephadm/tests/test_cephadm.py @@ -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): diff --git a/src/pybind/mgr/cephadm/tests/test_services.py b/src/pybind/mgr/cephadm/tests/test_services.py index da53e51014591..560ee412862de 100644 --- a/src/pybind/mgr/cephadm/tests/test_services.py +++ b/src/pybind/mgr/cephadm/tests/test_services.py @@ -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, '' diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index e3f6966476444..11c54bf0be0f3 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -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) diff --git a/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh b/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh index e6a7cef09e2c8..c375d677812e4 100755 --- a/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh +++ b/src/pybind/mgr/dashboard/run-frontend-e2e-tests.sh @@ -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 diff --git a/src/pybind/mgr/dashboard/services/access_control.py b/src/pybind/mgr/dashboard/services/access_control.py index 233f749981254..fbc3aa747c9ab 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -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 ') +@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 ') +@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 ') +@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 ') +@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')) diff --git a/src/pybind/mgr/dashboard/services/iscsi_cli.py b/src/pybind/mgr/dashboard/services/iscsi_cli.py index 2067d1e8d17ae..fa39db0a225da 100644 --- a/src/pybind/mgr/dashboard/services/iscsi_cli.py +++ b/src/pybind/mgr/dashboard/services/iscsi_cli.py @@ -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 ') +@CLICheckNonemptyFileInput +def add_iscsi_gateway(_, inbuf, name=None): + service_url = inbuf try: IscsiGatewaysConfig.validate_service_url(service_url) if name is None: diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py index 45cae2965a800..d42e6ed5b4559 100644 --- a/src/pybind/mgr/dashboard/settings.py +++ b/src/pybind/mgr/dashboard/settings.py @@ -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 '.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 diff --git a/src/pybind/mgr/dashboard/tests/__init__.py b/src/pybind/mgr/dashboard/tests/__init__.py index 49d28916833d7..9ea4585b25fe2 100644 --- a/src/pybind/mgr/dashboard/tests/__init__.py +++ b/src/pybind/mgr/dashboard/tests/__init__.py @@ -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: diff --git a/src/pybind/mgr/dashboard/tests/test_access_control.py b/src/pybind/mgr/dashboard/tests/test_access_control.py index 7ace71d6ce66c..ba0db6b159015 100644 --- a/src/pybind/mgr/dashboard/tests/test_access_control.py +++ b/src/pybind/mgr/dashboard/tests/test_access_control.py @@ -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, { diff --git a/src/pybind/mgr/dashboard/tests/test_iscsi.py b/src/pybind/mgr/dashboard/tests/test_iscsi.py index 223c9e61d4b3d..994a0b91a4366 100644 --- a/src/pybind/mgr/dashboard/tests/test_iscsi.py +++ b/src/pybind/mgr/dashboard/tests/test_iscsi.py @@ -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: " "'://:@[: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': { diff --git a/src/pybind/mgr/dashboard/tests/test_settings.py b/src/pybind/mgr/dashboard/tests/test_settings.py index 474e7a17bdd95..240eafee348ab 100644 --- a/src/pybind/mgr/dashboard/tests/test_settings.py +++ b/src/pybind/mgr/dashboard/tests/test_settings.py @@ -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, '') diff --git a/src/pybind/mgr/mgr_module.py b/src/pybind/mgr/mgr_module.py index 548255745e2dc..66e3f4caf734c 100644 --- a/src/pybind/mgr/mgr_module.py +++ b/src/pybind/mgr/mgr_module.py @@ -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): """ diff --git a/src/pybind/mgr/tests/__init__.py b/src/pybind/mgr/tests/__init__.py index a5a531a12f5e6..cee20003f4f69 100644 --- a/src/pybind/mgr/tests/__init__.py +++ b/src/pybind/mgr/tests/__init__.py @@ -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__) diff --git a/src/test/mgr/mgr-dashboard-smoke.sh b/src/test/mgr/mgr-dashboard-smoke.sh index 5ad7877c8b114..f50bd99c9f16b 100755 --- a/src/test/mgr/mgr-dashboard-smoke.sh +++ b/src/test/mgr/mgr-dashboard-smoke.sh @@ -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 diff --git a/src/vstart.sh b/src/vstart.sh index b38d6885232f2..1c09bcc39ea79 100755 --- a/src/vstart.sh +++ b/src/vstart.sh @@ -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!