samba-tool group: add 'edit' command to edit an AD group object
[Samba.git] / python / samba / netcmd / group.py
blob536c1cba613839dde4f832d6e46ddaaee1b77e6e
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
21 import ldb
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
38 from subprocess import check_call, CalledProcessError
39 from samba.compat import get_bytes
40 import os
41 import tempfile
43 security_group = dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
44 "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
45 "Global": GTYPE_SECURITY_GLOBAL_GROUP,
46 "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP})
47 distribution_group = dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP,
48 "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP,
49 "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP})
52 class cmd_group_add(Command):
53 """Creates a new AD group.
55 This command creates a new Active Directory group. The groupname specified on the command is a unique sAMAccountName.
57 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.
59 Groups may also be used to establish email distribution lists, using --group-type=Distribution.
61 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.
63 The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created.
65 The command may be run from the root userid or another authorized userid. The
66 -H or --URL= option can be used to execute the command on a remote server.
68 Example1:
69 samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group'
71 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.
73 Example2:
74 sudo samba-tool group add Group2 --group-type=Distribution
76 Example2 adds a new distribution group to the local server. The command is run under root using the sudo command.
78 Example3:
79 samba-tool group add Group3 --nis-domain=samdom --gid-number=12345
81 Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature).
82 """
84 synopsis = "%prog <groupname> [options]"
86 takes_optiongroups = {
87 "sambaopts": options.SambaOptions,
88 "versionopts": options.VersionOptions,
89 "credopts": options.CredentialsOptions,
92 takes_options = [
93 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
94 metavar="URL", dest="H"),
95 Option("--groupou",
96 help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created",
97 type=str),
98 Option("--group-scope", type="choice", choices=["Domain", "Global", "Universal"],
99 help="Group scope (Domain | Global | Universal)"),
100 Option("--group-type", type="choice", choices=["Security", "Distribution"],
101 help="Group type (Security | Distribution)"),
102 Option("--description", help="Group's description", type=str),
103 Option("--mail-address", help="Group's email address", type=str),
104 Option("--notes", help="Groups's notes", type=str),
105 Option("--gid-number", help="Group's Unix/RFC2307 GID number", type=int),
106 Option("--nis-domain", help="SFU30 NIS Domain", type=str),
109 takes_args = ["groupname"]
111 def run(self, groupname, credopts=None, sambaopts=None,
112 versionopts=None, H=None, groupou=None, group_scope=None,
113 group_type=None, description=None, mail_address=None, notes=None, gid_number=None, nis_domain=None):
115 if (group_type or "Security") == "Security":
116 gtype = security_group.get(group_scope, GTYPE_SECURITY_GLOBAL_GROUP)
117 else:
118 gtype = distribution_group.get(group_scope, GTYPE_DISTRIBUTION_GLOBAL_GROUP)
120 if (gid_number is None and nis_domain is not None) or (gid_number is not None and nis_domain is None):
121 raise CommandError('Both --gid-number and --nis-domain have to be set for a RFC2307-enabled group. Operation cancelled.')
123 lp = sambaopts.get_loadparm()
124 creds = credopts.get_credentials(lp, fallback_machine=True)
126 try:
127 samdb = SamDB(url=H, session_info=system_session(),
128 credentials=creds, lp=lp)
129 samdb.newgroup(groupname, groupou=groupou, grouptype=gtype,
130 description=description, mailaddress=mail_address, notes=notes,
131 gidnumber=gid_number, nisdomain=nis_domain)
132 except Exception as e:
133 # FIXME: catch more specific exception
134 raise CommandError('Failed to create group "%s"' % groupname, e)
135 self.outf.write("Added group %s\n" % groupname)
138 class cmd_group_delete(Command):
139 """Deletes an AD group.
141 The command deletes an existing AD group from the Active Directory domain. The groupname specified on the command is the sAMAccountName.
143 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.
145 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.
147 Example1:
148 samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
150 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.
152 Example2:
153 sudo samba-tool group delete Group2
155 Example2 deletes group Group2 from the local server. The command is run under root using the sudo command.
158 synopsis = "%prog <groupname> [options]"
160 takes_optiongroups = {
161 "sambaopts": options.SambaOptions,
162 "versionopts": options.VersionOptions,
163 "credopts": options.CredentialsOptions,
166 takes_options = [
167 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
168 metavar="URL", dest="H"),
171 takes_args = ["groupname"]
173 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None):
175 lp = sambaopts.get_loadparm()
176 creds = credopts.get_credentials(lp, fallback_machine=True)
177 samdb = SamDB(url=H, session_info=system_session(),
178 credentials=creds, lp=lp)
180 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
181 groupname)
183 try:
184 res = samdb.search(base=samdb.domain_dn(),
185 scope=ldb.SCOPE_SUBTREE,
186 expression=filter,
187 attrs=["dn"])
188 group_dn = res[0].dn
189 except IndexError:
190 raise CommandError('Unable to find group "%s"' % (groupname))
192 try:
193 samdb.delete(group_dn)
194 except Exception as e:
195 # FIXME: catch more specific exception
196 raise CommandError('Failed to remove group "%s"' % groupname, e)
197 self.outf.write("Deleted group %s\n" % groupname)
200 class cmd_group_add_members(Command):
201 """Add members to an AD group.
203 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.
205 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.
207 The member names specified on the command must be the sAMaccountName.
209 Example1:
210 samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
212 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.
214 Example2:
215 sudo samba-tool group addmembers supergroup User2
217 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.
220 synopsis = "%prog <groupname> <listofmembers> [options]"
222 takes_optiongroups = {
223 "sambaopts": options.SambaOptions,
224 "versionopts": options.VersionOptions,
225 "credopts": options.CredentialsOptions,
228 takes_options = [
229 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
230 metavar="URL", dest="H"),
233 takes_args = ["groupname", "listofmembers"]
235 def run(self, groupname, listofmembers, credopts=None, sambaopts=None,
236 versionopts=None, H=None):
238 lp = sambaopts.get_loadparm()
239 creds = credopts.get_credentials(lp, fallback_machine=True)
241 try:
242 samdb = SamDB(url=H, session_info=system_session(),
243 credentials=creds, lp=lp)
244 groupmembers = listofmembers.split(',')
245 samdb.add_remove_group_members(groupname, groupmembers,
246 add_members_operation=True)
247 except Exception as e:
248 # FIXME: catch more specific exception
249 raise CommandError('Failed to add members "%s" to group "%s"' % (
250 listofmembers, groupname), e)
251 self.outf.write("Added members to group %s\n" % groupname)
254 class cmd_group_remove_members(Command):
255 """Remove members from an AD group.
257 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.
259 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
261 Example1:
262 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
264 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.
266 Example2:
267 sudo samba-tool group removemembers supergroup User1
269 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.
272 synopsis = "%prog <groupname> <listofmembers> [options]"
274 takes_optiongroups = {
275 "sambaopts": options.SambaOptions,
276 "versionopts": options.VersionOptions,
277 "credopts": options.CredentialsOptions,
280 takes_options = [
281 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
282 metavar="URL", dest="H"),
285 takes_args = ["groupname", "listofmembers"]
287 def run(self, groupname, listofmembers, credopts=None, sambaopts=None,
288 versionopts=None, H=None):
290 lp = sambaopts.get_loadparm()
291 creds = credopts.get_credentials(lp, fallback_machine=True)
293 try:
294 samdb = SamDB(url=H, session_info=system_session(),
295 credentials=creds, lp=lp)
296 samdb.add_remove_group_members(groupname, listofmembers.split(","),
297 add_members_operation=False)
298 except Exception as e:
299 # FIXME: Catch more specific exception
300 raise CommandError('Failed to remove members "%s" from group "%s"' % (listofmembers, groupname), e)
301 self.outf.write("Removed members from group %s\n" % groupname)
304 class cmd_group_list(Command):
305 """List all groups."""
307 synopsis = "%prog [options]"
309 takes_options = [
310 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
311 metavar="URL", dest="H"),
312 Option("-v", "--verbose",
313 help="Verbose output, showing group type and group scope.",
314 action="store_true"),
318 takes_optiongroups = {
319 "sambaopts": options.SambaOptions,
320 "credopts": options.CredentialsOptions,
321 "versionopts": options.VersionOptions,
324 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None,
325 verbose=False):
326 lp = sambaopts.get_loadparm()
327 creds = credopts.get_credentials(lp, fallback_machine=True)
329 samdb = SamDB(url=H, session_info=system_session(),
330 credentials=creds, lp=lp)
331 attrs=["samaccountname"]
333 if verbose:
334 attrs += ["grouptype", "member"]
335 domain_dn = samdb.domain_dn()
336 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
337 expression=("(objectClass=group)"),
338 attrs=attrs)
339 if (len(res) == 0):
340 return
342 if verbose:
343 self.outf.write("Group Name Group Type Group Scope Members\n")
344 self.outf.write("--------------------------------------------------------------------------------\n")
346 for msg in res:
347 self.outf.write("%-44s" % msg.get("samaccountname", idx=0))
348 hgtype = hex(int("%s" % msg["grouptype"]) & 0x00000000FFFFFFFF)
349 if (hgtype == hex(int(security_group.get("Builtin")))):
350 self.outf.write("Security Builtin ")
351 elif (hgtype == hex(int(security_group.get("Domain")))):
352 self.outf.write("Security Domain ")
353 elif (hgtype == hex(int(security_group.get("Global")))):
354 self.outf.write("Security Global ")
355 elif (hgtype == hex(int(security_group.get("Universal")))):
356 self.outf.write("Security Universal")
357 elif (hgtype == hex(int(distribution_group.get("Global")))):
358 self.outf.write("Distribution Global ")
359 elif (hgtype == hex(int(distribution_group.get("Domain")))):
360 self.outf.write("Distribution Domain ")
361 elif (hgtype == hex(int(distribution_group.get("Universal")))):
362 self.outf.write("Distribution Universal")
363 else:
364 self.outf.write(" ")
365 num_members = len(msg.get("member", default=[]))
366 self.outf.write(" %6u\n" % num_members)
367 else:
368 for msg in res:
369 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
372 class cmd_group_list_members(Command):
373 """List all members of an AD group.
375 This command lists members from an existing Active Directory group. The command accepts one group name.
377 Example1:
378 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
381 synopsis = "%prog <groupname> [options]"
383 takes_options = [
384 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
385 metavar="URL", dest="H"),
388 takes_optiongroups = {
389 "sambaopts": options.SambaOptions,
390 "credopts": options.CredentialsOptions,
391 "versionopts": options.VersionOptions,
394 takes_args = ["groupname"]
396 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None):
397 lp = sambaopts.get_loadparm()
398 creds = credopts.get_credentials(lp, fallback_machine=True)
400 try:
401 samdb = SamDB(url=H, session_info=system_session(),
402 credentials=creds, lp=lp)
404 search_filter = "(&(objectClass=group)(samaccountname=%s))" % groupname
405 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
406 expression=(search_filter),
407 attrs=["objectSid"])
409 if (len(res) != 1):
410 return
412 group_dn = res[0].get('dn', idx=0)
413 object_sid = res[0].get('objectSid', idx=0)
415 object_sid = ndr_unpack(security.dom_sid, object_sid)
416 (group_dom_sid, rid) = object_sid.split()
418 search_filter = "(|(primaryGroupID=%s)(memberOf=%s))" % (rid, group_dn)
419 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
420 expression=(search_filter),
421 attrs=["samAccountName", "cn"])
423 if (len(res) == 0):
424 return
426 for msg in res:
427 member_name = msg.get("samAccountName", idx=0)
428 if member_name is None:
429 member_name = msg.get("cn", idx=0)
430 self.outf.write("%s\n" % member_name)
432 except Exception as e:
433 raise CommandError('Failed to list members of "%s" group ' % groupname, e)
436 class cmd_group_move(Command):
437 """Move a group to an organizational unit/container.
439 This command moves a group object into the specified organizational unit
440 or container.
441 The groupname specified on the command is the sAMAccountName.
442 The name of the organizational unit or container can be specified as a
443 full DN or without the domainDN component.
445 The command may be run from the root userid or another authorized userid.
447 The -H or --URL= option can be used to execute the command against a remote
448 server.
450 Example1:
451 samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
452 -H ldap://samba.samdom.example.com -U administrator
454 Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
455 unit on a remote LDAP server.
457 The -H parameter is used to specify the remote target server.
459 Example2:
460 samba-tool group move Group1 CN=Users
462 Example2 shows how to move a group Group1 back into the CN=Users container
463 on the local server.
466 synopsis = "%prog <groupname> <new_parent_dn> [options]"
468 takes_options = [
469 Option("-H", "--URL", help="LDB URL for database or target server",
470 type=str, metavar="URL", dest="H"),
473 takes_args = ["groupname", "new_parent_dn"]
474 takes_optiongroups = {
475 "sambaopts": options.SambaOptions,
476 "credopts": options.CredentialsOptions,
477 "versionopts": options.VersionOptions,
480 def run(self, groupname, new_parent_dn, credopts=None, sambaopts=None,
481 versionopts=None, H=None):
482 lp = sambaopts.get_loadparm()
483 creds = credopts.get_credentials(lp, fallback_machine=True)
484 samdb = SamDB(url=H, session_info=system_session(),
485 credentials=creds, lp=lp)
486 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
488 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
489 groupname)
490 try:
491 res = samdb.search(base=domain_dn,
492 expression=filter,
493 scope=ldb.SCOPE_SUBTREE)
494 group_dn = res[0].dn
495 except IndexError:
496 raise CommandError('Unable to find group "%s"' % (groupname))
498 try:
499 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
500 except Exception as e:
501 raise CommandError('Invalid new_parent_dn "%s": %s' %
502 (new_parent_dn, e.message))
504 full_new_group_dn = ldb.Dn(samdb, str(group_dn))
505 full_new_group_dn.remove_base_components(len(group_dn) - 1)
506 full_new_group_dn.add_base(full_new_parent_dn)
508 try:
509 samdb.rename(group_dn, full_new_group_dn)
510 except Exception as e:
511 raise CommandError('Failed to move group "%s"' % groupname, e)
512 self.outf.write('Moved group "%s" into "%s"\n' %
513 (groupname, full_new_parent_dn))
516 class cmd_group_show(Command):
517 """Display a group AD object.
519 This command displays a group object and it's attributes in the Active
520 Directory domain.
521 The group name specified on the command is the sAMAccountName of the group.
523 The command may be run from the root userid or another authorized userid.
525 The -H or --URL= option can be used to execute the command against a remote
526 server.
528 Example1:
529 samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
530 -U administrator --password=passw1rd
532 Example1 shows how to display a group's attributes in the domain against a
533 remote LDAP server.
535 The -H parameter is used to specify the remote target server.
537 Example2:
538 samba-tool group show Group2
540 Example2 shows how to display a group's attributes in the domain against a local
541 LDAP server.
543 Example3:
544 samba-tool group show Group3 --attributes=member,objectGUID
546 Example3 shows how to display a users objectGUID and member attributes.
548 synopsis = "%prog <group name> [options]"
550 takes_options = [
551 Option("-H", "--URL", help="LDB URL for database or target server",
552 type=str, metavar="URL", dest="H"),
553 Option("--attributes",
554 help=("Comma separated list of attributes, "
555 "which will be printed."),
556 type=str, dest="group_attrs"),
559 takes_args = ["groupname"]
560 takes_optiongroups = {
561 "sambaopts": options.SambaOptions,
562 "credopts": options.CredentialsOptions,
563 "versionopts": options.VersionOptions,
566 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
567 H=None, group_attrs=None):
569 lp = sambaopts.get_loadparm()
570 creds = credopts.get_credentials(lp, fallback_machine=True)
571 samdb = SamDB(url=H, session_info=system_session(),
572 credentials=creds, lp=lp)
574 attrs = None
575 if group_attrs:
576 attrs = group_attrs.split(",")
578 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
579 (ATYPE_SECURITY_GLOBAL_GROUP,
580 ldb.binary_encode(groupname)))
582 domaindn = samdb.domain_dn()
584 try:
585 res = samdb.search(base=domaindn, expression=filter,
586 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
587 user_dn = res[0].dn
588 except IndexError:
589 raise CommandError('Unable to find group "%s"' % (groupname))
591 for msg in res:
592 user_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
593 self.outf.write(user_ldif)
596 class cmd_group_stats(Command):
597 """Summary statistics about group memberships."""
599 synopsis = "%prog [options]"
601 takes_options = [
602 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
603 metavar="URL", dest="H"),
606 takes_optiongroups = {
607 "sambaopts": options.SambaOptions,
608 "credopts": options.CredentialsOptions,
609 "versionopts": options.VersionOptions,
612 def num_in_range(self, range_min, range_max, group_freqs):
613 total_count = 0
614 for members, count in group_freqs.items():
615 if range_min <= members and members <= range_max:
616 total_count += count
618 return total_count
620 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
621 lp = sambaopts.get_loadparm()
622 creds = credopts.get_credentials(lp, fallback_machine=True)
624 samdb = SamDB(url=H, session_info=system_session(),
625 credentials=creds, lp=lp)
627 domain_dn = samdb.domain_dn()
628 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
629 expression=("(objectClass=group)"),
630 attrs=["samaccountname", "member"])
632 # first count up how many members each group has
633 group_assignments = {}
634 total_memberships = 0
636 for msg in res:
637 name = str(msg.get("samaccountname"))
638 num_members = len(msg.get("member", default=[]))
639 group_assignments[name] = num_members
640 total_memberships += num_members
642 num_groups = res.count
643 self.outf.write("Group membership statistics*\n")
644 self.outf.write("-------------------------------------------------\n")
645 self.outf.write("Total groups: {0}\n".format(num_groups))
646 self.outf.write("Total memberships: {0}\n".format(total_memberships))
647 average = total_memberships / float(num_groups)
648 self.outf.write("Average members per group: %.2f\n" % average)
650 # find the max and median memberships (note that some default groups
651 # always have zero members, so displaying the min is not very helpful)
652 group_names = list(group_assignments.keys())
653 group_members = list(group_assignments.values())
654 idx = group_members.index(max(group_members))
655 max_members = group_members[idx]
656 self.outf.write("Max members: {0} ({1})\n".format(max_members,
657 group_names[idx]))
658 group_members.sort()
659 midpoint = num_groups // 2
660 median = group_members[midpoint]
661 if num_groups % 2 == 0:
662 median = (median + group_members[midpoint - 1]) / 2
663 self.outf.write("Median members per group: {0}\n\n".format(median))
665 # convert this to the frequency of group membership, i.e. how many
666 # groups have 5 members, how many have 6 members, etc
667 group_freqs = defaultdict(int)
668 for group, num_members in group_assignments.items():
669 group_freqs[num_members] += 1
671 # now squash this down even further, so that we just display the number
672 # of groups that fall into one of the following membership bands
673 bands = [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24),
674 (25, 29), (30, 39), (40, 49), (50, 59), (60, 69), (70, 79),
675 (80, 89), (90, 99), (100, 149), (150, 199), (200, 249),
676 (250, 299), (300, 399), (400, 499), (500, 999), (1000, 1999),
677 (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999),
678 (10000, max_members)]
680 self.outf.write("Members Number of Groups\n")
681 self.outf.write("-------------------------------------------------\n")
683 for band in bands:
684 band_start = band[0]
685 band_end = band[1]
686 if band_start > max_members:
687 break
689 num_groups = self.num_in_range(band_start, band_end, group_freqs)
691 if num_groups != 0:
692 band_str = "{0}-{1}".format(band_start, band_end)
693 self.outf.write("%13s %u\n" % (band_str, num_groups))
695 self.outf.write("\n* Note this does not include nested group memberships\n")
698 class cmd_group_edit(Command):
699 """Modify Group AD object.
701 This command will allow editing of a group account in the Active Directory
702 domain. You will then be able to add or change attributes and their values.
704 The groupname specified on the command is the sAMAccountName.
706 The command may be run from the root userid or another authorized userid.
708 The -H or --URL= option can be used to execute the command against a remote
709 server.
711 Example1:
712 samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
713 -U administrator --password=passw1rd
715 Example1 shows how to edit a groups attributes in the domain against a
716 remote LDAP server.
718 The -H parameter is used to specify the remote target server.
720 Example2:
721 samba-tool group edit Group2
723 Example2 shows how to edit a groups attributes in the domain against a local
724 server.
726 Example3:
727 samba-tool group edit Group3 --editor=nano
729 Example3 shows how to edit a groups attributes in the domain against a local
730 server using the 'nano' editor.
732 synopsis = "%prog <groupname> [options]"
734 takes_options = [
735 Option("-H", "--URL", help="LDB URL for database or target server",
736 type=str, metavar="URL", dest="H"),
737 Option("--editor", help="Editor to use instead of the system default,"
738 " or 'vi' if no system default is set.", type=str),
741 takes_args = ["groupname"]
742 takes_optiongroups = {
743 "sambaopts": options.SambaOptions,
744 "credopts": options.CredentialsOptions,
745 "versionopts": options.VersionOptions,
748 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
749 H=None, editor=None):
750 from . import common
752 lp = sambaopts.get_loadparm()
753 creds = credopts.get_credentials(lp, fallback_machine=True)
754 samdb = SamDB(url=H, session_info=system_session(),
755 credentials=creds, lp=lp)
757 filter = ("(&(sAMAccountName=%s)(objectClass=group))" % groupname)
759 domaindn = samdb.domain_dn()
761 try:
762 res = samdb.search(base=domaindn,
763 expression=filter,
764 scope=ldb.SCOPE_SUBTREE)
765 group_dn = res[0].dn
766 except IndexError:
767 raise CommandError('Unable to find group "%s"' % (groupname))
769 if len(res) != 1:
770 raise CommandError('Invalid number of results: for "%s": %d' %
771 ((groupname), len(res)))
773 msg = res[0]
774 result_ldif = common.get_ldif_for_editor(samdb, msg)
776 if editor is None:
777 editor = os.environ.get('EDITOR')
778 if editor is None:
779 editor = 'vi'
781 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
782 t_file.write(get_bytes(result_ldif))
783 t_file.flush()
784 try:
785 check_call([editor, t_file.name])
786 except CalledProcessError as e:
787 raise CalledProcessError("ERROR: ", e)
788 with open(t_file.name) as edited_file:
789 edited_message = edited_file.read()
791 msgs_edited = samdb.parse_ldif(edited_message)
792 msg_edited = next(msgs_edited)[1]
794 res_msg_diff = samdb.msg_diff(msg, msg_edited)
795 if len(res_msg_diff) == 0:
796 self.outf.write("Nothing to do\n")
797 return
799 try:
800 samdb.modify(res_msg_diff)
801 except Exception as e:
802 raise CommandError("Failed to modify group '%s': " % groupname, e)
804 self.outf.write("Modified group '%s' successfully\n" % groupname)
807 class cmd_group(SuperCommand):
808 """Group management."""
810 subcommands = {}
811 subcommands["add"] = cmd_group_add()
812 subcommands["delete"] = cmd_group_delete()
813 subcommands["edit"] = cmd_group_edit()
814 subcommands["addmembers"] = cmd_group_add_members()
815 subcommands["removemembers"] = cmd_group_remove_members()
816 subcommands["list"] = cmd_group_list()
817 subcommands["listmembers"] = cmd_group_list_members()
818 subcommands["move"] = cmd_group_move()
819 subcommands["show"] = cmd_group_show()
820 subcommands["stats"] = cmd_group_stats()