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_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 GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
30 GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
31 GTYPE_SECURITY_GLOBAL_GROUP,
32 GTYPE_SECURITY_UNIVERSAL_GROUP,
33 GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP,
34 GTYPE_DISTRIBUTION_GLOBAL_GROUP,
35 GTYPE_DISTRIBUTION_UNIVERSAL_GROUP,
37 from collections import defaultdict
39 security_group = dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
40 "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
41 "Global": GTYPE_SECURITY_GLOBAL_GROUP,
42 "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP})
43 distribution_group = dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP,
44 "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP,
45 "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP})
48 class cmd_group_add(Command):
49 """Creates a new AD group.
51 This command creates a new Active Directory group. The groupname specified on the command is a unique sAMAccountName.
53 An Active Directory group may contain user and computer accounts as well as other groups. An administrator creates a group and adds members to that group so they can be managed as a single entity. This helps to simplify security and system administration.
55 Groups may also be used to establish email distribution lists, using --group-type=Distribution.
57 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.
59 The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created.
61 The command may be run from the root userid or another authorized userid. The
62 -H or --URL= option can be used to execute the command on a remote server.
65 samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group'
67 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.
70 sudo samba-tool group add Group2 --group-type=Distribution
72 Example2 adds a new distribution group to the local server. The command is run under root using the sudo command.
75 samba-tool group add Group3 --nis-domain=samdom --gid-number=12345
77 Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature).
80 synopsis = "%prog <groupname> [options]"
82 takes_optiongroups = {
83 "sambaopts": options.SambaOptions,
84 "versionopts": options.VersionOptions,
85 "credopts": options.CredentialsOptions,
89 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
90 metavar="URL", dest="H"),
92 help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created",
94 Option("--group-scope", type="choice", choices=["Domain", "Global", "Universal"],
95 help="Group scope (Domain | Global | Universal)"),
96 Option("--group-type", type="choice", choices=["Security", "Distribution"],
97 help="Group type (Security | Distribution)"),
98 Option("--description", help="Group's description", type=str),
99 Option("--mail-address", help="Group's email address", type=str),
100 Option("--notes", help="Groups's notes", type=str),
101 Option("--gid-number", help="Group's Unix/RFC2307 GID number", type=int),
102 Option("--nis-domain", help="SFU30 NIS Domain", type=str),
105 takes_args = ["groupname"]
107 def run(self, groupname, credopts=None, sambaopts=None,
108 versionopts=None, H=None, groupou=None, group_scope=None,
109 group_type=None, description=None, mail_address=None, notes=None, gid_number=None, nis_domain=None):
111 if (group_type or "Security") == "Security":
112 gtype = security_group.get(group_scope, GTYPE_SECURITY_GLOBAL_GROUP)
114 gtype = distribution_group.get(group_scope, GTYPE_DISTRIBUTION_GLOBAL_GROUP)
116 if (gid_number is None and nis_domain is not None) or (gid_number is not None and nis_domain is None):
117 raise CommandError('Both --gid-number and --nis-domain have to be set for a RFC2307-enabled group. Operation cancelled.')
119 lp = sambaopts.get_loadparm()
120 creds = credopts.get_credentials(lp, fallback_machine=True)
123 samdb = SamDB(url=H, session_info=system_session(),
124 credentials=creds, lp=lp)
125 samdb.newgroup(groupname, groupou=groupou, grouptype=gtype,
126 description=description, mailaddress=mail_address, notes=notes,
127 gidnumber=gid_number, nisdomain=nis_domain)
128 except Exception as e:
129 # FIXME: catch more specific exception
130 raise CommandError('Failed to create group "%s"' % groupname, e)
131 self.outf.write("Added group %s\n" % groupname)
134 class cmd_group_delete(Command):
135 """Deletes an AD group.
137 The command deletes an existing AD group from the Active Directory domain. The groupname specified on the command is the sAMAccountName.
139 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.
141 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.
144 samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
146 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.
149 sudo samba-tool group delete Group2
151 Example2 deletes group Group2 from the local server. The command is run under root using the sudo command.
154 synopsis = "%prog <groupname> [options]"
156 takes_optiongroups = {
157 "sambaopts": options.SambaOptions,
158 "versionopts": options.VersionOptions,
159 "credopts": options.CredentialsOptions,
163 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
164 metavar="URL", dest="H"),
167 takes_args = ["groupname"]
169 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None):
171 lp = sambaopts.get_loadparm()
172 creds = credopts.get_credentials(lp, fallback_machine=True)
173 samdb = SamDB(url=H, session_info=system_session(),
174 credentials=creds, lp=lp)
176 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
180 res = samdb.search(base=samdb.domain_dn(),
181 scope=ldb.SCOPE_SUBTREE,
186 raise CommandError('Unable to find group "%s"' % (groupname))
189 samdb.delete(group_dn)
190 except Exception as e:
191 # FIXME: catch more specific exception
192 raise CommandError('Failed to remove group "%s"' % groupname, e)
193 self.outf.write("Deleted group %s\n" % groupname)
196 class cmd_group_add_members(Command):
197 """Add members to an AD group.
199 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.
201 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.
203 The member names specified on the command must be the sAMaccountName.
206 samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
208 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.
211 sudo samba-tool group addmembers supergroup User2
213 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.
216 synopsis = "%prog <groupname> <listofmembers> [options]"
218 takes_optiongroups = {
219 "sambaopts": options.SambaOptions,
220 "versionopts": options.VersionOptions,
221 "credopts": options.CredentialsOptions,
225 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
226 metavar="URL", dest="H"),
229 takes_args = ["groupname", "listofmembers"]
231 def run(self, groupname, listofmembers, credopts=None, sambaopts=None,
232 versionopts=None, H=None):
234 lp = sambaopts.get_loadparm()
235 creds = credopts.get_credentials(lp, fallback_machine=True)
238 samdb = SamDB(url=H, session_info=system_session(),
239 credentials=creds, lp=lp)
240 groupmembers = listofmembers.split(',')
241 samdb.add_remove_group_members(groupname, groupmembers,
242 add_members_operation=True)
243 except Exception as e:
244 # FIXME: catch more specific exception
245 raise CommandError('Failed to add members "%s" to group "%s"' % (
246 listofmembers, groupname), e)
247 self.outf.write("Added members to group %s\n" % groupname)
250 class cmd_group_remove_members(Command):
251 """Remove members from an AD group.
253 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.
255 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
258 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
260 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.
263 sudo samba-tool group removemembers supergroup User1
265 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.
268 synopsis = "%prog <groupname> <listofmembers> [options]"
270 takes_optiongroups = {
271 "sambaopts": options.SambaOptions,
272 "versionopts": options.VersionOptions,
273 "credopts": options.CredentialsOptions,
277 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
278 metavar="URL", dest="H"),
281 takes_args = ["groupname", "listofmembers"]
283 def run(self, groupname, listofmembers, credopts=None, sambaopts=None,
284 versionopts=None, H=None):
286 lp = sambaopts.get_loadparm()
287 creds = credopts.get_credentials(lp, fallback_machine=True)
290 samdb = SamDB(url=H, session_info=system_session(),
291 credentials=creds, lp=lp)
292 samdb.add_remove_group_members(groupname, listofmembers.split(","),
293 add_members_operation=False)
294 except Exception as e:
295 # FIXME: Catch more specific exception
296 raise CommandError('Failed to remove members "%s" from group "%s"' % (listofmembers, groupname), e)
297 self.outf.write("Removed members from group %s\n" % groupname)
300 class cmd_group_list(Command):
301 """List all groups."""
303 synopsis = "%prog [options]"
306 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
307 metavar="URL", dest="H"),
308 Option("-v", "--verbose",
309 help="Verbose output, showing group type and group scope.",
310 action="store_true"),
314 takes_optiongroups = {
315 "sambaopts": options.SambaOptions,
316 "credopts": options.CredentialsOptions,
317 "versionopts": options.VersionOptions,
320 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None,
322 lp = sambaopts.get_loadparm()
323 creds = credopts.get_credentials(lp, fallback_machine=True)
325 samdb = SamDB(url=H, session_info=system_session(),
326 credentials=creds, lp=lp)
327 attrs=["samaccountname"]
330 attrs += ["grouptype", "member"]
331 domain_dn = samdb.domain_dn()
332 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
333 expression=("(objectClass=group)"),
339 self.outf.write("Group Name Group Type Group Scope Members\n")
340 self.outf.write("--------------------------------------------------------------------------------\n")
343 self.outf.write("%-44s" % msg.get("samaccountname", idx=0))
344 hgtype = hex(int("%s" % msg["grouptype"]) & 0x00000000FFFFFFFF)
345 if (hgtype == hex(int(security_group.get("Builtin")))):
346 self.outf.write("Security Builtin ")
347 elif (hgtype == hex(int(security_group.get("Domain")))):
348 self.outf.write("Security Domain ")
349 elif (hgtype == hex(int(security_group.get("Global")))):
350 self.outf.write("Security Global ")
351 elif (hgtype == hex(int(security_group.get("Universal")))):
352 self.outf.write("Security Universal")
353 elif (hgtype == hex(int(distribution_group.get("Global")))):
354 self.outf.write("Distribution Global ")
355 elif (hgtype == hex(int(distribution_group.get("Domain")))):
356 self.outf.write("Distribution Domain ")
357 elif (hgtype == hex(int(distribution_group.get("Universal")))):
358 self.outf.write("Distribution Universal")
361 self.outf.write(" %u\n" % len(msg.get("member", default=[])))
364 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
367 class cmd_group_list_members(Command):
368 """List all members of an AD group.
370 This command lists members from an existing Active Directory group. The command accepts one group name.
373 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
376 synopsis = "%prog <groupname> [options]"
379 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
380 metavar="URL", dest="H"),
383 takes_optiongroups = {
384 "sambaopts": options.SambaOptions,
385 "credopts": options.CredentialsOptions,
386 "versionopts": options.VersionOptions,
389 takes_args = ["groupname"]
391 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None):
392 lp = sambaopts.get_loadparm()
393 creds = credopts.get_credentials(lp, fallback_machine=True)
396 samdb = SamDB(url=H, session_info=system_session(),
397 credentials=creds, lp=lp)
399 search_filter = "(&(objectClass=group)(samaccountname=%s))" % groupname
400 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
401 expression=(search_filter),
407 group_dn = res[0].get('dn', idx=0)
408 object_sid = res[0].get('objectSid', idx=0)
410 object_sid = ndr_unpack(security.dom_sid, object_sid)
411 (group_dom_sid, rid) = object_sid.split()
413 search_filter = "(|(primaryGroupID=%s)(memberOf=%s))" % (rid, group_dn)
414 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
415 expression=(search_filter),
416 attrs=["samAccountName", "cn"])
422 member_name = msg.get("samAccountName", idx=0)
423 if member_name is None:
424 member_name = msg.get("cn", idx=0)
425 self.outf.write("%s\n" % member_name)
427 except Exception as e:
428 raise CommandError('Failed to list members of "%s" group ' % groupname, e)
431 class cmd_group_move(Command):
432 """Move a group to an organizational unit/container.
434 This command moves a group object into the specified organizational unit
436 The groupname specified on the command is the sAMAccountName.
437 The name of the organizational unit or container can be specified as a
438 full DN or without the domainDN component.
440 The command may be run from the root userid or another authorized userid.
442 The -H or --URL= option can be used to execute the command against a remote
446 samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \
447 -H ldap://samba.samdom.example.com -U administrator
449 Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
450 unit on a remote LDAP server.
452 The -H parameter is used to specify the remote target server.
455 samba-tool group move Group1 CN=Users
457 Example2 shows how to move a group Group1 back into the CN=Users container
461 synopsis = "%prog <groupname> <new_parent_dn> [options]"
464 Option("-H", "--URL", help="LDB URL for database or target server",
465 type=str, metavar="URL", dest="H"),
468 takes_args = ["groupname", "new_parent_dn"]
469 takes_optiongroups = {
470 "sambaopts": options.SambaOptions,
471 "credopts": options.CredentialsOptions,
472 "versionopts": options.VersionOptions,
475 def run(self, groupname, new_parent_dn, credopts=None, sambaopts=None,
476 versionopts=None, H=None):
477 lp = sambaopts.get_loadparm()
478 creds = credopts.get_credentials(lp, fallback_machine=True)
479 samdb = SamDB(url=H, session_info=system_session(),
480 credentials=creds, lp=lp)
481 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
483 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
486 res = samdb.search(base=domain_dn,
488 scope=ldb.SCOPE_SUBTREE)
491 raise CommandError('Unable to find group "%s"' % (groupname))
494 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
495 except Exception as e:
496 raise CommandError('Invalid new_parent_dn "%s": %s' %
497 (new_parent_dn, e.message))
499 full_new_group_dn = ldb.Dn(samdb, str(group_dn))
500 full_new_group_dn.remove_base_components(len(group_dn) - 1)
501 full_new_group_dn.add_base(full_new_parent_dn)
504 samdb.rename(group_dn, full_new_group_dn)
505 except Exception as e:
506 raise CommandError('Failed to move group "%s"' % groupname, e)
507 self.outf.write('Moved group "%s" into "%s"\n' %
508 (groupname, full_new_parent_dn))
511 class cmd_group_show(Command):
512 """Display a group AD object.
514 This command displays a group object and it's attributes in the Active
516 The group name specified on the command is the sAMAccountName of the group.
518 The command may be run from the root userid or another authorized userid.
520 The -H or --URL= option can be used to execute the command against a remote
524 samba-tool group show Group1 -H ldap://samba.samdom.example.com \
525 -U administrator --password=passw1rd
527 Example1 shows how to display a group's attributes in the domain against a remote
530 The -H parameter is used to specify the remote target server.
533 samba-tool group show Group2
535 Example2 shows how to display a group's attributes in the domain against a local
539 samba-tool group show Group3 --attributes=member,objectGUID
541 Example3 shows how to display a users objectGUID and member attributes.
543 synopsis = "%prog <group name> [options]"
546 Option("-H", "--URL", help="LDB URL for database or target server",
547 type=str, metavar="URL", dest="H"),
548 Option("--attributes",
549 help=("Comma separated list of attributes, "
550 "which will be printed."),
551 type=str, dest="group_attrs"),
554 takes_args = ["groupname"]
555 takes_optiongroups = {
556 "sambaopts": options.SambaOptions,
557 "credopts": options.CredentialsOptions,
558 "versionopts": options.VersionOptions,
561 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
562 H=None, group_attrs=None):
564 lp = sambaopts.get_loadparm()
565 creds = credopts.get_credentials(lp, fallback_machine=True)
566 samdb = SamDB(url=H, session_info=system_session(),
567 credentials=creds, lp=lp)
571 attrs = group_attrs.split(",")
573 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
574 (ATYPE_SECURITY_GLOBAL_GROUP,
575 ldb.binary_encode(groupname)))
577 domaindn = samdb.domain_dn()
580 res = samdb.search(base=domaindn, expression=filter,
581 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
584 raise CommandError('Unable to find group "%s"' % (groupname))
587 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
588 self.outf.write(user_ldif)
591 class cmd_group_stats(Command):
592 """Summary statistics about group memberships."""
594 synopsis = "%prog [options]"
597 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
598 metavar="URL", dest="H"),
601 takes_optiongroups = {
602 "sambaopts": options.SambaOptions,
603 "credopts": options.CredentialsOptions,
604 "versionopts": options.VersionOptions,
607 def num_in_range(self, range_min, range_max, group_freqs):
609 for members, count in group_freqs.items():
610 if range_min <= members and members <= range_max:
615 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
616 lp = sambaopts.get_loadparm()
617 creds = credopts.get_credentials(lp, fallback_machine=True)
619 samdb = SamDB(url=H, session_info=system_session(),
620 credentials=creds, lp=lp)
622 domain_dn = samdb.domain_dn()
623 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
624 expression=("(objectClass=group)"),
625 attrs=["samaccountname", "member"])
627 # first count up how many members each group has
628 group_assignments = {}
629 total_memberships = 0
632 name = str(msg.get("samaccountname"))
633 memberships = len(msg.get("member", default=[]))
634 group_assignments[name] = memberships
635 total_memberships += memberships
637 self.outf.write("Group membership statistics*\n")
638 self.outf.write("-------------------------------------------------\n")
639 self.outf.write("Total groups: {0}\n".format(res.count))
640 self.outf.write("Total memberships: {0}\n".format(total_memberships))
641 average = float(total_memberships / res.count)
642 self.outf.write("Average members per group: %.2f\n" % average)
643 group_names = list(group_assignments.keys())
644 group_members = list(group_assignments.values())
645 # note that some builtin groups have no members, so this doesn't tell us much
646 idx = group_members.index(min(group_members))
647 self.outf.write("Min members: {0} ({1})\n".format(group_members[idx],
649 idx = group_members.index(max(group_members))
650 max_members = group_members[idx]
651 self.outf.write("Max members: {0} ({1})\n\n".format(max_members,
654 # convert this to the frequency of group membership, i.e. how many
655 # groups have 5 members, how many have 6 members, etc
656 group_freqs = defaultdict(int)
657 for group, count in group_assignments.items():
658 group_freqs[count] += 1
660 # now squash this down even further, so that we just display the number
661 # of groups that fall into one of the following membership bands
662 bands = [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24), (25, 29),
663 (30, 39), (40, 49), (50, 59), (60, 69), (70, 79), (80, 89),
664 (90, 99), (100, 149), (150, 199), (200, 249), (250, 299),
665 (300, 399), (400, 499), (500, 999), (1000, 1999),
666 (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999),
667 (10000, max_members)]
669 self.outf.write("Members Number of Groups\n")
670 self.outf.write("-------------------------------------------------\n")
675 if band_start > max_members:
678 num_groups = self.num_in_range(band_start, band_end, group_freqs)
681 band_str = "{0}-{1}".format(band_start, band_end)
682 self.outf.write("%13s %u\n" % (band_str, num_groups))
684 self.outf.write("\n* Note this does not include nested group memberships\n")
687 class cmd_group(SuperCommand):
688 """Group management."""
691 subcommands["add"] = cmd_group_add()
692 subcommands["delete"] = cmd_group_delete()
693 subcommands["addmembers"] = cmd_group_add_members()
694 subcommands["removemembers"] = cmd_group_remove_members()
695 subcommands["list"] = cmd_group_list()
696 subcommands["listmembers"] = cmd_group_list_members()
697 subcommands["move"] = cmd_group_move()
698 subcommands["show"] = cmd_group_show()
699 subcommands["stats"] = cmd_group_stats()