From c7ad83ae72b3447dfd13cf1a30a7e7fe0b6d7be6 Mon Sep 17 00:00:00 2001
From: William Brown <firstyear@redhat.com>
Date: Tue, 31 Jan 2017 13:05:10 +1000
Subject: [PATCH] Ticket 49101 - Python 2 generate example entries

Bug Description:  In order to move to the new python installer, we need feature
parity with the existing perl installer. Currently, we are missing one feature,
which is sample entry creation. We need this for our test cases to run.

However, we also don't want to be stuck on the current set of sample entries.
They suck pretty hard, and the acis are just plain terrible. Yet our tests and
customers may depend on them.

Fix Description:  This contains multiple parts to solve the problem.

First, we add a set of identity management objects for groups, ous, users,
services accounts and more. This will greatly simplify our test code, and what
we can create.

Next, we add a set of managers that can apply version configs or sample entries.

We then wire in the capability for instance.create() to use the python installer,
be it via explicit request or environment variable (PYINSTALL) being set.

This way, we can now use the python installer experimentally during tests. A number
of my tests show this already passes.

The great benefit of using python to create the sample entries and configs, is
that they are programatic, versioned, can be unit tested, refactored, they are
portable, no more text file manipulation. We can also use the same code for *upgrade*
as we use for installs, making our tools and proceedures more consistent and
reliable.

A key point of this is that we will be able to install a 1.3.7 server, but with
1.3.6 defaults, or a 1.4.x server with 1.3.7 settings etc. New installs can be
kept in sync with older replicas. Once they are upgraded, we can then do proper
upgrades with the same code that does installs.

This also paves the way for us to add basic user and object management tools for
those unwilling to go the whole way into an IPA install.

https://fedorahosted.org/389/ticket/49101

Author: wibrown

Review by: ???
---
 examples/ds-setup.inf                              |   3 +-
 lib389/__init__.py                                 |  64 ++++++++++++-
 lib389/_constants.py                               |   2 +
 lib389/_mapped_object.py                           |   7 ++
 lib389/backend.py                                  |  16 +++-
 lib389/cli_base/__init__.py                        |   2 -
 lib389/configurations/__init__.py                  |  25 +++++
 lib389/configurations/config.py                    |  44 +++++++++
 lib389/configurations/config_001003006.py          | 103 +++++++++++++++++++++
 lib389/configurations/sample.py                    |  19 ++++
 lib389/idm/__init__.py                             |   0
 lib389/idm/domain.py                               |  21 +++++
 lib389/idm/group.py                                |  84 +++++++++++++++++
 lib389/idm/organisationalunit.py                   |  38 ++++++++
 lib389/idm/posixgroup.py                           |  49 ++++++++++
 lib389/idm/services.py                             |  36 +++++++
 lib389/idm/user.py                                 |  63 +++++++++++++
 lib389/instance/options.py                         |   5 +-
 lib389/instance/setup.py                           |   9 +-
 lib389/tests/configurations/__init__.py            |   0
 .../tests/configurations/config_001003006_test.py  |  98 ++++++++++++++++++++
 lib389/tests/idm/__init__.py                       |   0
 lib389/tests/idm/services.py                       |  55 +++++++++++
 lib389/tests/idm/users_and_groups.py               | 101 ++++++++++++++++++++
 lib389/tests/instance/setup_test.py                |   4 -
 25 files changed, 828 insertions(+), 20 deletions(-)
 create mode 100644 lib389/configurations/__init__.py
 create mode 100644 lib389/configurations/config.py
 create mode 100644 lib389/configurations/config_001003006.py
 create mode 100644 lib389/configurations/sample.py
 create mode 100644 lib389/idm/__init__.py
 create mode 100644 lib389/idm/domain.py
 create mode 100644 lib389/idm/group.py
 create mode 100644 lib389/idm/organisationalunit.py
 create mode 100644 lib389/idm/posixgroup.py
 create mode 100644 lib389/idm/services.py
 create mode 100644 lib389/idm/user.py
 create mode 100644 lib389/tests/configurations/__init__.py
 create mode 100644 lib389/tests/configurations/config_001003006_test.py
 create mode 100644 lib389/tests/idm/__init__.py
 create mode 100644 lib389/tests/idm/services.py
 create mode 100644 lib389/tests/idm/users_and_groups.py

diff --git a/examples/ds-setup.inf b/examples/ds-setup.inf
index 3da0a51..1f1778b 100644
--- a/examples/ds-setup.inf
+++ b/examples/ds-setup.inf
@@ -29,12 +29,11 @@ secure_port=636
 root_dn=cn=Directory Manager
 root_password=password
 prefix=/opt/dirsrv
-defaults=latest
 
 [backend-userRoot]
 suffix=dc=example,dc=com
 ; this is controlled by slapd.InstallLdifFile == none, suggest or path in setup-ds.pl
-sample_entries=yes
+sample_entries=999999999
 
 
 
diff --git a/lib389/__init__.py b/lib389/__init__.py
index 61a39a4..e1ade9c 100644
--- a/lib389/__init__.py
+++ b/lib389/__init__.py
@@ -880,7 +880,7 @@ class DirSrv(SimpleLDAPObject, object):
             # This may conflict in some tests, we may need to use /etc/host
             # aliases or we may need to use server id
             self.krb5_realm.create_principal(principal='ldap/%s' % self.host)
-            ktab = '%s/etc/dirsrv/slapd-%s/ldap.keytab' % (self.prefix, self.serverid)
+            ktab = '%s/ldap.keytab' % (self.ds_paths.config_dir)
             self.krb5_realm.create_keytab(principal='ldap/%s' % self.host, keytab=ktab)
             with open('%s/dirsrv-%s' % (self.ds_paths.initconfig_dir, self.serverid), 'a') as sfile:
                 sfile.write("\nKRB5_KTNAME=%s/etc/dirsrv/slapd-%s/"
@@ -890,7 +890,57 @@ class DirSrv(SimpleLDAPObject, object):
 
             # Restart the instance
 
-    def create(self):
+    def _createPythonDirsrv(self, version):
+        """
+        Create a new dirsrv instance based on the new python installer, rather
+        than setup-ds.pl
+
+        version represents the config default and sample entry version to use.
+        """
+        from lib389.instance.setup import SetupDs
+        from lib389.instance.options import General2Base, Slapd2Base
+        # Import the new setup ds library.
+        sds = SetupDs(verbose=self.verbose, dryrun=False, log=self.log)
+        # Configure the options.
+        general_options = General2Base(self.log)
+        general_options.set('strict_host_checking', False)
+        general_options.verify()
+        general = general_options.collect()
+
+        slapd_options = Slapd2Base(self.log)
+        slapd_options.set('instance_name', self.serverid)
+        slapd_options.set('port', self.port)
+        slapd_options.set('secure_port', self.sslport)
+        slapd_options.set('root_password', self.bindpw)
+        slapd_options.set('root_dn', self.binddn)
+        slapd_options.set('defaults', version)
+
+        slapd_options.verify()
+        slapd = slapd_options.collect()
+
+        # In order to work by "default" for tests, we need to create a backend.
+        userroot = {
+            'cn': 'userRoot',
+            'nsslapd-suffix': self.creation_suffix,
+            BACKEND_SAMPLE_ENTRIES: version,
+        }
+        backends = [userroot,]
+
+        # Go!
+        sds.create_from_args(general, slapd, backends, None)
+        if self.realm is not None:
+            # This may conflict in some tests, we may need to use /etc/host
+            # aliases or we may need to use server id
+            self.krb5_realm.create_principal(principal='ldap/%s' % self.host)
+            ktab = '%s/ldap.keytab' % (self.ds_paths.config_dir)
+            self.krb5_realm.create_keytab(principal='ldap/%s' % self.host, keytab=ktab)
+            with open('%s/dirsrv-%s' % (self.ds_paths.initconfig_dir, self.serverid), 'a') as sfile:
+                sfile.write("\nKRB5_KTNAME=%s/etc/dirsrv/slapd-%s/"
+                            "ldap.keytab\nexport KRB5_KTNAME\n" %
+                            (self.prefix, self.serverid))
+            self.restart()
+
+    def create(self, pyinstall=False, version=INSTALL_LATEST_CONFIG):
         """
             Creates an instance with the parameters sets in dirsrv
             The state change from  DIRSRV_STATE_ALLOCATED ->
@@ -918,8 +968,16 @@ class DirSrv(SimpleLDAPObject, object):
             raise ValueError("SER_SERVERID_PROP is missing, " +
                              "it is required to create an instance")
 
+        # Check how we want to be installed.
+        env_pyinstall = False
+        if os.getenv('PYINSTALL', False) is not False:
+            env_pyinstall = True
         # Time to create the instance and retrieve the effective sroot
-        self._createDirsrv()
+
+        if (env_pyinstall or pyinstall):
+            self._createPythonDirsrv(version)
+        else:
+            self._createDirsrv()
 
         # Retrieve sroot from the sys/priv config file
         props = self.list()
diff --git a/lib389/_constants.py b/lib389/_constants.py
index 0e7c742..96e5517 100644
--- a/lib389/_constants.py
+++ b/lib389/_constants.py
@@ -15,6 +15,8 @@ from lib389.properties import *
     LEAF_TYPE
 ) = list(range(3))
 
+INSTALL_LATEST_CONFIG = '999999999'
+
 REPLICAROLE_MASTER = "master"
 REPLICAROLE_HUB = "hub"
 REPLICAROLE_CONSUMER = "consumer"
diff --git a/lib389/_mapped_object.py b/lib389/_mapped_object.py
index c05282a..7d3afa1 100644
--- a/lib389/_mapped_object.py
+++ b/lib389/_mapped_object.py
@@ -265,6 +265,13 @@ class DSLdapObject(DSLogging):
     def set_values(self, values, action=ldap.MOD_REPLACE):
         pass
 
+    # If the account can be bound to, this will attempt to do so. We don't check
+    # for exceptions, just pass them back!
+    def bind(self, password=None):
+        conn = self._instance.openConnection()
+        conn.simple_bind_s(self.dn, password)
+        return conn
+
     def delete(self):
         """
         Deletes the object defined by self._dn.
diff --git a/lib389/backend.py b/lib389/backend.py
index ef6b4ad..aa540e5 100644
--- a/lib389/backend.py
+++ b/lib389/backend.py
@@ -21,6 +21,8 @@ from lib389.exceptions import NoSuchEntryError, InvalidArgumentError
 # We need to be a factor to the backend monitor
 from lib389.monitor import MonitorBackend
 
+# This is for sample entry creation.
+from lib389.configurations import get_sample_entries
 
 class BackendLegacy(object):
     proxied_methods = 'search_s getEntry'.split()
@@ -392,8 +394,14 @@ class Backend(DSLdapObject):
         # Check if a mapping tree for this suffix exists.
         self._mts = MappingTrees(self._instance)
 
-    def create_sample_entries(self):
-        self._log.debug('Creating sample entries ....')
+    def create_sample_entries(self, version):
+        self._log.debug('Creating sample entries at version %s....' % version)
+        # Grab the correct sample entry config
+        centries = get_sample_entries(version)
+        # apply it.
+        basedn = self.get_attr_val('nsslapd-suffix')
+        cent = centries(self._instance, basedn)
+        cent.apply()
 
     def _validate(self, rdn, properties, basedn):
         # We always need to call the super validate first. This way we can
@@ -443,8 +451,8 @@ class Backend(DSLdapObject):
             'nsslapd-state' : 'backend',
             'nsslapd-backend' : self._nprops_stash['cn'] ,
         })
-        if sample_entries is True:
-            self.create_sample_entries()
+        if sample_entries is not False:
+            self.create_sample_entries(sample_entries)
         return self
 
     def delete(self):
diff --git a/lib389/cli_base/__init__.py b/lib389/cli_base/__init__.py
index a46f991..a3e59ac 100644
--- a/lib389/cli_base/__init__.py
+++ b/lib389/cli_base/__init__.py
@@ -160,5 +160,3 @@ class FakeArgs(object):
     def __init__(self):
         pass
 
-
-# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
diff --git a/lib389/configurations/__init__.py b/lib389/configurations/__init__.py
new file mode 100644
index 0000000..c7b9067
--- /dev/null
+++ b/lib389/configurations/__init__.py
@@ -0,0 +1,25 @@
+# --- 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 ---
+
+from lib389._constants import INSTALL_LATEST_CONFIG
+from .config_001003006 import c001003006, c001003006_sample_entries
+
+def get_config(version):
+    if (version == INSTALL_LATEST_CONFIG):
+        return c001003006
+    if (version == '001003006'):
+        return c001003006
+    raise Exception('version %s no match' % version)
+
+def get_sample_entries(version):
+    if (version == INSTALL_LATEST_CONFIG):
+        return c001003006_sample_entries
+    if (version == '001003006'):
+        return c001003006_sample_entries
+    raise Exception('version %s no match' % version)
+
diff --git a/lib389/configurations/config.py b/lib389/configurations/config.py
new file mode 100644
index 0000000..ba289bd
--- /dev/null
+++ b/lib389/configurations/config.py
@@ -0,0 +1,44 @@
+# --- 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 ---
+
+# These are the operation runners for configuring a server to look like a certain
+# version.
+
+# Generally these are one way, upgrade only.
+
+class baseconfig(object):
+    def __init__(self, instance):
+        self._instance = instance
+
+    def apply_config(self, install=False, upgrade=False, interactive=False):
+        # Go through the list of operations
+        # Can we assert the types?
+        for op in self._operations:
+            op.apply(install, upgrade, interactive)
+
+# This just serves as a base
+class configoperation(object):
+    def __init__(self, instance):
+        self._instance = instance
+        self.install = True
+        self.upgrade = True
+        self.description = None
+
+    def apply(self, install, upgrade, interactive):
+        # How do we want to handle interactivity?
+        if not ((install and self.install) or (upgrade and self.upgrade)):
+            instance.debug()
+            return False
+        if interactive:
+            raise Exception('Interaction not yet supported')
+        self._apply()
+
+    def _apply(self):
+        # The consumer must over-ride this.
+        raise Exception('Not implemented!')
+
diff --git a/lib389/configurations/config_001003006.py b/lib389/configurations/config_001003006.py
new file mode 100644
index 0000000..465d762
--- /dev/null
+++ b/lib389/configurations/config_001003006.py
@@ -0,0 +1,103 @@
+# --- 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 ---
+
+from ldap import dn
+
+from .config import baseconfig, configoperation
+from .sample import sampleentries
+
+from lib389.idm.domain import Domain
+from lib389.idm.organisationalunit import OrganisationalUnits
+from lib389.idm.group import UniqueGroups, UniqueGroup
+
+class c001003006_sample_entries(sampleentries):
+    def __init__(self, instance, basedn):
+        super(c001003006_sample_entries, self).__init__(instance, basedn)
+        self.description = """Apply sample entries matching the 1.3.6 sample data and access controls."""
+
+    # All the checks are done, apply them.
+    def _apply(self):
+        # Create the base domain object
+        domain = Domain(self._instance)
+        domain._dn = self._basedn
+        # Explode the dn to get the first bit.
+        avas = dn.str2dn(self._basedn)
+        dc_ava = avas[0][0][1]
+
+        domain.create(properties={
+            'dc': dc_ava,
+            'description': self._basedn,
+            'aci' : '(targetattr ="*")(version 3.0;acl "Directory Administrators Group";allow (all) (groupdn = "ldap:///cn=Directory Administrators, %{BASEDN}");)'.format(BASEDN=self._basedn)
+            })
+        # Create the OUs
+        ous = OrganisationalUnits(self._instance, self._basedn)
+        ous.create(properties = {
+            'ou': 'Groups',
+        })
+        ous.create(properties = {
+            'ou': 'People',
+            'aci' : [
+                '(targetattr ="userpassword || telephonenumber || facsimiletelephonenumber")(version 3.0;acl "Allow self entry modification";allow (write)(userdn = "ldap:///self");)',
+                '(targetattr !="cn || sn || uid")(targetfilter ="(ou=Accounting)")(version 3.0;acl "Accounting Managers Group Permissions";allow (write)(groupdn = "ldap:///cn=Accounting Managers,ou=groups,%{BASEDN}");)'.format(BASEDN=self._basedn),
+                '(targetattr !="cn || sn || uid")(targetfilter ="(ou=Human Resources)")(version 3.0;acl "HR Group Permissions";allow (write)(groupdn = "ldap:///cn=HR Managers,ou=groups,%{BASEDN}");)'.format(BASEDN=self._basedn),
+                '(targetattr !="cn ||sn || uid")(targetfilter ="(ou=Product Testing)")(version 3.0;acl "QA Group Permissions";allow (write)(groupdn = "ldap:///cn=QA Managers,ou=groups,%{BASEDN}");)'.format(BASEDN=self._basedn),
+                '(targetattr !="cn || sn || uid")(targetfilter ="(ou=Product Development)")(version 3.0;acl "Engineering Group Permissions";allow (write)(groupdn = "ldap:///cn=PD Managers,ou=groups,%{BASEDN}");)'.format(BASEDN=self._basedn),
+            ]
+        })
+        ous.create(properties = {
+            'ou': 'Special Users',
+            'description' : 'Special Administrative Accounts',
+        })
+        # Create the groups.
+        ugs = UniqueGroups(self._instance, self._basedn)
+        ugs.create(properties = {
+            'cn': 'Accounting Managers',
+            'description': 'People who can manage accounting entries',
+            'ou': 'groups',
+            'uniqueMember' : self._instance.binddn,
+        })
+        ugs.create(properties = {
+            'cn': 'HR Managers',
+            'description': 'People who can manage HR entries',
+            'ou': 'groups',
+            'uniqueMember' : self._instance.binddn,
+        })
+        ugs.create(properties = {
+            'cn': 'QA Managers',
+            'description': 'People who can manage QA entries',
+            'ou': 'groups',
+            'uniqueMember' : self._instance.binddn,
+        })
+        ugs.create(properties = {
+            'cn': 'PD Managers',
+            'description': 'People who can manage engineer entries',
+            'ou': 'groups',
+            'uniqueMember' : self._instance.binddn,
+        })
+        # Create the directory Admin group.
+        # We can't use the group factory here, as we need a custom DN override.
+        da_ug = UniqueGroup(self._instance)
+        da_ug._dn = 'cn=Directory Administrators,%s' % self._basedn
+        da_ug.create(properties={
+            'cn': 'Directory Administrators',
+            'uniqueMember' : self._instance.binddn,
+        })
+        # DONE!
+
+### Operations to be filled in soon!
+
+class c001003006(baseconfig):
+    def __init__(self, instance):
+        super(c001003006, self).__init__(instance)
+        self._operations = [
+            # Create our sample entries.
+            # op001003006_sample_entries(self._instance),
+        ]
+
+
+
diff --git a/lib389/configurations/sample.py b/lib389/configurations/sample.py
new file mode 100644
index 0000000..d0fbd16
--- /dev/null
+++ b/lib389/configurations/sample.py
@@ -0,0 +1,19 @@
+# --- 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 ---
+
+class sampleentries(object):
+    def __init__(self, instance, basedn):
+        self._instance = instance
+        self._basedn = basedn
+        self.description = None
+
+    def apply(self):
+        self._apply()
+
+    def _apply(self):
+        raise Exception('Not implemented')
diff --git a/lib389/idm/__init__.py b/lib389/idm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib389/idm/domain.py b/lib389/idm/domain.py
new file mode 100644
index 0000000..480c1c8
--- /dev/null
+++ b/lib389/idm/domain.py
@@ -0,0 +1,21 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, 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
+
+class Domain(DSLdapObject):
+    def __init__(self, instance, dn=None, batch=False):
+        super(Domain, self).__init__(instance, dn, batch)
+        self._rdn_attribute = 'dc'
+        self._must_attributes = ['dc']
+        self._create_objectclasses = [
+            'top',
+            'domain',
+        ]
+        self._protected = True
+
diff --git a/lib389/idm/group.py b/lib389/idm/group.py
new file mode 100644
index 0000000..e4c102b
--- /dev/null
+++ b/lib389/idm/group.py
@@ -0,0 +1,84 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, 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
+
+MUST_ATTRIBUTES = [
+    'cn',
+]
+RDN = 'cn'
+
+class Group(DSLdapObject):
+    def __init__(self, instance, dn=None, batch=False):
+        super(Group, self).__init__(instance, dn, batch)
+        self._rdn_attribute = RDN
+        # Can I generate these from schema?
+        self._must_attributes = MUST_ATTRIBUTES
+        self._create_objectclasses = [
+            'top',
+            'groupOfNames',
+        ]
+        self._protected = False
+
+    def is_member(self, dn):
+        # Check if dn is a member
+        return self.present('member', dn)
+
+    def add_member(self, dn):
+        self.add('member', dn)
+
+    def remove_member(self, dn):
+        self.remove('member', dn)
+
+class Groups(DSLdapObjects):
+    def __init__(self, instance, basedn, batch=False):
+        super(Groups, self).__init__(instance, batch)
+        self._objectclasses = [
+            'groupOfNames',
+        ]
+        self._filterattrs = [RDN]
+        self._childobject = Group
+        self._basedn = 'ou=Groups,%s' % basedn
+
+class UniqueGroup(DSLdapObject):
+    # WARNING!!!
+    # Use group, not unique group!!!
+    def __init__(self, instance, dn=None, batch=False):
+        super(UniqueGroup, self).__init__(instance, dn, batch)
+        self._rdn_attribute = RDN
+        self._must_attributes = MUST_ATTRIBUTES
+        self._create_objectclasses = [
+            'top',
+            'groupOfUniqueNames',
+        ]
+        self._protected = False
+
+    def is_member(self, dn):
+        # Check if dn is a member
+        return self.present('uniquemember', dn)
+
+    def add_member(self, dn):
+        self.add('uniquemember', dn)
+
+    def remove_member(self, dn):
+        self.remove('uniquemember', dn)
+
+class UniqueGroups(DSLdapObjects):
+    # WARNING!!!
+    # Use group, not unique group!!!
+    def __init__(self, instance, basedn, batch=False):
+        super(UniqueGroups, self).__init__(instance, batch)
+        self._objectclasses = [
+            'groupOfUniqueNames',
+        ]
+        self._filterattrs = [RDN]
+        self._childobject = UniqueGroup
+        self._basedn = 'ou=Groups,%s' % basedn
+
+
+
diff --git a/lib389/idm/organisationalunit.py b/lib389/idm/organisationalunit.py
new file mode 100644
index 0000000..a3854c9
--- /dev/null
+++ b/lib389/idm/organisationalunit.py
@@ -0,0 +1,38 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, 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
+
+MUST_ATTRIBUTES = [
+    'ou',
+]
+RDN = 'ou'
+
+class OrganisationalUnit(DSLdapObject):
+    def __init__(self, instance, dn=None, batch=False):
+        super(OrganisationalUnit, self).__init__(instance, dn, batch)
+        self._rdn_attribute = RDN
+        # Can I generate these from schema?
+        self._must_attributes = MUST_ATTRIBUTES
+        self._create_objectclasses = [
+            'top',
+            'organizationalunit',
+        ]
+        self._protected = False
+
+class OrganisationalUnits(DSLdapObjects):
+    def __init__(self, instance, basedn, batch=False):
+        super(OrganisationalUnits, self).__init__(instance, batch)
+        self._objectclasses = [
+            'organizationalunit',
+        ]
+        self._filterattrs = [RDN]
+        self._childobject = OrganisationalUnit
+        self._basedn = basedn
+
+
diff --git a/lib389/idm/posixgroup.py b/lib389/idm/posixgroup.py
new file mode 100644
index 0000000..aebfb5f
--- /dev/null
+++ b/lib389/idm/posixgroup.py
@@ -0,0 +1,49 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, 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
+
+MUST_ATTRIBUTES = [
+    'cn',
+    'gidNumber',
+]
+RDN = 'cn'
+
+class PosixGroup(DSLdapObject):
+    def __init__(self, instance, dn=None, batch=False):
+        super(PosixGroup, self).__init__(instance, dn, batch)
+        self._rdn_attribute = RDN
+        # Can I generate these from schema?
+        self._must_attributes = MUST_ATTRIBUTES
+        self._create_objectclasses = [
+            'top',
+            'groupOfNames',
+            'posixGroup',
+        ]
+        self._protected = False
+
+    def check_member(self, dn):
+        return dn in self.get_attr_vals('member')
+
+    def add_member(self, dn):
+        # Assert the DN exists?
+        self.add('member', dn)
+
+
+class PosixGroups(DSLdapObjects):
+    def __init__(self, instance, basedn, batch=False):
+        super(PosixGroups, self).__init__(instance, batch)
+        self._objectclasses = [
+            'groupOfNames',
+            'posixGroup',
+        ]
+        self._filterattrs = [RDN]
+        self._childobject = PosixGroup
+        self._basedn = 'ou=Groups,%s' % basedn
+
+
diff --git a/lib389/idm/services.py b/lib389/idm/services.py
new file mode 100644
index 0000000..b59e345
--- /dev/null
+++ b/lib389/idm/services.py
@@ -0,0 +1,36 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, 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
+
+RDN = 'cn'
+MUST_ATTRIBUTES = [
+    'cn',
+]
+
+class ServiceAccount(DSLdapObject):
+    def __init__(self, instance, dn=None, batch=False):
+        super(ServiceAccount, self).__init__(instance, dn, batch)
+        self._rdn_attribute = RDN
+        self._must_attributes = MUST_ATTRIBUTES
+        self._create_objectclasses = [
+            'top',
+            'netscapeServer',
+        ]
+        self._protected = False
+
+class ServiceAccounts(DSLdapObjects):
+    def __init__(self, instance, basedn, batch=False):
+        super(ServiceAccounts, self).__init__(instance, batch)
+        self._objectclasses = [
+            'netscapeServer',
+        ]
+        self._filterattrs = [RDN]
+        self._childobject = ServiceAccount
+        self._basedn = 'ou=Services,%s' % basedn
+
diff --git a/lib389/idm/user.py b/lib389/idm/user.py
new file mode 100644
index 0000000..5b7e4be
--- /dev/null
+++ b/lib389/idm/user.py
@@ -0,0 +1,63 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2016, 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
+
+MUST_ATTRIBUTES = [
+    'uid',
+    'cn',
+    'sn',
+    'uidNumber',
+    'gidNumber',
+    'homeDirectory',
+]
+RDN = 'uid'
+
+class UserAccount(DSLdapObject):
+    def __init__(self, instance, dn=None, batch=False):
+        super(UserAccount, self).__init__(instance, dn, batch)
+        self._rdn_attribute = RDN
+        # Can I generate these from schema?
+        self._must_attributes = MUST_ATTRIBUTES
+        self._create_objectclasses = [
+            'top',
+            'account',
+            'posixaccount',
+            'inetOrgPerson',
+            'person',
+            'inetUser',
+            'organizationalPerson',
+            # This may not always work at sites?
+            # Can we get this into core?
+            # 'ldapPublicKey',
+        ]
+        self._protected = False
+
+    def _validate(self, rdn, properties, basedn):
+        if properties.has_key('ntUserDomainId') and 'ntUser' not in self._create_objectclasses:
+            self._create_objectclasses.append('ntUser')
+
+        return super(UserAccount, self)._validate(rdn, properties, basedn)
+
+    # Add a set password function....
+    # Can't I actually just set, and it will hash?
+
+class UserAccounts(DSLdapObjects):
+    def __init__(self, instance, basedn, batch=False):
+        super(UserAccounts, self).__init__(instance, batch)
+        self._objectclasses = [
+            'account',
+            'posixaccount',
+            'inetOrgPerson',
+            'person',
+            'organizationalPerson',
+        ]
+        self._filterattrs = [RDN]
+        self._childobject = UserAccount
+        self._basedn = 'ou=People,%s' % basedn
+
diff --git a/lib389/instance/options.py b/lib389/instance/options.py
index b1a5418..2d8d228 100644
--- a/lib389/instance/options.py
+++ b/lib389/instance/options.py
@@ -10,6 +10,7 @@ import socket
 import sys
 import os
 from lib389.paths import Paths
+from lib389._constants import INSTALL_LATEST_CONFIG
 
 MAJOR, MINOR, _, _, _ = sys.version_info
 
@@ -118,9 +119,9 @@ class General2Base(Options2):
         self._type['selinux'] = bool
         self._helptext['selinux'] = "Enable SELinux detection and integration. Normally, this should always be True, and will correctly detect when SELinux is not present."
 
-        self._options['defaults'] = '99999'
+        self._options['defaults'] = INSTALL_LATEST_CONFIG
         self._type['defaults'] = str
-        self._helptext['defaults'] = "Set the configuration defaults version. If set to 99999, always use the latest values available for the slapd section. This allows pinning default values in cn=config to specific Directory Server releases."
+        self._helptext['defaults'] = "Set the configuration defaults version. If set to %{LATEST}, always use the latest values available for the slapd section. This allows pinning default values in cn=config to specific Directory Server releases. This maps to our versions such as 1.3.5 -> 001003005 -> 1003005".format(LATEST=INSTALL_LATEST_CONFIG)
 
 
 #
diff --git a/lib389/instance/setup.py b/lib389/instance/setup.py
index c4bebd8..4505db0 100644
--- a/lib389/instance/setup.py
+++ b/lib389/instance/setup.py
@@ -136,7 +136,7 @@ class SetupDs(object):
                 # TODO: Add the other BACKEND_ types
                 be[BACKEND_NAME] = section.replace('backend-', '')
                 be[BACKEND_SUFFIX] = config.get(section, 'suffix')
-                be[BACKEND_SAMPLE_ENTRIES] = config.getboolean(section, 'sample_entries')
+                be[BACKEND_SAMPLE_ENTRIES] = config.get(section, 'sample_entries')
                 backends.append(be)
 
         if self.verbose:
@@ -263,8 +263,10 @@ class SetupDs(object):
 
         assert(slapd['port'] is not None)
         assert(socket_check_open('::1', slapd['port']) is False)
-        assert(slapd['secure_port'] is not None)
-        assert(socket_check_open('::1', slapd['secure_port']) is False)
+        ## This causes some problems in tests :(
+        # assert(slapd['secure_port'] is not None)
+        if slapd['secure_port'] is not None:
+            assert(socket_check_open('::1', slapd['secure_port']) is False)
         if self.verbose:
             self.log.info("PASSED: network avaliability checking")
 
@@ -430,3 +432,4 @@ class SetupDs(object):
         for path in ('backup_dir', 'cert_dir', 'config_dir', 'db_dir',
                      'ldif_dir', 'lock_dir', 'log_dir', 'run_dir'):
             print(path)
+
diff --git a/lib389/tests/configurations/__init__.py b/lib389/tests/configurations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib389/tests/configurations/config_001003006_test.py b/lib389/tests/configurations/config_001003006_test.py
new file mode 100644
index 0000000..8a7546c
--- /dev/null
+++ b/lib389/tests/configurations/config_001003006_test.py
@@ -0,0 +1,98 @@
+# --- 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 ldap
+
+import logging
+import sys
+import time
+
+import pytest
+
+from lib389 import DirSrv
+from lib389._constants import *
+from lib389.properties import *
+
+DEBUGGING = os.getenv('DEBUGGING', default=False)
+if DEBUGGING:
+    logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+    logging.getLogger(__name__).setLevel(logging.INFO)
+log = logging.getLogger(__name__)
+
+### WARNING:
+# We can't use topology here, as we need to force python install!
+
+REQUIRED_DNS = [
+    'dc=example,dc=com',
+    'ou=Groups,dc=example,dc=com',
+    'ou=People,dc=example,dc=com',
+    'ou=Special Users,dc=example,dc=com',
+    'cn=Accounting Managers,ou=Groups,dc=example,dc=com',
+    'cn=HR Managers,ou=Groups,dc=example,dc=com',
+    'cn=QA Managers,ou=Groups,dc=example,dc=com',
+    'cn=PD Managers,ou=Groups,dc=example,dc=com',
+    'cn=Directory Administrators,dc=example,dc=com',
+]
+
+class TopologyMain(object):
+    def __init__(self, standalones=None, masters=None,
+                 consumers=None, hubs=None):
+        if standalones:
+            if isinstance(standalones, dict):
+                self.ins = standalones
+            else:
+                self.standalone = standalones
+        if masters:
+            self.ms = masters
+        if consumers:
+            self.cs = consumers
+        if hubs:
+            self.hs = hubs
+
+@pytest.fixture(scope="module")
+def topology_st(request):
+    """Create DS standalone instance"""
+
+    if DEBUGGING:
+        standalone = DirSrv(verbose=True)
+    else:
+        standalone = DirSrv(verbose=False)
+    args_instance[SER_HOST] = HOST_STANDALONE1
+    args_instance[SER_PORT] = PORT_STANDALONE1
+    # args_instance[SER_SECURE_PORT] = SECUREPORT_STANDALONE1
+    args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE1
+    args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX
+    args_standalone = args_instance.copy()
+    standalone.allocate(args_standalone)
+    instance_standalone = standalone.exists()
+    if instance_standalone:
+        standalone.delete()
+    standalone.create(pyinstall=True, version=INSTALL_LATEST_CONFIG)
+    standalone.open()
+
+    def fin():
+        if DEBUGGING:
+            standalone.stop()
+        else:
+            standalone.delete()
+
+    request.addfinalizer(fin)
+
+    return TopologyMain(standalones=standalone)
+
+def test_install_sample_entries(topology_st):
+    # Assert that our entries match.
+
+    entries = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE)
+
+    for entry in entries:
+        assert(entry.dn in REQUIRED_DNS)
+        # We can make this assert the full object content, plugins and more later.
+
+    assert(True)
diff --git a/lib389/tests/idm/__init__.py b/lib389/tests/idm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lib389/tests/idm/services.py b/lib389/tests/idm/services.py
new file mode 100644
index 0000000..6b6c457
--- /dev/null
+++ b/lib389/tests/idm/services.py
@@ -0,0 +1,55 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389 import DirSrv, Entry, tools, tasks
+from lib389.tools import DirSrvTools
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.idm.organisationalunit import OrganisationalUnits
+from lib389.idm.services import ServiceAccounts
+
+from lib389.topologies import topology_st as topology
+
+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_services(topology):
+    """
+    Test and assert that a simple service account can be bound to and created.
+
+    These are really useful in simple tests.
+    """
+    ous = OrganisationalUnits(topology.standalone, DEFAULT_SUFFIX)
+    services = ServiceAccounts(topology.standalone, DEFAULT_SUFFIX)
+
+    # Create the OU for them.
+    ous.create(properties={
+            'ou': 'Services',
+            'description': 'Computer Service accounts which request DS bind',
+        })
+    # Now, we can create the services from here.
+    service = services.create(properties={
+        'cn': 'testbind',
+        'userPassword': 'Password1'
+        })
+
+    conn = service.bind('Password1')
+    conn.unbind_s()
+
+
+
diff --git a/lib389/tests/idm/users_and_groups.py b/lib389/tests/idm/users_and_groups.py
new file mode 100644
index 0000000..f072dfd
--- /dev/null
+++ b/lib389/tests/idm/users_and_groups.py
@@ -0,0 +1,101 @@
+import os
+import sys
+import time
+import ldap
+import logging
+import pytest
+from lib389 import DirSrv, Entry, tools, tasks
+from lib389.tools import DirSrvTools
+from lib389._constants import *
+from lib389.properties import *
+from lib389.tasks import *
+from lib389.utils import *
+
+from lib389.idm.user import UserAccounts
+from lib389.idm.group import Groups
+
+from lib389.topologies import topology_st as topology
+
+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_users_and_groups(topology):
+    """
+    Ensure that user and group management works as expected.
+    """
+    if DEBUGGING:
+        # Add debugging steps(if any)...
+        pass
+
+    users = UserAccounts(topology.standalone, DEFAULT_SUFFIX)
+    groups = Groups(topology.standalone, DEFAULT_SUFFIX)
+
+    # No user should exist
+
+    assert(len(users.list()) == 0)
+
+    # Create a user
+
+    user_properties = {
+        'uid': 'testuser',
+        'cn' : 'testuser',
+        'sn' : 'user',
+        'uidNumber' : '1000',
+        'gidNumber' : '2000',
+        'homeDirectory' : '/home/testuser'
+    }
+    users.create(properties=user_properties)
+
+    assert(len(users.list()) == 1)
+
+    testuser = users.get('testuser')
+
+    # Set password
+    testuser.set('userPassword', 'password')
+    # bind
+
+    conn = testuser.bind('password')
+    conn.unbind_s()
+
+    # create group
+    group_properties = {
+        'cn' : 'group1',
+        'description' : 'testgroup'
+    }
+
+    group = groups.create(properties=group_properties)
+
+    # user shouldn't be a member
+    assert(not group.is_member(testuser.dn))
+
+    # add user as member
+    group.add_member(testuser.dn)
+    # check they are a member
+    assert(group.is_member(testuser.dn))
+    # remove user from group
+    group.remove_member(testuser.dn)
+    # check they are not a member
+    assert(not group.is_member(testuser.dn))
+
+    group.delete()
+    testuser.delete()
+
+    log.info('Test PASSED')
+
+
+if __name__ == '__main__':
+    # Run isolated
+    # -s for DEBUG mode
+    CURRENT_FILE = os.path.realpath(__file__)
+    pytest.main("-s %s" % CURRENT_FILE)
+
diff --git a/lib389/tests/instance/setup_test.py b/lib389/tests/instance/setup_test.py
index 382a922..04ae34f 100644
--- a/lib389/tests/instance/setup_test.py
+++ b/lib389/tests/instance/setup_test.py
@@ -49,8 +49,6 @@ def topology(request):
     return TopologyInstance(instance)
 
 def test_setup_ds_minimal_dry(topology):
-    if MAJOR < 3:
-        return
     # Create the setupDs
     lc = LogCapture()
     # Give it the right types.
@@ -78,8 +76,6 @@ def test_setup_ds_minimal_dry(topology):
     assert(len(insts) == 0)
 
 def test_setup_ds_minimal(topology):
-    if MAJOR < 3:
-        return
     # Create the setupDs
     lc = LogCapture()
     # Give it the right types.
-- 
1.8.3.1

