From 9f433e82e153f8f6ccd6a6d32cd6440fa9beec0c Mon Sep 17 00:00:00 2001 From: William Brown Date: Jan 13 2019 22:39:04 +0000 Subject: [PATCH 1/2] Update .gitignore to hide extra files Description: Add a small number of files to gitignore --- diff --git a/.gitignore b/.gitignore index b8bff61..c1f704c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,14 +31,18 @@ stamp-h1 .settings .cache *.a +*.rsa *.dirstamp *.la *.lo *.o +*.rso *.pyc +*.rej __pycache__ .libs .deps +Cargo.lock rpmbuild rpm/389-ds-base.spec Makefile @@ -216,3 +220,7 @@ wrappers/ds_systemd_ask_password_acl docs/slapi.doxy man/man3/ html/ +.pytest_cache/ +src/lib389/dist/ +src/lib389/man/ +src/libsds/target/ From b5e4fca0b956dafc923fb8ac3b06912f44253a85 Mon Sep 17 00:00:00 2001 From: William Brown Date: Jan 13 2019 22:39:04 +0000 Subject: [PATCH 2/2] Ticket 50136 - Allow resetting passwords on the CLI Bug Description: This allows resetting passwords on the CLI for accounts, as well as allowing accounts to self-change their passwords. Fix Description: Add reset and change password functions, and fix a number of issues with non-DM bind in the server, regrissions in tls enable during tests. https://pagure.io/389-ds-base/issue/50136 Author: William Brown Review by: ??? --- diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py index d76b34b..7a39603 100644 --- a/src/lib389/lib389/__init__.py +++ b/src/lib389/lib389/__init__.py @@ -1,5 +1,6 @@ # --- BEGIN COPYRIGHT BLOCK --- # Copyright (C) 2015 Red Hat, Inc. +# Copyright (C) 2019 William Brown # All rights reserved. # # License: GPL (version 3 or any later version). @@ -933,6 +934,10 @@ class DirSrv(SimpleLDAPObject, object): backends = [userroot,] # Go! + self.log.debug("DEBUG: creating with parameters:") + self.log.debug(general) + self.log.debug(slapd) + self.log.debug(backends) sds.create_from_args(general, slapd, backends, None) def create(self, pyinstall=False, version=INSTALL_LATEST_CONFIG): @@ -1153,7 +1158,7 @@ class DirSrv(SimpleLDAPObject, object): self.log.debug("Cannot connect to %r", uri) raise e except ldap.LDAPError as e: - self.log.debug("Error: Failed to authenticate: %s", e) + self.log.debug("Error: Failed to authenticate as %s: %s" % (self.binddn, e)) raise e """ diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py index b42e28a..e1f9adb 100644 --- a/src/lib389/lib389/_mapped_object.py +++ b/src/lib389/lib389/_mapped_object.py @@ -1,5 +1,6 @@ # --- BEGIN COPYRIGHT BLOCK --- # Copyright (C) 2016 Red Hat, Inc. +# Copyright (C) 2019 William Brown # All rights reserved. # # License: GPL (version 3 or any later version). @@ -312,6 +313,18 @@ class DSLdapObject(DSLogging): if not self.present(attr, value): self.add(attr, value) + def ensure_removed(self, attr, value): + """Ensure that a attribute and value has been removed and not present + or remove it. + + :param key: an attribute name + :type key: str + :param value: an attribute value + :type value: str + """ + if self.present(attr, value): + self.remove(attr, value) + # maybe this could be renamed? def set(self, key, value, action=ldap.MOD_REPLACE): """Perform a specified action on a key with value diff --git a/src/lib389/lib389/cli_base/__init__.py b/src/lib389/lib389/cli_base/__init__.py index b4acb49..3e2cdcc 100644 --- a/src/lib389/lib389/cli_base/__init__.py +++ b/src/lib389/lib389/cli_base/__init__.py @@ -1,5 +1,6 @@ # --- BEGIN COPYRIGHT BLOCK --- # Copyright (C) 2016 Red Hat, Inc. +# Copyright (C) 2019 William Brown # All rights reserved. # # License: GPL (version 3 or any later version). @@ -97,7 +98,10 @@ def connect_instance(dsrc_inst, verbose, args): else: # The instance name does not match any instances raise ValueError("Could not find configuration for instance: " + dsargs['ldapurl']) + ds = DirSrv(verbose=verbose) + # We do an empty allocate here to determine if we can autobind ... (really + # we should actually be inspect the URL ...) ds.allocate(dsargs) if args.pwdfile is not None or args.bindpw is not None or args.prompt is True: @@ -110,6 +114,7 @@ def connect_instance(dsrc_inst, verbose, args): raise ValueError("Failed to open password file: " + str(e)) elif args.bindpw is not None: # Password provided + # This shouldn't be needed? dsrc already inherits the args ... dsargs[SER_ROOT_PW] = args.bindpw else: # No password or we chose to prompt @@ -118,6 +123,13 @@ def connect_instance(dsrc_inst, verbose, args): # No LDAPI, prompt for password dsargs[SER_ROOT_PW] = getpass("Enter password for {} on {}: ".format(dsrc_inst['binddn'], dsrc_inst['uri'])) + if 'binddn' in dsrc_inst: + # Allocate is an awful interface that we should stop using, but for now + # just directly map the dsrc_inst args in (remember, dsrc_inst DOES + # overlay cli args into the map ...) + dsargs[SER_ROOT_DN] = dsrc_inst['binddn'] + + ds = DirSrv(verbose=verbose) ds.allocate(dsargs) ds.open(saslmethod=dsrc_inst['saslmech'], certdir=dsrc_inst['tls_cacertdir'], diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py index 1be4b6e..ebed52e 100644 --- a/src/lib389/lib389/cli_conf/backend.py +++ b/src/lib389/lib389/cli_conf/backend.py @@ -1,5 +1,6 @@ # --- BEGIN COPYRIGHT BLOCK --- # Copyright (C) 2018 Red Hat, Inc. +# Copyright (C) 2019 William Brown # All rights reserved. # # License: GPL (version 3 or any later version). @@ -594,7 +595,7 @@ def create_parser(subparsers): ##################################################### # Suffix parser ##################################################### - suffix_parser = subcommands.add_parser('suffix', help="Manage a backend suffix") + suffix_parser = subcommands.add_parser('suffix', help="Manage a backend suffix, including creating backends") suffix_subcommands = suffix_parser.add_subparsers(help="action") # List backends/suffixes diff --git a/src/lib389/lib389/cli_idm/account.py b/src/lib389/lib389/cli_idm/account.py index 54580a8..610ae7e 100644 --- a/src/lib389/lib389/cli_idm/account.py +++ b/src/lib389/lib389/cli_idm/account.py @@ -1,5 +1,6 @@ # --- BEGIN COPYRIGHT BLOCK --- # Copyright (C) 2017, Red Hat inc, +# Copyright (C) 2018, William Brown # All rights reserved. # # License: GPL (version 3 or any later version). @@ -9,7 +10,7 @@ import argparse from lib389.idm.account import Account, Accounts -from lib389.cli_idm import ( +from lib389.cli_base import ( _generic_list, _get_arg, ) @@ -41,6 +42,26 @@ def unlock(inst, basedn, log, args): acct.unlock() log.info('unlocked %s' % dn) +def reset_password(inst, basedn, log, args): + dn = _get_arg(args.dn, msg="Enter dn to reset password") + new_password = _get_arg(args.new_password, hidden=True, confirm=True, + msg="Enter new password for %s" % dn) + accounts = Accounts(inst, basedn) + acct = accounts.get(dn=dn) + acct.reset_password(new_password) + log.info('reset password for %s' % dn) + +def change_password(inst, basedn, log, args): + dn = _get_arg(args.dn, msg="Enter dn to change password") + cur_password = _get_arg(args.current_password, hidden=True, confirm=False, + msg="Enter current password for %s" % dn) + new_password = _get_arg(args.new_password, hidden=True, confirm=True, + msg="Enter new password for %s" % dn) + accounts = Accounts(inst, basedn) + acct = accounts.get(dn=dn) + acct.change_password(cur_password, new_password) + log.info('changed password for %s' % dn) + def create_parser(subparsers): account_parser = subparsers.add_parser('account', help='Manage generic accounts IE account locking and unlocking.') @@ -62,3 +83,15 @@ def create_parser(subparsers): unlock_parser.set_defaults(func=unlock) unlock_parser.add_argument('dn', nargs='?', help='The dn to unlock') + reset_pw_parser = subcommands.add_parser('reset_password', help='Reset the password of an account. This should be performed by a directory admin.') + reset_pw_parser.set_defaults(func=reset_password) + reset_pw_parser.add_argument('dn', nargs='?', help='The dn to reset the password for') + reset_pw_parser.add_argument('new_password', nargs='?', help='The new password to set') + + change_pw_parser = subcommands.add_parser('change_password', help='Change the password of an account. This can be performed by any user (with correct rights)') + change_pw_parser.set_defaults(func=change_password) + change_pw_parser.add_argument('dn', nargs='?', help='The dn to change the password for') + change_pw_parser.add_argument('new_password', nargs='?', help='The new password to set') + change_pw_parser.add_argument('current_password', nargs='?', help='The accounts current password') + + diff --git a/src/lib389/lib389/idm/account.py b/src/lib389/lib389/idm/account.py index 100bfec..d25a585 100644 --- a/src/lib389/lib389/idm/account.py +++ b/src/lib389/lib389/idm/account.py @@ -37,7 +37,7 @@ class Account(DSLdapObject): def unlock(self): """Unset nsAccountLock""" - self.remove('nsAccountLock', None) + self.ensure_removed('nsAccountLock', None) # If the account can be bound to, this will attempt to do so. We don't check # for exceptions, just pass them back! @@ -108,6 +108,32 @@ class Account(DSLdapObject): crt = f.read() self.add('usercertificate;binary', crt) + def reset_password(self, new_password): + """Set the password of the account: This requires write permission to + the userPassword attribute, so likely is only possible as an administrator + of the directory. + + :param new_password: The new password value to set + :type new_password: str + """ + self.set('userPassword', new_password) + + def change_password(self, current_password, new_password): + """Using the accounts current bind password, performan an ldap passwd + change extended operation. This does not required elevated permissions + to read/write the userPassword field, so is the way that most accounts + would change their password. This doesn't work on all classes of objects + so it could error. + + :param current_password: The existing password value + :type current_password: str + :param new_password: The new password value to set + :type new_password: str + """ + # Please see _mapped_object.py and DSLdapObject for why this is structured + # in this way re-controls. + self._instance.passwd_s(self._dn, current_password, new_password, + serverctrls=self._server_controls, clientctrls=self._client_controls) class Accounts(DSLdapObjects): """DSLdapObjects that represents Account entry diff --git a/src/lib389/lib389/idm/user.py b/src/lib389/lib389/idm/user.py index b6fe4ef..4d3acf8 100644 --- a/src/lib389/lib389/idm/user.py +++ b/src/lib389/lib389/idm/user.py @@ -104,6 +104,29 @@ class nsUserAccounts(DSLdapObjects): else: self._basedn = '{},{}'.format(rdn, basedn) + def create_test_user(self, uid=1000, gid=2000): + """Create a test user with uid=test_user_UID rdn + + :param uid: User id + :type uid: int + :param gid: Group id + :type gid: int + + :returns: DSLdapObject of the created entry + """ + + rdn_value = "test_user_{}".format(uid) + rdn = "uid={}".format(rdn_value) + properties = { + 'uid': rdn_value, + 'cn': rdn_value, + 'displayName': rdn_value, + 'uidNumber': str(uid), + 'gidNumber': str(gid), + 'homeDirectory': '/home/{}'.format(rdn_value), + } + return super(nsUserAccounts, self).create(rdn, properties) + #### Traditional style userAccounts. diff --git a/src/lib389/lib389/instance/setup.py b/src/lib389/lib389/instance/setup.py index 73bc80d..97a3abc 100644 --- a/src/lib389/lib389/instance/setup.py +++ b/src/lib389/lib389/instance/setup.py @@ -1,5 +1,6 @@ # --- BEGIN COPYRIGHT BLOCK --- # Copyright (C) 2016 Red Hat, Inc. +# Copyright (C) 2019 William Brown # All rights reserved. # # License: GPL (version 3 or any later version). @@ -828,8 +829,12 @@ class SetupDs(object): base_config_inst.apply_config(install=True) # Setup TLS with the instance. + + # We *ALWAYS* set secure port, even if security is off, because it breaks + # tests with standalone.enable_tls if we do not. It's only when security; on + # that we actually start listening on it. + ds_instance.config.set('nsslapd-secureport', '%s' % slapd['secure_port']) if slapd['self_sign_cert']: - ds_instance.config.set('nsslapd-secureport', '%s' % slapd['secure_port']) ds_instance.config.set('nsslapd-security', 'on') # Create the backends as listed diff --git a/src/lib389/lib389/tests/idm/account_test.py b/src/lib389/lib389/tests/idm/account_test.py index d78db03..417c0f4 100644 --- a/src/lib389/lib389/tests/idm/account_test.py +++ b/src/lib389/lib389/tests/idm/account_test.py @@ -1,5 +1,6 @@ # --- BEGIN COPYRIGHT BLOCK --- # Copyright (C) 2017 Red Hat, Inc. +# Copyright (C) 2019 William Brown # All rights reserved. # # License: GPL (version 3 or any later version). @@ -9,35 +10,17 @@ import os -import logging import pytest import ldap -from lib389.idm.user import UserAccounts +from lib389.idm.user import UserAccounts, nsUserAccounts from lib389.topologies import topology_st as topology from lib389._constants import DEFAULT_SUFFIX -DEBUGGING = os.getenv('DEBUGGING', False) - -if DEBUGGING is not False: - DEBUGGING = True - -if DEBUGGING: - logging.getLogger(__name__).setLevel(logging.DEBUG) -else: - logging.getLogger(__name__).setLevel(logging.INFO) - -log = logging.getLogger(__name__) - - def test_account_locking(topology): """ Ensure that user and group management works as expected. """ - if DEBUGGING: - # Add debugging steps(if any)... - pass - users = UserAccounts(topology.standalone, DEFAULT_SUFFIX) user_properties = { @@ -67,4 +50,33 @@ def test_account_locking(topology): conn = testuser.bind('password') conn.unbind_s() - log.info('Test PASSED') +def test_account_reset_pw(topology): + users = nsUserAccounts(topology.standalone, DEFAULT_SUFFIX) + testuser = users.create_test_user(uid=1001) + + # Make sure they are unlocked. + testuser.unlock() + + testuser.reset_password("test_password") + + # Assert we can bind as the new PW + c = testuser.bind('test_password') + c.unbind_s() + + +def test_account_change_pw(topology): + # This test requires a secure connection + topology.standalone.enable_tls() + + users = nsUserAccounts(topology.standalone, DEFAULT_SUFFIX) + testuser = users.create_test_user(uid=1002) + + # Make sure they are unlocked. + testuser.unlock() + + testuser.reset_password('password') + testuser.change_password('password', "test_password") + + # Assert we can bind as the new PW + c = testuser.bind('test_password') + c.unbind_s()