From 09965c45ed718595ecbd5804d00648b53123014c Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Mar 26 2019 22:07:47 +0000
Subject: Issue 50292 - Fix Plugin CLI and UI issues


Description: Fix 'All plugins' tab rendering issue.
Fix nsds5replicalastinitstatus typo.
Fix generic_object_add logic for cases when RDN is in props and BaseDN is supplied.
Add Posix Winsync API plugin
Add PAM PTA plugin
Fix underscore issues in plugin arguments.
Fix Linked Attribute plugin Fixup task arguments and name.
Change a 'print()' function to a 'log.info()' function.

https://pagure.io/389-ds-base/issue/50292

Reviewed by: mreynolds, wibrown (Thanks!)

---

diff --git a/src/cockpit/389-console/src/plugins.jsx b/src/cockpit/389-console/src/plugins.jsx
index e14b92d..cab8952 100644
--- a/src/cockpit/389-console/src/plugins.jsx
+++ b/src/cockpit/389-console/src/plugins.jsx
@@ -296,6 +296,8 @@ export class Plugins extends React.Component {
                                     }
                                     this.toggleLoading();
                                 });
+                    } else {
+                        this.pluginList();
                     }
                 });
     }
diff --git a/src/lib389/lib389/agreement.py b/src/lib389/lib389/agreement.py
index 1f3fefc..128a607 100644
--- a/src/lib389/lib389/agreement.py
+++ b/src/lib389/lib389/agreement.py
@@ -317,7 +317,7 @@ class Agreement(DSLdapObject):
         if 'nsds5beginreplicarefresh' not in status_attrs_dict:
             status_attrs_dict['nsds5beginreplicarefresh'] = [""]
         if 'nsds5replicalastinitstatus' not in status_attrs_dict:
-            status_attrs_dict['nsds5replicalastinitstatus'] = ["unavilable"]
+            status_attrs_dict['nsds5replicalastinitstatus'] = ["unavailable"]
         if 'nsds5replicachangessentsincestartup' not in status_attrs_dict:
             status_attrs_dict['nsds5replicachangessentsincestartup'] = ['0']
         if ensure_str(status_attrs_dict['nsds5replicachangessentsincestartup'][0]) == '':
diff --git a/src/lib389/lib389/cli_conf/__init__.py b/src/lib389/lib389/cli_conf/__init__.py
index 0ceb066..a3a0d03 100644
--- a/src/lib389/lib389/cli_conf/__init__.py
+++ b/src/lib389/lib389/cli_conf/__init__.py
@@ -30,18 +30,21 @@ def generic_object_add(dsldap_objects_class, inst, log, args, arg_to_attr, dn=No
     """
 
     log = log.getChild('generic_object_add')
+    # If Base DN was initially provided then 'props' should contain the RDN
+    # if 'props' doesn't have the RDN - it will fail with the right error during the validation in the 'create'
+    rdn = None
     # Gather the attributes
     attrs = _args_to_attrs(args, arg_to_attr)
     props.update({attr: value for (attr, value) in attrs.items() if value != ""})
 
     # Get RDN attribute and Base DN from the DN if Base DN is not specified
-    if dn is not None and basedn is None:
-        dn_parts = ldap.dn.explode_dn(dn)
-
-        rdn = dn_parts[0]
-        basedn = ",".join(dn_parts[1:])
-    else:
-        raise ValueError('If Base DN is not specified - DN parameter should be')
+    if basedn is None:
+        if dn is not None:
+            dn_parts = ldap.dn.explode_dn(dn)
+            rdn = dn_parts[0]
+            basedn = ",".join(dn_parts[1:])
+        else:
+            raise ValueError('If Base DN is not specified - DN parameter should be specified instead')
 
     new_object = dsldap_objects_class(inst, dn=dn)
     new_object.create(rdn=rdn, basedn=basedn, properties=props)
diff --git a/src/lib389/lib389/cli_conf/plugin.py b/src/lib389/lib389/cli_conf/plugin.py
index 9509b84..db2ba58 100644
--- a/src/lib389/lib389/cli_conf/plugin.py
+++ b/src/lib389/lib389/cli_conf/plugin.py
@@ -26,6 +26,7 @@ from lib389.cli_conf.plugins import managedentries as cli_managedentries
 from lib389.cli_conf.plugins import passthroughauth as cli_passthroughauth
 from lib389.cli_conf.plugins import retrochangelog as cli_retrochangelog
 from lib389.cli_conf.plugins import automember as cli_automember
+from lib389.cli_conf.plugins import posix_winsync as cli_posix_winsync
 
 SINGULAR = Plugin
 MANY = Plugins
@@ -111,6 +112,7 @@ def create_parser(subparsers):
     cli_managedentries.create_parser(subcommands)
     cli_passthroughauth.create_parser(subcommands)
     cli_retrochangelog.create_parser(subcommands)
+    cli_posix_winsync.create_parser(subcommands)
 
     list_parser = subcommands.add_parser('list', help="List current configured (enabled and disabled) plugins")
     list_parser.set_defaults(func=plugin_list)
diff --git a/src/lib389/lib389/cli_conf/plugins/accountpolicy.py b/src/lib389/lib389/cli_conf/plugins/accountpolicy.py
index e2fa1e1..e585d2a 100644
--- a/src/lib389/lib389/cli_conf/plugins/accountpolicy.py
+++ b/src/lib389/lib389/cli_conf/plugins/accountpolicy.py
@@ -55,9 +55,10 @@ def accountpolicy_show_config(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % targetdn)
     if args and args.json:
         o_str = config.get_all_attrs_json()
-        print(o_str)
+
+        log.info(o_str)
     else:
-        print(config.display())
+        log.info(config.display())
 
 
 def accountpolicy_del_config(inst, basedn, log, args):
@@ -69,7 +70,7 @@ def accountpolicy_del_config(inst, basedn, log, args):
 
 
 def _add_parser_args(parser):
-    parser.add_argument('--always-record-login', choices=['yes', 'no'],
+    parser.add_argument('--always-record-login', choices=['yes', 'no'], type=str.lower,
                         help='Sets that every entry records its last login time (alwaysRecordLogin)')
     parser.add_argument('--alt-state-attr',
                         help='Provides a backup attribute for the server to reference '
diff --git a/src/lib389/lib389/cli_conf/plugins/attruniq.py b/src/lib389/lib389/cli_conf/plugins/attruniq.py
index 17dac15..a26154c 100644
--- a/src/lib389/lib389/cli_conf/plugins/attruniq.py
+++ b/src/lib389/lib389/cli_conf/plugins/attruniq.py
@@ -9,15 +9,16 @@
 import json
 import ldap
 from lib389.plugins import AttributeUniquenessPlugin, AttributeUniquenessPlugins
-from lib389.cli_conf import add_generic_plugin_parsers, generic_object_edit, generic_object_add
+from lib389.cli_conf import (add_generic_plugin_parsers, generic_object_edit, generic_object_add,
+                             generic_enable, generic_disable, generic_status)
 from lib389._constants import DN_PLUGIN
 
 arg_to_attr = {
-    'attr-name': 'uniqueness-attribute-name',
+    'attr_name': 'uniqueness-attribute-name',
     'subtree': 'uniqueness-subtrees',
-    'across-all-subtrees': 'uniqueness-across-all-subtrees',
-    'top-entry-oc': 'uniqueness-top-entry-oc',
-    'subtree-entries-oc': 'uniqueness-subtree-entries-oc'
+    'across_all_subtrees': 'uniqueness-across-all-subtrees',
+    'top_entry_oc': 'uniqueness-top-entry-oc',
+    'subtree_entries_oc': 'uniqueness-subtree-entries-oc'
 }
 
 
@@ -32,13 +33,13 @@ def attruniq_list(inst, basedn, log, args):
         else:
             result.append(plugin.rdn)
     if args.json:
-        print(json.dumps({"type": "list", "items": result_json}))
+        log.info(json.dumps({"type": "list", "items": result_json}))
     else:
         if len(result) > 0:
             for i in result:
-                print(i)
+                log.info(i)
         else:
-            print("No Attribute Uniqueness plugin instances")
+            log.info("No Attribute Uniqueness plugin instances")
 
 
 def attruniq_add(inst, basedn, log, args):
@@ -63,9 +64,9 @@ def attruniq_show(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % args.name)
     if args and args.json:
         o_str = plugin.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(plugin.display())
+        log.info(plugin.display())
 
 
 def attruniq_del(inst, basedn, log, args):
@@ -85,7 +86,7 @@ def _add_parser_args(parser):
     parser.add_argument('--subtree', nargs='+',
                         help='Sets the DN under which the plug-in checks for uniqueness of '
                              'the attributes value. This attribute is multi-valued (uniqueness-subtrees)')
-    parser.add_argument('--across-all-subtrees', choices=['on', 'off'],
+    parser.add_argument('--across-all-subtrees', choices=['on', 'off'], type=str.lower,
                         help='If enabled (on), the plug-in checks that the attribute is unique across all subtrees '
                              'set. If you set the attribute to off, uniqueness is only enforced within the subtree '
                              'of the updated entry (uniqueness-across-all-subtrees)')
@@ -120,3 +121,15 @@ def create_parser(subparsers):
     delete = subcommands.add_parser('delete', help='Delete the config entry')
     delete.add_argument('NAME', help='Sets the name of the plug-in configuration record')
     delete.set_defaults(func=attruniq_del)
+
+    enable = subcommands.add_parser('enable', help='enable plugin')
+    enable.add_argument('NAME', help='Sets the name of the plug-in configuration record')
+    enable.set_defaults(func=generic_enable)
+
+    disable = subcommands.add_parser('disable', help='disable plugin')
+    disable.add_argument('NAME', help='Sets the name of the plug-in configuration record')
+    disable.set_defaults(func=generic_disable)
+
+    status = subcommands.add_parser('status', help='display plugin status')
+    status.add_argument('NAME', help='Sets the name of the plug-in configuration record')
+    status.set_defaults(func=generic_status)
diff --git a/src/lib389/lib389/cli_conf/plugins/automember.py b/src/lib389/lib389/cli_conf/plugins/automember.py
index d9fe1dd..fbd270a 100644
--- a/src/lib389/lib389/cli_conf/plugins/automember.py
+++ b/src/lib389/lib389/cli_conf/plugins/automember.py
@@ -14,16 +14,16 @@ from lib389.cli_conf import add_generic_plugin_parsers, generic_object_edit, gen
 
 
 arg_to_attr_definition = {
-    'default-group': 'autoMemberDefaultGroup',
+    'default_group': 'autoMemberDefaultGroup',
     'filter': 'autoMemberFilter',
-    'grouping-attr': 'autoMemberGroupingAttr',
+    'grouping_attr': 'autoMemberGroupingAttr',
     'scope': 'autoMemberScope'
 }
 
 arg_to_attr_regex = {
     'exclusive': 'autoMemberExclusiveRegex',
     'inclusive': 'autoMemberInclusiveRegex',
-    'target-group': 'autoMemberTargetGroup'
+    'target_group': 'autoMemberTargetGroup'
 }
 
 
@@ -42,7 +42,7 @@ def definition_list(inst, basedn, log, args):
         log.info("No automember definitions were found")
 
     if args.json:
-        print(json.dumps(result))
+        log.info(json.dumps(result))
 
 
 def definition_add(inst, basedn, log, args):
@@ -68,9 +68,9 @@ def definition_show(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % args.name)
     if args and args.json:
         o_str = definition.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(definition.display())
+        log.info(definition.display())
 
 
 def definition_del(inst, basedn, log, args):
@@ -98,7 +98,7 @@ def regex_list(inst, basedn, log, args):
         log.info("No automember regexes were found")
 
     if args.json:
-        print(json.dumps(result))
+        log.info(json.dumps(result))
 
 
 def regex_add(inst, basedn, log, args):
@@ -129,9 +129,9 @@ def regex_show(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % args.name)
     if args and args.json:
         o_str = regex.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(regex.display())
+        log.info(regex.display())
 
 
 def regex_del(inst, basedn, log, args):
diff --git a/src/lib389/lib389/cli_conf/plugins/dna.py b/src/lib389/lib389/cli_conf/plugins/dna.py
index 08f66a4..6033c17 100644
--- a/src/lib389/lib389/cli_conf/plugins/dna.py
+++ b/src/lib389/lib389/cli_conf/plugins/dna.py
@@ -54,7 +54,7 @@ def dna_list(inst, basedn, log, args):
         log.info("No DNA configurations were found")
 
     if args.json:
-        print(json.dumps(result))
+        log.info(json.dumps(result))
 
 
 def dna_add(inst, basedn, log, args):
@@ -80,9 +80,9 @@ def dna_show(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % args.NAME)
     if args and args.json:
         o_str = config.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(config.display())
+        log.info(config.display())
 
 
 def dna_del(inst, basedn, log, args):
@@ -109,7 +109,7 @@ def dna_config_list(inst, basedn, log, args):
         log.info("No DNA shared configurations were found")
 
     if args.json:
-        print(json.dumps(result))
+        log.info(json.dumps(result))
 
 
 def dna_config_add(inst, basedn, log, args):
@@ -146,9 +146,9 @@ def dna_config_show(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % targetdn)
     if args and args.json:
         o_str = shared_config.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(shared_config.display())
+        log.info(shared_config.display())
 
 
 def dna_config_del(inst, basedn, log, args):
@@ -160,7 +160,8 @@ def dna_config_del(inst, basedn, log, args):
 
 
 def _add_parser_args(parser):
-    parser.add_argument('--type', help='Sets which attributes have unique numbers being generated for them (dnaType)')
+    parser.add_argument('--type', nargs='+',
+                        help='Sets which attributes have unique numbers being generated for them (dnaType)')
     parser.add_argument('--prefix', help='Defines a prefix that can be prepended to the generated '
                                          'number values for the attribute (dnaPrefix)')
     parser.add_argument('--next-value', help='Gives the next available number which can be assigned (dnaNextValue)')
diff --git a/src/lib389/lib389/cli_conf/plugins/linkedattr.py b/src/lib389/lib389/cli_conf/plugins/linkedattr.py
index 1a1c6ec..12b75e1 100644
--- a/src/lib389/lib389/cli_conf/plugins/linkedattr.py
+++ b/src/lib389/lib389/cli_conf/plugins/linkedattr.py
@@ -30,13 +30,13 @@ def linkedattr_list(inst, basedn, log, args):
         else:
             result.append(config.rdn)
     if args.json:
-        print(json.dumps({"type": "list", "items": result_json}))
+        log.info(json.dumps({"type": "list", "items": result_json}))
     else:
         if len(result) > 0:
             for i in result:
-                print(i)
+                log.info(i)
         else:
-            print("No Linked Attributes plugin instances")
+            log.info("No Linked Attributes plugin config instances")
 
 
 def linkedattr_add(inst, basedn, log, args):
@@ -62,9 +62,9 @@ def linkedattr_show(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % args.name)
     if args and args.json:
         o_str = config.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(config.display())
+        log.info(config.display())
 
 
 def linkedattr_del(inst, basedn, log, args):
@@ -80,7 +80,7 @@ def fixup(inst, basedn, log, args):
     log.info('Attempting to add task entry... This will fail if LinkedAttributes plug-in is not enabled.')
     if not plugin.status():
         log.error("'%s' is disabled. Fix up task can't be executed" % plugin.rdn)
-    fixup_task = plugin.fixup(args.basedn, args.filter)
+    fixup_task = plugin.fixup(args.linkdn)
     fixup_task.wait()
     exitcode = fixup_task.get_exit_code()
     if exitcode != 0:
@@ -104,8 +104,7 @@ def create_parser(subparsers):
     add_generic_plugin_parsers(subcommands, LinkedAttributesPlugin)
 
     fixup_parser = subcommands.add_parser('fixup', help='Run the fix-up task for linked attributes plugin')
-    fixup_parser.add_argument('basedn', help="basedn that contains entries to fix up")
-    fixup_parser.add_argument('-f', '--filter', help='Filter for entries to fix up linked attributes.')
+    fixup_parser.add_argument('-l', '--linkdn', help="Base DN that contains entries to fix up")
     fixup_parser.set_defaults(func=fixup)
 
     list = subcommands.add_parser('list', help='List available plugin configs')
diff --git a/src/lib389/lib389/cli_conf/plugins/managedentries.py b/src/lib389/lib389/cli_conf/plugins/managedentries.py
index cb5235b..071df7b 100644
--- a/src/lib389/lib389/cli_conf/plugins/managedentries.py
+++ b/src/lib389/lib389/cli_conf/plugins/managedentries.py
@@ -48,13 +48,13 @@ def mep_config_list(inst, basedn, log, args):
         else:
             result.append(config.rdn)
     if args.json:
-        print(json.dumps({"type": "list", "items": result_json}))
+        log.info(json.dumps({"type": "list", "items": result_json}))
     else:
         if len(result) > 0:
             for i in result:
-                print(i)
+                log.info(i)
         else:
-            print("No Linked Attributes plugin instances")
+            log.info("No Linked Attributes plugin instances")
 
 
 def mep_config_add(inst, basedn, log, args):
@@ -87,9 +87,9 @@ def mep_config_show(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % args.name)
     if args and args.json:
         o_str = config.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(config.display())
+        log.info(config.display())
 
 
 def mep_config_del(inst, basedn, log, args):
@@ -113,13 +113,13 @@ def mep_template_list(inst, basedn, log, args):
         else:
             result.append(template.rdn)
     if args.json:
-        print(json.dumps({"type": "list", "items": result_json}))
+        log.info(json.dumps({"type": "list", "items": result_json}))
     else:
         if len(result) > 0:
             for i in result:
-                print(i)
+                log.info(i)
         else:
-            print("No Linked Attributes plugin instances")
+            log.info("No Linked Attributes plugin instances")
 
 
 def mep_template_add(inst, basedn, log, args):
@@ -148,9 +148,9 @@ def mep_template_show(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % targetdn)
     if args and args.json:
         o_str = template.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(template.display())
+        log.info(template.display())
 
 
 def mep_template_del(inst, basedn, log, args):
diff --git a/src/lib389/lib389/cli_conf/plugins/memberof.py b/src/lib389/lib389/cli_conf/plugins/memberof.py
index fe54d02..cacca04 100644
--- a/src/lib389/lib389/cli_conf/plugins/memberof.py
+++ b/src/lib389/lib389/cli_conf/plugins/memberof.py
@@ -56,9 +56,9 @@ def memberof_show_config(inst, basedn, log, args):
         raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % targetdn)
     if args and args.json:
         o_str = config.get_all_attrs_json()
-        print(o_str)
+        log.info(o_str)
     else:
-        print(config.display())
+        log.info(config.display())
 
 
 def memberof_del_config(inst, basedn, log, args):
@@ -69,11 +69,12 @@ def memberof_del_config(inst, basedn, log, args):
     log.info("Successfully deleted the %s", targetdn)
 
 
-def fixup(inst, basedn, log, args):
+def do_fixup(inst, basedn, log, args):
     plugin = MemberOfPlugin(inst)
-    log.info('Attempting to add task entry... This will fail if MemberOf plug-in is not enabled.')
+    log.info('Attempting to add task entry...')
     if not plugin.status():
         log.error("'%s' is disabled. Fix up task can't be executed" % plugin.rdn)
+        return
     fixup_task = plugin.fixup(args.DN, args.filter)
     fixup_task.wait()
     exitcode = fixup_task.get_exit_code()
@@ -135,7 +136,7 @@ def create_parser(subparsers):
     del_config_.add_argument('DN', help='The config entry full DN')
 
     fixup = subcommands.add_parser('fixup', help='Run the fix-up task for memberOf plugin')
-    fixup.set_defaults(func=fixup)
+    fixup.set_defaults(func=do_fixup)
     fixup.add_argument('DN', help="Base DN that contains entries to fix up")
     fixup.add_argument('-f', '--filter',
                        help='Filter for entries to fix up.\n If omitted, all entries with objectclass '
diff --git a/src/lib389/lib389/cli_conf/plugins/passthroughauth.py b/src/lib389/lib389/cli_conf/plugins/passthroughauth.py
index 616119a..c5d0646 100644
--- a/src/lib389/lib389/cli_conf/plugins/passthroughauth.py
+++ b/src/lib389/lib389/cli_conf/plugins/passthroughauth.py
@@ -8,8 +8,22 @@
 
 import json
 import ldap
-from lib389.plugins import PassThroughAuthenticationPlugin
-from lib389.cli_conf import add_generic_plugin_parsers
+from lib389.plugins import (PassThroughAuthenticationPlugin, PAMPassThroughAuthPlugin,
+                            PAMPassThroughAuthConfigs, PAMPassThroughAuthConfig)
+
+from lib389.cli_conf import add_generic_plugin_parsers, generic_object_edit, generic_object_add
+
+arg_to_attr_pam = {
+    'exclude_suffix': 'pamExcludeSuffix',
+    'include_suffix': 'pamIncludeSuffix',
+    'missing_suffix': 'pamMissingSuffix',
+    'filter': 'pamFilter',
+    'id_attr': 'pamIDAttr',
+    'id_map_method': 'pamIDMapMethod',
+    'fallback': 'pamFallback',
+    'secure': 'pamSecure',
+    'service': 'pamService'
+}
 
 
 def pta_list(inst, basedn, log, args):
@@ -18,13 +32,13 @@ def pta_list(inst, basedn, log, args):
     result = []
     urls = plugin.get_urls()
     if args.json:
-        print(json.dumps({"type": "list", "items": urls}))
+        log.info(json.dumps({"type": "list", "items": urls}))
     else:
         if len(urls) > 0:
             for i in result:
-                print(i)
+                log.info(i)
         else:
-            print("No Pass Through Auth attributes were found")
+            log.info("No Pass Through Auth URLs were found")
 
 
 def pta_add(inst, basedn, log, args):
@@ -62,27 +76,132 @@ def pta_del(inst, basedn, log, args):
     log.info("Successfully deleted %s", args.URL)
 
 
+def pam_pta_list(inst, basedn, log, args):
+    log = log.getChild('pam_pta_list')
+    configs = PAMPassThroughAuthConfigs(inst)
+    result = []
+    result_json = []
+    for config in configs.list():
+        if args.json:
+            result_json.append(config.get_all_attrs_json())
+        else:
+            result.append(config.rdn)
+    if args.json:
+        log.info(json.dumps({"type": "list", "items": result_json}))
+    else:
+        if len(result) > 0:
+            for i in result:
+                log.info(i)
+        else:
+            log.info("No PAM Pass Through Auth plugin config instances")
+
+
+def pam_pta_add(inst, basedn, log, args):
+    log = log.getChild('pam_pta_add')
+    plugin = PAMPassThroughAuthPlugin(inst)
+    props = {'cn': args.NAME}
+    generic_object_add(PAMPassThroughAuthConfig, inst, log, args, arg_to_attr_pam, basedn=plugin.dn, props=props)
+
+
+def pam_pta_edit(inst, basedn, log, args):
+    log = log.getChild('pam_pta_edit')
+    configs = PAMPassThroughAuthConfigs(inst)
+    config = configs.get(args.NAME)
+    generic_object_edit(config, log, args, arg_to_attr_pam)
+
+
+def pam_pta_show(inst, basedn, log, args):
+    log = log.getChild('pam_pta_show')
+    configs = PAMPassThroughAuthConfigs(inst)
+    config = configs.get(args.NAME)
+
+    if not config.exists():
+        raise ldap.NO_SUCH_OBJECT("Entry %s doesn't exists" % args.name)
+    if args and args.json:
+        o_str = config.get_all_attrs_json()
+        log.info(o_str)
+    else:
+        log.info(config.display())
+
+
+def pam_pta_del(inst, basedn, log, args):
+    log = log.getChild('pam_pta_del')
+    configs = PAMPassThroughAuthConfigs(inst)
+    config = configs.get(args.NAME)
+    config.delete()
+    log.info("Successfully deleted the %s", config.dn)
+
+
+def _add_parser_args_pam(parser):
+    parser.add_argument('--exclude-suffix', nargs='+',
+                        help='Specifies a suffix to exclude from PAM authentication (pamExcludeSuffix)')
+    parser.add_argument('--include-suffix', nargs='+',
+                        help='Sets a suffix to include for PAM authentication (pamIncludeSuffix)')
+    parser.add_argument('--missing-suffix', choices=['ERROR', 'ALLOW', 'IGNORE'],
+                        help='Identifies how to handle missing include or exclude suffixes (pamMissingSuffix)')
+    parser.add_argument('--filter',
+                        help='Sets an LDAP filter to use to identify specific entries within '
+                             'the included suffixes for which to use PAM pass-through authentication (pamFilter)')
+    parser.add_argument('--id-attr', nargs='+',
+                        help='Contains the attribute name which is used to hold the PAM user ID (pamIDAttr)')
+    parser.add_argument('--id_map_method',
+                        help='Gives the method to use to map the LDAP bind DN to a PAM identity (pamIDMapMethod)')
+    parser.add_argument('--fallback', choices=['TRUE', 'FALSE'], type=str.upper,
+                        help='Sets whether to fallback to regular LDAP authentication '
+                             'if PAM authentication fails (pamFallback)')
+    parser.add_argument('--secure', choices=['TRUE', 'FALSE'], type=str.upper,
+                        help='Requires secure TLS connection for PAM authentication (pamSecure)')
+    parser.add_argument('--service',
+                        help='Contains the service name to pass to PAM (pamService)')
+
+
 def create_parser(subparsers):
-    passthroughauth_parser = subparsers.add_parser('pass-through-auth', help='Manage and configure Pass-Through Authentication plugin')
+    passthroughauth_parser = subparsers.add_parser('pass-through-auth',
+                                                   help='Manage and configure Pass-Through Authentication plugins '
+                                                        '(URLs and PAM)')
     subcommands = passthroughauth_parser.add_subparsers(help='action')
     add_generic_plugin_parsers(subcommands, PassThroughAuthenticationPlugin)
 
-    list = subcommands.add_parser('list', help='List available plugin configs')
-    list.set_defaults(func=pta_list)
-
-    add = subcommands.add_parser('add', help='Add the config entry')
-    add.add_argument('URL', help='The full LDAP URL in format '
-                                  '"ldap|ldaps://authDS/subtree maxconns,maxops,timeout,ldver,connlifetime,startTLS". '
-                                  'If one optional parameter is specified the rest should be specified too')
-    add.set_defaults(func=pta_add)
-
-    edit = subcommands.add_parser('modify', help='Edit the config entry')
-    edit.add_argument('OLD_URL', help='The full LDAP URL you get from the "list" command')
-    edit.add_argument('NEW_URL', help='The full LDAP URL in format '
-                                 '"ldap|ldaps://authDS/subtree maxconns,maxops,timeout,ldver,connlifetime,startTLS". '
-                                 'If one optional parameter is specified the rest should be specified too')
-    edit.set_defaults(func=pta_edit)
-
-    delete = subcommands.add_parser('delete', help='Delete the config entry')
-    delete.add_argument('URL', help='The full LDAP URL you get from the "list" command')
-    delete.set_defaults(func=pta_del)
+    list = subcommands.add_parser('list', help='List pass-though plugin URLs or PAM configurations.')
+    subcommands_list = list.add_subparsers(help='action')
+    list_urls = subcommands_list.add_parser('urls', help='List URLs.')
+    list_urls.set_defaults(func=pta_list)
+    list_pam = subcommands_list.add_parser('pam-configs', help='List PAM configurations.')
+    list_pam.set_defaults(func=pam_pta_list)
+
+    url = subcommands.add_parser('url', help='Manage PTA URL configurations.')
+    subcommands_url = url.add_subparsers(help='action')
+
+    add_url = subcommands_url.add_parser('add', help='Add the config entry')
+    add_url.add_argument('URL',
+                         help='The full LDAP URL in format '
+                              '"ldap|ldaps://authDS/subtree maxconns,maxops,timeout,ldver,connlifetime,startTLS". '
+                              'If one optional parameter is specified the rest should be specified too')
+    add_url.set_defaults(func=pta_add)
+
+    edit_url = subcommands_url.add_parser('modify', help='Edit the config entry')
+    edit_url.add_argument('OLD_URL', help='The full LDAP URL you get from the "list" command')
+    edit_url.add_argument('NEW_URL',
+                          help='The full LDAP URL in format '
+                               '"ldap|ldaps://authDS/subtree maxconns,maxops,timeout,ldver,connlifetime,startTLS". '
+                               'If one optional parameter is specified the rest should be specified too')
+    edit_url.set_defaults(func=pta_edit)
+
+    delete_url = subcommands_url.add_parser('delete', help='Delete the config entry')
+    delete_url.add_argument('URL', help='The full LDAP URL you get from the "list" command')
+    delete_url.set_defaults(func=pta_del)
+
+    pam = subcommands.add_parser('pam-config', help='Manage PAM PTA configurations.')
+    pam.add_argument('NAME', help='The PAM PTA configuration name')
+    subcommands_pam = pam.add_subparsers(help='action')
+
+    add = subcommands_pam.add_parser('add', help='Add the config entry')
+    add.set_defaults(func=pam_pta_add)
+    _add_parser_args_pam(add)
+    edit = subcommands_pam.add_parser('set', help='Edit the config entry')
+    edit.set_defaults(func=pam_pta_edit)
+    _add_parser_args_pam(edit)
+    show = subcommands_pam.add_parser('show', help='Display the config entry')
+    show.set_defaults(func=pam_pta_show)
+    delete = subcommands_pam.add_parser('delete', help='Delete the config entry')
+    delete.set_defaults(func=pam_pta_del)
diff --git a/src/lib389/lib389/cli_conf/plugins/posix_winsync.py b/src/lib389/lib389/cli_conf/plugins/posix_winsync.py
new file mode 100644
index 0000000..5c5d6cc
--- /dev/null
+++ b/src/lib389/lib389/cli_conf/plugins/posix_winsync.py
@@ -0,0 +1,53 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2019 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+
+from lib389.plugins import POSIXWinsyncPlugin
+from lib389.cli_conf import add_generic_plugin_parsers, generic_object_edit
+
+arg_to_attr = {
+    'create_memberof_task': 'posixWinsyncCreateMemberOfTask',
+    'lower_case_uid': 'posixWinsyncLowerCaseUID',
+    'map_member_uid': 'posixWinsyncMapMemberUID',
+    'map_nested_grouping': 'posixWinsyncMapNestedGrouping',
+    'ms_sfu_schema': 'posixWinsyncMsSFUSchema'
+}
+
+
+def winsync_edit(inst, basedn, log, args):
+    log = log.getChild('winsync_edit')
+    plugin = POSIXWinsyncPlugin(inst)
+    generic_object_edit(plugin, log, args, arg_to_attr)
+
+
+def _add_parser_args(parser):
+    parser.add_argument('--create-memberof-task', choices=['true', 'false'], type=str.lower,
+                        help=' sets whether to run the memberOf fix-up task immediately after a sync run in order '
+                             'to update group memberships for synced users (posixWinsyncCreateMemberOfTask)')
+    parser.add_argument('--lower-case-uid', choices=['true', 'false'], type=str.lower,
+                        help='Sets whether to store (and, if necessary, convert) the UID value in the memberUID '
+                             'attribute in lower case.(posixWinsyncLowerCaseUID)')
+    parser.add_argument('--map-member-uid', choices=['true', 'false'], type=str.lower,
+                        help='Sets whether to map the memberUID attribute in an Active Directory group to '
+                             'the uniqueMember attribute in a Directory Server group (posixWinsyncMapMemberUID)')
+    parser.add_argument('--map-nested-grouping', choices=['true', 'false'], type=str.lower,
+                        help='Manages if nested groups are updated when memberUID attributes in '
+                             'an Active Directory POSIX group change (posixWinsyncMapNestedGrouping)')
+    parser.add_argument('--ms-sfu-schema', choices=['true', 'false'], type=str.lower,
+                        help='Sets whether to the older Microsoft System Services for Unix 3.0 (msSFU30) '
+                             'schema when syncing Posix attributes from Active Directory (posixWinsyncMsSFUSchema)')
+
+
+def create_parser(subparsers):
+    winsync = subparsers.add_parser('posix-winsync', help='Manage and configure The Posix Winsync API plugin')
+    subcommands = winsync.add_subparsers(help='action')
+    add_generic_plugin_parsers(subcommands, POSIXWinsyncPlugin)
+
+    edit = subcommands.add_parser('set', help='Edit the plugin')
+    edit.set_defaults(func=winsync_edit)
+    _add_parser_args(edit)
+
diff --git a/src/lib389/lib389/cli_conf/plugins/retrochangelog.py b/src/lib389/lib389/cli_conf/plugins/retrochangelog.py
index 912c127..e677310 100644
--- a/src/lib389/lib389/cli_conf/plugins/retrochangelog.py
+++ b/src/lib389/lib389/cli_conf/plugins/retrochangelog.py
@@ -10,10 +10,11 @@ from lib389.plugins import RetroChangelogPlugin
 from lib389.cli_conf import add_generic_plugin_parsers, generic_object_edit
 
 arg_to_attr = {
-    'is-replicated': 'isReplicated',
+    'is_replicated': 'isReplicated',
     'attribute': 'nsslapd-attribute',
     'directory': 'nsslapd-changelogdir',
-    'max-age': 'nsslapd-changelogmaxage',
+    'max_age': 'nsslapd-changelogmaxage',
+    'exclude_suffix': 'nsslapd-exclude-suffix'
 }
 
 
@@ -24,7 +25,7 @@ def retrochangelog_edit(inst, basedn, log, args):
 
 
 def _add_parser_args(parser):
-    parser.add_argument('--is-replicated', choices=['true', 'false'],
+    parser.add_argument('--is-replicated', choices=['true', 'false'], type=str.lower,
                         help='Sets a flag to indicate on a change in the changelog whether the change is newly made '
                              'on that server or whether it was replicated over from another server (isReplicated)')
     parser.add_argument('--attribute',
@@ -36,6 +37,9 @@ def _add_parser_args(parser):
     parser.add_argument('--max-age',
                         help='This attribute specifies the maximum age of any entry '
                              'in the changelog (nsslapd-changelogmaxage)')
+    parser.add_argument('--exclude-suffix',
+                        help='This attribute specifies the suffix which will be excluded '
+                             'from the scope of the plugin (nsslapd-exclude-suffix)')
 
 
 def create_parser(subparsers):
diff --git a/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py b/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
index 63838a9..1bd974d 100644
--- a/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
+++ b/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py
@@ -27,20 +27,20 @@ def rootdn_edit(inst, basedn, log, args):
 
 
 def _add_parser_args(parser):
-    parser.add_argument('--allow-host',
+    parser.add_argument('--allow-host', nargs='+',
                         help='Sets what hosts, by fully-qualified domain name, the root user is allowed to use '
                              'to access the Directory Server. Any hosts not listed are implicitly denied '
                              '(rootdn-allow-host)')
-    parser.add_argument('--deny-host',
+    parser.add_argument('--deny-host', nargs='+',
                         help='Sets what hosts, by fully-qualified domain name, the root user is not allowed to use '
                              'to access the Directory Server Any hosts not listed are implicitly allowed '
                              '(rootdn-deny-host). If an host address is listed in both the rootdn-allow-host and '
                              'rootdn-deny-host attributes, it is denied access.')
-    parser.add_argument('--allow-ip',
+    parser.add_argument('--allow-ip', nargs='+',
                         help='Sets what IP addresses, either IPv4 or IPv6, for machines the root user is allowed '
                              'to use to access the Directory Server Any IP addresses not listed are implicitly '
                              'denied (rootdn-allow-ip)')
-    parser.add_argument('--deny-ip',
+    parser.add_argument('--deny-ip', nargs='+',
                         help='Sets what IP addresses, either IPv4 or IPv6, for machines the root user is not allowed '
                              'to use to access the Directory Server. Any IP addresses not listed are implicitly '
                              'allowed (rootdn-deny-ip) If an IP address is listed in both the rootdn-allow-ip and '
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
index 18a2482..bd66fa6 100644
--- a/src/lib389/lib389/plugins.py
+++ b/src/lib389/lib389/plugins.py
@@ -122,8 +122,22 @@ class AttributeUniquenessPlugin(Plugin):
     :type dn: str
     """
 
+    _plugin_properties = {
+        'nsslapd-pluginEnabled': 'off',
+        'nsslapd-pluginPath': 'libattr-unique-plugin',
+        'nsslapd-pluginInitfunc': 'NSUniqueAttr_Init',
+        'nsslapd-pluginType': 'betxnpreoperation',
+        'nsslapd-plugin-depends-on-type': 'database',
+        'nsslapd-pluginId': 'NSUniqueAttr',
+        'nsslapd-pluginVendor': '389 Project',
+        'nsslapd-pluginVersion': 'none',
+        'nsslapd-pluginDescription': 'Enforce unique attribute values',
+    }
+
     def __init__(self, instance, dn="cn=attribute uniqueness,cn=plugins,cn=config"):
         super(AttributeUniquenessPlugin, self).__init__(instance, dn)
+        self._protected = False
+        self._create_objectclasses = ['top', 'nsslapdplugin', 'extensibleObject']
 
     ## These are some wrappers to the important attributes
     # This plugin will be "tricky" in that it can have "many" instance
@@ -170,14 +184,18 @@ class AttributeUniquenessPlugins(DSLdapObjects):
     """
 
     def __init__(self, instance, basedn="cn=plugins,cn=config"):
-        super(Plugins, self).__init__(instance=instance)
-        self._objectclasses = ['top', 'nsslapdplugin']
+        super(DSLdapObjects, self).__init__(instance)
+        self._instance = instance
+        self._objectclasses = ['top', 'nsslapdplugin', 'extensibleObject']
         self._filterattrs = ['cn', 'nsslapd-pluginPath']
         self._childobject = AttributeUniquenessPlugin
         self._basedn = basedn
         # This is used to allow entry to instance to work
         self._list_attrlist = ['dn', 'nsslapd-pluginPath']
         self._search_filter = "(nsslapd-pluginId=NSUniqueAttr)"
+        self._scope = ldap.SCOPE_SUBTREE
+        self._server_controls = None
+        self._client_controls = None
 
     def list(self):
         """Get a list of all plugin instances where nsslapd-pluginId: NSUniqueAttr
@@ -215,7 +233,7 @@ class AttributeUniquenessPlugins(DSLdapObjects):
         # 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 = "&(cn=%s)%s" % (selector, self._search_filter)
+        filterstr = "(&(cn=%s)%s)" % (selector, self._search_filter)
         self._log.debug('_gen_selector filter = %s' % filterstr)
         return self._instance.search_ext_s(
             base=self._basedn,
@@ -1185,21 +1203,19 @@ class LinkedAttributesPlugin(Plugin):
     def __init__(self, instance, dn="cn=Linked Attributes,cn=plugins,cn=config"):
         super(LinkedAttributesPlugin, self).__init__(instance, dn)
 
-    def fixup(self, basedn, _filter=None):
+    def fixup(self, linkdn):
         """Create a fixup linked attributes task
 
-        :param basedn: Basedn to fix up
-        :type basedn: str
-        :param _filter: a filter for entries to fix up
-        :type _filter: str
+        :param linkdn: Link DN to fix up
+        :type linkdn: str
 
         :returns: an instance of Task(DSLdapObject)
         """
 
         task = tasks.FixupLinkedAttributesTask(self._instance)
-        task_properties = {'basedn': basedn}
-        if _filter is not None:
-            task_properties['filter'] = _filter
+        task_properties = {}
+        if linkdn is not None:
+            task_properties['linkdn'] = linkdn
         task.create(properties=task_properties)
 
         return task
@@ -1235,6 +1251,7 @@ class LinkedAttributesConfigs(DSLdapObjects):
         super(LinkedAttributesConfigs, self).__init__(instance)
         self._objectclasses = ['top', 'extensibleObject']
         self._filterattrs = ['cn']
+        self._scope = ldap.SCOPE_ONELEVEL
         self._childobject = LinkedAttributesConfig
         self._basedn = basedn
 
@@ -1265,6 +1282,80 @@ class PassThroughAuthenticationPlugin(Plugin):
         return result
 
 
+class POSIXWinsyncPlugin(Plugin):
+    """A single instance of Posix Winsync API plugin entry
+
+    :param instance: An instance
+    :type instance: lib389.DirSrv
+    :param dn: Entry DN
+    :type dn: str
+    """
+
+    def __init__(self, instance, dn="cn=Posix Winsync API,cn=plugins,cn=config"):
+        super(POSIXWinsyncPlugin, self).__init__(instance, dn)
+
+
+class PAMPassThroughAuthPlugin(Plugin):
+    """A single instance of PAM Pass Through Auth plugin entry
+
+    :param instance: An instance
+    :type instance: lib389.DirSrv
+    :param dn: Entry DN
+    :type dn: str
+    """
+
+    def __init__(self, instance, dn="cn=PAM Pass Through Auth,cn=plugins,cn=config"):
+        super(PAMPassThroughAuthPlugin, self).__init__(instance, dn)
+
+
+class PAMPassThroughAuthConfig(Plugin):
+    """A single instance of PAM Pass Through Auth config entry
+
+    :param instance: An instance
+    :type instance: lib389.DirSrv
+    :param dn: Entry DN
+    :type dn: str
+    """
+
+    _plugin_properties = {
+        'cn' : 'USN',
+        'nsslapd-pluginEnabled': 'off',
+        'nsslapd-pluginPath': 'libpam-passthru-plugin',
+        'nsslapd-pluginInitfunc': 'pam_passthruauth_init',
+        'nsslapd-pluginType': 'betxnpreoperation',
+        'nsslapd-plugin-depends-on-type': 'database',
+        'nsslapd-pluginId': 'PAM',
+        'nsslapd-pluginVendor': '389 Project',
+        'nsslapd-pluginVersion': '1.3.7.0',
+        'nsslapd-pluginDescription': 'PAM Pass Through Auth plugin'
+    }
+
+    def __init__(self, instance, dn=None):
+        super(PAMPassThroughAuthConfig, self).__init__(instance, dn)
+        self._rdn_attribute = 'cn'
+        self._must_attributes = ['cn']
+        self._create_objectclasses = ['top', 'extensibleObject', 'nsslapdplugin', 'pamConfig']
+        self._protected = False
+
+
+class PAMPassThroughAuthConfigs(DSLdapObjects):
+    """A DSLdapObjects entity which represents PAM Pass Through Auth config entry
+
+    :param instance: An instance
+    :type instance: lib389.DirSrv
+    :param basedn: Base DN for all account entries below
+    :type basedn: str
+    """
+
+    def __init__(self, instance, basedn="cn=PAM Pass Through Auth,cn=plugins,cn=config"):
+        super(PAMPassThroughAuthConfigs, self).__init__(instance)
+        self._objectclasses = ['top', 'extensibleObject', 'nsslapdplugin', 'pamConfig']
+        self._filterattrs = ['cn']
+        self._scope = ldap.SCOPE_ONELEVEL
+        self._childobject = PAMPassThroughAuthConfig
+        self._basedn = basedn
+
+
 class USNPlugin(Plugin):
     """A single instance of USN (Update Sequence Number) plugin entry
 
diff --git a/src/lib389/lib389/tasks.py b/src/lib389/lib389/tasks.py
index 47df609..6a86a42 100644
--- a/src/lib389/lib389/tasks.py
+++ b/src/lib389/lib389/tasks.py
@@ -137,7 +137,6 @@ class FixupLinkedAttributesTask(Task):
         dn = "cn=" + self.cn + "," + DN_FIXUP_LINKED_ATTIBUTES
 
         super(FixupLinkedAttributesTask, self).__init__(instance, dn)
-        self._must_attributes.extend(['basedn'])
 
 
 class MemberOfFixupTask(Task):

