1 # Copyright Jelmer Vernooij 2008
3 # Based on the original in EJS:
4 # Copyright Andrew Tridgell 2005
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import samba
.getopt
as options
20 from samba
.netcmd
import Command
, SuperCommand
, CommandError
, Option
22 from samba
.ndr
import ndr_pack
, ndr_unpack
23 from samba
.dcerpc
import security
25 from samba
.auth
import system_session
26 from samba
.samdb
import SamDB
27 from samba
.dsdb
import (
28 ATYPE_SECURITY_GLOBAL_GROUP
,
29 DS_GUID_USERS_CONTAINER
,
30 GTYPE_SECURITY_BUILTIN_LOCAL_GROUP
,
31 GTYPE_SECURITY_DOMAIN_LOCAL_GROUP
,
32 GTYPE_SECURITY_GLOBAL_GROUP
,
33 GTYPE_SECURITY_UNIVERSAL_GROUP
,
34 GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP
,
35 GTYPE_DISTRIBUTION_GLOBAL_GROUP
,
36 GTYPE_DISTRIBUTION_UNIVERSAL_GROUP
,
37 SYSTEM_FLAG_DISALLOW_DELETE
,
38 SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE
,
39 SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME
,
42 from collections
import defaultdict
43 from subprocess
import check_call
, CalledProcessError
44 from samba
.common
import get_bytes
, normalise_int32
49 security_group
= dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP
,
50 "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP
,
51 "Global": GTYPE_SECURITY_GLOBAL_GROUP
,
52 "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP
})
53 distribution_group
= dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP
,
54 "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP
,
55 "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP
})
58 class cmd_group_add(Command
):
59 """Creates a new AD group.
61 This command adds a new Active Directory group. The groupname specified on the command is a unique sAMAccountName.
63 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.
65 Groups may also be used to establish email distribution lists, using --group-type=Distribution.
67 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.
69 The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created.
71 The command may be run from the root userid or another authorized userid. The
72 -H or --URL= option can be used to execute the command on a remote server.
75 samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group'
77 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.
80 sudo samba-tool group add Group2 --group-type=Distribution
82 Example2 adds a new distribution group to the local server. The command is run under root using the sudo command.
85 samba-tool group add Group3 --nis-domain=samdom --gid-number=12345
87 Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature).
90 synopsis
= "%prog <groupname> [options]"
92 takes_optiongroups
= {
93 "sambaopts": options
.SambaOptions
,
94 "versionopts": options
.VersionOptions
,
95 "credopts": options
.CredentialsOptions
,
99 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
100 metavar
="URL", dest
="H"),
102 help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created",
104 Option("--group-scope", type="choice", choices
=["Domain", "Global", "Universal"],
105 help="Group scope (Domain | Global | Universal)"),
106 Option("--group-type", type="choice", choices
=["Security", "Distribution"],
107 help="Group type (Security | Distribution)"),
108 Option("--description", help="Group's description", type=str),
109 Option("--mail-address", help="Group's email address", type=str),
110 Option("--notes", help="Group's notes", type=str),
111 Option("--gid-number", help="Group's Unix/RFC2307 GID number", type=int),
112 Option("--nis-domain", help="SFU30 NIS Domain", type=str),
113 Option("--special", help="Add a special predefined group", action
="store_true", default
=False),
116 takes_args
= ["groupname"]
118 def run(self
, groupname
, credopts
=None, sambaopts
=None,
119 versionopts
=None, H
=None, groupou
=None, group_scope
=None,
120 group_type
=None, description
=None, mail_address
=None, notes
=None, gid_number
=None, nis_domain
=None,
123 if (group_type
or "Security") == "Security":
124 gtype
= security_group
.get(group_scope
, GTYPE_SECURITY_GLOBAL_GROUP
)
126 gtype
= distribution_group
.get(group_scope
, GTYPE_DISTRIBUTION_GLOBAL_GROUP
)
128 if (gid_number
is None and nis_domain
is not None) or (gid_number
is not None and nis_domain
is None):
129 raise CommandError('Both --gid-number and --nis-domain have to be set for a RFC2307-enabled group. Operation cancelled.')
131 lp
= sambaopts
.get_loadparm()
132 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
135 samdb
= SamDB(url
=H
, session_info
=system_session(),
136 credentials
=creds
, lp
=lp
)
137 except Exception as e
:
138 # FIXME: catch more specific exception
139 raise CommandError(f
'Failed to add group "{groupname}"', e
)
142 invalid_option
= None
143 if group_scope
is not None:
144 invalid_option
= 'group-scope'
145 elif group_type
is not None:
146 invalid_option
= 'group-type'
147 elif description
is not None:
148 invalid_option
= 'description'
149 elif mail_address
is not None:
150 invalid_option
= 'mail-address'
151 elif notes
is not None:
152 invalid_option
= 'notes'
153 elif gid_number
is not None:
154 invalid_option
= 'gid-number'
155 elif nis_domain
is not None:
156 invalid_option
= 'nis-domain'
158 if invalid_option
is not None:
159 raise CommandError(f
'Superfluous option --{invalid_option} '
160 f
'specified with --special')
162 if not samdb
.am_pdc():
163 raise CommandError('Adding special groups is only permitted '
167 # On Windows, this group is added automatically when the PDC
168 # role is held by a DC running Windows Server 2012 R2 or later.
169 # https://docs.microsoft.com/en-us/windows-server/security/credentials-protection-and-management/protected-users-security-group#BKMK_Requirements
170 'Protected Users'.lower(): (
172 GTYPE_SECURITY_GLOBAL_GROUP
,
173 security
.DOMAIN_RID_PROTECTED_USERS
,
174 'Members of this group are afforded additional '
175 'protections against authentication security threats'),
178 special_group
= special_groups
.get(groupname
.lower())
179 if special_group
is None:
180 raise CommandError(f
'Unknown special group "{groupname}".')
182 groupname
, gtype
, rid
, description
= special_group
183 group_type
= normalise_int32(gtype
)
185 group_dn
= samdb
.get_default_basedn()
187 if gtype
== GTYPE_SECURITY_GLOBAL_GROUP
:
188 object_sid
= security
.dom_sid(
189 f
'{samdb.get_domain_sid()}-{rid}')
193 group_dn
= samdb
.get_wellknown_dn(group_dn
,
194 DS_GUID_USERS_CONTAINER
)
196 elif gtype
== GTYPE_SECURITY_BUILTIN_LOCAL_GROUP
:
197 object_sid
= security
.dom_sid(f
'S-1-5-32-{rid}')
198 system_flags
= (SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE |
199 SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME |
200 SYSTEM_FLAG_DISALLOW_DELETE
)
204 group_dn
.add_child('CN=Builtin')
206 raise RuntimeError('Error getting Builtin objects DN')
208 raise RuntimeError(f
'Unknown group type {gtype}')
212 group_dn
.add_child(groupou
)
214 raise CommandError(f
'Invalid group OU "{groupou}"')
217 group_dn
.add_child(f
'CN={groupname}')
219 raise CommandError(f
'Invalid group name "{groupname}"')
223 'sAMAccountName': groupname
,
224 'objectClass': 'group',
225 'groupType': group_type
,
226 'description': description
,
227 'objectSid': ndr_pack(object_sid
),
228 'isCriticalSystemObject': 'TRUE',
231 if system_flags
is not None:
232 msg
['systemFlags'] = system_flags
235 samdb
.add(msg
, controls
=['relax:0'])
236 except ldb
.LdbError
as e
:
238 if num
== ldb
.ERR_CONSTRAINT_VIOLATION
:
241 expression
=f
'(objectSid={object_sid})',
242 attrs
=['sAMAccountName'])
245 f
'Failed to add group "{groupname}"', e
)
249 f
'Failed to add group "{groupname}"', e
)
251 name
= res
[0].get('sAMAccountName', idx
=0)
253 with_name
= f
' with name "{name}"'
258 f
'Failed to add group "{groupname}" - Special group '
259 f
'already exists{with_name} at "{res[0].dn}".')
261 elif num
== ldb
.ERR_ENTRY_ALREADY_EXISTS
:
263 res
= samdb
.search(base
=group_dn
,
264 scope
=ldb
.SCOPE_BASE
,
265 attrs
=['sAMAccountName',
271 expression
=f
'(sAMAccountName={groupname})',
272 attrs
=['sAMAccountName',
277 f
'Failed to add group "{groupname}"', e
)
281 f
'Failed to add group "{groupname}"', e
)
283 got_name
= res
[0].get('sAMAccountName', idx
=0)
285 named
= f
'named "{got_name}"'
287 named
= 'with no name'
289 got_group_type
= res
[0].get('groupType',
290 idx
=0).decode('utf-8')
291 if group_type
!= got_group_type
:
293 f
'Failed to add group "{groupname}" - An object '
294 f
'{named} at "{res[0].dn}" already exists, but it '
295 f
'is not a security group. Rename or remove this '
296 f
'existing object before attempting to add this '
299 sid
= res
[0].get('objectSid', idx
=0)
302 f
'Failed to add group "{groupname}" - A security '
303 f
'group {named} at "{res[0].dn}" already exists, '
304 f
'but it lacks a SID. Rename or remove this '
305 f
'existing object before attempting to add this '
308 sid
= ndr_unpack(security
.dom_sid
, sid
)
309 if sid
== object_sid
:
311 f
'Failed to add group "{groupname}" - The '
312 f
'security group {named} at "{res[0].dn}" '
316 f
'Failed to add group "{groupname}" - A '
317 f
'security group {named} at "{res[0].dn}" '
318 f
'already exists, but it has the wrong SID, '
319 f
'and will not function as expected. Rename '
320 f
'or remove this existing object before '
321 f
'attempting to add this special group.')
323 raise CommandError(f
'Failed to add group "{groupname}"', e
)
325 self
.outf
.write(f
'Added group {groupname}\n')
330 samdb
.newgroup(groupname
, groupou
=groupou
, grouptype
=gtype
,
331 description
=description
, mailaddress
=mail_address
, notes
=notes
,
332 gidnumber
=gid_number
, nisdomain
=nis_domain
)
333 except Exception as e
:
334 # FIXME: catch more specific exception
335 raise CommandError('Failed to add group "%s"' % groupname
, e
)
336 self
.outf
.write("Added group %s\n" % groupname
)
339 class cmd_group_delete(Command
):
340 """Deletes an AD group.
342 The command deletes an existing AD group from the Active Directory domain. The groupname specified on the command is the sAMAccountName.
344 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.
346 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.
349 samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
351 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.
354 sudo samba-tool group delete Group2
356 Example2 deletes group Group2 from the local server. The command is run under root using the sudo command.
359 synopsis
= "%prog <groupname> [options]"
361 takes_optiongroups
= {
362 "sambaopts": options
.SambaOptions
,
363 "versionopts": options
.VersionOptions
,
364 "credopts": options
.CredentialsOptions
,
368 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
369 metavar
="URL", dest
="H"),
372 takes_args
= ["groupname"]
374 def run(self
, groupname
, credopts
=None, sambaopts
=None, versionopts
=None, H
=None):
376 lp
= sambaopts
.get_loadparm()
377 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
378 samdb
= SamDB(url
=H
, session_info
=system_session(),
379 credentials
=creds
, lp
=lp
)
381 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
382 ldb
.binary_encode(groupname
))
385 res
= samdb
.search(base
=samdb
.domain_dn(),
386 scope
=ldb
.SCOPE_SUBTREE
,
391 raise CommandError('Unable to find group "%s"' % (groupname
))
394 samdb
.delete(group_dn
)
395 except Exception as e
:
396 # FIXME: catch more specific exception
397 raise CommandError('Failed to remove group "%s"' % groupname
, e
)
398 self
.outf
.write("Deleted group %s\n" % groupname
)
401 class cmd_group_add_members(Command
):
402 """Add members to an AD group.
404 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.
406 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.
408 The member names specified on the command must be the sAMaccountName.
411 samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
413 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.
416 sudo samba-tool group addmembers supergroup User2
418 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.
421 synopsis
= "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
423 takes_optiongroups
= {
424 "sambaopts": options
.SambaOptions
,
425 "versionopts": options
.VersionOptions
,
426 "credopts": options
.CredentialsOptions
,
430 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
431 metavar
="URL", dest
="H"),
432 Option("--member-dn",
433 help=("DN of the new group member to be added.\n"
434 "The --object-types option will be ignored."),
437 Option("--object-types",
438 help=("Comma separated list of object types.\n"
439 "The types are used to filter the search for the "
440 "specified members.\n"
441 "Valid values are: user, group, computer, serviceaccount, "
443 "Default: user,group,computer"),
444 default
="user,group,computer",
446 Option("--member-base-dn",
447 help=("Base DN for group member search.\n"
448 "Default is the domain DN."),
452 takes_args
= ["groupname", "listofmembers?"]
463 object_types
="user,group,computer"):
465 lp
= sambaopts
.get_loadparm()
466 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
468 if member_dn
is None and listofmembers
is None:
471 'Either listofmembers or --member-dn must be specified.')
474 samdb
= SamDB(url
=H
, session_info
=system_session(),
475 credentials
=creds
, lp
=lp
)
477 if member_dn
is not None:
478 groupmembers
+= member_dn
479 if listofmembers
is not None:
480 groupmembers
+= listofmembers
.split(',')
481 group_member_types
= object_types
.split(',')
483 if member_base_dn
is not None:
484 member_base_dn
= samdb
.normalize_dn_in_domain(member_base_dn
)
486 samdb
.add_remove_group_members(groupname
, groupmembers
,
487 add_members_operation
=True,
488 member_types
=group_member_types
,
489 member_base_dn
=member_base_dn
)
490 except Exception as e
:
491 # FIXME: catch more specific exception
492 raise CommandError('Failed to add members %r to group "%s" - %s' % (
493 groupmembers
, groupname
, e
))
494 self
.outf
.write("Added members to group %s\n" % groupname
)
497 class cmd_group_remove_members(Command
):
498 """Remove members from an AD group.
500 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.
502 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
505 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
507 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.
510 sudo samba-tool group removemembers supergroup User1
512 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.
515 synopsis
= "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
517 takes_optiongroups
= {
518 "sambaopts": options
.SambaOptions
,
519 "versionopts": options
.VersionOptions
,
520 "credopts": options
.CredentialsOptions
,
524 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
525 metavar
="URL", dest
="H"),
526 Option("--member-dn",
527 help=("DN of the group member to be removed.\n"
528 "The --object-types option will be ignored."),
531 Option("--object-types",
532 help=("Comma separated list of object types.\n"
533 "The types are used to filter the search for the "
534 "specified members.\n"
535 "Valid values are: user, group, computer, serviceaccount, "
537 "Default: user,group,computer"),
538 default
="user,group,computer",
540 Option("--member-base-dn",
541 help=("Base DN for group member search.\n"
542 "Default is the domain DN."),
546 takes_args
= ["groupname", "listofmembers?"]
557 object_types
="user,group,computer"):
559 lp
= sambaopts
.get_loadparm()
560 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
562 if member_dn
is None and listofmembers
is None:
565 'Either listofmembers or --member-dn must be specified.')
568 samdb
= SamDB(url
=H
, session_info
=system_session(),
569 credentials
=creds
, lp
=lp
)
571 if member_dn
is not None:
572 groupmembers
+= member_dn
573 if listofmembers
is not None:
574 groupmembers
+= listofmembers
.split(',')
575 group_member_types
= object_types
.split(',')
577 if member_base_dn
is not None:
578 member_base_dn
= samdb
.normalize_dn_in_domain(member_base_dn
)
580 samdb
.add_remove_group_members(groupname
,
582 add_members_operation
=False,
583 member_types
=group_member_types
,
584 member_base_dn
=member_base_dn
)
585 except Exception as e
:
586 # FIXME: Catch more specific exception
587 raise CommandError('Failed to remove members %r from group "%s"' % (listofmembers
, groupname
), e
)
588 self
.outf
.write("Removed members from group %s\n" % groupname
)
591 class cmd_group_list(Command
):
592 """List all groups."""
594 synopsis
= "%prog [options]"
597 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
598 metavar
="URL", dest
="H"),
599 Option("-v", "--verbose",
600 help="Verbose output, showing group type and group scope.",
601 action
="store_true"),
602 Option("-b", "--base-dn",
603 help="Specify base DN to use.",
605 Option("--full-dn", dest
="full_dn",
608 help="Display DN instead of the sAMAccountName."),
611 takes_optiongroups
= {
612 "sambaopts": options
.SambaOptions
,
613 "credopts": options
.CredentialsOptions
,
614 "versionopts": options
.VersionOptions
,
625 lp
= sambaopts
.get_loadparm()
626 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
628 samdb
= SamDB(url
=H
, session_info
=system_session(),
629 credentials
=creds
, lp
=lp
)
630 attrs
=["samaccountname"]
633 attrs
+= ["grouptype", "member"]
634 domain_dn
= samdb
.domain_dn()
636 domain_dn
= samdb
.normalize_dn_in_domain(base_dn
)
637 res
= samdb
.search(domain_dn
, scope
=ldb
.SCOPE_SUBTREE
,
638 expression
=("(objectClass=group)"),
644 self
.outf
.write("Group Name Group Type Group Scope Members\n")
645 self
.outf
.write("--------------------------------------------------------------------------------\n")
648 self
.outf
.write("%-44s" % msg
.get("samaccountname", idx
=0))
649 hgtype
= hex(int("%s" % msg
["grouptype"]) & 0x00000000FFFFFFFF)
650 if (hgtype
== hex(int(security_group
.get("Builtin")))):
651 self
.outf
.write("Security Builtin ")
652 elif (hgtype
== hex(int(security_group
.get("Domain")))):
653 self
.outf
.write("Security Domain ")
654 elif (hgtype
== hex(int(security_group
.get("Global")))):
655 self
.outf
.write("Security Global ")
656 elif (hgtype
== hex(int(security_group
.get("Universal")))):
657 self
.outf
.write("Security Universal")
658 elif (hgtype
== hex(int(distribution_group
.get("Global")))):
659 self
.outf
.write("Distribution Global ")
660 elif (hgtype
== hex(int(distribution_group
.get("Domain")))):
661 self
.outf
.write("Distribution Domain ")
662 elif (hgtype
== hex(int(distribution_group
.get("Universal")))):
663 self
.outf
.write("Distribution Universal")
666 num_members
= len(msg
.get("member", default
=[]))
667 self
.outf
.write(" %6u\n" % num_members
)
671 self
.outf
.write("%s\n" % msg
.get("dn"))
674 self
.outf
.write("%s\n" % msg
.get("samaccountname", idx
=0))
677 class cmd_group_list_members(Command
):
678 """List all members of an AD group.
680 This command lists members from an existing Active Directory group. The command accepts one group name.
683 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
686 synopsis
= "%prog <groupname> [options]"
689 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
690 metavar
="URL", dest
="H"),
691 Option("--hide-expired",
692 help="Do not list expired group members",
694 action
='store_true'),
695 Option("--hide-disabled",
698 help="Do not list disabled group members"),
699 Option("--full-dn", dest
="full_dn",
702 help="Display DN instead of the sAMAccountName.")
705 takes_optiongroups
= {
706 "sambaopts": options
.SambaOptions
,
707 "credopts": options
.CredentialsOptions
,
708 "versionopts": options
.VersionOptions
,
711 takes_args
= ["groupname"]
722 lp
= sambaopts
.get_loadparm()
723 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
726 samdb
= SamDB(url
=H
, session_info
=system_session(),
727 credentials
=creds
, lp
=lp
)
729 search_filter
= ("(&(objectClass=group)(sAMAccountName=%s))" %
730 ldb
.binary_encode(groupname
))
732 res
= samdb
.search(samdb
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
733 expression
=(search_filter
),
735 group_sid_binary
= res
[0].get('objectSid', idx
=0)
737 raise CommandError('Unable to find group "%s"' % (groupname
))
739 group_sid
= ndr_unpack(security
.dom_sid
, group_sid_binary
)
740 (group_dom_sid
, rid
) = group_sid
.split()
741 group_sid_dn
= "<SID=%s>" % (group_sid
)
744 if hide_expired
is True:
745 current_nttime
= samdb
.get_nttime()
746 filter_expires
= ("(|"
747 "(!(accountExpires=*))"
749 "(accountExpires>=%u)"
750 ")" % (current_nttime
))
753 if hide_disabled
is True:
754 filter_disabled
= "(!(userAccountControl:%s:=%u))" % (
755 ldb
.OID_COMPARATOR_AND
, UF_ACCOUNTDISABLE
)
757 filter = "(&(|(primaryGroupID=%s)(memberOf=%s))%s%s)" % (
758 rid
, group_sid_dn
, filter_disabled
, filter_expires
)
760 res
= samdb
.search(samdb
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
762 attrs
=["samAccountName", "cn"])
769 self
.outf
.write("%s\n" % msg
.get("dn"))
772 member_name
= msg
.get("samAccountName", idx
=0)
773 if member_name
is None:
774 member_name
= msg
.get("cn", idx
=0)
775 self
.outf
.write("%s\n" % member_name
)
777 except Exception as e
:
778 raise CommandError('Failed to list members of "%s" group - %s' %
782 class cmd_group_move(Command
):
783 """Move a group to an organizational unit/container.
785 This command moves a group object into the specified organizational unit
787 The groupname specified on the command is the sAMAccountName.
788 The name of the organizational unit or container can be specified as a
789 full DN or without the domainDN component.
791 The command may be run from the root userid or another authorized userid.
793 The -H or --URL= option can be used to execute the command against a remote
797 samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
798 -H ldap://samba.samdom.example.com -U administrator
800 Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
801 unit on a remote LDAP server.
803 The -H parameter is used to specify the remote target server.
806 samba-tool group move Group1 CN=Users
808 Example2 shows how to move a group Group1 back into the CN=Users container
812 synopsis
= "%prog <groupname> <new_parent_dn> [options]"
815 Option("-H", "--URL", help="LDB URL for database or target server",
816 type=str, metavar
="URL", dest
="H"),
819 takes_args
= ["groupname", "new_parent_dn"]
820 takes_optiongroups
= {
821 "sambaopts": options
.SambaOptions
,
822 "credopts": options
.CredentialsOptions
,
823 "versionopts": options
.VersionOptions
,
826 def run(self
, groupname
, new_parent_dn
, credopts
=None, sambaopts
=None,
827 versionopts
=None, H
=None):
828 lp
= sambaopts
.get_loadparm()
829 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
830 samdb
= SamDB(url
=H
, session_info
=system_session(),
831 credentials
=creds
, lp
=lp
)
832 domain_dn
= ldb
.Dn(samdb
, samdb
.domain_dn())
834 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
835 ldb
.binary_encode(groupname
))
837 res
= samdb
.search(base
=domain_dn
,
839 scope
=ldb
.SCOPE_SUBTREE
)
842 raise CommandError('Unable to find group "%s"' % (groupname
))
845 full_new_parent_dn
= samdb
.normalize_dn_in_domain(new_parent_dn
)
846 except Exception as e
:
847 raise CommandError('Invalid new_parent_dn "%s": %s' %
848 (new_parent_dn
, e
.message
))
850 full_new_group_dn
= ldb
.Dn(samdb
, str(group_dn
))
851 full_new_group_dn
.remove_base_components(len(group_dn
) - 1)
852 full_new_group_dn
.add_base(full_new_parent_dn
)
855 samdb
.rename(group_dn
, full_new_group_dn
)
856 except Exception as e
:
857 raise CommandError('Failed to move group "%s"' % groupname
, e
)
858 self
.outf
.write('Moved group "%s" into "%s"\n' %
859 (groupname
, full_new_parent_dn
))
862 class cmd_group_show(Command
):
863 """Display a group AD object.
865 This command displays a group object and it's attributes in the Active
867 The group name specified on the command is the sAMAccountName of the group.
869 The command may be run from the root userid or another authorized userid.
871 The -H or --URL= option can be used to execute the command against a remote
875 samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
876 -U administrator --password=passw1rd
878 Example1 shows how to display a group's attributes in the domain against a
881 The -H parameter is used to specify the remote target server.
884 samba-tool group show Group2
886 Example2 shows how to display a group's attributes in the domain against a local
890 samba-tool group show Group3 --attributes=member,objectGUID
892 Example3 shows how to display a groups objectGUID and member attributes.
894 synopsis
= "%prog <group name> [options]"
897 Option("-H", "--URL", help="LDB URL for database or target server",
898 type=str, metavar
="URL", dest
="H"),
899 Option("--attributes",
900 help=("Comma separated list of attributes, "
901 "which will be printed."),
902 type=str, dest
="group_attrs"),
905 takes_args
= ["groupname"]
906 takes_optiongroups
= {
907 "sambaopts": options
.SambaOptions
,
908 "credopts": options
.CredentialsOptions
,
909 "versionopts": options
.VersionOptions
,
912 def run(self
, groupname
, credopts
=None, sambaopts
=None, versionopts
=None,
913 H
=None, group_attrs
=None):
915 lp
= sambaopts
.get_loadparm()
916 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
917 samdb
= SamDB(url
=H
, session_info
=system_session(),
918 credentials
=creds
, lp
=lp
)
922 attrs
= group_attrs
.split(",")
924 filter = ("(&(objectCategory=group)(sAMAccountName=%s))" %
925 ldb
.binary_encode(groupname
))
927 domaindn
= samdb
.domain_dn()
930 res
= samdb
.search(base
=domaindn
, expression
=filter,
931 scope
=ldb
.SCOPE_SUBTREE
, attrs
=attrs
)
934 raise CommandError('Unable to find group "%s"' % (groupname
))
937 group_ldif
= common
.get_ldif_for_editor(samdb
, msg
)
938 self
.outf
.write(group_ldif
)
941 class cmd_group_stats(Command
):
942 """Summary statistics about group memberships."""
944 synopsis
= "%prog [options]"
947 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
948 metavar
="URL", dest
="H"),
951 takes_optiongroups
= {
952 "sambaopts": options
.SambaOptions
,
953 "credopts": options
.CredentialsOptions
,
954 "versionopts": options
.VersionOptions
,
957 def num_in_range(self
, range_min
, range_max
, group_freqs
):
959 for members
, count
in group_freqs
.items():
960 if range_min
<= members
and members
<= range_max
:
965 def run(self
, sambaopts
=None, credopts
=None, versionopts
=None, H
=None):
966 lp
= sambaopts
.get_loadparm()
967 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
969 samdb
= SamDB(url
=H
, session_info
=system_session(),
970 credentials
=creds
, lp
=lp
)
972 domain_dn
= samdb
.domain_dn()
973 res
= samdb
.search(domain_dn
, scope
=ldb
.SCOPE_SUBTREE
,
974 expression
=("(objectClass=group)"),
975 attrs
=["samaccountname", "member"])
977 # first count up how many members each group has
978 group_assignments
= {}
979 total_memberships
= 0
982 name
= str(msg
.get("samaccountname"))
983 num_members
= len(msg
.get("member", default
=[]))
984 group_assignments
[name
] = num_members
985 total_memberships
+= num_members
987 num_groups
= res
.count
988 self
.outf
.write("Group membership statistics*\n")
989 self
.outf
.write("-------------------------------------------------\n")
990 self
.outf
.write("Total groups: {0}\n".format(num_groups
))
991 self
.outf
.write("Total memberships: {0}\n".format(total_memberships
))
992 average
= total_memberships
/ float(num_groups
)
993 self
.outf
.write("Average members per group: %.2f\n" % average
)
995 # find the max and median memberships (note that some default groups
996 # always have zero members, so displaying the min is not very helpful)
997 group_names
= list(group_assignments
.keys())
998 group_members
= list(group_assignments
.values())
999 idx
= group_members
.index(max(group_members
))
1000 max_members
= group_members
[idx
]
1001 self
.outf
.write("Max members: {0} ({1})\n".format(max_members
,
1003 group_members
.sort()
1004 midpoint
= num_groups
// 2
1005 median
= group_members
[midpoint
]
1006 if num_groups
% 2 == 0:
1007 median
= (median
+ group_members
[midpoint
- 1]) / 2
1008 self
.outf
.write("Median members per group: {0}\n\n".format(median
))
1010 # convert this to the frequency of group membership, i.e. how many
1011 # groups have 5 members, how many have 6 members, etc
1012 group_freqs
= defaultdict(int)
1013 for group
, num_members
in group_assignments
.items():
1014 group_freqs
[num_members
] += 1
1016 # now squash this down even further, so that we just display the number
1017 # of groups that fall into one of the following membership bands
1018 bands
= [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24),
1019 (25, 29), (30, 39), (40, 49), (50, 59), (60, 69), (70, 79),
1020 (80, 89), (90, 99), (100, 149), (150, 199), (200, 249),
1021 (250, 299), (300, 399), (400, 499), (500, 999), (1000, 1999),
1022 (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999),
1023 (10000, max_members
)]
1025 self
.outf
.write("Members Number of Groups\n")
1026 self
.outf
.write("-------------------------------------------------\n")
1029 band_start
= band
[0]
1031 if band_start
> max_members
:
1034 num_groups
= self
.num_in_range(band_start
, band_end
, group_freqs
)
1037 band_str
= "{0}-{1}".format(band_start
, band_end
)
1038 self
.outf
.write("%13s %u\n" % (band_str
, num_groups
))
1040 self
.outf
.write("\n* Note this does not include nested group memberships\n")
1043 class cmd_group_edit(Command
):
1044 """Modify Group AD object.
1046 This command will allow editing of a group account in the Active Directory
1047 domain. You will then be able to add or change attributes and their values.
1049 The groupname specified on the command is the sAMAccountName.
1051 The command may be run from the root userid or another authorized userid.
1053 The -H or --URL= option can be used to execute the command against a remote
1057 samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
1058 -U administrator --password=passw1rd
1060 Example1 shows how to edit a groups attributes in the domain against a
1063 The -H parameter is used to specify the remote target server.
1066 samba-tool group edit Group2
1068 Example2 shows how to edit a groups attributes in the domain against a local
1072 samba-tool group edit Group3 --editor=nano
1074 Example3 shows how to edit a groups attributes in the domain against a local
1075 server using the 'nano' editor.
1077 synopsis
= "%prog <groupname> [options]"
1080 Option("-H", "--URL", help="LDB URL for database or target server",
1081 type=str, metavar
="URL", dest
="H"),
1082 Option("--editor", help="Editor to use instead of the system default,"
1083 " or 'vi' if no system default is set.", type=str),
1086 takes_args
= ["groupname"]
1087 takes_optiongroups
= {
1088 "sambaopts": options
.SambaOptions
,
1089 "credopts": options
.CredentialsOptions
,
1090 "versionopts": options
.VersionOptions
,
1093 def run(self
, groupname
, credopts
=None, sambaopts
=None, versionopts
=None,
1094 H
=None, editor
=None):
1095 lp
= sambaopts
.get_loadparm()
1096 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
1097 samdb
= SamDB(url
=H
, session_info
=system_session(),
1098 credentials
=creds
, lp
=lp
)
1100 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
1101 ldb
.binary_encode(groupname
))
1103 domaindn
= samdb
.domain_dn()
1106 res
= samdb
.search(base
=domaindn
,
1108 scope
=ldb
.SCOPE_SUBTREE
)
1109 group_dn
= res
[0].dn
1111 raise CommandError('Unable to find group "%s"' % (groupname
))
1114 raise CommandError('Invalid number of results: for "%s": %d' %
1115 ((groupname
), len(res
)))
1118 result_ldif
= common
.get_ldif_for_editor(samdb
, msg
)
1121 editor
= os
.environ
.get('EDITOR')
1125 with tempfile
.NamedTemporaryFile(suffix
=".tmp") as t_file
:
1126 t_file
.write(get_bytes(result_ldif
))
1129 check_call([editor
, t_file
.name
])
1130 except CalledProcessError
as e
:
1131 raise CalledProcessError("ERROR: ", e
)
1132 with
open(t_file
.name
) as edited_file
:
1133 edited_message
= edited_file
.read()
1135 msgs_edited
= samdb
.parse_ldif(edited_message
)
1136 msg_edited
= next(msgs_edited
)[1]
1138 res_msg_diff
= samdb
.msg_diff(msg
, msg_edited
)
1139 if len(res_msg_diff
) == 0:
1140 self
.outf
.write("Nothing to do\n")
1144 samdb
.modify(res_msg_diff
)
1145 except Exception as e
:
1146 raise CommandError("Failed to modify group '%s': " % groupname
, e
)
1148 self
.outf
.write("Modified group '%s' successfully\n" % groupname
)
1151 class cmd_group_add_unix_attrs(Command
):
1152 """Add RFC2307 attributes to a group.
1154 This command adds Unix attributes to a group account in the Active
1156 The groupname specified on the command is the sAMaccountName.
1158 Unix (RFC2307) attributes will be added to the group account.
1160 Add 'idmap_ldb:use rfc2307 = Yes' to smb.conf to use these attributes for
1163 The command may be run from the root userid or another authorized userid.
1164 The -H or --URL= option can be used to execute the command against a
1168 samba-tool group addunixattrs Group1 10000
1170 Example1 shows how to add RFC2307 attributes to a domain enabled group
1173 The groups Unix ID will be set to '10000', provided this ID isn't already
1177 synopsis
= "%prog <groupname> <gidnumber> [options]"
1180 Option("-H", "--URL", help="LDB URL for database or target server",
1181 type=str, metavar
="URL", dest
="H"),
1184 takes_args
= ["groupname", "gidnumber"]
1186 takes_optiongroups
= {
1187 "sambaopts": options
.SambaOptions
,
1188 "credopts": options
.CredentialsOptions
,
1189 "versionopts": options
.VersionOptions
,
1192 def run(self
, groupname
, gidnumber
, credopts
=None, sambaopts
=None,
1193 versionopts
=None, H
=None):
1195 lp
= sambaopts
.get_loadparm()
1196 creds
= credopts
.get_credentials(lp
)
1198 samdb
= SamDB(url
=H
, session_info
=system_session(),
1199 credentials
=creds
, lp
=lp
)
1201 domaindn
= samdb
.domain_dn()
1203 # Check group exists and doesn't have a gidNumber
1204 filter = "(samaccountname={})".format(ldb
.binary_encode(groupname
))
1205 res
= samdb
.search(domaindn
,
1206 scope
=ldb
.SCOPE_SUBTREE
,
1209 raise CommandError("Unable to find group '{}'".format(groupname
))
1211 group_dn
= res
[0].dn
1213 if "gidNumber" in res
[0]:
1214 raise CommandError("Group {} is a Unix group.".format(groupname
))
1216 # Check if supplied gidnumber isn't already being used
1217 filter = "(&(objectClass=group)(gidNumber={}))".format(gidnumber
)
1218 res
= samdb
.search(domaindn
,
1219 scope
=ldb
.SCOPE_SUBTREE
,
1222 raise CommandError('gidNumber {} already used.'.format(gidnumber
))
1224 if not lp
.get("idmap_ldb:use rfc2307"):
1225 self
.outf
.write("You are setting a Unix/RFC2307 GID. "
1226 "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
1227 " in smb.conf to use the attributes for "
1228 "XID/SID-mapping.\n")
1235 """.format(group_dn
, gidnumber
)
1238 samdb
.modify_ldif(group_mod
)
1239 except ldb
.LdbError
as e
:
1240 raise CommandError("Failed to modify group '{0}': {1}"
1241 .format(groupname
, e
))
1243 self
.outf
.write("Modified Group '{}' successfully\n".format(groupname
))
1246 class cmd_group_rename(Command
):
1247 """Rename a group and related attributes.
1249 This command allows to set the group's name related attributes. The
1250 group's CN will be renamed automatically.
1252 The group's CN will be the sAMAccountName.
1253 Use the --force-new-cn option to specify the new CN manually and the
1254 --reset-cn to reset this change.
1256 Use an empty attribute value to remove the specified attribute.
1258 The groupname specified on the command is the sAMAccountName.
1260 The command may be run locally from the root userid or another authorized
1263 The -H or --URL= option can be used to execute the command against a remote
1267 samba-tool group rename employees --samaccountname=staff
1269 Example1 shows how to change the samaccountname of a group 'employees' to
1270 'staff'. The CN of the group employees will also be changed to 'staff',
1271 if the previous CN was the previous sAMAccountName.
1274 samba-tool group rename employees --mail-address='staff@company.com' \\
1275 -H ldap://samba.samdom.example.com -U administrator
1277 Example2 shows how to rename the mail address of a group 'employees' to
1278 'staff@company.com'.
1279 The -H parameter is used to specify the remote target server.
1282 synopsis
= "%prog <groupname> [options]"
1285 Option("-H", "--URL",
1286 help="LDB URL for database or target server",
1287 type=str, metavar
="URL", dest
="H"),
1288 Option("--force-new-cn",
1289 help="Specify a new CN (RND) instead of using the sAMAccountName.",
1291 Option("--reset-cn",
1292 help="Set the CN (RDN) to the sAMAccountName. Use this option "
1293 "to reset the changes made with the --force-new-cn option.",
1294 action
="store_true"),
1295 Option("--mail-address",
1296 help="New mail address",
1298 Option("--samaccountname",
1299 help="New account name (sAMAccountName/logon name)",
1303 takes_args
= ["groupname"]
1304 takes_optiongroups
= {
1305 "sambaopts": options
.SambaOptions
,
1306 "credopts": options
.CredentialsOptions
,
1307 "versionopts": options
.VersionOptions
,
1310 def run(self
, groupname
, credopts
=None, sambaopts
=None, versionopts
=None,
1311 H
=None, mail_address
=None, samaccountname
=None, force_new_cn
=None,
1314 if force_new_cn
and reset_cn
:
1315 raise CommandError("It is not allowed to specify --force-new-cn "
1316 "together with --reset-cn.")
1317 if force_new_cn
== "":
1318 raise CommandError("Failed to rename group - delete protected "
1320 if samaccountname
== "":
1321 raise CommandError("Failed to rename group - delete protected "
1322 "attribute 'sAMAccountName'")
1324 lp
= sambaopts
.get_loadparm()
1325 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
1326 samdb
= SamDB(url
=H
, session_info
=system_session(),
1327 credentials
=creds
, lp
=lp
)
1328 domain_dn
= ldb
.Dn(samdb
, samdb
.domain_dn())
1330 filter = ("(&(objectClass=group)(samaccountname=%s))" %
1331 ldb
.binary_encode(groupname
))
1333 res
= samdb
.search(base
=domain_dn
,
1334 scope
=ldb
.SCOPE_SUBTREE
,
1336 attrs
=["sAMAccountName",
1341 group_dn
= old_group
.dn
1343 raise CommandError('Unable to find group "%s"' % (groupname
))
1345 group_parent_dn
= group_dn
.parent()
1346 old_cn
= old_group
["cn"][0]
1348 # get the actual and the new group cn and the new dn
1349 if force_new_cn
is not None:
1350 new_cn
= force_new_cn
1351 elif samaccountname
is not None:
1352 new_cn
= samaccountname
1354 new_cn
= old_group
["sAMAccountName"]
1356 # CN must change, if the new CN is different and the old CN is the
1357 # standard CN or the change is forced with force-new-cn or reset-cn
1358 expected_cn
= old_group
["sAMAccountName"]
1359 must_change_cn
= str(old_cn
) != str(new_cn
) and \
1360 (str(old_cn
) == str(expected_cn
) or \
1361 reset_cn
or bool(force_new_cn
))
1363 new_group_dn
= ldb
.Dn(samdb
, "CN=%s" % new_cn
)
1364 new_group_dn
.add_base(group_parent_dn
)
1366 # format given attributes
1367 group_attrs
= ldb
.Message()
1368 group_attrs
.dn
= group_dn
1369 samdb
.prepare_attr_replace(group_attrs
, old_group
, "sAMAccountName",
1371 samdb
.prepare_attr_replace(group_attrs
, old_group
, "mail", mail_address
)
1373 group_attributes_changed
= len(group_attrs
) > 0
1375 # update the group with formatted attributes
1376 samdb
.transaction_start()
1378 if group_attributes_changed
:
1379 samdb
.modify(group_attrs
)
1381 samdb
.rename(group_dn
, new_group_dn
)
1382 except Exception as e
:
1383 samdb
.transaction_cancel()
1384 raise CommandError('Failed to rename group "%s"' % groupname
, e
)
1385 samdb
.transaction_commit()
1388 self
.outf
.write('Renamed CN of group "%s" from "%s" to "%s" '
1389 'successfully\n' % (groupname
, old_cn
, new_cn
))
1391 if group_attributes_changed
:
1392 self
.outf
.write('Following attributes of group "%s" have been '
1393 'changed successfully:\n' % (groupname
))
1394 for attr
in group_attrs
.keys():
1397 self
.outf
.write('%s: %s\n' % (attr
, group_attrs
[attr
]
1398 if group_attrs
[attr
] else '[removed]'))
1400 class cmd_group(SuperCommand
):
1401 """Group management."""
1404 subcommands
["add"] = cmd_group_add()
1405 subcommands
["create"] = cmd_group_add()
1406 subcommands
["delete"] = cmd_group_delete()
1407 subcommands
["edit"] = cmd_group_edit()
1408 subcommands
["addmembers"] = cmd_group_add_members()
1409 subcommands
["removemembers"] = cmd_group_remove_members()
1410 subcommands
["list"] = cmd_group_list()
1411 subcommands
["listmembers"] = cmd_group_list_members()
1412 subcommands
["move"] = cmd_group_move()
1413 subcommands
["show"] = cmd_group_show()
1414 subcommands
["stats"] = cmd_group_stats()
1415 subcommands
["addunixattrs"] = cmd_group_add_unix_attrs()
1416 subcommands
["rename"] = cmd_group_rename()