Monday, May 15, 2017

[389-commits] [lib389] 01/01: Ticket 30 - Add initial support for account lock and unlock.

This is an automated email from the git hooks/post-receive script.

firstyear pushed a commit to branch master
in repository lib389.

commit 4e6651afe94163ccb10a2ace8030ed02789d827f
Author: William Brown <firstyear@redhat.com>
Date: Fri May 12 14:10:59 2017 +1000

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: mreynolds (Thanks!)
---
cli/dsidm | 2 ++
lib389/_mapped_object.py | 47 +++++++++++++++++----------
lib389/cli_idm/account.py | 64 ++++++++++++++++++++++++++++++++++++
lib389/idm/account.py | 50 ++++++++++++++++++++++++++++
lib389/idm/user.py | 8 ++---
lib389/tests/idm/account_test.py | 70 ++++++++++++++++++++++++++++++++++++++++
6 files changed, 220 insertions(+), 21 deletions(-)

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 <william at blackhats.net.au>
+# 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..81f3ee0 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
@@ -62,7 +63,6 @@ class UserAccounts(DSLdapObjects):
'account',
'posixaccount',
'inetOrgPerson',
- 'person',
'organizationalPerson',
]
self._filterattrs = [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')

--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
_______________________________________________
389-commits mailing list -- 389-commits@lists.fedoraproject.org
To unsubscribe send an email to 389-commits-leave@lists.fedoraproject.org

No comments:

Post a Comment