From 2020477e7a9e716f964434335de07753b174d729 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Fri, 27 Oct 2023 09:57:00 -0700 Subject: [PATCH] Issue 5971 - CLI - Fix password prompt for repl status Description: dsconf replication status is failing with 'Invalid credentials' when the password of the Directory Manager is not identical on servers. Ask for each instance's password separently. Expand the help message in CLI for replication and agreement status commands. Fixes: https://github.com/389ds/389-ds-base/issues/5971 Reviewed by: ? --- src/lib389/lib389/agreement.py | 13 +++++- src/lib389/lib389/cli_conf/replication.py | 54 +++++++++++------------ src/lib389/lib389/replica.py | 4 +- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/lib389/lib389/agreement.py b/src/lib389/lib389/agreement.py index c7d90544a5..170ab9050d 100644 --- a/src/lib389/lib389/agreement.py +++ b/src/lib389/lib389/agreement.py @@ -13,6 +13,7 @@ import datetime from lib389._constants import * from lib389.properties import * +from lib389.cli_base import _get_arg from lib389._entry import FormatDict from lib389.utils import normalizeDN, ensure_bytes, ensure_str, ensure_dict_str, ensure_list_str from lib389 import Entry, DirSrv, NoSuchEntryError, InvalidArgumentError @@ -305,7 +306,7 @@ def get_lag_time(self, suffix, agmt_name, binddn=None, bindpw=None): # Return a nice formated timestamp return "{:0>8}".format(str(lag)) - def status(self, winsync=False, just_status=False, use_json=False, binddn=None, bindpw=None): + def status(self, winsync=False, just_status=False, use_json=False, binddn=None, bindpw=None, pwprompt=False): """Get the status of a replication agreement :param winsync: Specifies if the the agreement is a winsync replication agreement :type winsync: boolean @@ -317,6 +318,8 @@ def status(self, winsync=False, just_status=False, use_json=False, binddn=None, :type binddn: str :param bindpw: Password for the bind DN :type bindpw: str + :param pwprompt: If binddn or bindpw is None, ask for them interactively + :type pwprompt: boolean :returns: A status message :raises: ValueError - if failing to get agmt status """ @@ -328,6 +331,14 @@ def status(self, winsync=False, just_status=False, use_json=False, binddn=None, # RUV entry under the suffix, then we can't get the status. So in this case we # need to provide a DN and password. if not winsync: + if pwprompt: + host = self.get_attr_val_utf8(AGMT_HOST) + port = self.get_attr_val_utf8(AGMT_PORT) + suffix = self.get_attr_val_utf8(REPL_ROOT) + if binddn is None: + binddn = _get_arg(None, msg=f"Enter bind DN for the replicated suffix ({suffix}) on {host}:{port}") + if bindpw is None: + bindpw = _get_arg(None, msg=f"Enter password for ({binddn}) to the replicated suffix ({suffix}) on {host}:{port}", hidden=True) try: status = self.get_agmt_status(binddn=binddn, bindpw=bindpw) except ldap.INVALID_CREDENTIALS as e: diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py index f2d55c8dcf..35233c5f50 100644 --- a/src/lib389/lib389/cli_conf/replication.py +++ b/src/lib389/lib389/cli_conf/replication.py @@ -317,13 +317,12 @@ def list_suffixes(inst, basedn, log, args): def get_repl_status(inst, basedn, log, args): replicas = Replicas(inst) replica = replicas.get(args.suffix) + pw_and_dn_prompt = False if args.bind_passwd_file is not None: - passwd = get_passwd_from_file(args.bind_passwd_file) - elif args.bind_passwd_prompt: - passwd = _get_arg(None, msg=f"Enter password for ({args.bind_dn})", hidden=True, confirm=True) - else: - passwd = args.bind_passwd - status = replica.status(binddn=args.bind_dn, bindpw=passwd) + args.bind_passwd = get_passwd_from_file(args.bind_passwd_file) + if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None: + pw_and_dn_prompt = True + status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=pw_and_dn_prompt) if args.json: log.info(json.dumps({"type": "list", "items": status}, indent=4)) else: @@ -334,14 +333,12 @@ def get_repl_status(inst, basedn, log, args): def get_repl_winsync_status(inst, basedn, log, args): replicas = Replicas(inst) replica = replicas.get(args.suffix) + pw_and_dn_prompt = False if args.bind_passwd_file is not None: - passwd = get_passwd_from_file(args.bind_passwd_file) - elif args.bind_passwd_prompt: - passwd = _get_arg(None, msg=f"Enter password for ({args.bind_dn})", hidden=True, confirm=True) - else: - passwd = args.bind_passwd - - status = replica.status(binddn=args.bind_dn, bindpw=passwd, winsync=True) + args.bind_passwd = get_passwd_from_file(args.bind_passwd_file) + if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None: + pw_and_dn_prompt = True + status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, winsync=True, pwprompt=pw_and_dn_prompt) if args.json: log.info(json.dumps({"type": "list", "items": status}, indent=4)) else: @@ -927,11 +924,12 @@ def poke_agmt(inst, basedn, log, args): def get_agmt_status(inst, basedn, log, args): agmt = get_agmt(inst, args) + pw_and_dn_prompt = False if args.bind_passwd_file is not None: args.bind_passwd = get_passwd_from_file(args.bind_passwd_file) - if (args.bind_dn is not None and args.bind_passwd is None) or args.bind_passwd_prompt: - args.bind_passwd = _get_arg(None, msg=f"Enter password for \"{args.bind_dn}\"", hidden=True, confirm=True) - status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd) + if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None: + pw_and_dn_prompt = True + status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=pw_and_dn_prompt) log.info(status) @@ -1321,19 +1319,19 @@ def create_parser(subparsers): repl_status_parser = repl_subcommands.add_parser('status', help='Display the current status of all the replication agreements') repl_status_parser.set_defaults(func=get_repl_status) repl_status_parser.add_argument('--suffix', required=True, help="Sets the DN of the replication suffix") - repl_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer") - repl_status_parser.add_argument('--bind-passwd', help="Sets the password for the bind DN") - repl_status_parser.add_argument('--bind-passwd-file', help="File containing the password") - repl_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") + repl_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer. If not set, current instance's root DN will be used. It will be used for all agreements") + repl_status_parser.add_argument('--bind-passwd', help="Sets the password for the bind DN. It will be used for all agreements") + repl_status_parser.add_argument('--bind-passwd-file', help="File containing the password. It will be used for all agreements") + repl_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for passwords for each agreement's instance separately") repl_winsync_status_parser = repl_subcommands.add_parser('winsync-status', help='Display the current status of all ' 'the replication agreements') repl_winsync_status_parser.set_defaults(func=get_repl_winsync_status) repl_winsync_status_parser.add_argument('--suffix', required=True, help="Sets the DN of the replication suffix") - repl_winsync_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer") - repl_winsync_status_parser.add_argument('--bind-passwd', help="Sets the password of the bind DN") - repl_winsync_status_parser.add_argument('--bind-passwd-file', help="File containing the password") - repl_winsync_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") + repl_winsync_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer. Currectly not used") + repl_winsync_status_parser.add_argument('--bind-passwd', help="Sets the password of the bind DN. Currectly not used") + repl_winsync_status_parser.add_argument('--bind-passwd-file', help="File containing the password. Currectly not used") + repl_winsync_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password. Currectly not used") repl_promote_parser = repl_subcommands.add_parser('promote', help='Promote a replica to a hub or supplier') repl_promote_parser.set_defaults(func=promote_replica) @@ -1510,10 +1508,10 @@ def create_parser(subparsers): agmt_status_parser.set_defaults(func=get_agmt_status) agmt_status_parser.add_argument('AGMT_NAME', nargs=1, help='The name of the replication agreement') agmt_status_parser.add_argument('--suffix', required=True, help="Sets the DN of the replication suffix") - agmt_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer") - agmt_status_parser.add_argument('--bind-passwd', help="Sets the password for the bind DN") - agmt_status_parser.add_argument('--bind-passwd-file', help="File containing the password") - agmt_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for password") + agmt_status_parser.add_argument('--bind-dn', help="Sets the DN to use to authenticate to the consumer. If not set, current instance's root DN will be used. It will be used for all agreements") + agmt_status_parser.add_argument('--bind-passwd', help="Sets the password for the bind DN. It will be used for all agreements") + agmt_status_parser.add_argument('--bind-passwd-file', help="File containing the password. It will be used for all agreements") + agmt_status_parser.add_argument('--bind-passwd-prompt', action='store_true', help="Prompt for passwords for each agreement's instance separately") # Delete agmt_del_parser = agmt_subcommands.add_parser('delete', help='Delete replication agreement') diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py index e89664eeff..177128202a 100644 --- a/src/lib389/lib389/replica.py +++ b/src/lib389/lib389/replica.py @@ -1734,13 +1734,13 @@ def get_suffix(self): return self._suffix - def status(self, binddn=None, bindpw=None, winsync=False): + def status(self, binddn=None, bindpw=None, winsync=False, pwprompt=False): """Get a list of the status for every agreement """ agmtList = [] agmts = Agreements(self._instance, self.dn, winsync=winsync).list() for agmt in agmts: - raw_status = agmt.status(binddn=binddn, bindpw=bindpw, use_json=True, winsync=winsync) + raw_status = agmt.status(binddn=binddn, bindpw=bindpw, use_json=True, winsync=winsync, pwprompt=pwprompt) agmtList.append(json.loads(raw_status)) # sort the list of agreements by the lag time