From 7ac9b5548b1ad12ef6a5259e5215308feebfe3d3 Mon Sep 17 00:00:00 2001 From: William Brown Date: Fri, 12 May 2017 14:10:59 +1000 Subject: [PATCH] Ticket 30 - Add initial support for account lock and unlock. Bug Description: Add support for account lock and unlock to eventually replace ns-inactivate, ns-activate and ns-accountlock. Fix Description: Add a new account type that looks for any object that supports userPassword. It can lock / unlock them. Additionally, subclass UserAccount on this so inherit the account lock mech. https://pagure.io/lib389/issue/30 Author: wibrown Review by: ??? --- cli/dsidm | 2 ++ lib389/_mapped_object.py | 47 +++++++++++++++++---------- lib389/cli_idm/account.py | 64 ++++++++++++++++++++++++++++++++++++ lib389/idm/account.py | 50 ++++++++++++++++++++++++++++ lib389/idm/user.py | 7 ++-- lib389/tests/idm/account_test.py | 70 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 220 insertions(+), 20 deletions(-) create mode 100644 lib389/cli_idm/account.py create mode 100644 lib389/idm/account.py create mode 100644 lib389/tests/idm/account_test.py diff --git a/cli/dsidm b/cli/dsidm index e253b4d..1c62acb 100755 --- a/cli/dsidm +++ b/cli/dsidm @@ -17,6 +17,7 @@ import logging logging.basicConfig(format='%(message)s') from lib389._constants import DN_DM +from lib389.cli_idm import account as cli_account from lib389.cli_idm import initialise as cli_init from lib389.cli_idm import organisationalunit as cli_ou from lib389.cli_idm import group as cli_group @@ -58,6 +59,7 @@ if __name__ == '__main__': # Call all the other cli modules to register their bits + cli_account.create_parser(subparsers) cli_group.create_parser(subparsers) cli_init.create_parser(subparsers) cli_ou.create_parser(subparsers) diff --git a/lib389/_mapped_object.py b/lib389/_mapped_object.py index e2eef9f..38e52f9 100644 --- a/lib389/_mapped_object.py +++ b/lib389/_mapped_object.py @@ -114,6 +114,12 @@ class DSLdapObject(DSLogging): e = self._instance.getEntry(self._dn) return e.__repr__() + def display_attr(self, attr): + out = "" + for v in self.get_attr_vals(attr): + out += "%s: %s\n" % (attr, v) + return out + def _jsonify(self, fn, *args, **kwargs): # This needs to map all the values to ensure_str attrs = fn(*args, **kwargs) @@ -466,6 +472,11 @@ class DSLdapObjects(DSLogging): self._batch = batch self._scope = ldap.SCOPE_SUBTREE + def _get_objectclass_filter(self): + return _gen_and( + _gen_filter(_term_gen('objectclass'), self._objectclasses) + ) + def _entry_to_instance(self, dn=None, entry=None): # Normally this won't be used. But for say the plugin type where we # have "many" possible child types, this allows us to overload @@ -476,14 +487,14 @@ class DSLdapObjects(DSLogging): def list(self): # Filter based on the objectclasses and the basedn insts = None + # This will yield and & filter for objectClass with as many terms as needed. + filterstr = self._get_objectclass_filter() + self._log.debug('list filter = %s' % filterstr) try: results = self._instance.search_s( base=self._basedn, scope=self._scope, - # This will yield and & filter for objectClass with as many terms as needed. - filterstr=_gen_and( - _gen_filter(_term_gen('objectclass'), self._objectclasses) - ), + filterstr=filterstr, attrlist=self._list_attrlist, ) # def __init__(self, instance, dn=None, batch=False): @@ -507,31 +518,33 @@ class DSLdapObjects(DSLogging): return self._entry_to_instance(results[0].dn, results[0]) def _get_dn(self, dn): + # This will yield and & filter for objectClass with as many terms as needed. + filterstr = self._get_objectclass_filter() + self._log.debug('_gen_dn filter = %s' % filterstr) return self._instance.search_s( base=dn, scope=ldap.SCOPE_BASE, - # This will yield and & filter for objectClass with as many terms as needed. - filterstr=_gen_and( - _gen_filter(_term_gen('objectclass'), self._objectclasses,) - ), + filterstr=filterstr, attrlist=self._list_attrlist, ) def _get_selector(self, selector): # Filter based on the objectclasses and the basedn # Based on the selector, we should filter on that too. + # This will yield and & filter for objectClass with as many terms as needed. + filterstr=_gen_and([ + self._get_objectclass_filter(), + _gen_or( + # This will yield all combinations of selector to filterattrs. + # This won't work with multiple values in selector (yet) + _gen_filter(self._filterattrs, _term_gen(selector)) + ), + ]) + self._log.debug('_gen_selector filter = %s' % filterstr) return self._instance.search_s( base=self._basedn, scope=self._scope, - # This will yield and & filter for objectClass with as many terms as needed. - filterstr=_gen_and( - _gen_filter(_term_gen('objectclass'), self._objectclasses, extra=_gen_or( - # This will yield all combinations of selector to filterattrs. - # This won't work with multiple values in selector (yet) - _gen_filter(self._filterattrs, _term_gen(selector)) - ) - ) - ), + filterstr=filterstr, attrlist=self._list_attrlist, ) diff --git a/lib389/cli_idm/account.py b/lib389/cli_idm/account.py new file mode 100644 index 0000000..c98bd23 --- /dev/null +++ b/lib389/cli_idm/account.py @@ -0,0 +1,64 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2017, Red Hat inc, +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +import argparse + +from lib389.idm.account import Account, Accounts +from lib389.cli_base import ( + _generic_list, + _get_arg, + ) + +MANY = Accounts + +def list(inst, basedn, log, args): + _generic_list(inst, basedn, log.getChild('_generic_list'), MANY) + +def status(inst, basedn, log, args): + dn = _get_arg( args.dn, msg="Enter dn to check") + accounts = Accounts(inst, basedn) + acct = accounts.get(dn=dn) + acct_str = "locked: %s" % acct.is_locked() + log.info('dn: %s' % dn) + log.info(acct_str) + +def lock(inst, basedn, log, args): + dn = _get_arg( args.dn, msg="Enter dn to check") + accounts = Accounts(inst, basedn) + acct = accounts.get(dn=dn) + acct.lock() + log.info('locked %s' % dn) + +def unlock(inst, basedn, log, args): + dn = _get_arg( args.dn, msg="Enter dn to check") + accounts = Accounts(inst, basedn) + acct = accounts.get(dn=dn) + acct.unlock() + log.info('unlocked %s' % dn) + + +def create_parser(subparsers): + account_parser = subparsers.add_parser('account', help='Manage generic accounts IE account locking and unlocking.') + + subcommands = account_parser.add_subparsers(help='action') + + list_parser = subcommands.add_parser('list', help='list') + list_parser.set_defaults(func=list) + + lock_parser = subcommands.add_parser('lock', help='lock') + lock_parser.set_defaults(func=lock) + lock_parser.add_argument('dn', nargs='?', help='The dn to lock') + + status_parser = subcommands.add_parser('status', help='status') + status_parser.set_defaults(func=status) + status_parser.add_argument('dn', nargs='?', help='The dn to check') + + unlock_parser = subcommands.add_parser('unlock', help='unlock') + unlock_parser.set_defaults(func=unlock) + unlock_parser.add_argument('dn', nargs='?', help='The dn to unlock') + diff --git a/lib389/idm/account.py b/lib389/idm/account.py new file mode 100644 index 0000000..02e1874 --- /dev/null +++ b/lib389/idm/account.py @@ -0,0 +1,50 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2017, William Brown +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +from lib389._mapped_object import DSLdapObject, DSLdapObjects, _gen_or, _gen_filter, _term_gen + + +class Account(DSLdapObject): + def is_locked(self): + # Check if nsAccountLock is set. + return self.present('nsAccountLock') + + def lock(self): + self.replace('nsAccountLock', 'true') + + def unlock(self): + self.remove('nsAccountLock', None) + +class Accounts(DSLdapObjects): + def __init__(self, instance, basedn, batch=False): + super(Accounts, self).__init__(instance, batch) + # These are all the objects capable of holding a password. + self._objectclasses = [ + 'simpleSecurityObject', + 'organization', + 'personperson', + 'organizationalUnit', + 'netscapeServer', + 'domain', + 'posixAccount', + 'shadowAccount', + 'posixGroup', + 'mailRecipient', + ] + # MUST BE NONE. + self._filterattrs = None + self._childobject = Account + self._basedn = basedn + + #### This is copied from DSLdapObjects, but change _gen_and to _gen_or!!! + + def _get_objectclass_filter(self): + return _gen_or( + _gen_filter(_term_gen('objectclass'), self._objectclasses) + ) + diff --git a/lib389/idm/user.py b/lib389/idm/user.py index 990cdaf..14a6d89 100644 --- a/lib389/idm/user.py +++ b/lib389/idm/user.py @@ -6,7 +6,9 @@ # See LICENSE for details. # --- END COPYRIGHT BLOCK --- -from lib389._mapped_object import DSLdapObject, DSLdapObjects +from lib389._mapped_object import DSLdapObjects +# Account derives DSLdapObject - it gives us the lock / unlock functions. +from lib389.idm.account import Account MUST_ATTRIBUTES = [ 'uid', @@ -19,8 +21,7 @@ MUST_ATTRIBUTES = [ RDN = 'uid' -class UserAccount(DSLdapObject): - +class UserAccount(Account): def __init__(self, instance, dn=None, batch=False): super(UserAccount, self).__init__(instance, dn, batch) self._rdn_attribute = RDN diff --git a/lib389/tests/idm/account_test.py b/lib389/tests/idm/account_test.py new file mode 100644 index 0000000..d78db03 --- /dev/null +++ b/lib389/tests/idm/account_test.py @@ -0,0 +1,70 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2017 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- +# + + +import os +import logging +import pytest +import ldap + +from lib389.idm.user import UserAccounts +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 = { + 'uid': 'testuser', + 'cn' : 'testuser', + 'sn' : 'user', + 'uidNumber' : '1000', + 'gidNumber' : '2000', + 'homeDirectory' : '/home/testuser', + 'userPassword' : 'password' + } + testuser = users.create(properties=user_properties) + + assert(testuser.is_locked() is False) + testuser.lock() + assert(testuser.is_locked() is True) + + # Check a bind fails + with pytest.raises(ldap.UNWILLING_TO_PERFORM): + conn = testuser.bind('password') + conn.unbind_s() + + testuser.unlock() + assert(testuser.is_locked() is False) + + # Check the bind works. + conn = testuser.bind('password') + conn.unbind_s() + + log.info('Test PASSED') -- 1.8.3.1