#50143 Ticket 50136 - Allow resetting passwords on the CLI
Closed 3 years ago by spichugi. Opened 5 years ago by firstyear.
firstyear/389-ds-base 50136-lib389-user-password  into  master

file modified
+8
@@ -31,14 +31,18 @@ 

  .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 @@ 

  docs/slapi.doxy

  man/man3/

  html/

+ .pytest_cache/

+ src/lib389/dist/

+ src/lib389/man/

+ src/libsds/target/

@@ -1,5 +1,6 @@ 

  # --- BEGIN COPYRIGHT BLOCK ---

  # Copyright (C) 2015 Red Hat, Inc.

+ # Copyright (C) 2019 William Brown <william@blackhats.net.au>

  # All rights reserved.

  #

  # License: GPL (version 3 or any later version).
@@ -933,6 +934,10 @@ 

              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 @@ 

                  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

  

          """

@@ -1,5 +1,6 @@ 

  # --- BEGIN COPYRIGHT BLOCK ---

  # Copyright (C) 2016 Red Hat, Inc.

+ # Copyright (C) 2019 William Brown <william@blackhats.net.au>

  # All rights reserved.

  #

  # License: GPL (version 3 or any later version).
@@ -312,6 +313,18 @@ 

          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

@@ -1,5 +1,6 @@ 

  # --- BEGIN COPYRIGHT BLOCK ---

  # Copyright (C) 2016 Red Hat, Inc.

+ # Copyright (C) 2019 William Brown <william@blackhats.net.au>

  # All rights reserved.

  #

  # License: GPL (version 3 or any later version).
@@ -97,7 +98,10 @@ 

          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 @@ 

                  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 @@ 

          # 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'],

@@ -1,5 +1,6 @@ 

  # --- BEGIN COPYRIGHT BLOCK ---

  # Copyright (C) 2018 Red Hat, Inc.

+ # Copyright (C) 2019 William Brown <william@blackhats.net.au>

  # All rights reserved.

  #

  # License: GPL (version 3 or any later version).
@@ -594,7 +595,7 @@ 

      #####################################################

      # 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

@@ -1,5 +1,6 @@ 

  # --- BEGIN COPYRIGHT BLOCK ---

  # Copyright (C) 2017, Red Hat inc,

+ # Copyright (C) 2018, William Brown <william@blackhats.net.au>

  # 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 @@ 

      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 @@ 

      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')

+ 

+ 

@@ -37,7 +37,7 @@ 

      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 @@ 

              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

@@ -104,6 +104,29 @@ 

          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.

  

@@ -1,5 +1,6 @@ 

  # --- BEGIN COPYRIGHT BLOCK ---

  # Copyright (C) 2016 Red Hat, Inc.

+ # Copyright (C) 2019 William Brown <william@blackhats.net.au>

  # All rights reserved.

  #

  # License: GPL (version 3 or any later version).
@@ -828,8 +829,12 @@ 

          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'])

Is the secure port already set, and this is a duplicate "set"?

              ds_instance.config.set('nsslapd-security', 'on')

  

          # Create the backends as listed

@@ -1,5 +1,6 @@ 

  # --- BEGIN COPYRIGHT BLOCK ---

  # Copyright (C) 2017 Red Hat, Inc.

+ # Copyright (C) 2019 William Brown <william@blackhats.net.au>

  # 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 @@ 

      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()

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 <william@blackhats.net.au>

Review by: ???

So as this doesn't work on all classes of objects, shouldn't we just add it to Account(DSLDapObject)?
The same way we have bind() method only for Account objects.

It's not a real issue but just for the information.
You can use create_test_user() method for a generic user entry creation.

Is the secure port already set, and this is a duplicate "set"?

So as this doesn't work on all classes of objects, shouldn't we just add it to Account(DSLDapObject)?
The same way we have bind() method only for Account objects.

I wish you were sitting next to me yesterday when I had this internal conflict about where to put this function :)

I think you are indeed correct, and I'll move the method.

It's not a real issue but just for the information.
You can use create_test_user() method for a generic user entry creation.

I knew there was a test_user create method, but I forgot what it was! I'll check to see if I can use it here.

2 new commits added

  • Ticket 50136 - Allow resetting passwords on the CLI
  • Update .gitignore to hide extra files
5 years ago

Updated based on your feedback

rebased onto 9f433e8

5 years ago

Pull-Request has been merged by firstyear

5 years ago

389-ds-base is moving from Pagure to Github. This means that new issues and pull requests
will be accepted only in 389-ds-base's github repository.

This pull request has been cloned to Github as issue and is available here:
- https://github.com/389ds/389-ds-base/issues/3202

If you want to continue to work on the PR, please navigate to the github issue,
download the patch from the attachments and file a new pull request.

Thank you for understanding. We apologize for all inconvenience.

Pull-Request has been closed by spichugi

3 years ago