From ddd3f51c0dbaebfef2a4554465589a726593b7d7 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Alfonso=20Mart=C3=ADnez?= Date: Tue, 2 Mar 2021 16:09:06 +0100 Subject: [PATCH] mgr/dashboard: CLI commands: read passwords from file MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Fixes: https://tracker.ceph.com/issues/48355 Signed-off-by: Alfonso Martínez Signed-off-by: Juan Miguel Olmo Martínez (cherry picked from commit 5d7ee7c1f0ad971fd0079f917e2b44cdef1d6f9f) Conflicts: qa/tasks/mgr/dashboard/helper.py qa/tasks/mgr/dashboard/test_auth.py qa/workunits/cephadm/test_dashboard_e2e.sh src/pybind/mgr/dashboard/services/access_control.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 - Resolved conflicts in these files due to code divergence. - Removed test_dashboard_e2e.sh as it does not exists in octopus. --- doc/mgr/dashboard.rst | 17 +++---- qa/tasks/mgr/dashboard/helper.py | 27 ++++++++++-- qa/tasks/mgr/dashboard/test_auth.py | 39 +++++----------- qa/tasks/mgr/dashboard/test_ganesha.py | 4 +- qa/tasks/mgr/dashboard/test_rgw.py | 12 ++--- qa/tasks/mgr/dashboard/test_user.py | 37 ++++++++++------ src/cephadm/cephadm | 5 ++- .../mgr/cephadm/services/cephadmservice.py | 3 +- src/pybind/mgr/cephadm/services/iscsi.py | 2 +- src/pybind/mgr/cephadm/tests/test_cephadm.py | 3 +- src/pybind/mgr/cephadm/tests/test_services.py | 2 +- src/pybind/mgr/dashboard/module.py | 2 +- .../mgr/dashboard/run-frontend-e2e-tests.sh | 8 +++- .../mgr/dashboard/services/access_control.py | 35 ++++++++------- .../mgr/dashboard/services/iscsi_cli.py | 9 ++-- src/pybind/mgr/dashboard/settings.py | 29 ++++++++++-- src/pybind/mgr/dashboard/tests/__init__.py | 4 +- .../dashboard/tests/test_access_control.py | 34 ++++++++------ src/pybind/mgr/dashboard/tests/test_iscsi.py | 18 ++++++-- .../mgr/dashboard/tests/test_settings.py | 44 ++++++++++++++++--- src/pybind/mgr/mgr_module.py | 33 +++++++++++--- src/pybind/mgr/tests/__init__.py | 4 +- src/test/mgr/mgr-dashboard-smoke.sh | 5 ++- src/vstart.sh | 5 ++- 24 files changed, 253 insertions(+), 128 deletions(-) diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index cdedfd8e8ee0e..c6738e510dd59 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -268,7 +268,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 ^^^^^^^^^^^^^^^^ @@ -333,8 +333,8 @@ The credentials of an existing user can also be obtained by using 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 typical default configuration with a single RGW endpoint, this is all you have to do to get the Object Gateway management functionality working. The @@ -396,7 +396,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 @@ -795,7 +796,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 the password policy checks use the `force-password` option. Use the option `pwd_update_required` so that a newly created user has @@ -807,11 +808,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. @@ -948,7 +949,7 @@ view and create Ceph pools, and have read-only access to any 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 64cbba9e3f623..93d755937151a 100644 --- a/qa/tasks/mgr/dashboard/helper.py +++ b/qa/tasks/mgr/dashboard/helper.py @@ -4,8 +4,12 @@ from __future__ import absolute_import import json import logging -from collections import namedtuple +import random +import re +import string import time +from collections import namedtuple +from typing import List import requests import six @@ -65,14 +69,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): @@ -433,6 +436,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 e1c9b8e63e62e..e5e9282dcbbfb 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, JObj, JLeaf @@ -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): # test with Authorization header self.create_user('admin2', 'admin2', ['administrator']) @@ -94,29 +100,6 @@ class AuthTest(DashboardTestCase): "detail": "Invalid credentials" }) - def test_login_without_password(self): - # test with Authorization header - 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') - - # test with Cookies set - self.create_user('admin2', '', ['administrator']) - self._post("/api/auth", {'username': 'admin2', 'password': ''}, set_cookies=True) - self.assertStatus(400) - self.assertJsonBody({ - "component": "auth", - "code": "invalid_credentials", - "detail": "Invalid credentials" - }) - self.delete_user('admin2') - def test_lockout_user(self): # test with Authorization header self._ceph_cmd(['dashboard', 'set-account-lockout-attempts', '3']) @@ -288,8 +271,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) @@ -312,8 +296,9 @@ class AuthTest(DashboardTestCase): self._get("/api/host", set_cookies=True) 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", set_cookies=True) self.assertStatus(401) diff --git a/qa/tasks/mgr/dashboard/test_ganesha.py b/qa/tasks/mgr/dashboard/test_ganesha.py index 8583f3d66e728..c1acda14351a6 100644 --- a/qa/tasks/mgr/dashboard/test_ganesha.py +++ b/qa/tasks/mgr/dashboard/test_ganesha.py @@ -40,8 +40,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 6f1acebece78d..9760276a3d3ae 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 ea7beee6d7a50..171a8f3abeb98 100644 --- a/qa/tasks/mgr/dashboard/test_user.py +++ b/qa/tasks/mgr/dashboard/test_user.py @@ -286,39 +286,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/src/cephadm/cephadm b/src/cephadm/cephadm index 8de809d75bf12..8cff1f2b2f5a9 100755 --- a/src/cephadm/cephadm +++ b/src/cephadm/cephadm @@ -3253,10 +3253,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 1cc564a66d7ec..b1019f55fb6f2 100644 --- a/src/pybind/mgr/cephadm/services/cephadmservice.py +++ b/src/pybind/mgr/cephadm/services/cephadmservice.py @@ -197,7 +197,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 6454893d9425e..2fce233e3696a 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 e6ec2ae243f23..82f5812ab1aee 100644 --- a/src/pybind/mgr/cephadm/tests/test_cephadm.py +++ b/src/pybind/mgr/cephadm/tests/test_cephadm.py @@ -306,7 +306,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 0874c395eb551..cf37b47d9f75b 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 136252e01374c..e3af2ff579891 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -386,7 +386,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 6512cb1bf1abc..427ef614de442 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 75381cca10e67..1bf6daf9e9576 100644 --- a/src/pybind/mgr/dashboard/services/access_control.py +++ b/src/pybind/mgr/dashboard/services/access_control.py @@ -15,8 +15,7 @@ import re from datetime import datetime, timedelta import bcrypt - -from mgr_module import CLIReadCommand, CLIWriteCommand +from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand from .. import mgr from ..security import Scope, Permission @@ -584,10 +583,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) @@ -717,7 +717,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 ' @@ -725,10 +724,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: @@ -868,10 +869,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: @@ -887,10 +889,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 e894a56caef73..a290337c10c24 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 .iscsi_client import IscsiClient from .iscsi_config import IscsiGatewaysConfig, IscsiGatewayAlreadyExists, InvalidServiceUrl, \ @@ -18,10 +18,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 aab54ab9489ab..226ff74e9cfd9 100644 --- a/src/pybind/mgr/dashboard/settings.py +++ b/src/pybind/mgr/dashboard/settings.py @@ -5,6 +5,8 @@ import errno import inspect from six import add_metaclass +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 421378a155d80..ca049c088bed2 100644 --- a/src/pybind/mgr/dashboard/tests/__init__.py +++ b/src/pybind/mgr/dashboard/tests/__init__.py @@ -38,6 +38,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: @@ -49,8 +50,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 0039efc167ba8..fa7a0cf32452a 100644 --- a/src/pybind/mgr/dashboard/tests/test_access_control.py +++ b/src/pybind/mgr/dashboard/tests/test_access_control.py @@ -9,6 +9,8 @@ import unittest from datetime import datetime, timedelta +from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE + from . import CmdException, CLICommandTestMixin from .. import mgr from ..security import Scope, Permission @@ -279,7 +281,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, @@ -328,7 +330,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") @@ -339,7 +341,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: @@ -360,7 +362,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) @@ -566,7 +568,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', @@ -586,16 +588,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', @@ -615,8 +624,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") @@ -625,14 +633,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, { @@ -652,7 +660,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 49dfc81d78c10..5115d849d09a5 100644 --- a/src/pybind/mgr/dashboard/tests/test_iscsi.py +++ b/src/pybind/mgr/dashboard/tests/test_iscsi.py @@ -1,4 +1,4 @@ -# pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods, too-many-lines import copy import errno @@ -10,6 +10,8 @@ try: except ImportError: import unittest.mock as mock +from mgr_module import ERROR_MSG_NO_INPUT_FILE + from . import CmdException, ControllerTestCase, CLICommandTestMixin, KVStoreMockMixin from .. import mgr from ..controllers.iscsi import Iscsi, IscsiTarget @@ -29,18 +31,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 da54a20655def..39c0aaaa6ce26 100644 --- a/src/pybind/mgr/dashboard/tests/test_settings.py +++ b/src/pybind/mgr/dashboard/tests/test_settings.py @@ -3,6 +3,9 @@ from __future__ import absolute_import import errno import unittest + +from mgr_module import ERROR_MSG_EMPTY_INPUT_FILE + from . import KVStoreMockMixin, ControllerTestCase from .. import settings from ..controllers.settings import Settings as SettingsController @@ -42,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, '') @@ -50,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( @@ -66,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 " @@ -75,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 6f3e799187724..8f87064050d2f 100644 --- a/src/pybind/mgr/mgr_module.py +++ b/src/pybind/mgr/mgr_module.py @@ -16,6 +16,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", @@ -337,6 +339,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) @@ -1091,18 +1104,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. @@ -1114,7 +1127,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() @@ -1124,7 +1137,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. @@ -1144,8 +1164,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 5e5a541fbdf82..1427be92d2c4b 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 3a4bbff3fe89a..40e29e462157f 100755 --- a/src/test/mgr/mgr-dashboard-smoke.sh +++ b/src/test/mgr/mgr-dashboard-smoke.sh @@ -51,7 +51,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 58deb58d07056..d6ff4e4b976d0 100755 --- a/src/vstart.sh +++ b/src/vstart.sh @@ -958,7 +958,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! -- 2.39.5