The branch, master has been updated
       via  21a88df032c samba-tool user disable: add new 
--remove-supplemental-groups option
       via  f924724e462 samba-tool user disable: make sure that filter matches 
only one user
       via  b19445a8f00 samba-tool user disable: rename filter variable to 
search_filter
       via  462d0d667c0 samba-tool user disable: set proper --filter option 
description
       via  dd0892a1be8 samba-tool group removemembers: avoid python backtrace 
on error
       via  300e14674cf python/samdb: no need to set member_base_dn multiple 
times
       via  c9d8e96d2b1 python/samdb: fix group member removal by SID
       via  a74bc627794 python/samdb: fix check which checks if user is already 
member of group
       via  190a635b38d python/samdb: rename filter variable to search_filter
       via  a4f84ba8970 python/samdb: add missing function parameter description
       via  bba6bb164e8 python/samdb: fix attribute name in parameter 
description
      from  2c44022c512 third_party: Update socket_wrapper to version 1.4.4

https://git.samba.org/?p=samba.git;a=shortlog;h=master


- Log -----------------------------------------------------------------
commit 21a88df032c6e481b547fad2c48d14243a514035
Author: Björn Baumbach <[email protected]>
Date:   Wed Nov 20 17:54:17 2024 +0100

    samba-tool user disable: add new --remove-supplemental-groups option
    
    Removes all supplemental groups from a user, what is commonly
    wanted when a user is disabled.
    
    Pair-programmed-with: Stefan Metzmacher <[email protected]>
    Signed-off-by: Björn Baumbach <[email protected]>
    Signed-off-by: Stefan Metzmacher <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>
    
    Autobuild-User(master): Björn Baumbach <[email protected]>
    Autobuild-Date(master): Thu Jan 23 19:51:05 UTC 2025 on atb-devel-224

commit f924724e462577d2fb6a5077a5ef7b5254cdec53
Author: Björn Baumbach <[email protected]>
Date:   Wed Nov 20 17:24:10 2024 +0100

    samba-tool user disable: make sure that filter matches only one user
    
    toggle_userAccountFlags() can only handle one user.
    
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit b19445a8f00cbb64e81e604b818b905f6ae17011
Author: Björn Baumbach <[email protected]>
Date:   Wed Nov 20 17:10:12 2024 +0100

    samba-tool user disable: rename filter variable to search_filter
    
    filter() is a Python built-in function to filter iterables.
    
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit 462d0d667c09012f7c81d1b94f739ef8ebe5fd03
Author: Björn Baumbach <[email protected]>
Date:   Wed Nov 20 17:03:24 2024 +0100

    samba-tool user disable: set proper --filter option description
    
    Seems to be copied from samba-tool user setpassword command.
    
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit dd0892a1be8cb8715b59b542b688b28a7fafa150
Author: Björn Baumbach <[email protected]>
Date:   Tue Nov 26 17:47:30 2024 +0100

    samba-tool group removemembers: avoid python backtrace on error
    
    Pair-programmed-with: Stefan Metzmacher <[email protected]>
    Signed-off-by: Björn Baumbach <[email protected]>
    Signed-off-by: Stefan Metzmacher <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit 300e14674cfbf051f50686297e067a7b96bf091f
Author: Björn Baumbach <[email protected]>
Date:   Fri Nov 22 22:35:29 2024 +0100

    python/samdb: no need to set member_base_dn multiple times
    
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit c9d8e96d2b1f12605585c0c2b3a037f9e45fe64c
Author: Björn Baumbach <[email protected]>
Date:   Tue Nov 26 15:46:02 2024 +0100

    python/samdb: fix group member removal by SID
    
    Otherwise the removal of groupmembers by SID fails silently, because the
    DN does not match the the DN in group member list.
    
    Pair-programmed-with: Stefan Metzmacher <[email protected]>
    Signed-off-by: Stefan Metzmacher <[email protected]>
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit a74bc6277944efe417fb474a03728742ba1d89f0
Author: Björn Baumbach <[email protected]>
Date:   Mon Nov 25 14:05:40 2024 +0100

    python/samdb: fix check which checks if user is already member of group
    
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit 190a635b38d9f2a20dff1f2942872a08338060bf
Author: Björn Baumbach <[email protected]>
Date:   Wed Nov 20 23:28:51 2024 +0100

    python/samdb: rename filter variable to search_filter
    
    filter() is a Python built-in function to filter iterables.
    
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit a4f84ba8970457117040ef69f64ac4c284b12f8c
Author: Björn Baumbach <[email protected]>
Date:   Wed Nov 20 21:33:49 2024 +0100

    python/samdb: add missing function parameter description
    
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

commit bba6bb164e8672d7bb7581c7df06435065e61330
Author: Björn Baumbach <[email protected]>
Date:   Wed Sep 18 19:22:29 2024 +0200

    python/samdb: fix attribute name in parameter description
    
    Signed-off-by: Björn Baumbach <[email protected]>
    Reviewed-by: Jule Anger <[email protected]>

-----------------------------------------------------------------------

Summary of changes:
 docs-xml/manpages/samba-tool.8.xml    |   8 ++
 python/samba/netcmd/group.py          |   2 +-
 python/samba/netcmd/user/disable.py   |  64 ++++++++++++++--
 python/samba/samdb.py                 | 136 ++++++++++++++++++++++++++++------
 python/samba/tests/samba_tool/user.py |  78 +++++++++++++++++++
 5 files changed, 259 insertions(+), 29 deletions(-)


Changeset truncated at 500 lines:

diff --git a/docs-xml/manpages/samba-tool.8.xml 
b/docs-xml/manpages/samba-tool.8.xml
index 62ce4e690d4..3bd7b75a73e 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -2816,6 +2816,14 @@
 <refsect3>
        <title>user disable <replaceable>username</replaceable></title>
        <para>Disable a user account.</para>
+       <variablelist>
+       <varlistentry>
+       <term>--remove-supplemental-groups</term>
+       <listitem><para>
+       Remove user from all groups, but keep the primary group.
+       </para></listitem>
+       </varlistentry>
+       </variablelist>
 </refsect3>
 
 <refsect3>
diff --git a/python/samba/netcmd/group.py b/python/samba/netcmd/group.py
index a7055602253..ea17cebe1e5 100644
--- a/python/samba/netcmd/group.py
+++ b/python/samba/netcmd/group.py
@@ -584,7 +584,7 @@ Example2 shows how to remove a single user account, User2, 
from the supergroup A
                                            member_base_dn=member_base_dn)
         except Exception as e:
             # FIXME: Catch more specific exception
-            raise CommandError('Failed to remove members %r from group "%s"' % 
(listofmembers, groupname), e)
+            raise CommandError('Failed to remove members %r from group "%s" - 
%s' % (listofmembers, groupname, e))
         self.outf.write("Removed members from group %s\n" % groupname)
 
 
diff --git a/python/samba/netcmd/user/disable.py 
b/python/samba/netcmd/user/disable.py
index 5042eead3a5..d959f6d45f8 100644
--- a/python/samba/netcmd/user/disable.py
+++ b/python/samba/netcmd/user/disable.py
@@ -24,6 +24,8 @@ from samba import ldb
 from samba.auth import system_session
 from samba.netcmd import Command, CommandError, Option
 from samba.samdb import SamDB
+from samba.dcerpc import security
+from samba.ndr import ndr_unpack
 
 
 class cmd_user_disable(Command):
@@ -34,7 +36,13 @@ class cmd_user_disable(Command):
     takes_options = [
         Option("-H", "--URL", help="LDB URL for database or target server", 
type=str,
                metavar="URL", dest="H"),
-        Option("--filter", help="LDAP Filter to set password on", type=str),
+        Option("--filter",
+               help="LDAP filter to select user",
+               type=str,
+               dest="search_filter"),
+        Option("--remove-supplemental-groups",
+               help="Remove user's supplemental groups",
+               action="store_true"),
     ]
 
     takes_args = ["username?"]
@@ -46,19 +54,61 @@ class cmd_user_disable(Command):
     }
 
     def run(self, username=None, sambaopts=None, credopts=None,
-            versionopts=None, filter=None, H=None):
-        if username is None and filter is None:
+            versionopts=None, search_filter=None, H=None,
+            remove_supplemental_groups=False):
+        if username is None and search_filter is None:
             raise CommandError("Either the username or '--filter' must be 
specified!")
 
-        if filter is None:
-            filter = "(&(objectClass=user)(sAMAccountName=%s))" % 
(ldb.binary_encode(username))
+        if search_filter is None:
+            search_filter = "(&(objectClass=user)(sAMAccountName=%s))" % (
+                ldb.binary_encode(username))
 
         lp = sambaopts.get_loadparm()
         creds = credopts.get_credentials(lp, fallback_machine=True)
 
         samdb = SamDB(url=H, session_info=system_session(),
                       credentials=creds, lp=lp)
+
+        samdb.transaction_start()
+        try:
+            res = samdb.search(base=samdb.domain_dn(),
+                               expression=search_filter,
+                               scope=ldb.SCOPE_SUBTREE,
+                               controls=["extended_dn:1:1"],
+                               attrs=["objectSid", "memberOf"])
+            user_groups = res[0].get("memberOf")
+            if user_groups is None:
+                user_groups = []
+            user_binary_sid = res[0].get("objectSid", idx=0)
+            user_sid = ndr_unpack(security.dom_sid, user_binary_sid)
+        except IndexError:
+            samdb.transaction_cancel()
+            raise CommandError("Unable to find user '%s'" % (
+                               username or search_filter))
+        except Exception as msg:
+            samdb.transaction_cancel()
+            raise CommandError("Failed to find user '%s': '%s'" % (
+                               username or search_filter, msg))
+        if len(res) > 1:
+            samdb.transaction_cancel()
+            raise CommandError("Found more than one user '%s'" % (
+                               username or search_filter))
+
+        if remove_supplemental_groups:
+            for user_group in user_groups:
+                try:
+                    samdb.add_remove_group_members(str(user_group),
+                                                   [str(user_sid)],
+                                                   add_members_operation=False)
+                except Exception as msg:
+                    samdb.transaction_cancel()
+                    raise CommandError("Failed to remove user from group "
+                                       "'%s': %s" % (user_group, msg))
+
         try:
-            samdb.disable_account(filter)
+            samdb.disable_account(search_filter)
         except Exception as msg:
-            raise CommandError("Failed to disable user '%s': %s" % (username 
or filter, msg))
+            samdb.transaction_cancel()
+            raise CommandError("Failed to disable user '%s': %s" % (
+                username or search_filter, msg))
+        samdb.transaction_commit()
diff --git a/python/samba/samdb.py b/python/samba/samdb.py
index eced40a6541..0545aed98eb 100644
--- a/python/samba/samdb.py
+++ b/python/samba/samdb.py
@@ -35,6 +35,7 @@ from samba.common import normalise_int32
 from samba.common import get_bytes, cmp
 from samba.dcerpc import security
 from samba import is_ad_dc_built
+from samba import NTSTATUSError, ntstatus
 import binascii
 
 __docformat__ = "restructuredText"
@@ -132,7 +133,7 @@ class SamDB(samba.Ldb):
         """Disables an account
 
         :param search_filter: LDAP filter to find the user (eg
-            samccountname=name)
+            sAMAccountName=name)
         """
 
         flags = samba.dsdb.UF_ACCOUNTDISABLE
@@ -142,7 +143,7 @@ class SamDB(samba.Ldb):
         """Enables an account
 
         :param search_filter: LDAP filter to find the user (eg
-            samccountname=name)
+            sAMAccountName=name)
         """
 
         flags = samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_PASSWD_NOTREQD
@@ -153,7 +154,7 @@ class SamDB(samba.Ldb):
         """Toggle_userAccountFlags
 
         :param search_filter: LDAP filter to find the user (eg
-            samccountname=name)
+            sAMAccountName=name)
         :param flags: samba.dsdb.UF_* flags
         :param on: on=True (default) => set, on=False => unset
         :param strict: strict=False (default) ignore if no action is needed
@@ -197,7 +198,7 @@ userAccountControl: %u
         """Forces a password change at next login
 
         :param search_filter: LDAP filter to find the user (eg
-            samccountname=name)
+            sAMAccountName=name)
         """
         res = self.search(base=self.domain_dn(), scope=ldb.SCOPE_SUBTREE,
                           expression=search_filter, attrs=[])
@@ -365,43 +366,116 @@ lockoutTime: 0
 
         return filter
 
-    def add_remove_group_members(self, groupname, members,
+    def add_remove_group_members(self, group, members,
                                  add_members_operation=True,
                                  member_types=None,
                                  member_base_dn=None):
         """Adds or removes group members
 
-        :param groupname: Name of the target group
+        :param group: sAMAccountName, DN, SID or GUID of the target group
         :param members: list of group members
         :param add_members_operation: Defines if its an add or remove
             operation
+        :param member_types: List of object types, used to filter the search
+            for the specified members
+        :param member_base_dn: Base dn for member search
         """
         if member_types is None:
             member_types = ['user', 'group', 'computer']
 
-        groupfilter = "(&(sAMAccountName=%s)(objectCategory=%s,%s))" % (
-            ldb.binary_encode(groupname), 
"CN=Group,CN=Schema,CN=Configuration", self.domain_dn())
+        if member_base_dn is None:
+            member_base_dn = self.domain_dn()
+
+        partial_groupfilter = None
+
+        group_sid = None
+        try:
+            group_sid = security.dom_sid(group)
+        except ValueError:
+            pass
+        if group_sid is not None:
+            partial_groupfilter = "(objectClass=*)"
+
+        group_guid = None
+        if partial_groupfilter is None:
+            try:
+                group_guid = misc.GUID(group)
+            except NTSTATUSError as e:
+                (status, _) = e.args
+                if status != ntstatus.NT_STATUS_INVALID_PARAMETER:
+                    raise e
+            if group_guid is not None:
+                partial_groupfilter = "(objectClass=*)"
+
+        if partial_groupfilter is None:
+            group_dn = None
+            try:
+                if isinstance(group, ldb.Dn):
+                    group_dn = ldb.Dn(self, group.extended_str(1))
+                else:
+                    group_dn = ldb.Dn(self, str(group))
+            except ValueError:
+                pass
+            if group_dn is not None:
+                group_b_sid = group_dn.get_extended_component("SID")
+                group_b_guid = group_dn.get_extended_component("GUID")
+                if group_b_sid is not None:
+                    group_sid = ndr_unpack(security.dom_sid, group_b_sid)
+                    partial_groupfilter = "(objectClass=*)"
+                elif group_b_guid is not None:
+                    group_guid = ndr_unpack(misc.GUID, group_b_guid)
+                    partial_groupfilter = "(objectClass=*)"
+                else:
+                    search_base = str(group_dn)
+                    search_scope = ldb.SCOPE_BASE
+
+        if group_sid is not None:
+            search_base = '<SID=%s>' % group_sid
+            search_scope = ldb.SCOPE_BASE
+
+        if group_guid is not None:
+            search_base = '<GUID=%s>' % group_guid
+            search_scope = ldb.SCOPE_BASE
+
+        if partial_groupfilter is None:
+            search_base = self.domain_dn()
+            search_scope = ldb.SCOPE_SUBTREE
+            partial_groupfilter = "(sAMAccountName=%s)" % (
+                ldb.binary_encode(group))
+
+        groupfilter = "(&%s(objectCategory=%s,%s))" % (
+            partial_groupfilter,
+            "CN=Group,CN=Schema,CN=Configuration",
+            self.domain_dn())
 
         self.transaction_start()
         try:
-            targetgroup = self.search(base=self.domain_dn(), 
scope=ldb.SCOPE_SUBTREE,
-                                      expression=groupfilter, attrs=['member'])
+            targetgroup = self.search(base=search_base,
+                                      scope=search_scope,
+                                      expression=groupfilter,
+                                      controls=["extended_dn:1:1"],
+                                      attrs=['member'])
             if len(targetgroup) == 0:
-                raise Exception('Unable to find group "%s"' % groupname)
+                raise Exception('Unable to find group "%s"' % group)
             assert(len(targetgroup) == 1)
 
             modified = False
 
+            if group_sid is not None:
+                targetgroup_dn = '<SID=%s>' % group_sid
+            elif group_guid is not None:
+                targetgroup_dn = '<GUID=%s>' % group_guid
+            else:
+                targetgroup_dn = str(targetgroup[0].dn)
+
             addtargettogroup = """
 dn: %s
 changetype: modify
-""" % (str(targetgroup[0].dn))
+""" % (targetgroup_dn)
 
             for member in members:
                 targetmember_dn = None
-                if member_base_dn is None:
-                    member_base_dn = self.domain_dn()
-
+                membersid = None
                 try:
                     membersid = security.dom_sid(member)
                     targetmember_dn = "<SID=%s>" % str(membersid)
@@ -420,10 +494,10 @@ changetype: modify
                         pass
 
                 if targetmember_dn is None:
-                    filter = self.group_member_filter(member, member_types)
+                    search_filter = self.group_member_filter(member, 
member_types)
                     targetmember = self.search(base=member_base_dn,
                                                scope=ldb.SCOPE_SUBTREE,
-                                               expression=filter,
+                                               expression=search_filter,
                                                attrs=[])
 
                     if len(targetmember) > 1:
@@ -436,13 +510,33 @@ changetype: modify
                         raise Exception('Unable to find "%s". Operation 
cancelled.' % member)
                     targetmember_dn = targetmember[0].dn.extended_str(1)
 
-                if add_members_operation is True and 
(targetgroup[0].get('member') is None or get_bytes(targetmember_dn) not in 
[str(x) for x in targetgroup[0]['member']]):
+                def _is_member(samdb, group, member_dn, member_sid):
+                    if group.get('member') is None:
+                        return False
+
+                    for m in group.get('member'):
+                        m_ext_dn = ldb.Dn(samdb, str(m))
+                        m_binary_sid = m_ext_dn.get_extended_component("SID")
+                        if m_binary_sid:
+                            m_sid = ndr_unpack(security.dom_sid, m_binary_sid)
+                            if member_sid == m_sid:
+                                return True
+                        if member_dn == str(m_ext_dn):
+                            return True
+
+                    return False
+
+                is_member = _is_member(self,
+                                       targetgroup[0],
+                                       targetmember_dn,
+                                       membersid)
+                if add_members_operation is True and not is_member:
                     modified = True
                     addtargettogroup += """add: member
 member: %s
 """ % (str(targetmember_dn))
 
-                elif add_members_operation is False and 
(targetgroup[0].get('member') is not None and get_bytes(targetmember_dn) in 
targetgroup[0]['member']):
+                elif add_members_operation is False and is_member:
                     modified = True
                     addtargettogroup += """delete: member
 member: %s
@@ -875,7 +969,7 @@ member: %s
         """Sets the password for a user
 
         :param search_filter: LDAP filter to find the user (eg
-            samccountname=name)
+            sAMAccountName=name)
         :param password: Password for the user
         :param force_change_at_next_login: Force password change
         """
@@ -918,7 +1012,7 @@ unicodePwd:: %s
         """Sets the account expiry for a user
 
         :param search_filter: LDAP filter to find the user (eg
-            samaccountname=name)
+            sAMAccountName=name)
         :param expiry_seconds: expiry time from now in seconds
         :param no_expiry_req: if set, then don't expire password
         """
diff --git a/python/samba/tests/samba_tool/user.py 
b/python/samba/tests/samba_tool/user.py
index 290d5daebe1..caef93407bf 100644
--- a/python/samba/tests/samba_tool/user.py
+++ b/python/samba/tests/samba_tool/user.py
@@ -1126,6 +1126,41 @@ sAMAccountName: %s
             self.assertCmdSuccess(result, out, err, "Error running user 
unlock")
             self.assertEqual(err, "", "Shouldn't be any error messages")
 
+    def test_disable_remove_supplemental_groups(self):
+        """disable user and remove supplemental groups"""
+        username = "userRemoveGroups"
+        user = self._randomUser({"name": username})
+        self._create_user(user)
+
+        usergroups = self._get_groups(username)
+        self.assertTrue(len(usergroups) == 1, "exactly one membership 
expected")
+        self.assertEqual(usergroups[0],
+                         "Domain Users",
+                         "Unexpected groupmembership")
+
+        self._add_groupmember("Domain Admins", username)
+        self._add_groupmember("Print Operators", username)
+
+        usergroups = self._get_groups(username)
+        self.assertTrue(len(usergroups) == 3, "exactly 3 memberships expected")
+
+        (result, out, err) = self.runsubcmd(
+            "user", "disable", username,
+            "--remove-supplemental-groups",
+            "-H", "ldap://%s"; % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"],
+            os.environ["DC_PASSWORD"]))
+        self.assertCmdSuccess(
+            result, out, err,
+            "Error running user disable --remove-supplemental-groups")
+        self.assertEqual(err, "",
+                         "Shouldn't be any error messages from user disable")
+
+        usergroups = self._get_groups(username)
+        self.assertTrue(len(usergroups) == 1, "exactly one membership 
expected")
+        self.assertEqual(usergroups[0], "Domain Users",
+                         "Unexpected groupmembership")
+
     def _randomUser(self, base=None):
         """create a user with random attribute values, you can specify base 
attributes"""
         if base is None:
@@ -1271,3 +1306,46 @@ template """
             return userlist[0]
         else:
             return None
+
+    def _add_groupmember(self, group, user):
+        (result, out, err) =  self.runsubcmd(
+            "group", "addmembers", group, user,
+            "-H", "ldap://%s"; % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                          os.environ["DC_PASSWORD"]))
+        self.assertCmdSuccess(
+            result, out, err, "Error running group addmembers")
+        self.assertEqual(
+            err,
+            "",
+            "Shouldn't be any error messages from group addmembers")
+
+        return out.rstrip().split("\n")
+
+    def _remove_groupmember(self, group, user):
+        (result, out, err) = self.runsubcmd(
+            "group", "removemembers", group, user,
+            "-H", "ldap://%s"; % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"],
+                          os.environ["DC_PASSWORD"]))
+        self.assertCmdSuccess(
+            result, out, err, "Error running group removemembers")
+        self.assertEqual(
+            err,
+            "",
+            "Shouldn't be any error messages from group removemembers")
+
+        return out.rstrip().split("\n")
+
+    def _get_groups(self, user):
+        (result, out, err) = self.runsubcmd(
+            "user", "getgroups", user,
+            "-H", "ldap://%s"; % os.environ["DC_SERVER"],
+            "-U%s%%%s" % (os.environ["DC_USERNAME"],
+            os.environ["DC_PASSWORD"]))
+        self.assertCmdSuccess(result, out, err, "Error running user getgroups")
+        self.assertEqual(err,
+                         "",
+                         "Shouldn't be any error messages from user getgroups")
+
+        return out.rstrip().split("\n")


-- 
Samba Shared Repository

Reply via email to