diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:20:00 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:20:00 +0000 |
commit | 8daa83a594a2e98f39d764422bfbdbc62c9efd44 (patch) | |
tree | 4099e8021376c7d8c05bdf8503093d80e9c7bad0 /python/samba/netcmd/group.py | |
parent | Initial commit. (diff) | |
download | samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.tar.xz samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.zip |
Adding upstream version 2:4.20.0+dfsg.upstream/2%4.20.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'python/samba/netcmd/group.py')
-rw-r--r-- | python/samba/netcmd/group.py | 1416 |
1 files changed, 1416 insertions, 0 deletions
diff --git a/python/samba/netcmd/group.py b/python/samba/netcmd/group.py new file mode 100644 index 0000000..a705560 --- /dev/null +++ b/python/samba/netcmd/group.py @@ -0,0 +1,1416 @@ +# Copyright Jelmer Vernooij 2008 +# +# Based on the original in EJS: +# Copyright Andrew Tridgell 2005 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import samba.getopt as options +from samba.netcmd import Command, SuperCommand, CommandError, Option +import ldb +from samba.ndr import ndr_pack, ndr_unpack +from samba.dcerpc import security + +from samba.auth import system_session +from samba.samdb import SamDB +from samba.dsdb import ( + ATYPE_SECURITY_GLOBAL_GROUP, + DS_GUID_USERS_CONTAINER, + GTYPE_SECURITY_BUILTIN_LOCAL_GROUP, + GTYPE_SECURITY_DOMAIN_LOCAL_GROUP, + GTYPE_SECURITY_GLOBAL_GROUP, + GTYPE_SECURITY_UNIVERSAL_GROUP, + GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP, + GTYPE_DISTRIBUTION_GLOBAL_GROUP, + GTYPE_DISTRIBUTION_UNIVERSAL_GROUP, + SYSTEM_FLAG_DISALLOW_DELETE, + SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE, + SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME, + UF_ACCOUNTDISABLE, +) +from collections import defaultdict +from subprocess import check_call, CalledProcessError +from samba.common import get_bytes, normalise_int32 +import os +import tempfile +from . import common + +security_group = dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP, + "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP, + "Global": GTYPE_SECURITY_GLOBAL_GROUP, + "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP}) +distribution_group = dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP, + "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP, + "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP}) + + +class cmd_group_add(Command): + """Creates a new AD group. + +This command adds a new Active Directory group. The groupname specified on the command is a unique sAMAccountName. + +An Active Directory group may contain user and computer accounts as well as other groups. An administrator adds a new group and adds members to that group so they can be managed as a single entity. This helps to simplify security and system administration. + +Groups may also be used to establish email distribution lists, using --group-type=Distribution. + +Groups are located in domains in organizational units (OUs). The group's scope is a characteristic of the group that designates the extent to which the group is applied within the domain tree or forest. + +The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created. + +The command may be run from the root userid or another authorized userid. The +-H or --URL= option can be used to execute the command on a remote server. + +Example1: +samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group' + +Example1 adds a new group with the name Group1 added to the Users container on a remote LDAP server. The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server. It defaults to the security type and global scope. + +Example2: +sudo samba-tool group add Group2 --group-type=Distribution + +Example2 adds a new distribution group to the local server. The command is run under root using the sudo command. + +Example3: +samba-tool group add Group3 --nis-domain=samdom --gid-number=12345 + +Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature). +""" + + synopsis = "%prog <groupname> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--groupou", + help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created", + type=str), + Option("--group-scope", type="choice", choices=["Domain", "Global", "Universal"], + help="Group scope (Domain | Global | Universal)"), + Option("--group-type", type="choice", choices=["Security", "Distribution"], + help="Group type (Security | Distribution)"), + Option("--description", help="Group's description", type=str), + Option("--mail-address", help="Group's email address", type=str), + Option("--notes", help="Group's notes", type=str), + Option("--gid-number", help="Group's Unix/RFC2307 GID number", type=int), + Option("--nis-domain", help="SFU30 NIS Domain", type=str), + Option("--special", help="Add a special predefined group", action="store_true", default=False), + ] + + takes_args = ["groupname"] + + def run(self, groupname, credopts=None, sambaopts=None, + versionopts=None, H=None, groupou=None, group_scope=None, + group_type=None, description=None, mail_address=None, notes=None, gid_number=None, nis_domain=None, + special=False): + + if (group_type or "Security") == "Security": + gtype = security_group.get(group_scope, GTYPE_SECURITY_GLOBAL_GROUP) + else: + gtype = distribution_group.get(group_scope, GTYPE_DISTRIBUTION_GLOBAL_GROUP) + + if (gid_number is None and nis_domain is not None) or (gid_number is not None and nis_domain is None): + raise CommandError('Both --gid-number and --nis-domain have to be set for a RFC2307-enabled group. Operation cancelled.') + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + except Exception as e: + # FIXME: catch more specific exception + raise CommandError(f'Failed to add group "{groupname}"', e) + + if special: + invalid_option = None + if group_scope is not None: + invalid_option = 'group-scope' + elif group_type is not None: + invalid_option = 'group-type' + elif description is not None: + invalid_option = 'description' + elif mail_address is not None: + invalid_option = 'mail-address' + elif notes is not None: + invalid_option = 'notes' + elif gid_number is not None: + invalid_option = 'gid-number' + elif nis_domain is not None: + invalid_option = 'nis-domain' + + if invalid_option is not None: + raise CommandError(f'Superfluous option --{invalid_option} ' + f'specified with --special') + + if not samdb.am_pdc(): + raise CommandError('Adding special groups is only permitted ' + 'against the PDC!') + + special_groups = { + # On Windows, this group is added automatically when the PDC + # role is held by a DC running Windows Server 2012 R2 or later. + # https://docs.microsoft.com/en-us/windows-server/security/credentials-protection-and-management/protected-users-security-group#BKMK_Requirements + 'Protected Users'.lower(): ( + 'Protected Users', + GTYPE_SECURITY_GLOBAL_GROUP, + security.DOMAIN_RID_PROTECTED_USERS, + 'Members of this group are afforded additional ' + 'protections against authentication security threats'), + } + + special_group = special_groups.get(groupname.lower()) + if special_group is None: + raise CommandError(f'Unknown special group "{groupname}".') + + groupname, gtype, rid, description = special_group + group_type = normalise_int32(gtype) + + group_dn = samdb.get_default_basedn() + + if gtype == GTYPE_SECURITY_GLOBAL_GROUP: + object_sid = security.dom_sid( + f'{samdb.get_domain_sid()}-{rid}') + system_flags = None + + if not groupou: + group_dn = samdb.get_wellknown_dn(group_dn, + DS_GUID_USERS_CONTAINER) + + elif gtype == GTYPE_SECURITY_BUILTIN_LOCAL_GROUP: + object_sid = security.dom_sid(f'S-1-5-32-{rid}') + system_flags = (SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE | + SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME | + SYSTEM_FLAG_DISALLOW_DELETE) + + if not groupou: + try: + group_dn.add_child('CN=Builtin') + except ldb.LdbError: + raise RuntimeError('Error getting Builtin objects DN') + else: + raise RuntimeError(f'Unknown group type {gtype}') + + if groupou: + try: + group_dn.add_child(groupou) + except ldb.LdbError: + raise CommandError(f'Invalid group OU "{groupou}"') + + try: + group_dn.add_child(f'CN={groupname}') + except ldb.LdbError: + raise CommandError(f'Invalid group name "{groupname}"') + + msg = { + 'dn': group_dn, + 'sAMAccountName': groupname, + 'objectClass': 'group', + 'groupType': group_type, + 'description': description, + 'objectSid': ndr_pack(object_sid), + 'isCriticalSystemObject': 'TRUE', + } + + if system_flags is not None: + msg['systemFlags'] = system_flags + + try: + samdb.add(msg, controls=['relax:0']) + except ldb.LdbError as e: + num, estr = e.args + if num == ldb.ERR_CONSTRAINT_VIOLATION: + try: + res = samdb.search( + expression=f'(objectSid={object_sid})', + attrs=['sAMAccountName']) + except ldb.LdbError: + raise CommandError( + f'Failed to add group "{groupname}"', e) + + if len(res) != 1: + raise CommandError( + f'Failed to add group "{groupname}"', e) + + name = res[0].get('sAMAccountName', idx=0) + if name: + with_name = f' with name "{name}"' + else: + with_name = '' + + raise CommandError( + f'Failed to add group "{groupname}" - Special group ' + f'already exists{with_name} at "{res[0].dn}".') + + elif num == ldb.ERR_ENTRY_ALREADY_EXISTS: + try: + res = samdb.search(base=group_dn, + scope=ldb.SCOPE_BASE, + attrs=['sAMAccountName', + 'objectSid', + 'groupType']) + except ldb.LdbError: + try: + res = samdb.search( + expression=f'(sAMAccountName={groupname})', + attrs=['sAMAccountName', + 'objectSid', + 'groupType']) + except ldb.LdbError: + raise CommandError( + f'Failed to add group "{groupname}"', e) + + if len(res) != 1: + raise CommandError( + f'Failed to add group "{groupname}"', e) + + got_name = res[0].get('sAMAccountName', idx=0) + if got_name: + named = f'named "{got_name}"' + else: + named = 'with no name' + + got_group_type = res[0].get('groupType', + idx=0).decode('utf-8') + if group_type != got_group_type: + raise CommandError( + f'Failed to add group "{groupname}" - An object ' + f'{named} at "{res[0].dn}" already exists, but it ' + f'is not a security group. Rename or remove this ' + f'existing object before attempting to add this ' + f'special group.') + + sid = res[0].get('objectSid', idx=0) + if sid is None: + raise CommandError( + f'Failed to add group "{groupname}" - A security ' + f'group {named} at "{res[0].dn}" already exists, ' + f'but it lacks a SID. Rename or remove this ' + f'existing object before attempting to add this ' + f'special group.') + else: + sid = ndr_unpack(security.dom_sid, sid) + if sid == object_sid: + raise CommandError( + f'Failed to add group "{groupname}" - The ' + f'security group {named} at "{res[0].dn}" ' + f'already exists.') + else: + raise CommandError( + f'Failed to add group "{groupname}" - A ' + f'security group {named} at "{res[0].dn}" ' + f'already exists, but it has the wrong SID, ' + f'and will not function as expected. Rename ' + f'or remove this existing object before ' + f'attempting to add this special group.') + else: + raise CommandError(f'Failed to add group "{groupname}"', e) + else: + self.outf.write(f'Added group {groupname}\n') + + return + + try: + samdb.newgroup(groupname, groupou=groupou, grouptype=gtype, + description=description, mailaddress=mail_address, notes=notes, + gidnumber=gid_number, nisdomain=nis_domain) + except Exception as e: + # FIXME: catch more specific exception + raise CommandError('Failed to add group "%s"' % groupname, e) + self.outf.write("Added group %s\n" % groupname) + + +class cmd_group_delete(Command): + """Deletes an AD group. + +The command deletes an existing AD group from the Active Directory domain. The groupname specified on the command is the sAMAccountName. + +Deleting a group is a permanent operation. When a group is deleted, all permissions and rights that users in the group had inherited from the group account are deleted as well. + +The command may be run from the root userid or another authorized userid. The -H or --URL option can be used to execute the command on a remote server. + +Example1: +samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd + +Example1 shows how to delete an AD group from a remote LDAP server. The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server. + +Example2: +sudo samba-tool group delete Group2 + +Example2 deletes group Group2 from the local server. The command is run under root using the sudo command. +""" + + synopsis = "%prog <groupname> [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_args = ["groupname"] + + def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + filter = ("(&(sAMAccountName=%s)(objectClass=group))" % + ldb.binary_encode(groupname)) + + try: + res = samdb.search(base=samdb.domain_dn(), + scope=ldb.SCOPE_SUBTREE, + expression=filter, + attrs=["dn"]) + group_dn = res[0].dn + except IndexError: + raise CommandError('Unable to find group "%s"' % (groupname)) + + try: + samdb.delete(group_dn) + except Exception as e: + # FIXME: catch more specific exception + raise CommandError('Failed to remove group "%s"' % groupname, e) + self.outf.write("Deleted group %s\n" % groupname) + + +class cmd_group_add_members(Command): + """Add members to an AD group. + +This command adds one or more members to an existing Active Directory group. The command accepts one or more group member names separated by commas. A group member may be a user or computer account or another Active Directory group. + +When a member is added to a group the member may inherit permissions and rights from the group. Likewise, when permission or rights of a group are changed, the changes may reflect in the members through inheritance. + +The member names specified on the command must be the sAMaccountName. + +Example1: +samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd + +Example1 shows how to add two groups, Group1 and Group2 and one user account, User1, to the existing AD group named supergroup. The command will be run on a remote server specified with the -H. The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server. + +Example2: +sudo samba-tool group addmembers supergroup User2 + +Example2 shows how to add a single user account, User2, to the supergroup AD group. It uses the sudo command to run as root when issuing the command. +""" + + synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--member-dn", + help=("DN of the new group member to be added.\n" + "The --object-types option will be ignored."), + type=str, + action="append"), + Option("--object-types", + help=("Comma separated list of object types.\n" + "The types are used to filter the search for the " + "specified members.\n" + "Valid values are: user, group, computer, serviceaccount, " + "contact and all.\n" + "Default: user,group,computer"), + default="user,group,computer", + type=str), + Option("--member-base-dn", + help=("Base DN for group member search.\n" + "Default is the domain DN."), + type=str), + ] + + takes_args = ["groupname", "listofmembers?"] + + def run(self, + groupname, + listofmembers=None, + credopts=None, + sambaopts=None, + versionopts=None, + H=None, + member_base_dn=None, + member_dn=None, + object_types="user,group,computer"): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + if member_dn is None and listofmembers is None: + self.usage() + raise CommandError( + 'Either listofmembers or --member-dn must be specified.') + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + groupmembers = [] + if member_dn is not None: + groupmembers += member_dn + if listofmembers is not None: + groupmembers += listofmembers.split(',') + group_member_types = object_types.split(',') + + if member_base_dn is not None: + member_base_dn = samdb.normalize_dn_in_domain(member_base_dn) + + samdb.add_remove_group_members(groupname, groupmembers, + add_members_operation=True, + member_types=group_member_types, + member_base_dn=member_base_dn) + except Exception as e: + # FIXME: catch more specific exception + raise CommandError('Failed to add members %r to group "%s" - %s' % ( + groupmembers, groupname, e)) + self.outf.write("Added members to group %s\n" % groupname) + + +class cmd_group_remove_members(Command): + """Remove members from an AD group. + +This command removes one or more members from an existing Active Directory group. The command accepts one or more group member names separated by commas. A group member may be a user or computer account or another Active Directory group that is a member of the group specified on the command. + +When a member is removed from a group, inherited permissions and rights will no longer apply to the member. + +Example1: +samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd + +Example1 shows how to remove Group1 from supergroup. The command will run on the remote server specified on the -H parameter. The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server. + +Example2: +sudo samba-tool group removemembers supergroup User1 + +Example2 shows how to remove a single user account, User2, from the supergroup AD group. It uses the sudo command to run as root when issuing the command. +""" + + synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]" + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--member-dn", + help=("DN of the group member to be removed.\n" + "The --object-types option will be ignored."), + type=str, + action="append"), + Option("--object-types", + help=("Comma separated list of object types.\n" + "The types are used to filter the search for the " + "specified members.\n" + "Valid values are: user, group, computer, serviceaccount, " + "contact and all.\n" + "Default: user,group,computer"), + default="user,group,computer", + type=str), + Option("--member-base-dn", + help=("Base DN for group member search.\n" + "Default is the domain DN."), + type=str), + ] + + takes_args = ["groupname", "listofmembers?"] + + def run(self, + groupname, + listofmembers=None, + credopts=None, + sambaopts=None, + versionopts=None, + H=None, + member_base_dn=None, + member_dn=None, + object_types="user,group,computer"): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + if member_dn is None and listofmembers is None: + self.usage() + raise CommandError( + 'Either listofmembers or --member-dn must be specified.') + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + groupmembers = [] + if member_dn is not None: + groupmembers += member_dn + if listofmembers is not None: + groupmembers += listofmembers.split(',') + group_member_types = object_types.split(',') + + if member_base_dn is not None: + member_base_dn = samdb.normalize_dn_in_domain(member_base_dn) + + samdb.add_remove_group_members(groupname, + groupmembers, + add_members_operation=False, + member_types=group_member_types, + 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) + self.outf.write("Removed members from group %s\n" % groupname) + + +class cmd_group_list(Command): + """List all groups.""" + + synopsis = "%prog [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("-v", "--verbose", + help="Verbose output, showing group type and group scope.", + action="store_true"), + Option("-b", "--base-dn", + help="Specify base DN to use.", + type=str), + Option("--full-dn", dest="full_dn", + default=False, + action='store_true', + help="Display DN instead of the sAMAccountName."), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, + sambaopts=None, + credopts=None, + versionopts=None, + H=None, + verbose=False, + base_dn=None, + full_dn=False): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + attrs=["samaccountname"] + + if verbose: + attrs += ["grouptype", "member"] + domain_dn = samdb.domain_dn() + if base_dn: + domain_dn = samdb.normalize_dn_in_domain(base_dn) + res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE, + expression=("(objectClass=group)"), + attrs=attrs) + if (len(res) == 0): + return + + if verbose: + self.outf.write("Group Name Group Type Group Scope Members\n") + self.outf.write("--------------------------------------------------------------------------------\n") + + for msg in res: + self.outf.write("%-44s" % msg.get("samaccountname", idx=0)) + hgtype = hex(int("%s" % msg["grouptype"]) & 0x00000000FFFFFFFF) + if (hgtype == hex(int(security_group.get("Builtin")))): + self.outf.write("Security Builtin ") + elif (hgtype == hex(int(security_group.get("Domain")))): + self.outf.write("Security Domain ") + elif (hgtype == hex(int(security_group.get("Global")))): + self.outf.write("Security Global ") + elif (hgtype == hex(int(security_group.get("Universal")))): + self.outf.write("Security Universal") + elif (hgtype == hex(int(distribution_group.get("Global")))): + self.outf.write("Distribution Global ") + elif (hgtype == hex(int(distribution_group.get("Domain")))): + self.outf.write("Distribution Domain ") + elif (hgtype == hex(int(distribution_group.get("Universal")))): + self.outf.write("Distribution Universal") + else: + self.outf.write(" ") + num_members = len(msg.get("member", default=[])) + self.outf.write(" %6u\n" % num_members) + else: + for msg in res: + if full_dn: + self.outf.write("%s\n" % msg.get("dn")) + continue + + self.outf.write("%s\n" % msg.get("samaccountname", idx=0)) + + +class cmd_group_list_members(Command): + """List all members of an AD group. + +This command lists members from an existing Active Directory group. The command accepts one group name. + +Example1: +samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd +""" + + synopsis = "%prog <groupname> [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + Option("--hide-expired", + help="Do not list expired group members", + default=False, + action='store_true'), + Option("--hide-disabled", + default=False, + action='store_true', + help="Do not list disabled group members"), + Option("--full-dn", dest="full_dn", + default=False, + action='store_true', + help="Display DN instead of the sAMAccountName.") + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + takes_args = ["groupname"] + + def run(self, + groupname, + credopts=None, + sambaopts=None, + versionopts=None, + H=None, + hide_expired=False, + hide_disabled=False, + full_dn=False): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + try: + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + search_filter = ("(&(objectClass=group)(sAMAccountName=%s))" % + ldb.binary_encode(groupname)) + try: + res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=(search_filter), + attrs=["objectSid"]) + group_sid_binary = res[0].get('objectSid', idx=0) + except IndexError: + raise CommandError('Unable to find group "%s"' % (groupname)) + + group_sid = ndr_unpack(security.dom_sid, group_sid_binary) + (group_dom_sid, rid) = group_sid.split() + group_sid_dn = "<SID=%s>" % (group_sid) + + filter_expires = "" + if hide_expired is True: + current_nttime = samdb.get_nttime() + filter_expires = ("(|" + "(!(accountExpires=*))" + "(accountExpires=0)" + "(accountExpires>=%u)" + ")" % (current_nttime)) + + filter_disabled = "" + if hide_disabled is True: + filter_disabled = "(!(userAccountControl:%s:=%u))" % ( + ldb.OID_COMPARATOR_AND, UF_ACCOUNTDISABLE) + + filter = "(&(|(primaryGroupID=%s)(memberOf=%s))%s%s)" % ( + rid, group_sid_dn, filter_disabled, filter_expires) + + res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE, + expression=filter, + attrs=["samAccountName", "cn"]) + + if (len(res) == 0): + return + + for msg in res: + if full_dn: + self.outf.write("%s\n" % msg.get("dn")) + continue + + member_name = msg.get("samAccountName", idx=0) + if member_name is None: + member_name = msg.get("cn", idx=0) + self.outf.write("%s\n" % member_name) + + except Exception as e: + raise CommandError('Failed to list members of "%s" group - %s' % + (groupname, e)) + + +class cmd_group_move(Command): + """Move a group to an organizational unit/container. + + This command moves a group object into the specified organizational unit + or container. + The groupname specified on the command is the sAMAccountName. + The name of the organizational unit or container can be specified as a + full DN or without the domainDN component. + + The command may be run from the root userid or another authorized userid. + + The -H or --URL= option can be used to execute the command against a remote + server. + + Example1: + samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\ + -H ldap://samba.samdom.example.com -U administrator + + Example1 shows how to move a group Group1 into the 'OrgUnit' organizational + unit on a remote LDAP server. + + The -H parameter is used to specify the remote target server. + + Example2: + samba-tool group move Group1 CN=Users + + Example2 shows how to move a group Group1 back into the CN=Users container + on the local server. + """ + + synopsis = "%prog <groupname> <new_parent_dn> [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + takes_args = ["groupname", "new_parent_dn"] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, groupname, new_parent_dn, credopts=None, sambaopts=None, + versionopts=None, H=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + domain_dn = ldb.Dn(samdb, samdb.domain_dn()) + + filter = ("(&(sAMAccountName=%s)(objectClass=group))" % + ldb.binary_encode(groupname)) + try: + res = samdb.search(base=domain_dn, + expression=filter, + scope=ldb.SCOPE_SUBTREE) + group_dn = res[0].dn + except IndexError: + raise CommandError('Unable to find group "%s"' % (groupname)) + + try: + full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn) + except Exception as e: + raise CommandError('Invalid new_parent_dn "%s": %s' % + (new_parent_dn, e.message)) + + full_new_group_dn = ldb.Dn(samdb, str(group_dn)) + full_new_group_dn.remove_base_components(len(group_dn) - 1) + full_new_group_dn.add_base(full_new_parent_dn) + + try: + samdb.rename(group_dn, full_new_group_dn) + except Exception as e: + raise CommandError('Failed to move group "%s"' % groupname, e) + self.outf.write('Moved group "%s" into "%s"\n' % + (groupname, full_new_parent_dn)) + + +class cmd_group_show(Command): + """Display a group AD object. + +This command displays a group object and it's attributes in the Active +Directory domain. +The group name specified on the command is the sAMAccountName of the group. + +The command may be run from the root userid or another authorized userid. + +The -H or --URL= option can be used to execute the command against a remote +server. + +Example1: +samba-tool group show Group1 -H ldap://samba.samdom.example.com \\ + -U administrator --password=passw1rd + +Example1 shows how to display a group's attributes in the domain against a +remote LDAP server. + +The -H parameter is used to specify the remote target server. + +Example2: +samba-tool group show Group2 + +Example2 shows how to display a group's attributes in the domain against a local +LDAP server. + +Example3: +samba-tool group show Group3 --attributes=member,objectGUID + +Example3 shows how to display a groups objectGUID and member attributes. +""" + synopsis = "%prog <group name> [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + Option("--attributes", + help=("Comma separated list of attributes, " + "which will be printed."), + type=str, dest="group_attrs"), + ] + + takes_args = ["groupname"] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, + H=None, group_attrs=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + attrs = None + if group_attrs: + attrs = group_attrs.split(",") + + filter = ("(&(objectCategory=group)(sAMAccountName=%s))" % + ldb.binary_encode(groupname)) + + domaindn = samdb.domain_dn() + + try: + res = samdb.search(base=domaindn, expression=filter, + scope=ldb.SCOPE_SUBTREE, attrs=attrs) + user_dn = res[0].dn + except IndexError: + raise CommandError('Unable to find group "%s"' % (groupname)) + + for msg in res: + group_ldif = common.get_ldif_for_editor(samdb, msg) + self.outf.write(group_ldif) + + +class cmd_group_stats(Command): + """Summary statistics about group memberships.""" + + synopsis = "%prog [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", type=str, + metavar="URL", dest="H"), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def num_in_range(self, range_min, range_max, group_freqs): + total_count = 0 + for members, count in group_freqs.items(): + if range_min <= members and members <= range_max: + total_count += count + + return total_count + + def run(self, sambaopts=None, credopts=None, versionopts=None, H=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domain_dn = samdb.domain_dn() + res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE, + expression=("(objectClass=group)"), + attrs=["samaccountname", "member"]) + + # first count up how many members each group has + group_assignments = {} + total_memberships = 0 + + for msg in res: + name = str(msg.get("samaccountname")) + num_members = len(msg.get("member", default=[])) + group_assignments[name] = num_members + total_memberships += num_members + + num_groups = res.count + self.outf.write("Group membership statistics*\n") + self.outf.write("-------------------------------------------------\n") + self.outf.write("Total groups: {0}\n".format(num_groups)) + self.outf.write("Total memberships: {0}\n".format(total_memberships)) + average = total_memberships / float(num_groups) + self.outf.write("Average members per group: %.2f\n" % average) + + # find the max and median memberships (note that some default groups + # always have zero members, so displaying the min is not very helpful) + group_names = list(group_assignments.keys()) + group_members = list(group_assignments.values()) + idx = group_members.index(max(group_members)) + max_members = group_members[idx] + self.outf.write("Max members: {0} ({1})\n".format(max_members, + group_names[idx])) + group_members.sort() + midpoint = num_groups // 2 + median = group_members[midpoint] + if num_groups % 2 == 0: + median = (median + group_members[midpoint - 1]) / 2 + self.outf.write("Median members per group: {0}\n\n".format(median)) + + # convert this to the frequency of group membership, i.e. how many + # groups have 5 members, how many have 6 members, etc + group_freqs = defaultdict(int) + for group, num_members in group_assignments.items(): + group_freqs[num_members] += 1 + + # now squash this down even further, so that we just display the number + # of groups that fall into one of the following membership bands + bands = [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24), + (25, 29), (30, 39), (40, 49), (50, 59), (60, 69), (70, 79), + (80, 89), (90, 99), (100, 149), (150, 199), (200, 249), + (250, 299), (300, 399), (400, 499), (500, 999), (1000, 1999), + (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999), + (10000, max_members)] + + self.outf.write("Members Number of Groups\n") + self.outf.write("-------------------------------------------------\n") + + for band in bands: + band_start = band[0] + band_end = band[1] + if band_start > max_members: + break + + num_groups = self.num_in_range(band_start, band_end, group_freqs) + + if num_groups != 0: + band_str = "{0}-{1}".format(band_start, band_end) + self.outf.write("%13s %u\n" % (band_str, num_groups)) + + self.outf.write("\n* Note this does not include nested group memberships\n") + + +class cmd_group_edit(Command): + """Modify Group AD object. + + This command will allow editing of a group account in the Active Directory + domain. You will then be able to add or change attributes and their values. + + The groupname specified on the command is the sAMAccountName. + + The command may be run from the root userid or another authorized userid. + + The -H or --URL= option can be used to execute the command against a remote + server. + + Example1: + samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\ + -U administrator --password=passw1rd + + Example1 shows how to edit a groups attributes in the domain against a + remote LDAP server. + + The -H parameter is used to specify the remote target server. + + Example2: + samba-tool group edit Group2 + + Example2 shows how to edit a groups attributes in the domain against a local + server. + + Example3: + samba-tool group edit Group3 --editor=nano + + Example3 shows how to edit a groups attributes in the domain against a local + server using the 'nano' editor. + """ + synopsis = "%prog <groupname> [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + Option("--editor", help="Editor to use instead of the system default," + " or 'vi' if no system default is set.", type=str), + ] + + takes_args = ["groupname"] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, + H=None, editor=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + filter = ("(&(sAMAccountName=%s)(objectClass=group))" % + ldb.binary_encode(groupname)) + + domaindn = samdb.domain_dn() + + try: + res = samdb.search(base=domaindn, + expression=filter, + scope=ldb.SCOPE_SUBTREE) + group_dn = res[0].dn + except IndexError: + raise CommandError('Unable to find group "%s"' % (groupname)) + + if len(res) != 1: + raise CommandError('Invalid number of results: for "%s": %d' % + ((groupname), len(res))) + + msg = res[0] + result_ldif = common.get_ldif_for_editor(samdb, msg) + + if editor is None: + editor = os.environ.get('EDITOR') + if editor is None: + editor = 'vi' + + with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file: + t_file.write(get_bytes(result_ldif)) + t_file.flush() + try: + check_call([editor, t_file.name]) + except CalledProcessError as e: + raise CalledProcessError("ERROR: ", e) + with open(t_file.name) as edited_file: + edited_message = edited_file.read() + + msgs_edited = samdb.parse_ldif(edited_message) + msg_edited = next(msgs_edited)[1] + + res_msg_diff = samdb.msg_diff(msg, msg_edited) + if len(res_msg_diff) == 0: + self.outf.write("Nothing to do\n") + return + + try: + samdb.modify(res_msg_diff) + except Exception as e: + raise CommandError("Failed to modify group '%s': " % groupname, e) + + self.outf.write("Modified group '%s' successfully\n" % groupname) + + +class cmd_group_add_unix_attrs(Command): + """Add RFC2307 attributes to a group. + +This command adds Unix attributes to a group account in the Active +Directory domain. +The groupname specified on the command is the sAMaccountName. + +Unix (RFC2307) attributes will be added to the group account. + +Add 'idmap_ldb:use rfc2307 = Yes' to smb.conf to use these attributes for +UID/GID mapping. + +The command may be run from the root userid or another authorized userid. +The -H or --URL= option can be used to execute the command against a +remote server. + +Example1: +samba-tool group addunixattrs Group1 10000 + +Example1 shows how to add RFC2307 attributes to a domain enabled group +account. + +The groups Unix ID will be set to '10000', provided this ID isn't already +in use. + +""" + synopsis = "%prog <groupname> <gidnumber> [options]" + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + takes_args = ["groupname", "gidnumber"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, groupname, gidnumber, credopts=None, sambaopts=None, + versionopts=None, H=None): + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + domaindn = samdb.domain_dn() + + # Check group exists and doesn't have a gidNumber + filter = "(samaccountname={})".format(ldb.binary_encode(groupname)) + res = samdb.search(domaindn, + scope=ldb.SCOPE_SUBTREE, + expression=filter) + if (len(res) == 0): + raise CommandError("Unable to find group '{}'".format(groupname)) + + group_dn = res[0].dn + + if "gidNumber" in res[0]: + raise CommandError("Group {} is a Unix group.".format(groupname)) + + # Check if supplied gidnumber isn't already being used + filter = "(&(objectClass=group)(gidNumber={}))".format(gidnumber) + res = samdb.search(domaindn, + scope=ldb.SCOPE_SUBTREE, + expression=filter) + if (len(res) != 0): + raise CommandError('gidNumber {} already used.'.format(gidnumber)) + + if not lp.get("idmap_ldb:use rfc2307"): + self.outf.write("You are setting a Unix/RFC2307 GID. " + "You may want to set 'idmap_ldb:use rfc2307 = Yes'" + " in smb.conf to use the attributes for " + "XID/SID-mapping.\n") + + group_mod = """ +dn: {0} +changetype: modify +add: gidNumber +gidNumber: {1} +""".format(group_dn, gidnumber) + + try: + samdb.modify_ldif(group_mod) + except ldb.LdbError as e: + raise CommandError("Failed to modify group '{0}': {1}" + .format(groupname, e)) + + self.outf.write("Modified Group '{}' successfully\n".format(groupname)) + + +class cmd_group_rename(Command): + """Rename a group and related attributes. + + This command allows to set the group's name related attributes. The + group's CN will be renamed automatically. + + The group's CN will be the sAMAccountName. + Use the --force-new-cn option to specify the new CN manually and the + --reset-cn to reset this change. + + Use an empty attribute value to remove the specified attribute. + + The groupname specified on the command is the sAMAccountName. + + The command may be run locally from the root userid or another authorized + userid. + + The -H or --URL= option can be used to execute the command against a remote + server. + + Example1: + samba-tool group rename employees --samaccountname=staff + + Example1 shows how to change the samaccountname of a group 'employees' to + 'staff'. The CN of the group employees will also be changed to 'staff', + if the previous CN was the previous sAMAccountName. + + Example2: + samba-tool group rename employees --mail-address='staff@company.com' \\ + -H ldap://samba.samdom.example.com -U administrator + + Example2 shows how to rename the mail address of a group 'employees' to + 'staff@company.com'. + The -H parameter is used to specify the remote target server. + """ + + synopsis = "%prog <groupname> [options]" + + takes_options = [ + Option("-H", "--URL", + help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + Option("--force-new-cn", + help="Specify a new CN (RND) instead of using the sAMAccountName.", + type=str), + Option("--reset-cn", + help="Set the CN (RDN) to the sAMAccountName. Use this option " + "to reset the changes made with the --force-new-cn option.", + action="store_true"), + Option("--mail-address", + help="New mail address", + type=str), + Option("--samaccountname", + help="New account name (sAMAccountName/logon name)", + type=str) + ] + + takes_args = ["groupname"] + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "versionopts": options.VersionOptions, + } + + def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, + H=None, mail_address=None, samaccountname=None, force_new_cn=None, + reset_cn=None): + # illegal options + if force_new_cn and reset_cn: + raise CommandError("It is not allowed to specify --force-new-cn " + "together with --reset-cn.") + if force_new_cn == "": + raise CommandError("Failed to rename group - delete protected " + "attribute 'CN'") + if samaccountname == "": + raise CommandError("Failed to rename group - delete protected " + "attribute 'sAMAccountName'") + + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp, fallback_machine=True) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + domain_dn = ldb.Dn(samdb, samdb.domain_dn()) + + filter = ("(&(objectClass=group)(samaccountname=%s))" % + ldb.binary_encode(groupname)) + try: + res = samdb.search(base=domain_dn, + scope=ldb.SCOPE_SUBTREE, + expression=filter, + attrs=["sAMAccountName", + "cn", + "mail"] + ) + old_group = res[0] + group_dn = old_group.dn + except IndexError: + raise CommandError('Unable to find group "%s"' % (groupname)) + + group_parent_dn = group_dn.parent() + old_cn = old_group["cn"][0] + + # get the actual and the new group cn and the new dn + if force_new_cn is not None: + new_cn = force_new_cn + elif samaccountname is not None: + new_cn = samaccountname + else: + new_cn = old_group["sAMAccountName"] + + # CN must change, if the new CN is different and the old CN is the + # standard CN or the change is forced with force-new-cn or reset-cn + expected_cn = old_group["sAMAccountName"] + must_change_cn = str(old_cn) != str(new_cn) and \ + (str(old_cn) == str(expected_cn) or \ + reset_cn or bool(force_new_cn)) + + new_group_dn = ldb.Dn(samdb, "CN=%s" % new_cn) + new_group_dn.add_base(group_parent_dn) + + # format given attributes + group_attrs = ldb.Message() + group_attrs.dn = group_dn + samdb.prepare_attr_replace(group_attrs, old_group, "sAMAccountName", + samaccountname) + samdb.prepare_attr_replace(group_attrs, old_group, "mail", mail_address) + + group_attributes_changed = len(group_attrs) > 0 + + # update the group with formatted attributes + samdb.transaction_start() + try: + if group_attributes_changed: + samdb.modify(group_attrs) + if must_change_cn: + samdb.rename(group_dn, new_group_dn) + except Exception as e: + samdb.transaction_cancel() + raise CommandError('Failed to rename group "%s"' % groupname, e) + samdb.transaction_commit() + + if must_change_cn: + self.outf.write('Renamed CN of group "%s" from "%s" to "%s" ' + 'successfully\n' % (groupname, old_cn, new_cn)) + + if group_attributes_changed: + self.outf.write('Following attributes of group "%s" have been ' + 'changed successfully:\n' % (groupname)) + for attr in group_attrs.keys(): + if attr == "dn": + continue + self.outf.write('%s: %s\n' % (attr, group_attrs[attr] + if group_attrs[attr] else '[removed]')) + +class cmd_group(SuperCommand): + """Group management.""" + + subcommands = {} + subcommands["add"] = cmd_group_add() + subcommands["create"] = cmd_group_add() + subcommands["delete"] = cmd_group_delete() + subcommands["edit"] = cmd_group_edit() + subcommands["addmembers"] = cmd_group_add_members() + subcommands["removemembers"] = cmd_group_remove_members() + subcommands["list"] = cmd_group_list() + subcommands["listmembers"] = cmd_group_list_members() + subcommands["move"] = cmd_group_move() + subcommands["show"] = cmd_group_show() + subcommands["stats"] = cmd_group_stats() + subcommands["addunixattrs"] = cmd_group_add_unix_attrs() + subcommands["rename"] = cmd_group_rename() |