Wednesday, May 31, 2017

[389-commits] [lib389] 01/02: Ticket 60 - add dsrc to dsconf and dsidm

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

firstyear pushed a commit to branch master
in repository lib389.

commit be2de10af12475ed6ad086a2819bd73866757e68
Author: William Brown <firstyear@redhat.com>
Date: Tue May 30 15:42:13 2017 +1000

Ticket 60 - add dsrc to dsconf and dsidm

Bug Description: Previously we had to always specifiy a backend to
the dsidm commands. As well, many options like TLS auth were not accesible.

Fix Description: adding a ~/.dsrc allows an admin not only to
shorthand connect to instances IE:

dsidm -v -b dc=example,dc=com -D 'cn=Directory Manager' ldap://localhost ....

becomes:

dsidm -v localhost ....

It allows us to specify *multiple instances* (ie can swap identities easily)
and allows us to use SASL options like TLS external from our CLI tools.

https://pagure.io/lib389/issue/60

Author: wibrown

Review by: spichugi (Thanks!)
---
cli/dsconf | 31 +++++---
cli/dscreate | 2 +-
cli/dsctl | 2 +-
cli/dsidm | 28 ++++---
lib389/cli_base/__init__.py | 19 +++--
lib389/cli_base/dsrc.py | 118 +++++++++++++++++++++++++++
lib389/tests/cli/dsrc_test.py | 180 ++++++++++++++++++++++++++++++++++++++++++
7 files changed, 352 insertions(+), 28 deletions(-)

diff --git a/cli/dsconf b/cli/dsconf
index 7b60a10..e6cb95d 100755
--- a/cli/dsconf
+++ b/cli/dsconf
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3

# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016 Red Hat, Inc.
@@ -24,13 +24,15 @@ from lib389.cli_conf import schema as cli_schema
from lib389.cli_conf import lint as cli_lint
from lib389.cli_base import disconnect_instance, connect_instance

+from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat
+
log = logging.getLogger("dsconf")

if __name__ == '__main__':

defbase = ldap.get_option(ldap.OPT_DEFBASE)

- parser = argparse.ArgumentParser()
+ parser = argparse.ArgumentParser(allow_abbrev=True)
# Build the base ldap options, this keeps in unified.

# Can we get default options for these from .rc file?
@@ -44,7 +46,11 @@ if __name__ == '__main__':
)
parser.add_argument('-D', '--binddn',
help="The account to bind as for executing operations",
- default=DN_DM,
+ default=None,
+ )
+ parser.add_argument('-b', '--basedn',
+ help="Basedn (root naming context) of the instance to manage",
+ default=None
)
parser.add_argument('-Z', '--starttls',
help="Connect with StartTLS",
@@ -71,26 +77,31 @@ if __name__ == '__main__':
# I gave attribution. -- wibrown
log.debug("Inspired by works of: ITS, The University of Adelaide")

- log.debug("Called with: %s", args)
+ # Now that we have our args, see how they relate with our instance.
+ dsrc_inst = dsrc_to_ldap("~/.dsrc", args.instance, log.getChild('dsrc'))
+
+ # Now combine this with our arguments
+
+ dsrc_inst = dsrc_arg_concat(args, dsrc_inst)
+
+ log.debug("Called with: %s" % args)
+ log.debug("Instance details: %s" % dsrc_inst)

# Assert we have a resources to work on.
if not hasattr(args, 'func'):
log.error("No resource provided to act upon")
- log.error("USAGE: dsadm [options] <resource> <action> [action options]")
+ log.error("USAGE: dsconf [options] <resource> <action> [action options]")
sys.exit(1)

- # TODO: Parse instance to a url in some cases.
- ldapurl = args.instance
-
# Connect
# We don't need a basedn, because the config objects derive it properly
inst = None
if args.verbose:
- inst = connect_instance(ldapurl=ldapurl, binddn=args.binddn, verbose=args.verbose, starttls=args.starttls)
+ inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose)
args.func(inst, None, log, args)
else:
try:
- inst = connect_instance(ldapurl=ldapurl, binddn=args.binddn, verbose=args.verbose, starttls=args.starttls)
+ inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose)
args.func(inst, None, log, args)
except Exception as e:
log.debug(e, exc_info=True)
diff --git a/cli/dscreate b/cli/dscreate
index ffcb114..1b1ef7a 100755
--- a/cli/dscreate
+++ b/cli/dscreate
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3

# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016 Red Hat, Inc.
diff --git a/cli/dsctl b/cli/dsctl
index 6c38bce..bfaba4a 100755
--- a/cli/dsctl
+++ b/cli/dsctl
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3

# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016 Red Hat, Inc.
diff --git a/cli/dsidm b/cli/dsidm
index 1c62acb..996b71e 100755
--- a/cli/dsidm
+++ b/cli/dsidm
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3

# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016, William Brown <william at blackhats.net.au>
@@ -26,13 +26,15 @@ from lib389.cli_idm import user as cli_user

from lib389.cli_base import connect_instance, disconnect_instance

+from lib389.cli_base.dsrc import dsrc_to_ldap, dsrc_arg_concat
+
log = logging.getLogger("dsidm")

if __name__ == '__main__':

defbase = ldap.get_option(ldap.OPT_DEFBASE)

- parser = argparse.ArgumentParser()
+ parser = argparse.ArgumentParser(allow_abbrev=True)
# First, add the LDAP options

parser.add_argument('instance',
@@ -40,7 +42,7 @@ if __name__ == '__main__':
)
parser.add_argument('-b', '--basedn',
help="Basedn (root naming context) of the instance to manage",
- default=defbase
+ default=None
)
parser.add_argument('-v', '--verbose',
help="Display verbose operation tracing during command execution",
@@ -48,7 +50,7 @@ if __name__ == '__main__':
)
parser.add_argument('-D', '--binddn',
help="The account to bind as for executing operations",
- default=DN_DM,
+ default=None,
)
parser.add_argument('-Z', '--starttls',
help="Connect with StartTLS",
@@ -79,9 +81,17 @@ if __name__ == '__main__':
# I gave attribution. -- wibrown
log.debug("Inspired by works of: ITS, The University of Adelaide")

+ # Now that we have our args, see how they relate with our instance.
+ dsrc_inst = dsrc_to_ldap("~/.dsrc", args.instance, log.getChild('dsrc'))
+
+ # Now combine this with our arguments
+
+ dsrc_inst = dsrc_arg_concat(args, dsrc_inst)
+
log.debug("Called with: %s", args)
+ log.debug("Instance details: %s" % dsrc_inst)

- if args.basedn is None:
+ if dsrc_inst['basedn'] is None:
log.error("Must provide a basedn!")

ldapurl = args.instance
@@ -89,12 +99,12 @@ if __name__ == '__main__':
# Connect
inst = None
if args.verbose:
- inst = connect_instance(ldapurl=ldapurl, binddn=args.binddn, verbose=args.verbose, starttls=args.starttls)
- args.func(inst, args.basedn, log, args)
+ inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose)
+ args.func(inst, dsrc_inst['basedn'], log, args)
else:
try:
- inst = connect_instance(ldapurl=ldapurl, binddn=args.binddn, verbose=args.verbose, starttls=args.starttls)
- args.func(inst, args.basedn, log, args)
+ inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose)
+ args.func(inst, dsrc_inst['basedn'], log, args)
except Exception as e:
log.debug(e, exc_info=True)
log.error("Error: %s" % e.message)
diff --git a/lib389/cli_base/__init__.py b/lib389/cli_base/__init__.py
index 3ede30f..3c34366 100644
--- a/lib389/cli_base/__init__.py
+++ b/lib389/cli_base/__init__.py
@@ -68,19 +68,24 @@ def _warn(data, msg=None):
return data

# We'll need another of these that does a "connect via instance name?"
-def connect_instance(ldapurl, binddn, verbose, starttls):
+def connect_instance(dsrc_inst, verbose):
dsargs = {
- SER_LDAP_URL: ldapurl,
- SER_ROOT_DN: binddn
+ SER_LDAP_URL: dsrc_inst['uri'],
+ SER_ROOT_DN: dsrc_inst['binddn'],
}
ds = DirSrv(verbose=verbose)
ds.allocate(dsargs)
- if not ds.can_autobind() and binddn is not None:
- dsargs[SER_ROOT_PW] = getpass("Enter password for %s on %s : " % (binddn, ldapurl))
- elif binddn is None:
+ if not ds.can_autobind() and dsrc_inst['binddn'] is not None:
+ dsargs[SER_ROOT_PW] = getpass("Enter password for %s on %s : " % (dsrc_inst['binddn'], dsrc_inst['uri']))
+ elif dsrc_inst['binddn'] is None:
raise Exception("Must provide a binddn to connect with")
ds.allocate(dsargs)
- ds.open(starttls=starttls, connOnly=True)
+ ds.open(saslmethod=dsrc_inst['saslmech'],
+ certdir=dsrc_inst['tls_cacertdir'],
+ reqcert=dsrc_inst['tls_reqcert'],
+ usercert=dsrc_inst['tls_cert'],
+ userkey=dsrc_inst['tls_key'],
+ starttls=dsrc_inst['starttls'], connOnly=True)
return ds

def disconnect_instance(inst):
diff --git a/lib389/cli_base/dsrc.py b/lib389/cli_base/dsrc.py
new file mode 100644
index 0000000..4a2d0ac
--- /dev/null
+++ b/lib389/cli_base/dsrc.py
@@ -0,0 +1,118 @@
+# --- 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 sys
+import os
+import ldap
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+
+if MAJOR >= 3:
+ import configparser
+
+def dsrc_arg_concat(args, dsrc_inst):
+ """
+ Given a set of argparse args containing:
+
+ instance
+ binddn
+ starttls
+
+ and a dsrc_inst (as from dsrc_to_ldap)
+
+ Using this, overlay the settings together to form an instance
+
+ Generally, if dsrc_inst is None, we make a new instance.
+
+ If dsrc_inst is populated we overlay args as needed on top.
+ """
+ if dsrc_inst is None:
+ new_dsrc_inst = {
+ 'uri': args.instance,
+ 'basedn': args.basedn,
+ 'binddn': args.binddn,
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': args.starttls,
+ }
+ # Make new
+ return new_dsrc_inst
+ # overlay things.
+ if args.basedn is not None:
+ dsrc_inst['basedn'] = args.basedn
+ if args.binddn is not None:
+ dsrc_inst['binddn'] = args.binddn
+ if args.starttls is True:
+ dsrc_inst['starttls'] = True
+ return dsrc_inst
+
+def dsrc_to_ldap(path, instance_name, log):
+ """
+ Given a path to a file, return the required details for an instance.
+
+ The file should be an ini file, and instance should identify a section.
+
+ The ini fileshould have the content:
+
+ [instance]
+ uri = ldaps://hostname:port
+ basedn = dc=example,dc=com
+ binddn = uid=user,....
+ saslmech = [EXTERNAL|PLAIN]
+ tls_cacertdir = /path/to/cadir
+ tls_cert = /path/to/user.crt
+ tls_key = /path/to/user.key
+ tls_reqcert = [never, hard, allow]
+ starttls = [true, false]
+ """
+ path = os.path.expanduser(path)
+ log.debug("dsrc path: %s" % path)
+ # First read our config
+ # No such file?
+ config = configparser.SafeConfigParser()
+ config.read([path])
+
+ log.debug("dsrc instances: %s" % config.sections())
+
+ # Does our section exist?
+ if not config.has_section(instance_name):
+ # If not, return none.
+ log.debug("dsrc no such section %s" % instance_name)
+ return None
+
+ dsrc_inst = {}
+ # Read all the values
+ dsrc_inst['uri'] = config.get(instance_name, 'uri')
+ dsrc_inst['basedn'] = config.get(instance_name, 'basedn', fallback=None)
+ dsrc_inst['binddn'] = config.get(instance_name, 'binddn', fallback=None)
+ dsrc_inst['saslmech'] = config.get(instance_name, 'saslmech', fallback=None)
+ if dsrc_inst['saslmech'] is not None and dsrc_inst['saslmech'] not in ['EXTERNAL', 'PLAIN']:
+ raise Exception("~/.dsrc [%s] saslmech must be one of EXTERNAL or PLAIN" % instance_name)
+
+ dsrc_inst['tls_cacertdir'] = config.get(instance_name, 'tls_cacertdir', fallback=None)
+ dsrc_inst['tls_cert'] = config.get(instance_name, 'tls_cert', fallback=None)
+ dsrc_inst['tls_key'] = config.get(instance_name, 'tls_key', fallback=None)
+ dsrc_inst['tls_reqcert'] = config.get(instance_name, 'tls_reqcert', fallback='hard')
+ if dsrc_inst['tls_reqcert'] not in ['never', 'allow', 'hard']:
+ raise Exception("dsrc tls_reqcert value invalid. ~/.dsrc [%s] tls_reqcert should be one of never, allow or hard" % instance_name)
+ if dsrc_inst['tls_reqcert'] == 'never':
+ dsrc_inst['tls_reqcert'] = ldap.OPT_X_TLS_NEVER
+ elif dsrc_inst['tls_reqcert'] == 'allow':
+ dsrc_inst['tls_reqcert'] = ldap.OPT_X_TLS_ALLOW
+ else:
+ dsrc_inst['tls_reqcert'] = ldap.OPT_X_TLS_HARD
+ dsrc_inst['starttls'] = config.getboolean(instance_name, 'starttls', fallback=False)
+
+ # Return the dict.
+ log.debug("dsrc completed with %s" % dsrc_inst)
+ return dsrc_inst
+
+
diff --git a/lib389/tests/cli/dsrc_test.py b/lib389/tests/cli/dsrc_test.py
new file mode 100644
index 0000000..6e2155c
--- /dev/null
+++ b/lib389/tests/cli/dsrc_test.py
@@ -0,0 +1,180 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+import pytest
+import logging
+import ldap
+import sys
+
+from lib389.cli_base.dsrc import dsrc_to_ldap
+
+MAJOR, MINOR, _, _, _ = sys.version_info
+if MAJOR >= 3:
+ import configparser
+
+log = logging.getLogger(__name__)
+
+def write_inf(content):
+ with open('/tmp/dsrc_test.inf', 'w') as f:
+ f.write(content)
+
+@pytest.mark.skipif(sys.version_info < (3,0), reason="requires python3")
+def test_dsrc_single_section():
+ # Write out inf.
+ write_inf("""
+[localhost]
+ """)
+ # Parse it and assert the content
+ with pytest.raises(configparser.NoOptionError):
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i == {
+ 'uri': 'ldaps://localhost:636',
+ 'basedn': None,
+ 'binddn': None,
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': False}
+ )
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+basedn = dc=example,dc=com
+binddn = cn=Directory Manager
+starttls = true
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i == {
+ 'uri': 'ldaps://localhost:636',
+ 'basedn': 'dc=example,dc=com',
+ 'binddn': 'cn=Directory Manager',
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': True}
+ )
+
+@pytest.mark.skipif(sys.version_info < (3,0), reason="requires python3")
+def test_dsrc_two_section():
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+
+[localhost2]
+uri = ldaps://localhost:6362
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i == {
+ 'uri': 'ldaps://localhost:636',
+ 'basedn': None,
+ 'binddn': None,
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': False}
+ )
+
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost2', log)
+ assert(i == {
+ 'uri': 'ldaps://localhost:6362',
+ 'basedn': None,
+ 'binddn': None,
+ 'saslmech': None,
+ 'tls_cacertdir': None,
+ 'tls_cert': None,
+ 'tls_key': None,
+ 'tls_reqcert': ldap.OPT_X_TLS_HARD,
+ 'starttls': False}
+ )
+
+ # Section doesn't exist
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost3', log)
+ assert(i is None)
+
+@pytest.mark.skipif(sys.version_info < (3,0), reason="requires python3")
+def test_dsrc_reqcert():
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+tls_reqcert = invalid
+ """)
+ # Parse it and assert the content
+ with pytest.raises(Exception):
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+tls_reqcert = hard
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['tls_reqcert'] == ldap.OPT_X_TLS_HARD)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+tls_reqcert = allow
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['tls_reqcert'] == ldap.OPT_X_TLS_ALLOW)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+tls_reqcert = never
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['tls_reqcert'] == ldap.OPT_X_TLS_NEVER)
+
+@pytest.mark.skipif(sys.version_info < (3,0), reason="requires python3")
+def test_dsrc_saslmech():
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+saslmech = INVALID_MECH
+ """)
+ # Parse it and assert the content
+ with pytest.raises(Exception):
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+saslmech = EXTERNAL
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['saslmech'] == 'EXTERNAL')
+
+ write_inf("""
+[localhost]
+uri = ldaps://localhost:636
+saslmech = PLAIN
+ """)
+ # Parse it and assert the content
+ i = dsrc_to_ldap('/tmp/dsrc_test.inf', 'localhost', log)
+ assert(i['saslmech'] == 'PLAIN')

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