Hello community,

here is the log from the commit of package yast-gpmc for openSUSE:Factory 
checked in at 2018-05-30 12:23:51
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/yast-gpmc (Old)
 and      /work/SRC/openSUSE:Factory/.yast-gpmc.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "yast-gpmc"

Wed May 30 12:23:51 2018 rev:2 rq:612934 version:1.3.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/yast-gpmc/yast-gpmc.changes      2017-11-29 
10:53:51.482042237 +0100
+++ /work/SRC/openSUSE:Factory/.yast-gpmc.new/yast-gpmc.changes 2018-05-30 
12:42:50.758178382 +0200
@@ -1,0 +2,5 @@
+Tue May 29 20:12:50 UTC 2018 - dmul...@suse.com
+
+- v1.3.1: Lots of improvements and fixes, py3 compatability, etc.
+
+-------------------------------------------------------------------

Old:
----
  yast-gpmc-1.0.tar.gz

New:
----
  yast-gpmc-1.3.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ yast-gpmc.spec ++++++
--- /var/tmp/diff_new_pack.tqNUCk/_old  2018-05-30 12:42:51.522162074 +0200
+++ /var/tmp/diff_new_pack.tqNUCk/_new  2018-05-30 12:42:51.526161989 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package yast-gpmc
 #
-# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany.
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,7 +17,7 @@
 
 
 Name:           yast-gpmc
-Version:        1.0
+Version:        1.3.1
 Release:        0
 Summary:        Group Policy Management Console for YaST
 License:        GPL-3.0
@@ -26,6 +26,7 @@
 Source:         %{name}-%{version}.tar.gz
 BuildArch:      noarch
 Requires:       krb5-client
+Requires:       python-configparser
 Requires:       python-ldap
 Requires:       samba-client
 Requires:       samba-python

++++++ yast-gpmc-1.0.tar.gz -> yast-gpmc-1.3.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/.gitignore 
new/yast-gpmc-1.3.1/.gitignore
--- old/yast-gpmc-1.0/.gitignore        2017-11-08 19:20:04.000000000 +0100
+++ new/yast-gpmc-1.3.1/.gitignore      2018-05-29 20:44:25.000000000 +0200
@@ -9,3 +9,4 @@
 autom4te.cache
 *.pyc
 src/gpmc.desktop
+src/gpmc.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/configure.ac 
new/yast-gpmc-1.3.1/configure.ac
--- old/yast-gpmc-1.0/configure.ac      2017-11-08 19:20:04.000000000 +0100
+++ new/yast-gpmc-1.3.1/configure.ac    2018-05-29 20:44:25.000000000 +0200
@@ -5,6 +5,7 @@
 
 AC_INIT([yast2-gpmc],[1.0],[http://bugs.opensuse.org/],[yast2-gpmc])
 AM_INIT_AUTOMAKE
+AM_PATH_PYTHON([2.7])
 
 dnl Checks for programs.
 AC_PROG_INSTALL
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/package/yast-gpmc.spec 
new/yast-gpmc-1.3.1/package/yast-gpmc.spec
--- old/yast-gpmc-1.0/package/yast-gpmc.spec    1970-01-01 01:00:00.000000000 
+0100
+++ new/yast-gpmc-1.3.1/package/yast-gpmc.spec  2018-05-29 20:44:25.000000000 
+0200
@@ -0,0 +1,59 @@
+#
+# spec file for package yast-gpmc
+#
+
+Name:       yast-gpmc
+Version:    1.0
+Release:    1
+License:    GPL-3.0
+Summary:    Group Policy Management Console for YaST
+Url:        http://www.github.com/dmulder/yast-gpmc
+Group:      Productivity/Networking/Samba
+Source:     %{name}-%{version}.tar.gz
+BuildArch:  noarch
+Requires:   yast2-python-bindings >= 4.0.0
+Requires:   samba-client
+Requires:   python-ldap
+Requires:   samba-python
+Requires:   krb5-client
+Requires:   yast2
+BuildRequires:  autoconf
+BuildRequires:  automake
+BuildRequires:  python
+BuildRequires: perl-XML-Writer
+BuildRequires:  update-desktop-files
+BuildRequires:  yast2
+BuildRequires:  yast2-devtools
+BuildRequires:  yast2-testsuite
+
+%description
+The Group Policy Management console for YaST provides tools for creating and
+modifying Group Policy Objects in Active Directory.
+
+%prep
+%setup -q
+
+%build
+autoreconf -if
+%configure --prefix=%{_prefix}
+make
+
+%install
+make DESTDIR=$RPM_BUILD_ROOT install
+
+%clean
+%{__rm} -rf $RPM_BUILD_ROOT
+
+%files
+%defattr(-,root,root)
+%dir %{_datadir}/YaST2/include/gpmc
+%{_datadir}/YaST2/clients/gpmc.py
+%{_datadir}/YaST2/include/gpmc/complex.py
+%{_datadir}/YaST2/include/gpmc/dialogs.py
+%{_datadir}/YaST2/include/gpmc/wizards.py
+%{_datadir}/YaST2/include/gpmc/defaults.py
+%{_datadir}/applications/YaST2/gpmc.desktop
+%dir %{_datadir}/doc/yast2-gpmc
+%{_datadir}/doc/yast2-gpmc/COPYING
+
+%changelog
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/src/Makefile.am 
new/yast-gpmc-1.3.1/src/Makefile.am
--- old/yast-gpmc-1.0/src/Makefile.am   2017-11-08 19:20:04.000000000 +0100
+++ new/yast-gpmc-1.3.1/src/Makefile.am 2018-05-29 20:44:25.000000000 +0200
@@ -22,9 +22,13 @@
 
 moduledir = ${prefix}/share/YaST2/modules
 
+gpmc.py: gpmc.py.in
+       sed -e 's;[@]INCLUDEDIR[@];$(yncludedir);g' < $(srcdir)/gpmc.py.in > 
$(srcdir)/gpmc.py
+
 # create a symlink for local build, #145327
 gpmc:
        ln -sf . $@
 ycpchook = gpmc
 
 EXTRA_DIST = $(client_DATA) $(ynclude_DATA) $(module_DATA) $(desktop_DATA)
+CLEANFILES = gpmc.py gpmc.desktop
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/src/complex.py 
new/yast-gpmc-1.3.1/src/complex.py
--- old/yast-gpmc-1.0/src/complex.py    2017-11-17 16:10:53.000000000 +0100
+++ new/yast-gpmc-1.3.1/src/complex.py  2018-05-29 20:44:25.000000000 +0200
@@ -1,7 +1,8 @@
+from __future__ import absolute_import, division, print_function, 
unicode_literals
 import ldap, ldap.modlist, ldap.sasl
 from samba import smb
-from ConfigParser import ConfigParser
-from StringIO import StringIO
+from configparser import ConfigParser
+from io import StringIO
 import xml.etree.ElementTree as etree
 import os.path, sys
 from samba.net import Net
@@ -11,30 +12,185 @@
 from ldap.modlist import addModlist as addlist
 from ldap.modlist import modifyModlist as modlist
 import re
+import traceback
+import ldb
+from samba.dcerpc import security
+from samba.ndr import ndr_unpack
+import samba.security
+from samba.ntacls import dsacl2fsacl
+
+PY3 = sys.version_info[0] == 3
+PY2 = sys.version_info[0] == 2
+
+class LdapException(Exception):
+    def __init__(self, *args, **kwargs):
+        Exception.__init__(self, *args, **kwargs)
+        if len(self.args) > 0:
+            self.msg = self.args[0]
+        else:
+            self.msg = None
+        if len(self.args) > 1:
+            self.info = self.args[1]
+        else:
+            self.info = None
+
+def _ldap_exc_msg(e):
+    if len(e.args) > 0 and \
+      type(e.args[-1]) is dict and \
+      'desc' in e.args[-1]:
+        return e.args[-1]['desc']
+    else:
+        return str(e)
+
+def _ldap_exc_info(e):
+    if len(e.args) > 0 and \
+      type(e.args[-1]) is dict and \
+      'info' in e.args[-1]:
+        return e.args[-1]['info']
+    else:
+        return ''
+
+def ldap_search(l, *args):
+    try:
+        return l.search_s(*args)
+    except Exception as e:
+        traceback.print_exc(file=sys.stderr)
+        sys.stderr.write('ldap.search_s: %s\n' % _ldap_exc_msg(e))
+
+def ldap_add(l, *args):
+    try:
+        return l.add_s(*args)
+    except Exception as e:
+        raise LdapException(_ldap_exc_msg(e), _ldap_exc_info(e))
+
+def ldap_modify(l, *args):
+    try:
+        return l.modify(*args)
+    except Exception as e:
+        traceback.print_exc(file=sys.stderr)
+        sys.stderr.write('ldap.modify: %s\n' % _ldap_exc_msg(e))
+
+def ldap_delete(l, *args):
+    try:
+        return l.delete_s(*args)
+    except Exception as e:
+        traceback.print_exc(file=sys.stderr)
+        sys.stderr.write('ldap.delete_s: %s\n' % _ldap_exc_msg(e))
+
+def open_bytes(filename):
+    if PY3:
+        return open(filename, errors='ignore')
+    else:
+        return open(filename, 'rb')
+
+def dict_to_bytes(d):
+    for key in d.keys():
+        if type(d[key]) is dict:
+            d[key] = dict_to_bytes(d[key])
+        elif type(d[key]) is list:
+            vals = []
+            for val in d[key]:
+                if type(val) is str:
+                    vals.append(val.encode('utf-8'))
+                else:
+                    vals.append(val)
+            d[key] = vals
+        elif type(d[key]) is str:
+            d[key] = d[key].encode('utf-8')
+    return d
+
+def stringify_ldap(data):
+    if type(data) == dict:
+        for key, value in data.items():
+            data[key] = stringify_ldap(value)
+        return data
+    elif type(data) == list:
+        new_list = []
+        for item in data:
+            new_list.append(stringify_ldap(item))
+        return new_list
+    elif type(data) == tuple:
+        new_tuple = []
+        for item in data:
+            new_tuple.append(stringify_ldap(item))
+        return tuple(new_tuple)
+    elif PY2 and type(data) == unicode:
+        return str(data)
+    elif PY3 and type(data) == bytes:
+        try:
+            return data.decode('utf-8')
+        except UnicodeDecodeError:
+            return data
+    else:
+        return data
+
+def parse_unc(unc):
+    '''Parse UNC string into a hostname, a service, and a filepath'''
+    if unc.startswith('\\\\') and unc.startswith('//'):
+        raise ValueError("UNC doesn't start with \\\\ or //")
+    tmp = unc[2:].split('/', 2)
+    if len(tmp) == 3:
+        return tmp
+    tmp = unc[2:].split('\\', 2)
+    if len(tmp) == 3:
+        return tmp
+    raise ValueError("Invalid UNC string: %s" % unc)
+
+def dn_to_path(realm, dn):
+    base_dn = (','.join(['DC=%s' % part for part in 
realm.lower().split('.')])).encode('utf-8')
+    parts = [p.split(b'=')[-1].title() for p in 
dn.lower().replace(base_dn.lower(), b'').split(b',') if p]
+    parts.append(realm.encode('utf-8'))
+    return b'/'.join(reversed(parts))
+
+def parse_gplink(gplink):
+    '''parse a gPLink into an array of dn and options'''
+    ret = {}
+    a = gplink.split(b']')
+    for g in a:
+        if not g:
+            continue
+        d = g.split(b';')
+        if len(d) != 2 or not d[0].startswith(b"[LDAP://"):
+            raise RuntimeError("Badly formed gPLink '%s'" % g)
+        options = bin(int(d[1]))[2:].zfill(2)
+        name = d[0][8:].split(b',')[0][3:].decode()
+        ret[name] = {'enforced' : 'Yes' if int(options[-2]) else 'No', 
'enabled' : 'No' if int(options[-1]) else 'Yes', 'dn' : d[0][8:].decode(), 
'options' : int(d[1])}
+    return ret
+
+def encode_gplink(gplist):
+    '''Encode an array of dn and options into gPLink string'''
+    ret = ''
+    for g in gplist:
+        ret += "[LDAP://%s;%d]" % (g['dn'], g['options'])
+    return ret
 
 class GPConnection:
     def __init__(self, lp, creds):
         self.lp = lp
         self.creds = creds
-        self.realm = lp.get('realm')
         net = Net(creds=creds, lp=lp)
-        cldap_ret = net.finddc(domain=self.realm, flags=(nbt.NBT_SERVER_LDAP | 
nbt.NBT_SERVER_DS))
+        cldap_ret = net.finddc(domain=lp.get('realm'), 
flags=(nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS | nbt.NBT_SERVER_WRITABLE))
+        self.realm = cldap_ret.dns_domain
+        self.dc_hostname = cldap_ret.pdc_dns_name
         self.l = ldap.initialize('ldap://%s' % cldap_ret.pdc_dns_name)
         if self.__kinit_for_gssapi():
             auth_tokens = ldap.sasl.gssapi('')
             self.l.sasl_interactive_bind_s('', auth_tokens)
         else:
             self.l.bind_s('%s@%s' % (creds.get_username(), self.realm) if not 
self.realm in creds.get_username() else creds.get_username(), 
creds.get_password())
+        self.l.set_option(ldap.OPT_REFERRALS,0)
 
     def __kinit_for_gssapi(self):
         p = Popen(['kinit', '%s@%s' % (self.creds.get_username(), self.realm) 
if not self.realm in self.creds.get_username() else self.creds.get_username()], 
stdin=PIPE, stdout=PIPE)
-        p.stdin.write('%s\n' % self.creds.get_password())
+        p.stdin.write(('%s\n'%self.creds.get_password()).encode())
+        p.stdin.flush()
         return p.wait() == 0
 
     def realm_to_dn(self, realm):
         return ','.join(['DC=%s' % part for part in realm.lower().split('.')])
 
     def __well_known_container(self, container):
+        res = None
         if container == 'system':
             wkguiduc = 'AB1D30F3768811D1ADED00C04FD8D5CD'
         elif container == 'computers':
@@ -43,51 +199,207 @@
             wkguiduc = 'A361B2FFFFD211D1AA4B00C04FD7D83A'
         elif container == 'users':
             wkguiduc = 'A9D1CA15768811D1ADED00C04FD8D5CD'
-        result = self.l.search_s('<WKGUID=%s,%s>' % (wkguiduc, 
self.realm_to_dn(self.realm)), ldap.SCOPE_SUBTREE, '(objectClass=container)', 
['distinguishedName'])
+        result = ldap_search(self.l, '<WKGUID=%s,%s>' % (wkguiduc, 
self.realm_to_dn(self.realm)), ldap.SCOPE_SUBTREE, '(objectClass=container)', 
stringify_ldap(['distinguishedName']))
+        result = stringify_ldap(result)
         if result and len(result) > 0 and len(result[0]) > 1 and 
'distinguishedName' in result[0][1] and len(result[0][1]['distinguishedName']) 
> 0:
-            return result[0][1]['distinguishedName'][-1]
+            res = result[0][1]['distinguishedName'][-1]
+
+        return stringify_ldap(res)
 
-    def gpo_list(self):
-        return self.l.search_s(self.__well_known_container('system'), 
ldap.SCOPE_SUBTREE, '(objectCategory=groupPolicyContainer)', [])
+    def user_from_sid(self, sid, attrs=[]):
+        res = ldap_search(self.l, self.__well_known_container('users'), 
ldap.SCOPE_SUBTREE, '(objectSID=%s)' % sid, stringify_ldap(attrs))
+        return res[0][1]
+
+    def get_domain_sid(self):
+        res = ldap_search(self.l, self.realm_to_dn(self.realm), 
ldap.SCOPE_BASE, "(objectClass=*)", [])
+        return ndr_unpack(security.dom_sid, res[0][1]["objectSid"][0])
+
+    def gpo_list(self, displayName=None, attrs=[]):
+        result = None
+        res = self.__well_known_container('system')
+        search_expr = '(objectClass=groupPolicyContainer)'
+        if displayName is not None:
+            search_expr = 
'(&(objectClass=groupPolicyContainer)(displayname=%s))' % 
ldb.binary_encode(displayName)
+        result = ldap_search(self.l, res, ldap.SCOPE_SUBTREE, search_expr, 
stringify_ldap(attrs))
+        result = stringify_ldap(result)
+        return result
 
     def set_attr(self, dn, key, value):
-        self.l.modify(dn, [(1, key, None), (0, key, value)])
+        ldap_modify(self.l, dn, stringify_ldap([(1, key, None), (0, key, 
value)]))
+
+    def create_gpo(self, displayName, container=None):
+        msg = self.gpo_list(displayName)
+        if len(msg) > 0:
+            print("A GPO already existing with name '%s'" % displayName)
+            return
 
-    def create_gpo(self, displayName):
         gpouuid = uuid.uuid4()
         realm_dn = self.realm_to_dn(self.realm)
         name = '{%s}' % str(gpouuid).upper()
         dn = 'CN=%s,CN=Policies,CN=System,%s' % (name, realm_dn)
-        ldap_mod = { 'displayName': [displayName], 'gPCFileSysPath': 
['\\\\%s\\SysVol\\%s\\Policies\\%s' % (self.realm, self.realm, name)], 
'objectClass': ['top', 'container', 'groupPolicyContainer'], 
'gPCFunctionalityVersion': ['2'], 'flags': ['0'], 'versionNumber': ['0'] }
+        unc_path = "\\\\%s\\sysvol\\%s\\Policies\\%s" % (self.realm, 
self.realm, name)
+        ldap_mod = { 'displayName': [displayName.encode('utf-8')], 
'gPCFileSysPath': [unc_path.encode('utf-8')], 'objectClass': 
[b'groupPolicyContainer'], 'gPCFunctionalityVersion': [b'2'], 'flags': [b'0'], 
'versionNumber': [b'0'] }
         # gPCMachineExtensionNames MUST be assigned as gpos are modified 
(currently not doing this!)
 
         machine_dn = 'CN=Machine,%s' % dn
         user_dn = 'CN=User,%s' % dn
-        sub_ldap_mod = { 'objectClass': ['top', 'container'] }
+        sub_ldap_mod = { 'objectClass': [b'container'] }
 
-        gpo = GPOConnection(self.lp, self.creds, 
ldap_mod['gPCFileSysPath'][-1])
+        gpo = GPOConnection(self.lp, self.creds, unc_path)
         try:
-            self.l.add_s(dn, addlist(ldap_mod))
-            self.l.add_s(machine_dn, addlist(sub_ldap_mod))
-            self.l.add_s(user_dn, addlist(sub_ldap_mod))
+            ldap_add(self.l, dn, addlist(stringify_ldap(ldap_mod)))
+            ldap_add(self.l, machine_dn, addlist(stringify_ldap(sub_ldap_mod)))
+            ldap_add(self.l, user_dn, addlist(stringify_ldap(sub_ldap_mod)))
+        except LdapException as e:
+            traceback.print_exc(file=sys.stderr)
+            sys.stderr.write('ldap.add_s: %s\n' % e.info if e.info else e.msg)
+        gpo.initialize_empty_gpo(displayName)
+        if container:
+            self.set_link(dn, container)
+
+    def set_link(self, gpo_dn, container_dn, disabled=False, enforced=False):
+        gplink_options = 0
+        if disabled:
+            gplink_options |= (1 << 0)
+        if enforced:
+            gplink_options |= (1 << 1)
+
+        # Check if valid Container DN
+        msg = ldap_search(self.l,
+                         container_dn, ldap.SCOPE_BASE,
+                         "(objectClass=*)",
+                         stringify_ldap(['gPLink']))[0][1]
+
+        # Update existing GPlinks or Add new one
+        existing_gplink = False
+        if 'gPLink' in msg:
+            gplist = parse_gplink(msg['gPLink'][0])
+            gplist = [gplist[k] for k in gplist]
+            existing_gplink = True
+            found = False
+            for g in gplist:
+                if g['dn'].lower() == gpo_dn.lower():
+                    found = True
+                    break
+            if found:
+                print("GPO '%s' already linked to this container" % gpo)
+                return
+            else:
+                gplist.insert(0, { 'dn' : gpo_dn, 'options' : gplink_options })
+        else:
+            gplist = []
+            gplist.append({ 'dn' : gpo_dn, 'options' : gplink_options })
+
+        gplink_str = encode_gplink(gplist)
 
-            gpo.initialize_empty_gpo()
-            # TODO: GPO links
+        if existing_gplink:
+            ldap_modify(self.l, container_dn, stringify_ldap([(1, 'gPLink', 
None), (0, 'gPLink', [gplink_str.encode('utf-8')])]))
+        else:
+            ldap_modify(self.l, container_dn, stringify_ldap([(0, 'gPLink', 
[gplink_str.encode('utf-8')])]))
+
+    def delete_link(self, gpo_dn, container_dn):
+        # Check if valid Container DN
+        msg = ldap_search(self.l,
+                         container_dn, ldap.SCOPE_BASE,
+                         "(objectClass=*)",
+                         stringify_ldap(['gPLink']))[0][1]
+
+        found = False
+        if 'gPLink' in msg:
+            gplist = parse_gplink(msg['gPLink'][0])
+            gplist = [gplist[k] for k in gplist]
+            for g in gplist:
+                if g['dn'].lower() == gpo_dn.lower():
+                    gplist.remove(g)
+                    found = True
+                    break
+        else:
+            raise Exception("No GPO(s) linked to this container")
+
+        if not found:
+            raise Exception("GPO '%s' not linked to this container" % gpo_dn)
+
+        if gplist:
+            gplink_str = encode_gplink(gplist)
+            ldap_modify(self.l, container_dn, 
stringify_ldap([(ldap.MOD_DELETE, 'gPLink', None), (ldap.MOD_ADD, 'gPLink', 
[gplink_str.encode('utf-8')])]))
+        else:
+            ldap_modify(self.l, container_dn, 
stringify_ldap([(ldap.MOD_DELETE, 'gPLink', None)]))
+
+    def delete_gpo(self, displayName):
+        msg = self.gpo_list(displayName)
+        if len(msg) == 0:
+            raise Exception("GPO '%s' does not exist" % displayName)
+
+        unc_path = msg[0][1]['gPCFileSysPath'][0]
+        gpo_dn = msg[0][1]['distinguishedName'][0]
+
+        # Remove links before deleting
+        linked_containers = self.get_gpo_containers(gpo_dn)
+        for container in linked_containers:
+            self.delete_link(gpo_dn, 
container['distinguishedName'][0].decode())
+
+        # Remove LDAP entries
+        ldap_delete(self.l, "CN=User,%s" % str(gpo_dn))
+        ldap_delete(self.l, "CN=Machine,%s" % str(gpo_dn))
+        ldap_delete(self.l, gpo_dn)
+        try:
+            # Remove GPO files
+            gpo = GPOConnection(self.lp, self.creds, unc_path)
+            gpo.cleanup_gpo()
         except Exception as e:
-            print str(e)
+            print(str(e))
+            traceback.print_exc(file=sys.stdout)
+
+    def get_gpo_containers(self, gpo):
+        '''lists dn of containers for a GPO'''
+
+        search_expr = "(&(objectClass=*)(gPLink=*%s*))" % gpo
+        msg = ldap_search(self.l, self.realm_to_dn(self.realm), 
ldap.SCOPE_SUBTREE, search_expr, [])
+        if not msg:
+            return []
+
+        return [res[1] for res in msg if type(res[1]) is dict]
+
+    def get_gpos_for_container(self, container_dn):
+        search_expr = '(distinguishedName=%s)' % container_dn
+        msg = ldap_search(self.l, self.realm_to_dn(self.realm), 
ldap.SCOPE_SUBTREE, search_expr, [])
+        if not msg:
+            return None
+
+        results = []
+        if 'gPLink' in msg[0][1]:
+            gpos = parse_gplink(msg[0][1]['gPLink'][-1])
+        else:
+            gpos = []
+        for gpo in gpos:
+            search_expr = '(distinguishedName=%s)' % gpos[gpo]['dn']
+            msg = ldap_search(self.l, self.realm_to_dn(self.realm), 
ldap.SCOPE_SUBTREE, search_expr, [])
+            results.append(msg[0])
+
+        return results
+
+    def get_containers_with_gpos(self):
+        search_expr = "(|(objectClass=organizationalUnit)(objectClass=domain))"
+        msg = ldap_search(self.l, self.realm_to_dn(self.realm), 
ldap.SCOPE_SUBTREE, search_expr, [])
+        if not msg:
+            return []
+
+        return [res[1] for res in msg if type(res[1]) is dict]
 
 class GPOConnection(GPConnection):
     def __init__(self, lp, creds, gpo_path):
         GPConnection.__init__(self, lp, creds)
+        [dom_name, service, self.path] = parse_unc(gpo_path)
         path_parts = [n for n in gpo_path.split('\\') if n]
-        self.path_start = '\\\\' + '\\'.join(path_parts[:2])
-        self.path = '\\'.join(path_parts[2:])
-        self.name = path_parts[-1]
+        self.path_start = '\\\\' + '\\'.join([dom_name, service])
+        self.name = gpo_path.split('\\')[-1]
         self.realm_dn = self.realm_to_dn(self.realm)
         self.gpo_dn = 'CN=%s,CN=Policies,CN=System,%s' % (self.name, 
self.realm_dn)
         try:
-            self.conn = smb.SMB(path_parts[0], path_parts[1], lp=self.lp, 
creds=self.creds)
-        except:
+            self.conn = smb.SMB(self.dc_hostname, service, lp=self.lp, 
creds=self.creds)
+        except Exception as e:
+            print ("Exception %s"%str(e))
+            traceback.print_exc(file=sys.stdout)
             self.conn = None
 
     def update_machine_gpe_ini(self, extension):
@@ -97,6 +409,8 @@
         machine_extension_versions = ''
         if ini_conf.has_option('General', 'MachineExtensionVersions'):
             machine_extension_versions = ini_conf.get('General', 
'MachineExtensionVersions').encode('ascii')
+        if type(machine_extension_versions) is bytes:
+            machine_extension_versions = 
machine_extension_versions.decode('utf-8')
         itr = re.finditer('\[%s:\d+]' % extension, machine_extension_versions)
         try:
             new_ext_str = machine_extension_versions[:m.start()] + 
machine_extension_versions[m.end():]
@@ -109,11 +423,35 @@
         ini_conf.set('General', 'MachineExtensionVersions', 
machine_extension_versions)
         self.write('Group Policy\\GPE.INI', ini_conf)
 
-    def initialize_empty_gpo(self):
+    def initialize_empty_gpo(self, displayName):
+        # Get new security descriptor
+        ds_sd_flags = ( security.SECINFO_OWNER |
+                        security.SECINFO_GROUP |
+                        security.SECINFO_DACL )
+        msg = self.gpo_list(displayName, 
attrs=stringify_ldap(['nTSecurityDescriptor']))
+        ds_sd_ndr = msg[0][1]['nTSecurityDescriptor'][0]
+        ds_sd = ndr_unpack(security.descriptor, ds_sd_ndr).as_sddl()
+
+        # Create a file system security descriptor
+        domain_sid = self.get_domain_sid()
+        sddl = dsacl2fsacl(ds_sd, domain_sid)
+        fs_sd = security.descriptor.from_sddl(sddl, domain_sid)
+
         self.__smb_mkdir_p('\\'.join([self.path, 'MACHINE']))
         self.__smb_mkdir_p('\\'.join([self.path, 'USER']))
+
+        # Set ACL
+        sio = ( security.SECINFO_OWNER |
+                security.SECINFO_GROUP |
+                security.SECINFO_DACL |
+                security.SECINFO_PROTECTED_DACL )
+        self.conn.set_acl(self.path, fs_sd, sio)
+
         self.__increment_gpt_ini()
 
+    def cleanup_gpo(self):
+        self.conn.deltree(self.path)
+
     def __get_gpo_version(self, ini_conf=None):
         if not ini_conf:
             ini_conf = self.parse('GPT.INI')
@@ -137,7 +475,7 @@
 
         if not ini_conf.has_section('General'):
             ini_conf.add_section('General')
-        ini_conf.set('General', 'Version', current)
+        ini_conf.set('General', 'Version', str(current))
         self.write('GPT.INI', ini_conf)
 
         self.set_attr(self.gpo_dn, 'versionNumber', current)
@@ -170,73 +508,77 @@
 
     def __parse_dn(self, dn):
         dn = dn % self.gpo_dn
-        try:
-            resp = self.l.search_s(dn, ldap.SCOPE_SUBTREE, 
'(objectCategory=packageRegistration)', [])
+        resp = ldap_search(self.l, dn, ldap.SCOPE_SUBTREE, 
'(objectCategory=packageRegistration)', [])
+        resp = stringify_ldap(resp)
+        if resp:
             keys = ['objectClass', 'msiFileList', 'msiScriptPath', 
'displayName', 'versionNumberHi', 'versionNumberLo']
             results = {a[-1]['name'][-1]: {k: a[-1][k] for k in a[-1].keys() 
if k in keys} for a in resp}
-        except Exception as e:
-            if 'No such object' in str(e):
-                results = {}
-            else:
-                raise
+        else:
+            results = {}
         return results
 
     def __mkdn_p(self, dn):
-        attrs = { 'objectClass' : ['top', 'container'] }
+        attrs = { 'objectClass' : [b'top', b'container'] }
         try:
-            self.l.add_s(dn, addlist(attrs))
-        except Exception as e:
-            if e.args[-1]['desc'] == 'No such object':
+            ldap_add(self.l, dn, addlist(stringify_ldap(attrs)))
+        except LdapException as e:
+            if e.msg == 'No such object':
                 self.__mkdn_p(','.join(dn.split(',')[1:]))
-            elif e.args[-1]['desc'] == 'Already exists':
+            elif e.msg == 'Already exists':
                 return
             else:
-                sys.stderr.write(e.args[-1]['info'])
+                traceback.print_exc(file=sys.stderr)
+                sys.stderr.write('ldap.add_s: %s\n' % e.info if e.info else 
e.msg)
         try:
-            self.l.add_s(dn, addlist(attrs))
-        except Exception as e:
-            if e.args[-1]['desc'] != 'Already exists':
-                sys.stderr.write(e.args[-1]['info'])
+            ldap_add(self.l, dn, addlist(stringify_ldap(attrs)))
+        except LdapException as e:
+            if e.msg != 'Already exists':
+                traceback.print_exc(file=sys.stderr)
+                sys.stderr.write('ldap.add_s: %s\n' % e.info if e.info else 
e.msg)
 
     def __write_dn(self, dn, ldap_config):
         for cn in ldap_config.keys():
             obj_dn = 'CN=%s,%s' % (cn, dn % self.gpo_dn)
             if 'objectClass' not in ldap_config[cn]:
-                ldap_config[cn]['objectClass'] = ['top', 'packageRegistration']
+                ldap_config[cn]['objectClass'] = [b'top', 
b'packageRegistration']
             if 'msiFileList' not in ldap_config[cn]:
-                ldap_config[cn]['msiFileList'] = 
os.path.splitext(ldap_config[cn]['msiScriptPath'][-1])[0] + '.zap'
+                ldap_config[cn]['msiFileList'] = 
[os.path.splitext(ldap_config[cn]['msiScriptPath'][-1])[0] + '.zap']
             self.__mkdn_p(','.join(obj_dn.split(',')[1:]))
+            ldap_config[cn] = dict_to_bytes(ldap_config[cn])
             try:
-                self.l.add_s(obj_dn, addlist(ldap_config[cn]))
-            except Exception as e:
-                if e.args[-1]['desc'] == 'Already exists':
-                    try:
-                        self.l.modify_s(obj_dn, modlist({}, ldap_config[cn]))
-                    except Exception as e:
-                        sys.stderr.write(e.args[-1]['info'])
+                ldap_add(self.l, obj_dn, 
addlist(stringify_ldap(ldap_config[cn])))
+            except LdapException as e:
+                if e.msg == 'Already exists':
+                    ldap_modify(self.l, obj_dn, modlist({}, 
stringify_ldap(ldap_config[cn])))
                 else:
-                    sys.stderr.write(e.args[-1]['info'])
+                    traceback.print_exc(file=sys.stderr)
+                    sys.stderr.write('ldap.add_s: %s\n' % e.info if e.info 
else e.msg)
 
-            if os.path.splitext(ldap_config[cn]['msiFileList'][-1])[-1] == 
'.zap':
+            if os.path.splitext(ldap_config[cn]['msiFileList'][-1])[-1] == 
b'.zap':
                 inf_conf = self.__parse_inf(ldap_config[cn]['msiFileList'][-1])
                 if not inf_conf.has_section('Application'):
                     inf_conf.add_section('Application')
-                inf_conf.set('Application', 'FriendlyName', 
ldap_config[cn]['displayName'][-1])
-                inf_conf.set('Application', 'SetupCommand', 'rpm -i "%s"' % 
ldap_config[cn]['msiScriptPath'][-1])
-                self.__write_inf(ldap_config[cn]['msiFileList'][-1], inf_conf)
+                inf_conf.set('Application', 'FriendlyName', 
ldap_config[cn]['displayName'][-1].decode('utf-8'))
+                inf_conf.set('Application', 'SetupCommand', 'rpm -i "%s"' % 
ldap_config[cn]['msiScriptPath'][-1].decode('utf-8'))
+                filename = 
ldap_config[cn]['msiFileList'][-1].split(self.path.encode('utf-8'))[-1]
+                self.__write_inf(filename, inf_conf)
 
     def __parse_inf(self, filename):
         inf_conf = ConfigParser()
         if self.conn:
             try:
                 policy = self.conn.loadfile('\\'.join([self.path, filename]))
-            except:
+            except Exception as e:
+                sys.stderr.write(str(e))
                 policy = ''
             inf_conf.optionxform=str
-            try:
+            if PY3 and type(policy) is str:
                 inf_conf.readfp(StringIO(policy))
-            except:
-                inf_conf.readfp(StringIO(policy.decode('utf-16')))
+            else:
+                try:
+                    inf_conf.readfp(StringIO(policy.decode('utf-8')))
+                except:
+                    inf_conf.readfp(StringIO(policy.decode('utf-16')))
         return inf_conf
 
     def __parse_xml(self, filename):
@@ -254,31 +596,35 @@
         try:
             self.conn.mkdir(directory)
         except Exception as e:
-            if e[0] == -1073741766: # 0xC000003A: STATUS_OBJECT_PATH_NOT_FOUND
+            if e.args[0] == 0xC000003A: # STATUS_OBJECT_PATH_NOT_FOUND
                 self.__smb_mkdir_p(directory)
-            elif e[0] == -1073741771: # 0xC0000035: 
STATUS_OBJECT_NAME_COLLISION
+            elif e.args[0] == 0xC0000035: # STATUS_OBJECT_NAME_COLLISION
                 pass
             else:
-                print e[1]
+                print(e.args[1])
         try:
             self.conn.mkdir(path)
         except Exception as e:
-            if e[0] == -1073741771: # 0xC0000035: STATUS_OBJECT_NAME_COLLISION
+            if e.args[0] == 0xC0000035: # STATUS_OBJECT_NAME_COLLISION
                 pass
             else:
-                print e[1]
+                print(e.args[1])
 
     def __write(self, filename, text):
+        if type(filename) is bytes:
+            filename = filename.decode('utf-8')
         path = '\\'.join([self.path, filename])
         filedir = os.path.dirname((path).replace('\\', '/')).replace('/', '\\')
         self.__smb_mkdir_p(filedir)
+        if PY3 and type(text) is str:
+            text = text.encode('utf-8')
         try:
             self.conn.savefile(path, text)
         except Exception as e:
-            if e[0] == -1073741766: # 0xC000003A: STATUS_OBJECT_PATH_NOT_FOUND
-                print e[1] % (path)
+            if e.args[0] == 0xC000003A: # STATUS_OBJECT_PATH_NOT_FOUND
+                print(e.args[1] % (path))
             else:
-                print e[1]
+                print(e)
 
     def __write_inf(self, filename, inf_config):
         out = StringIO()
@@ -287,21 +633,22 @@
         self.__write(filename, value)
 
     def __write_xml(self, filename, xml_config):
-        value = '<?xml version="1.0" encoding="utf-8"?>\r\n' + 
etree.tostring(xml_config, 'utf-8')
+        value = '<?xml version="1.0" encoding="utf-8"?>\r\n' + 
etree.tostring(xml_config, 'utf-8').decode('utf-8')
         self.__write(filename, value)
 
     def upload_file(self, local, remote_dir):
         remote_path = '\\'.join([self.path, remote_dir])
         self.__smb_mkdir_p(remote_path)
         if os.path.exists(local):
-            value = open(local).read()
+            value = open_bytes(local).read()
             filename = '\\'.join([remote_path, os.path.basename(local)])
+            if PY3 and type(value) is str:
+                value = value.encode('utf-8')
             try:
                 self.conn.savefile(filename, value)
             except Exception as e:
-                if e[0] == -1073741771: # 0xC0000035: 
STATUS_OBJECT_NAME_COLLISION
+                if e.args[0] == 0xC0000035: # STATUS_OBJECT_NAME_COLLISION
                     sys.stderr.write('The file \'%s\' already exists at \'%s\' 
and could not be saved.' % (os.path.basename(local), remote_path))
                 else:
-                    sys.stderr.write(e[1])
+                    sys.stderr.write(e.args[1])
             return filename
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/src/defaults.py 
new/yast-gpmc-1.3.1/src/defaults.py
--- old/yast-gpmc-1.0/src/defaults.py   2017-11-17 16:11:00.000000000 +0100
+++ new/yast-gpmc-1.3.1/src/defaults.py 2018-05-29 20:44:25.000000000 +0200
@@ -1,8 +1,11 @@
+from __future__ import absolute_import, division, print_function, 
unicode_literals
 import xml.etree.ElementTree as etree
 import uuid
 import os.path
 from subprocess import Popen, PIPE
 
+from yast import import_module
+import_module('UI')
 from yast import UI
 def select_script(title, policy, conn):
     full_path = UI.AskForExistingFile('/', '*.sh *.py *.pl', title)
@@ -15,12 +18,12 @@
 
 def query_rpm(filename):
     out,_ = Popen(['rpm', '-qip', filename], stdout=PIPE, 
stderr=PIPE).communicate()
-    return {line.split(':')[0].strip() : ':'.join(line.split(':')[1:]).strip() 
for line in out.strip().split('\n')}
+    return {line.split(b':')[0].strip() : 
b':'.join(line.split(b':')[1:]).strip() for line in out.strip().split(b'\n')}
 
 def select_exec(title, policy, conn):
     full_path = UI.AskForExistingFile('/', '*.rpm', title)
     rpm_data = query_rpm(full_path)
-    others = {'Name' : rpm_data['Name'], 'Version': rpm_data['Release']}
+    others = {'Name' : rpm_data[b'Name'], 'Version': rpm_data[b'Release']}
     path = '%s\\%s' % (conn.path_start, conn.upload_file(full_path, 
'MACHINE\\Applications'))
     return (others, path)
 
@@ -373,6 +376,17 @@
                     'options' : None,
                 },
             },
+            'partial' : {
+                'order' : 4,
+                'title' : 'Parital',
+                'get' : a.find('Properties').attrib['partial'] if a is not 
None and a.find('Properties') is not None and 'partial' in 
a.find('Properties').attrib.keys() else '',
+                'set' : (lambda v : a.find('Properties').set('partial', v)),
+                'valstr' : (lambda v : 'False' if v and int(v) == 0 else 
'True'),
+                'input' : {
+                    'type' : 'ComboBox',
+                    'options' : {'True' : '1', 'False' : '0'},
+                },
+            },
             'value' : {
                 'order' : 2,
                 'title' : 'Value',
@@ -880,5 +894,5 @@
 }
 
 if __name__ == "__main__":
-    print Policies
+    print(Policies)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/src/dialogs.py 
new/yast-gpmc-1.3.1/src/dialogs.py
--- old/yast-gpmc-1.0/src/dialogs.py    2017-11-17 16:11:09.000000000 +0100
+++ new/yast-gpmc-1.3.1/src/dialogs.py  2018-05-29 20:44:25.000000000 +0200
@@ -1,7 +1,22 @@
+from __future__ import absolute_import, division, print_function, 
unicode_literals
 from defaults import Policies, fetch_inf_value
-from complex import GPConnection, GPOConnection
+from complex import GPConnection, GPOConnection, dn_to_path, parse_gplink
+from yast import import_module
+import_module('Wizard')
+import_module('UI')
 from yast import *
 import re
+from functools import cmp_to_key
+from samba.dcerpc import security
+from samba.ndr import ndr_unpack
+import samba.security
+from samba.ntacls import dsacl2fsacl
+
+def have_x():
+    from subprocess import Popen, PIPE
+    p = Popen(['xset', '-q'], stdout=PIPE, stderr=PIPE)
+    return p.wait() == 0
+have_advanced_gui = have_x()
 
 selected_gpo = None
 
@@ -10,17 +25,28 @@
         global selected_gpo
         self.conn = GPOConnection(lp, creds, 
selected_gpo[1]['gPCFileSysPath'][-1])
 
+    def __reset(self):
+        global have_advanced_gui
+        if not have_advanced_gui:
+            Wizard.RestoreNextButton()
+        Wizard.SetContentsButtons('Group Policy Management Editor', 
self.__gpme_page(), 'Group Policy Management Editor', '', 'Close')
+        if have_advanced_gui:
+            Wizard.HideNextButton()
+        else:
+            Wizard.HideAbortButton()
+            Wizard.HideBackButton()
+
     def Show(self):
         if not self.conn:
             return Symbol('back')
-        Wizard.SetContentsButtons('Group Policy Management Editor', 
self.__gpme_page(), 'Group Policy Management Editor', 'Back', 'Close')
-        Wizard.DisableAbortButton()
+        self.__reset()
         UI.SetFocus('gpme_tree')
 
         policy = None
         while True:
             ret = UI.UserInput()
-            if str(ret) in ['back', 'abort', 'next']:
+            if str(ret) in ['back', 'abort', 'next', 'cancel']:
+                ret = 'back'
                 break
             elif str(ret) == 'gpme_tree':
                 policy = UI.QueryWidget('gpme_tree', 'CurrentItem')
@@ -72,7 +98,8 @@
 
     def __change_values_prompt(self, values):
         items = []
-        for value in sorted(values.iteritems(), cmp=(lambda a,b : 
a[-1]['order']-b[-1]['order'])):
+        ckey = cmp_to_key(lambda a,b : a[-1]['order']-b[-1]['order'])
+        for value in sorted(values.items(), key=ckey):
             k = value[0]
             if not value[-1]['input']:
                 continue
@@ -127,7 +154,7 @@
         header = Header(*header)
         for key in opts:
             values = sorted(opts[key]['values'].values(), key=(lambda x : 
x['order']))
-            vals = tuple([k['valstr'](k['get']) for k in values])
+            vals = tuple([k['valstr'](k['get'].decode('utf-8')) if 
type(k['get']) is bytes else k['valstr'](k['get']) for k in values])
             items.append(Item(Id(key), *vals))
         buttons = []
         if terms['add']:
@@ -224,35 +251,57 @@
 
 class GPMC:
     def __init__(self, lp, creds):
-        global selected_gpo
-        self.__get_creds(creds)
+        global selected_gpo, have_advanced_gui
         self.realm = lp.get('realm')
         self.lp = lp
         self.creds = creds
-        try:
-            self.q = GPConnection(lp, creds)
-            self.gpos = self.q.gpo_list()
-        except:
-            self.gpos = []
+        self.gpos = []
         selected_gpo = None
+        self.__setup_menus()
+        if have_advanced_gui:
+            Wizard.HideAbortButton()
+            Wizard.HideBackButton()
+            Wizard.HideNextButton()
+        self.got_creds = self.__get_creds(creds)
+        while self.got_creds:
+            try:
+                self.q = GPConnection(lp, creds)
+                self.gpos = self.q.gpo_list()
+                self.realm_dn = self.q.realm_to_dn(self.realm)
+                break
+            except Exception as e:
+                print(str(e))
+                creds.set_password('')
+                self.got_creds = self.__get_creds(creds)
+
+    def __setup_menus(self):
+        UI.WizardCommand(Term('DeleteMenus'))
+        UI.WizardCommand(Term('AddMenu', '&File', 'file-menu'))
+        UI.WizardCommand(Term('AddMenuEntry', 'file-menu', 'Close', 'abort'))
 
     def __get_creds(self, creds):
-        if not creds.get_username() or not creds.get_password():
-            UI.OpenDialog(self.__password_prompt(creds.get_username(), 
creds.get_password()))
+        if not creds.get_password():
+            UI.OpenDialog(self.__password_prompt(creds.get_username()))
             while True:
                 subret = UI.UserInput()
                 if str(subret) == 'creds_ok':
                     user = UI.QueryWidget('username_prompt', 'Value')
                     password = UI.QueryWidget('password_prompt', 'Value')
+                    UI.CloseDialog()
+                    if not password:
+                        return False
                     creds.set_username(user)
                     creds.set_password(password)
-                if str(subret) == 'creds_cancel' or str(subret) == 'creds_ok':
+                    return True
+                if str(subret) == 'creds_cancel':
                     UI.CloseDialog()
-                    break
+                    return False
+        return True
 
-    def __password_prompt(self, user, password):
+    def __password_prompt(self, user):
         return MinWidth(30, VBox(
-            Left(TextEntry(Id('username_prompt'), Opt('hstretch'), 
'Username')),
+            Left(Label('To continue, type an administrator password')),
+            Left(TextEntry(Id('username_prompt'), Opt('hstretch'), 'Username', 
user)),
             Left(Password(Id('password_prompt'), Opt('hstretch'), 'Password')),
             Right(HBox(
                 PushButton(Id('creds_ok'), 'OK'),
@@ -260,69 +309,168 @@
             ))
         ))
 
-    def __select_gpo(self, gpo_guid):
-        global selected_gpo
-        selected_gpo = None
+    def __find_gpo(self, gpo_guid):
+        fgpo = None
         for gpo in self.gpos:
             if gpo[1]['name'][-1] == gpo_guid:
-                selected_gpo = gpo
+                fgpo = gpo
                 break
-        return selected_gpo
+        return fgpo
+
+    def add_gpo(self, container=None):
+        UI.OpenDialog(self.__name_gpo())
+        sret = UI.UserInput()
+        if str(sret) == 'ok_name_gpo':
+            gpo_name = UI.QueryWidget('gpo_name_entry', 'Value')
+            self.q.create_gpo(gpo_name, container)
+        UI.CloseDialog()
+        try:
+            self.gpos = self.q.gpo_list()
+        except:
+            self.gpos = []
+
+    def del_gpo(self, displayName):
+        UI.OpenDialog(self.__request_delete_gpo())
+        sret = UI.UserInput()
+        if str(sret) == 'delete_gpo':
+            self.q.delete_gpo(displayName)
+        UI.CloseDialog()
+        try:
+            self.gpos = self.q.gpo_list()
+        except:
+            self.gpos = []
+
+    def del_link(self, child, parent):
+        UI.OpenDialog(self.__request_delete_link())
+        sret = UI.UserInput()
+        if str(sret) == 'delete_link':
+            self.q.delete_link(child, parent)
+        UI.CloseDialog()
+        try:
+            self.gpos = self.q.gpo_list()
+        except:
+            self.gpos = []
+
+    def __reset(self):
+        global have_advanced_gui
+        if not have_advanced_gui:
+            Wizard.RestoreBackButton()
+            Wizard.RestoreNextButton()
+            Wizard.RestoreAbortButton()
+        Wizard.SetContentsButtons('Group Policy Management Console', 
self.__gpmc_page(), self.__help(), 'Back', 'Edit GPO')
+        if have_advanced_gui:
+            Wizard.HideAbortButton()
+            Wizard.HideBackButton()
+            Wizard.HideNextButton()
+        else:
+            Wizard.DisableBackButton()
+            Wizard.DisableNextButton()
 
     def Show(self):
         global selected_gpo
-        Wizard.SetContentsButtons('Group Policy Management Console', 
self.__gpmc_page(), self.__help(), 'Back', 'Edit GPO')
-        Wizard.DisableBackButton()
-        Wizard.DisableNextButton()
+        if not self.got_creds:
+            return Symbol('abort')
+        self.__reset()
         UI.SetFocus('gpmc_tree')
 
         current_page = 'Domains'
         old_gpo_guid = None
         gpo_guid = None
         while True:
-            ret = UI.UserInput()
+            event = UI.WaitForEvent()
+            if 'WidgetID' in event:
+                ret = event['WidgetID']
+            elif 'ID' in event:
+                ret = event['ID']
+            else:
+                raise Exception('ID not found in response %s' % str(event))
             old_gpo_guid = gpo_guid
             gpo_guid = UI.QueryWidget('gpmc_tree', 'CurrentItem')
-            if str(ret) in ['back', 'abort']:
+            if str(ret) in ['back', 'abort', 'cancel']:
                 break
             elif str(ret) == 'next':
                 break
             elif str(ret) == 'add_gpo':
-                UI.OpenDialog(self.__name_gpo())
-                while True:
-                    sret = UI.UserInput()
-                    if str(sret) == 'ok_name_gpo':
-                        gpo_name = UI.QueryWidget('gpo_name_entry', 'Value')
-                        self.q.create_gpo(gpo_name)
-                    UI.CloseDialog()
-                    try:
-                        self.gpos = self.q.gpo_list()
-                    except:
-                        self.gpos = []
-                    Wizard.SetContentsButtons('Group Policy Management 
Console', self.__gpmc_page(), self.__help(), 'Back', 'Edit GPO')
-                    break
+                self.add_gpo()
+                self.__reset()
+                UI.ReplaceWidget('rightPane', self.__container(gpo_guid))
+                current_page = 'Realm'
+            elif str(ret) == 'del_gpo':
+                self.del_gpo(UI.QueryWidget('link_order', 'CurrentItem'))
+                self.__reset()
+                UI.ReplaceWidget('rightPane', self.__container(gpo_guid))
+                current_page = 'Realm'
+            elif ret == 'gpmc_tree' and event['EventReason'] == 
'ContextMenuActivated':
+                parent = UI.QueryWidget('gpmc_tree', 'CurrentBranch')[-2]
+                if gpo_guid == 'Group Policy Objects':
+                    UI.OpenContextMenu(self.__objs_context_menu())
+                elif gpo_guid != 'Domains' and self.__find_gpo(gpo_guid):
+                    if parent != 'Group Policy Objects' and parent != 
self.realm_dn:
+                        UI.OpenContextMenu(self.__gpo_context_menu(parent))
+                    else:
+                        UI.OpenContextMenu(self.__gpo_context_menu())
+                elif gpo_guid != 'Domains':
+                    UI.OpenContextMenu(self.__objs_context_menu(gpo_guid))
+            elif ret == 'edit_gpo':
+                selected_gpo = self.__find_gpo(gpo_guid)
+                ret = 'next'
+                break
+            elif ret == 'context_del_gpo':
+                selected_gpo = self.__find_gpo(gpo_guid)
+                current_page = self.del_gpo(selected_gpo[1]['displayName'][-1])
+                self.__reset()
+                UI.ReplaceWidget('rightPane', Empty())
+                current_page = None
+            elif ret == 'context_del_link':
+                selected_gpo = self.__find_gpo(gpo_guid)
+                parent = UI.QueryWidget('gpmc_tree', 'CurrentBranch')[-2]
+                self.del_link(selected_gpo[1]['distinguishedName'][-1], parent)
+                self.__reset()
+                UI.ReplaceWidget('rightPane', Empty())
+                current_page = None
+            elif ret == 'context_add_gpo':
+                self.add_gpo()
+                self.__reset()
+                UI.ReplaceWidget('rightPane', Empty())
+                current_page = None
+            elif ret == 'context_add_gpo_and_link':
+                self.add_gpo(gpo_guid)
+                self.__reset()
+                UI.ReplaceWidget('rightPane', Empty())
+                current_page = None
             elif UI.HasSpecialWidget('DumbTab'):
                 if gpo_guid == 'Domains':
                     if current_page != None:
                         Wizard.DisableNextButton()
                         UI.ReplaceWidget('rightPane', Empty())
                         current_page = None
-                elif gpo_guid == self.realm:
+                elif gpo_guid == self.realm_dn:
                     if current_page != 'Realm':
                         Wizard.DisableNextButton()
-                        UI.ReplaceWidget('rightPane', self.__realm())
+                        UI.ReplaceWidget('rightPane', 
self.__container(gpo_guid))
                         current_page = 'Realm'
+                    if ret == 'Linked Group Policy Objects':
+                        UI.ReplaceWidget(Id('realm_tabContainer'), 
self.__container_links(gpo_guid))
+                    elif ret == 'Delegation':
+                        UI.ReplaceWidget(Id('realm_tabContainer'), 
self.__realm_delegation())
+                    elif ret == 'Group Policy Inheritance':
+                        UI.ReplaceWidget(Id('realm_tabContainer'), 
self.__realm_inheritance())
+                elif gpo_guid == 'Group Policy Objects':
+                    if current_page != 'Group Policy Objects':
+                        Wizard.DisableNextButton()
+                        UI.ReplaceWidget('rightPane', Empty())
+                        current_page = 'Group Policy Objects'
+                elif gpo_guid.lower().startswith('ou='):
+                    UI.ReplaceWidget('rightPane', self.__container(gpo_guid))
+                    current_page = None
                 else:
-                    if str(ret) == 'advanced':
-                        self.__gpo_tab_adv(gpo_guid)
-                        continue
                     if current_page != 'Dumbtab' or old_gpo_guid != gpo_guid:
                         Wizard.EnableNextButton()
-                        selected_gpo = self.__select_gpo(gpo_guid)
+                        selected_gpo = self.__find_gpo(gpo_guid)
                         UI.ReplaceWidget('rightPane', self.__gpo_tab(gpo_guid))
                         current_page = 'Dumbtab'
                     if str(ret) == 'Scope':
-                        UI.ReplaceWidget('gpo_tabContents', 
self.__scope_page())
+                        UI.ReplaceWidget('gpo_tabContents', 
self.__scope_page(gpo_guid))
                     elif str(ret) == 'Details':
                         UI.ReplaceWidget('gpo_tabContents', 
self.__details_page(gpo_guid))
                     elif str(ret) == 'Settings':
@@ -342,6 +490,26 @@
 
         return Symbol(ret)
 
+    def __gpo_context_menu(self, parent=None):
+        if parent:
+            delete_id = 'context_del_link'
+        else:
+            delete_id = 'context_del_gpo'
+        return Term('menu', [
+            Item(Id('edit_gpo'), 'Edit...'),
+            Item(Id(delete_id), 'Delete')
+        ])
+
+    def __objs_context_menu(self, container=None):
+        if container:
+            return Term('menu', [
+                Item(Id('context_add_gpo_and_link'), 'Create a GPO in this 
domain, and Link it here...')
+            ])
+        else:
+            return Term('menu', [
+                Item(Id('context_add_gpo'), 'New')
+            ])
+
     def __name_gpo(self):
         return MinWidth(30, VBox(
             TextEntry(Id('gpo_name_entry'), Opt('hstretch'), 'GPO Name'),
@@ -351,13 +519,47 @@
             ))
         ))
 
+    def __request_delete_gpo(self):
+        return MinWidth(30, VBox(
+            Label('Do you want to delete this GPO and all links to it in 
this\ndomain? This will not delete links in other domains.'),
+            Right(HBox(
+                PushButton(Id('delete_gpo'), 'Yes'),
+                PushButton(Id('cancel_delete_gpo'), 'No'),
+            ))
+        ))
+
+    def __request_delete_link(self):
+        return MinWidth(30, VBox(
+            Label('Do you want to delete this link?\nThis will not delete the 
GPO itself.'),
+            Right(HBox(
+                PushButton(Id('delete_link'), 'OK'),
+                PushButton(Id('cancel_delete_link'), 'Cancel'),
+            ))
+        ))
+
     def __help(self):
         return 'Group Policy Management Console'
 
-    def __scope_page(self):
-        return RichText('Contents of the scope page')
+    def __scope_page(self, gpo_guid):
+        header = Header('Location', 'Enforced', 'Link Enabled', 'Path')
+        contents = []
+        links = self.q.get_gpo_containers(gpo_guid)
+        for link in links:
+            if b'domain' in link['objectClass']:
+                name = self.realm.lower()
+            else:
+                name = link['name'][-1]
+            gplist = parse_gplink(link['gPLink'][-1])[gpo_guid]
+            vals = Item(name, str(gplist['enforced']), str(gplist['enabled']), 
dn_to_path(self.realm.lower(), link['distinguishedName'][-1]))
+            contents.append(vals)
+        return VBox(
+            Left(Label('Links')),
+            Table(Id('scope_links'), header, contents)
+        )
 
     def __ms_time_to_readable(self, timestamp):
+        if type(timestamp) is bytes:
+            timestamp = timestamp.decode()
         m = 
re.match('(?P<year>\d\d\d\d)(?P<month>\d\d)(?P<day>\d\d)(?P<hour>\d\d)(?P<minute>\d\d)(?P<second>\d\d)\..*',
 timestamp)
         if m:
             return '%s/%s/%s %s:%s:%s UTC' % (m.group('month'), 
m.group('day'), m.group('year'), m.group('hour'), m.group('minute'), 
m.group('second'))
@@ -375,6 +577,16 @@
             status_selection[0] = True
         combo_options = [Item('All settings disabled', status_selection[0]), 
Item('Computer configuration settings disabled', status_selection[1]), 
Item('Enabled', status_selection[2]), Item('User configuration settings 
disabled', status_selection[3])]
 
+
+        msg = self.q.gpo_list(selected_gpo[1]['displayName'][-1], 
attrs=['nTSecurityDescriptor'])
+        if msg:
+            ds_sd_ndr = msg[0][1]['nTSecurityDescriptor'][0]
+            ds_sd = ndr_unpack(security.descriptor, ds_sd_ndr)
+            owner_obj = self.q.user_from_sid(ds_sd.owner_sid)
+            owner = owner_obj['sAMAccountName'][-1].decode('utf-8')
+        else:
+            owner = 'Unknown'
+
         return Top(
             HBox(
                 HWeight(1, VBox(
@@ -389,7 +601,7 @@
                 )),
                 HWeight(2, VBox(
                     Left(Label(self.realm)), VSpacing(),
-                    Left(Label('Unknown')), VSpacing(),
+                    Left(Label(owner)), VSpacing(),
                     
Left(Label(self.__ms_time_to_readable(selected_gpo[1]['whenCreated'][-1]))), 
VSpacing(),
                     
Left(Label(self.__ms_time_to_readable(selected_gpo[1]['whenChanged'][-1]))), 
VSpacing(),
                     Left(Label('%d' % 
(int(selected_gpo[1]['versionNumber'][-1]) >> 16))), VSpacing(),
@@ -401,30 +613,74 @@
         )
 
     def __settings_page(self):
-        return RichText('Contents of the settings page')
+        return Top(HBox(Empty()))
 
     def __delegation_page(self):
-        return RichText('Contents of the delegation page')
+        return Top(HBox(Empty()))
 
     def __forest(self):
+        gp_containers = self.q.get_containers_with_gpos()
         items = []
         for gpo in self.gpos:
             items.append(Item(Id(gpo[1]['name'][-1]), 
gpo[1]['displayName'][-1]))
-        forest = [Item('Domains', True, [Item(self.realm, True, items)])]
-        contents = Tree(Id('gpmc_tree'), Opt('notify'), 'Group Policy 
Management', forest)
+        folders = []
+        for container in gp_containers:
+            if b'domain' in container['objectClass']:
+                gplists = parse_gplink(container['gPLink'][-1])
+                for gpname in gplists:
+                    gpo = self.__find_gpo(gpname)
+                    displayName = gpo[1]['displayName'][-1] if gpo else gpname
+                    folders.append(Item(Id(gpname), displayName))
+            else:
+                container_objs = []
+                if 'gPLink' in container:
+                    gplists = parse_gplink(container['gPLink'][-1])
+                else:
+                    gplists = []
+                for gpname in gplists:
+                    gpo = self.__find_gpo(gpname)
+                    displayName = gpo[1]['displayName'][-1] if gpo else gpname
+                    container_objs.append(Item(Id(gpname), displayName))
+                folders.append(Item(Id(container['distinguishedName'][-1]), 
container['name'][-1], False, container_objs))
+        folders.append(Item('Group Policy Objects', False, items))
+        forest = [
+            Item('Domains', True,
+            [
+                Item(Id(self.realm_dn), self.realm, True, folders)
+            ])
+        ]
+        contents = Tree(Id('gpmc_tree'), Opt('notify', 'immediate', 
'notifyContextMenu'), 'Group Policy Management', forest)
         
         return contents
 
-    def __realm(self):
+    def __container(self, dn):
+        global have_advanced_gui
+        if have_advanced_gui:
+            buttons = Empty()
+        else:
+            buttons = Right(HBox(
+                PushButton(Id('del_gpo'), 'Delete GPO'),
+                PushButton(Id('add_gpo'), 'Create a GPO')
+            ))
         return VBox(
-            Frame(self.realm, DumbTab(['Linked Group Policy Objects', 'Group 
Policy Inheritance', 'Delegation'], ReplacePoint(Id('realm_tabContainer'), 
self.__realm_links()))),
-            Right(HBox(PushButton(Id('add_gpo'), 'Create a GPO'))),
+            Frame(self.realm, DumbTab([
+                'Linked Group Policy Objects',
+                #'Group Policy Inheritance',
+                #'Delegation'
+            ], ReplacePoint(Id('realm_tabContainer'), 
self.__container_links(dn)))),
+            buttons,
         )
 
-    def __realm_links(self):
+    def __realm_delegation(self):
+        return Top(HBox(Empty()))
+
+    def __realm_inheritance(self):
+        return Top(HBox(Empty()))
+
+    def __container_links(self, dn):
         header = Header('Link Order', 'GPO', 'Enforced', 'Link Enabled', 'GPO 
Status', 'WMI Filter', 'Modified', 'Domain')
         contents = []
-        for gpo in self.gpos:
+        for gpo in self.q.get_gpos_for_container(dn):
             status = ''
             if gpo[1]['flags'][-1] == '0':
                 status = 'Enabled'
@@ -441,11 +697,15 @@
 
     def __gpo_tab(self, gpo_guid):
         global selected_gpo
+        if not selected_gpo:
+            return Top(HBox(Empty()))
         gpo_name = selected_gpo[1]['displayName'][-1]
-        return Frame(gpo_name, ReplacePoint(Id('gpo_tabContainer'), 
VBox(self.__details_page(gpo_guid), Right(PushButton(Id('advanced'), 
'Advanced')))))
-
-    def __gpo_tab_adv(self, gpo_guid):
-        UI.ReplaceWidget('gpo_tabContainer', DumbTab(Id('gpo_tab'), ['Scope', 
'Details', 'Settings', 'Delegation'], ReplacePoint(Id('gpo_tabContents'), 
self.__scope_page())))
+        return Frame(gpo_name, DumbTab(Id('gpo_tab'), [
+            'Scope',
+            Item('Details', True),
+            #'Settings',
+            #'Delegation'
+        ], ReplacePoint(Id('gpo_tabContents'), self.__details_page(gpo_guid))))
 
     def __gpmc_page(self):
         return HBox(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/src/gpmc.desktop.in 
new/yast-gpmc-1.3.1/src/gpmc.desktop.in
--- old/yast-gpmc-1.0/src/gpmc.desktop.in       2017-11-17 16:16:59.000000000 
+0100
+++ new/yast-gpmc-1.3.1/src/gpmc.desktop.in     2018-05-29 20:44:25.000000000 
+0200
@@ -16,7 +16,7 @@
 X-SuSE-YaST-AutoInstResource=gpmc
 
 Icon=yast-gpmc
-Exec="/usr/bin/xdg-su -c '@CLIENTDIR@/gpmc.py'"
+Exec=yast2 '@CLIENTDIR@/gpmc.py'
 
 Name=Group Policy Management Console
 GenericName=Manage Group Policy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/src/gpmc.py 
new/yast-gpmc-1.3.1/src/gpmc.py
--- old/yast-gpmc-1.0/src/gpmc.py       2017-11-17 16:11:18.000000000 +0100
+++ new/yast-gpmc-1.3.1/src/gpmc.py     1970-01-01 01:00:00.000000000 +0100
@@ -1,56 +0,0 @@
-import sys, os, traceback
-import optparse
-
-from samba.param import LoadParm
-from samba.credentials import Credentials
-
-from subprocess import Popen, PIPE
-
-sys.path.append(sys.path[0]+"/../include/gpmc")
-
-from wizards import GPMCSequence
-
-if __name__ == "__main__":
-    parser = optparse.OptionParser('gpmc [options]')
-
-    # Yast command line args
-    yast_opt = optparse.OptionGroup(parser, 'Command line options for the 
YaST2 Qt UI')
-    yast_opt.add_option('--nothreads', help='run without additional UI 
threads', action='store_true')
-    yast_opt.add_option('--fullscreen', help='use full screen for 
`opt(`defaultsize) dialogs', action='store_true')
-    yast_opt.add_option('--noborder', help='no window manager border for 
`opt(`defaultsize) dialogs', action='store_true')
-    yast_opt.add_option('--auto-fonts', help='automatically pick fonts, 
disregard Qt standard settings', action='store_true')
-    yast_opt.add_option('--macro', help='play a macro right on startup')
-    parser.add_option_group(yast_opt)
-
-    # Get the command line options
-    parser.add_option('--ncurses', dest='ncurses', help='Whether to run yast 
via ncurses interface', action='store_true')
-    credopts = optparse.OptionGroup(parser, 'Credentials Options')
-    credopts.add_option('--password', dest='password', help='Password')
-    credopts.add_option('-U', '--username', dest='username', help='Username')
-    credopts.add_option('--krb5-ccache', dest='krb5_ccache', help='Kerberos 
Credentials cache')
-    parser.add_option_group(credopts)
-
-    # Set the options and the arguments
-    (opts, args) = parser.parse_args()
-
-    # Set the loadparm context
-    lp = LoadParm()
-    if os.getenv("SMB_CONF_PATH") is not None:
-        lp.load(os.getenv("SMB_CONF_PATH"))
-    else:
-        lp.load_default()
-
-    # Initialize the session
-    creds = Credentials()
-    if opts.username and opts.password:
-        creds.set_username(opts.username)
-        creds.set_password(opts.password)
-    elif opts.krb5_ccache:
-        creds.set_named_ccache(opts.krb5_ccache)
-    creds.guess(lp)
-
-    try:
-        GPMCSequence(lp, creds)
-    except:
-        traceback.print_exc(file=sys.stdout)
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/src/gpmc.py.in 
new/yast-gpmc-1.3.1/src/gpmc.py.in
--- old/yast-gpmc-1.0/src/gpmc.py.in    1970-01-01 01:00:00.000000000 +0100
+++ new/yast-gpmc-1.3.1/src/gpmc.py.in  2018-05-29 20:44:25.000000000 +0200
@@ -0,0 +1,60 @@
+from __future__ import absolute_import, division, print_function, 
unicode_literals
+import sys, os, traceback
+import optparse
+
+from samba.param import LoadParm
+from samba.credentials import Credentials
+
+from subprocess import Popen, PIPE
+
+sys.path.append('@INCLUDEDIR@')
+
+from wizards import GPMCSequence
+
+if __name__ == "__main__":
+    parser = optparse.OptionParser('gpmc [options]')
+
+    # Yast command line args
+    yast_opt = optparse.OptionGroup(parser, 'Command line options for the 
YaST2 Qt UI')
+    yast_opt.add_option('--nothreads', help='run without additional UI 
threads', action='store_true')
+    yast_opt.add_option('--fullscreen', help='use full screen for 
`opt(`defaultsize) dialogs', action='store_true')
+    yast_opt.add_option('--noborder', help='no window manager border for 
`opt(`defaultsize) dialogs', action='store_true')
+    yast_opt.add_option('--auto-fonts', help='automatically pick fonts, 
disregard Qt standard settings', action='store_true')
+    yast_opt.add_option('--macro', help='play a macro right on startup')
+    parser.add_option_group(yast_opt)
+
+    # Get the command line options
+    parser.add_option('--ncurses', dest='ncurses', help='Whether to run yast 
via ncurses interface', action='store_true')
+    credopts = optparse.OptionGroup(parser, 'Credentials Options')
+    credopts.add_option('--password', dest='password', help='Password')
+    credopts.add_option('-U', '--username', dest='username', help='Username')
+    credopts.add_option('--krb5-ccache', dest='krb5_ccache', help='Kerberos 
Credentials cache')
+    parser.add_option_group(credopts)
+
+    # Set the options and the arguments
+    (opts, args) = parser.parse_args()
+
+    # Set the loadparm context
+    lp = LoadParm()
+    if os.getenv("SMB_CONF_PATH") is not None:
+        lp.load(os.getenv("SMB_CONF_PATH"))
+    else:
+        try:
+            lp.load_default()
+        except RuntimeError:
+            pass
+
+    # Initialize the session
+    creds = Credentials()
+    if opts.username and opts.password:
+        creds.set_username(opts.username)
+        creds.set_password(opts.password)
+    elif opts.krb5_ccache:
+        creds.set_named_ccache(opts.krb5_ccache)
+    creds.guess(lp)
+
+    try:
+        GPMCSequence(lp, creds)
+    except:
+        traceback.print_exc(file=sys.stdout)
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/src/wizards.py 
new/yast-gpmc-1.3.1/src/wizards.py
--- old/yast-gpmc-1.0/src/wizards.py    2017-11-17 16:11:27.000000000 +0100
+++ new/yast-gpmc-1.3.1/src/wizards.py  2018-05-29 20:44:25.000000000 +0200
@@ -1,4 +1,9 @@
+from __future__ import absolute_import, division, print_function, 
unicode_literals
 from dialogs import GPMC, GPME
+from yast import import_module
+import_module('Wizard')
+import_module('UI')
+import_module('Sequencer')
 from yast import Wizard, UI, Sequencer, Code, Symbol
 
 def GPMCSequence(lp, creds):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast-gpmc-1.0/yast2-gpmc.spec.in 
new/yast-gpmc-1.3.1/yast2-gpmc.spec.in
--- old/yast-gpmc-1.0/yast2-gpmc.spec.in        2017-11-08 19:20:04.000000000 
+0100
+++ new/yast-gpmc-1.3.1/yast2-gpmc.spec.in      1970-01-01 01:00:00.000000000 
+0100
@@ -1,31 +0,0 @@
-@HEADER-COMMENT@
-
-@HEADER@
-Requires:      yast2
-BuildRequires: perl-XML-Writer update-desktop-files yast2 yast2-devtools 
yast2-testsuite
-
-BuildArchitectures:    noarch
-
-Summary:       Configuration of gpmc
-
-%description
--
-
-@PREP@
-
-@BUILD@
-
-@INSTALL@
-
-@CLEAN@
-
-%files
-%defattr(-,root,root)
-%dir @yncludedir@/gpmc
-@yncludedir@/gpmc/*
-@clientdir@/gpmc.ycp
-@clientdir@/gpmc_*.ycp
-@moduledir@/Gpmc.*
-@moduledir@/Gpmc2.*
-@desktopdir@/gpmc.desktop
-%doc @docdir@


Reply via email to