samba-tool: Optionally hide disabled/expired accounts in "group listmembers"
[Samba.git] / python / samba / netcmd / group.py
bloba958db2c42c360323813f7d9a5777e36d8b99f4d
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,
36 UF_ACCOUNTDISABLE,
38 from collections import defaultdict
39 from subprocess import check_call, CalledProcessError
40 from samba.common import get_bytes
41 import os
42 import tempfile
43 from . import common
45 security_group = dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
46 "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
47 "Global": GTYPE_SECURITY_GLOBAL_GROUP,
48 "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP})
49 distribution_group = dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP,
50 "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP,
51 "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP})
54 class cmd_group_add(Command):
55 """Creates a new AD group.
57 This command adds a new Active Directory group. The groupname specified on the command is a unique sAMAccountName.
59 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.
61 Groups may also be used to establish email distribution lists, using --group-type=Distribution.
63 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.
65 The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created.
67 The command may be run from the root userid or another authorized userid. The
68 -H or --URL= option can be used to execute the command on a remote server.
70 Example1:
71 samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group'
73 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.
75 Example2:
76 sudo samba-tool group add Group2 --group-type=Distribution
78 Example2 adds a new distribution group to the local server. The command is run under root using the sudo command.
80 Example3:
81 samba-tool group add Group3 --nis-domain=samdom --gid-number=12345
83 Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature).
84 """
86 synopsis = "%prog <groupname> [options]"
88 takes_optiongroups = {
89 "sambaopts": options.SambaOptions,
90 "versionopts": options.VersionOptions,
91 "credopts": options.CredentialsOptions,
94 takes_options = [
95 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
96 metavar="URL", dest="H"),
97 Option("--groupou",
98 help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created",
99 type=str),
100 Option("--group-scope", type="choice", choices=["Domain", "Global", "Universal"],
101 help="Group scope (Domain | Global | Universal)"),
102 Option("--group-type", type="choice", choices=["Security", "Distribution"],
103 help="Group type (Security | Distribution)"),
104 Option("--description", help="Group's description", type=str),
105 Option("--mail-address", help="Group's email address", type=str),
106 Option("--notes", help="Groups's notes", type=str),
107 Option("--gid-number", help="Group's Unix/RFC2307 GID number", type=int),
108 Option("--nis-domain", help="SFU30 NIS Domain", type=str),
111 takes_args = ["groupname"]
113 def run(self, groupname, credopts=None, sambaopts=None,
114 versionopts=None, H=None, groupou=None, group_scope=None,
115 group_type=None, description=None, mail_address=None, notes=None, gid_number=None, nis_domain=None):
117 if (group_type or "Security") == "Security":
118 gtype = security_group.get(group_scope, GTYPE_SECURITY_GLOBAL_GROUP)
119 else:
120 gtype = distribution_group.get(group_scope, GTYPE_DISTRIBUTION_GLOBAL_GROUP)
122 if (gid_number is None and nis_domain is not None) or (gid_number is not None and nis_domain is None):
123 raise CommandError('Both --gid-number and --nis-domain have to be set for a RFC2307-enabled group. Operation cancelled.')
125 lp = sambaopts.get_loadparm()
126 creds = credopts.get_credentials(lp, fallback_machine=True)
128 try:
129 samdb = SamDB(url=H, session_info=system_session(),
130 credentials=creds, lp=lp)
131 samdb.newgroup(groupname, groupou=groupou, grouptype=gtype,
132 description=description, mailaddress=mail_address, notes=notes,
133 gidnumber=gid_number, nisdomain=nis_domain)
134 except Exception as e:
135 # FIXME: catch more specific exception
136 raise CommandError('Failed to add group "%s"' % groupname, e)
137 self.outf.write("Added group %s\n" % groupname)
140 class cmd_group_delete(Command):
141 """Deletes an AD group.
143 The command deletes an existing AD group from the Active Directory domain. The groupname specified on the command is the sAMAccountName.
145 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.
147 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.
149 Example1:
150 samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
152 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.
154 Example2:
155 sudo samba-tool group delete Group2
157 Example2 deletes group Group2 from the local server. The command is run under root using the sudo command.
160 synopsis = "%prog <groupname> [options]"
162 takes_optiongroups = {
163 "sambaopts": options.SambaOptions,
164 "versionopts": options.VersionOptions,
165 "credopts": options.CredentialsOptions,
168 takes_options = [
169 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
170 metavar="URL", dest="H"),
173 takes_args = ["groupname"]
175 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None):
177 lp = sambaopts.get_loadparm()
178 creds = credopts.get_credentials(lp, fallback_machine=True)
179 samdb = SamDB(url=H, session_info=system_session(),
180 credentials=creds, lp=lp)
182 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
183 ldb.binary_encode(groupname))
185 try:
186 res = samdb.search(base=samdb.domain_dn(),
187 scope=ldb.SCOPE_SUBTREE,
188 expression=filter,
189 attrs=["dn"])
190 group_dn = res[0].dn
191 except IndexError:
192 raise CommandError('Unable to find group "%s"' % (groupname))
194 try:
195 samdb.delete(group_dn)
196 except Exception as e:
197 # FIXME: catch more specific exception
198 raise CommandError('Failed to remove group "%s"' % groupname, e)
199 self.outf.write("Deleted group %s\n" % groupname)
202 class cmd_group_add_members(Command):
203 """Add members to an AD group.
205 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.
207 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.
209 The member names specified on the command must be the sAMaccountName.
211 Example1:
212 samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
214 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.
216 Example2:
217 sudo samba-tool group addmembers supergroup User2
219 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.
222 synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
224 takes_optiongroups = {
225 "sambaopts": options.SambaOptions,
226 "versionopts": options.VersionOptions,
227 "credopts": options.CredentialsOptions,
230 takes_options = [
231 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
232 metavar="URL", dest="H"),
233 Option("--member-dn",
234 help=("DN of the new group member to be added.\n"
235 "The --object-types option will be ignored."),
236 type=str,
237 action="append"),
238 Option("--object-types",
239 help=("Comma separated list of object types.\n"
240 "The types are used to filter the search for the "
241 "specified members.\n"
242 "Valid values are: user, group, computer, serviceaccount, "
243 "contact and all.\n"
244 "Default: user,group,computer"),
245 default="user,group,computer",
246 type=str),
247 Option("--member-base-dn",
248 help=("Base DN for group member search.\n"
249 "Default is the domain DN."),
250 type=str),
253 takes_args = ["groupname", "listofmembers?"]
255 def run(self,
256 groupname,
257 listofmembers=None,
258 credopts=None,
259 sambaopts=None,
260 versionopts=None,
261 H=None,
262 member_base_dn=None,
263 member_dn=None,
264 object_types="user,group,computer"):
266 lp = sambaopts.get_loadparm()
267 creds = credopts.get_credentials(lp, fallback_machine=True)
269 if member_dn is None and listofmembers is None:
270 self.usage()
271 raise CommandError(
272 'Either listofmembers or --member-dn must be specified.')
274 try:
275 samdb = SamDB(url=H, session_info=system_session(),
276 credentials=creds, lp=lp)
277 groupmembers = []
278 if member_dn is not None:
279 groupmembers += member_dn
280 if listofmembers is not None:
281 groupmembers += listofmembers.split(',')
282 group_member_types = object_types.split(',')
284 if member_base_dn is not None:
285 member_base_dn = samdb.normalize_dn_in_domain(member_base_dn)
287 samdb.add_remove_group_members(groupname, groupmembers,
288 add_members_operation=True,
289 member_types=group_member_types,
290 member_base_dn=member_base_dn)
291 except Exception as e:
292 # FIXME: catch more specific exception
293 raise CommandError('Failed to add members %r to group "%s" - %s' % (
294 groupmembers, groupname, e))
295 self.outf.write("Added members to group %s\n" % groupname)
298 class cmd_group_remove_members(Command):
299 """Remove members from an AD group.
301 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.
303 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
305 Example1:
306 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
308 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.
310 Example2:
311 sudo samba-tool group removemembers supergroup User1
313 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.
316 synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
318 takes_optiongroups = {
319 "sambaopts": options.SambaOptions,
320 "versionopts": options.VersionOptions,
321 "credopts": options.CredentialsOptions,
324 takes_options = [
325 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
326 metavar="URL", dest="H"),
327 Option("--member-dn",
328 help=("DN of the group member to be removed.\n"
329 "The --object-types option will be ignored."),
330 type=str,
331 action="append"),
332 Option("--object-types",
333 help=("Comma separated list of object types.\n"
334 "The types are used to filter the search for the "
335 "specified members.\n"
336 "Valid values are: user, group, computer, serviceaccount, "
337 "contact and all.\n"
338 "Default: user,group,computer"),
339 default="user,group,computer",
340 type=str),
341 Option("--member-base-dn",
342 help=("Base DN for group member search.\n"
343 "Default is the domain DN."),
344 type=str),
347 takes_args = ["groupname", "listofmembers?"]
349 def run(self,
350 groupname,
351 listofmembers=None,
352 credopts=None,
353 sambaopts=None,
354 versionopts=None,
355 H=None,
356 member_base_dn=None,
357 member_dn=None,
358 object_types="user,group,computer"):
360 lp = sambaopts.get_loadparm()
361 creds = credopts.get_credentials(lp, fallback_machine=True)
363 if member_dn is None and listofmembers is None:
364 self.usage()
365 raise CommandError(
366 'Either listofmembers or --member-dn must be specified.')
368 try:
369 samdb = SamDB(url=H, session_info=system_session(),
370 credentials=creds, lp=lp)
371 groupmembers = []
372 if member_dn is not None:
373 groupmembers += member_dn
374 if listofmembers is not None:
375 groupmembers += listofmembers.split(',')
376 group_member_types = object_types.split(',')
378 if member_base_dn is not None:
379 member_base_dn = samdb.normalize_dn_in_domain(member_base_dn)
381 samdb.add_remove_group_members(groupname,
382 groupmembers,
383 add_members_operation=False,
384 member_types=group_member_types,
385 member_base_dn=member_base_dn)
386 except Exception as e:
387 # FIXME: Catch more specific exception
388 raise CommandError('Failed to remove members %r from group "%s"' % (listofmembers, groupname), e)
389 self.outf.write("Removed members from group %s\n" % groupname)
392 class cmd_group_list(Command):
393 """List all groups."""
395 synopsis = "%prog [options]"
397 takes_options = [
398 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
399 metavar="URL", dest="H"),
400 Option("-v", "--verbose",
401 help="Verbose output, showing group type and group scope.",
402 action="store_true"),
403 Option("-b", "--base-dn",
404 help="Specify base DN to use.",
405 type=str),
406 Option("--full-dn", dest="full_dn",
407 default=False,
408 action='store_true',
409 help="Display DN instead of the sAMAccountName."),
412 takes_optiongroups = {
413 "sambaopts": options.SambaOptions,
414 "credopts": options.CredentialsOptions,
415 "versionopts": options.VersionOptions,
418 def run(self,
419 sambaopts=None,
420 credopts=None,
421 versionopts=None,
422 H=None,
423 verbose=False,
424 base_dn=None,
425 full_dn=False):
426 lp = sambaopts.get_loadparm()
427 creds = credopts.get_credentials(lp, fallback_machine=True)
429 samdb = SamDB(url=H, session_info=system_session(),
430 credentials=creds, lp=lp)
431 attrs=["samaccountname"]
433 if verbose:
434 attrs += ["grouptype", "member"]
435 domain_dn = samdb.domain_dn()
436 if base_dn:
437 domain_dn = samdb.normalize_dn_in_domain(base_dn)
438 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
439 expression=("(objectClass=group)"),
440 attrs=attrs)
441 if (len(res) == 0):
442 return
444 if verbose:
445 self.outf.write("Group Name Group Type Group Scope Members\n")
446 self.outf.write("--------------------------------------------------------------------------------\n")
448 for msg in res:
449 self.outf.write("%-44s" % msg.get("samaccountname", idx=0))
450 hgtype = hex(int("%s" % msg["grouptype"]) & 0x00000000FFFFFFFF)
451 if (hgtype == hex(int(security_group.get("Builtin")))):
452 self.outf.write("Security Builtin ")
453 elif (hgtype == hex(int(security_group.get("Domain")))):
454 self.outf.write("Security Domain ")
455 elif (hgtype == hex(int(security_group.get("Global")))):
456 self.outf.write("Security Global ")
457 elif (hgtype == hex(int(security_group.get("Universal")))):
458 self.outf.write("Security Universal")
459 elif (hgtype == hex(int(distribution_group.get("Global")))):
460 self.outf.write("Distribution Global ")
461 elif (hgtype == hex(int(distribution_group.get("Domain")))):
462 self.outf.write("Distribution Domain ")
463 elif (hgtype == hex(int(distribution_group.get("Universal")))):
464 self.outf.write("Distribution Universal")
465 else:
466 self.outf.write(" ")
467 num_members = len(msg.get("member", default=[]))
468 self.outf.write(" %6u\n" % num_members)
469 else:
470 for msg in res:
471 if full_dn:
472 self.outf.write("%s\n" % msg.get("dn"))
473 continue
475 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
478 class cmd_group_list_members(Command):
479 """List all members of an AD group.
481 This command lists members from an existing Active Directory group. The command accepts one group name.
483 Example1:
484 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
487 synopsis = "%prog <groupname> [options]"
489 takes_options = [
490 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
491 metavar="URL", dest="H"),
492 Option("--hide-expired",
493 help="Do not list expired group members",
494 default=False,
495 action='store_true'),
496 Option("--hide-disabled",
497 default=False,
498 action='store_true',
499 help="Do not list disabled group members"),
500 Option("--full-dn", dest="full_dn",
501 default=False,
502 action='store_true',
503 help="Display DN instead of the sAMAccountName.")
506 takes_optiongroups = {
507 "sambaopts": options.SambaOptions,
508 "credopts": options.CredentialsOptions,
509 "versionopts": options.VersionOptions,
512 takes_args = ["groupname"]
514 def run(self,
515 groupname,
516 credopts=None,
517 sambaopts=None,
518 versionopts=None,
519 H=None,
520 hide_expired=False,
521 hide_disabled=False,
522 full_dn=False):
523 lp = sambaopts.get_loadparm()
524 creds = credopts.get_credentials(lp, fallback_machine=True)
526 try:
527 samdb = SamDB(url=H, session_info=system_session(),
528 credentials=creds, lp=lp)
530 search_filter = ("(&(objectClass=group)(sAMAccountName=%s))" %
531 ldb.binary_encode(groupname))
532 try:
533 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
534 expression=(search_filter),
535 attrs=["objectSid"])
536 group_sid_binary = res[0].get('objectSid', idx=0)
537 except IndexError:
538 raise CommandError('Unable to find group "%s"' % (groupname))
540 group_sid = ndr_unpack(security.dom_sid, group_sid_binary)
541 (group_dom_sid, rid) = group_sid.split()
542 group_sid_dn = "<SID=%s>" % (group_sid)
544 filter_expires = ""
545 if hide_expired is True:
546 current_nttime = samdb.get_nttime()
547 filter_expires = \
548 "(|(accountExpires=0)(accountExpires>=%u))" % (current_nttime)
550 filter_disabled = ""
551 if hide_disabled is True:
552 filter_disabled = "(!(userAccountControl:%s:=%u))" % (
553 ldb.OID_COMPARATOR_AND, UF_ACCOUNTDISABLE)
555 filter = "(&(|(primaryGroupID=%s)(memberOf=%s))%s%s)" % (
556 rid, group_sid_dn, filter_disabled, filter_expires)
558 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
559 expression=filter,
560 attrs=["samAccountName", "cn"])
562 if (len(res) == 0):
563 return
565 for msg in res:
566 if full_dn:
567 self.outf.write("%s\n" % msg.get("dn"))
568 continue
570 member_name = msg.get("samAccountName", idx=0)
571 if member_name is None:
572 member_name = msg.get("cn", idx=0)
573 self.outf.write("%s\n" % member_name)
575 except Exception as e:
576 raise CommandError('Failed to list members of "%s" group - %s' %
577 (groupname, e))
580 class cmd_group_move(Command):
581 """Move a group to an organizational unit/container.
583 This command moves a group object into the specified organizational unit
584 or container.
585 The groupname specified on the command is the sAMAccountName.
586 The name of the organizational unit or container can be specified as a
587 full DN or without the domainDN component.
589 The command may be run from the root userid or another authorized userid.
591 The -H or --URL= option can be used to execute the command against a remote
592 server.
594 Example1:
595 samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
596 -H ldap://samba.samdom.example.com -U administrator
598 Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
599 unit on a remote LDAP server.
601 The -H parameter is used to specify the remote target server.
603 Example2:
604 samba-tool group move Group1 CN=Users
606 Example2 shows how to move a group Group1 back into the CN=Users container
607 on the local server.
610 synopsis = "%prog <groupname> <new_parent_dn> [options]"
612 takes_options = [
613 Option("-H", "--URL", help="LDB URL for database or target server",
614 type=str, metavar="URL", dest="H"),
617 takes_args = ["groupname", "new_parent_dn"]
618 takes_optiongroups = {
619 "sambaopts": options.SambaOptions,
620 "credopts": options.CredentialsOptions,
621 "versionopts": options.VersionOptions,
624 def run(self, groupname, new_parent_dn, credopts=None, sambaopts=None,
625 versionopts=None, H=None):
626 lp = sambaopts.get_loadparm()
627 creds = credopts.get_credentials(lp, fallback_machine=True)
628 samdb = SamDB(url=H, session_info=system_session(),
629 credentials=creds, lp=lp)
630 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
632 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
633 ldb.binary_encode(groupname))
634 try:
635 res = samdb.search(base=domain_dn,
636 expression=filter,
637 scope=ldb.SCOPE_SUBTREE)
638 group_dn = res[0].dn
639 except IndexError:
640 raise CommandError('Unable to find group "%s"' % (groupname))
642 try:
643 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
644 except Exception as e:
645 raise CommandError('Invalid new_parent_dn "%s": %s' %
646 (new_parent_dn, e.message))
648 full_new_group_dn = ldb.Dn(samdb, str(group_dn))
649 full_new_group_dn.remove_base_components(len(group_dn) - 1)
650 full_new_group_dn.add_base(full_new_parent_dn)
652 try:
653 samdb.rename(group_dn, full_new_group_dn)
654 except Exception as e:
655 raise CommandError('Failed to move group "%s"' % groupname, e)
656 self.outf.write('Moved group "%s" into "%s"\n' %
657 (groupname, full_new_parent_dn))
660 class cmd_group_show(Command):
661 """Display a group AD object.
663 This command displays a group object and it's attributes in the Active
664 Directory domain.
665 The group name specified on the command is the sAMAccountName of the group.
667 The command may be run from the root userid or another authorized userid.
669 The -H or --URL= option can be used to execute the command against a remote
670 server.
672 Example1:
673 samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
674 -U administrator --password=passw1rd
676 Example1 shows how to display a group's attributes in the domain against a
677 remote LDAP server.
679 The -H parameter is used to specify the remote target server.
681 Example2:
682 samba-tool group show Group2
684 Example2 shows how to display a group's attributes in the domain against a local
685 LDAP server.
687 Example3:
688 samba-tool group show Group3 --attributes=member,objectGUID
690 Example3 shows how to display a groups objectGUID and member attributes.
692 synopsis = "%prog <group name> [options]"
694 takes_options = [
695 Option("-H", "--URL", help="LDB URL for database or target server",
696 type=str, metavar="URL", dest="H"),
697 Option("--attributes",
698 help=("Comma separated list of attributes, "
699 "which will be printed."),
700 type=str, dest="group_attrs"),
703 takes_args = ["groupname"]
704 takes_optiongroups = {
705 "sambaopts": options.SambaOptions,
706 "credopts": options.CredentialsOptions,
707 "versionopts": options.VersionOptions,
710 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
711 H=None, group_attrs=None):
713 lp = sambaopts.get_loadparm()
714 creds = credopts.get_credentials(lp, fallback_machine=True)
715 samdb = SamDB(url=H, session_info=system_session(),
716 credentials=creds, lp=lp)
718 attrs = None
719 if group_attrs:
720 attrs = group_attrs.split(",")
722 filter = ("(&(objectCategory=group)(sAMAccountName=%s))" %
723 ldb.binary_encode(groupname))
725 domaindn = samdb.domain_dn()
727 try:
728 res = samdb.search(base=domaindn, expression=filter,
729 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
730 user_dn = res[0].dn
731 except IndexError:
732 raise CommandError('Unable to find group "%s"' % (groupname))
734 for msg in res:
735 group_ldif = common.get_ldif_for_editor(samdb, msg)
736 self.outf.write(group_ldif)
739 class cmd_group_stats(Command):
740 """Summary statistics about group memberships."""
742 synopsis = "%prog [options]"
744 takes_options = [
745 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
746 metavar="URL", dest="H"),
749 takes_optiongroups = {
750 "sambaopts": options.SambaOptions,
751 "credopts": options.CredentialsOptions,
752 "versionopts": options.VersionOptions,
755 def num_in_range(self, range_min, range_max, group_freqs):
756 total_count = 0
757 for members, count in group_freqs.items():
758 if range_min <= members and members <= range_max:
759 total_count += count
761 return total_count
763 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
764 lp = sambaopts.get_loadparm()
765 creds = credopts.get_credentials(lp, fallback_machine=True)
767 samdb = SamDB(url=H, session_info=system_session(),
768 credentials=creds, lp=lp)
770 domain_dn = samdb.domain_dn()
771 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
772 expression=("(objectClass=group)"),
773 attrs=["samaccountname", "member"])
775 # first count up how many members each group has
776 group_assignments = {}
777 total_memberships = 0
779 for msg in res:
780 name = str(msg.get("samaccountname"))
781 num_members = len(msg.get("member", default=[]))
782 group_assignments[name] = num_members
783 total_memberships += num_members
785 num_groups = res.count
786 self.outf.write("Group membership statistics*\n")
787 self.outf.write("-------------------------------------------------\n")
788 self.outf.write("Total groups: {0}\n".format(num_groups))
789 self.outf.write("Total memberships: {0}\n".format(total_memberships))
790 average = total_memberships / float(num_groups)
791 self.outf.write("Average members per group: %.2f\n" % average)
793 # find the max and median memberships (note that some default groups
794 # always have zero members, so displaying the min is not very helpful)
795 group_names = list(group_assignments.keys())
796 group_members = list(group_assignments.values())
797 idx = group_members.index(max(group_members))
798 max_members = group_members[idx]
799 self.outf.write("Max members: {0} ({1})\n".format(max_members,
800 group_names[idx]))
801 group_members.sort()
802 midpoint = num_groups // 2
803 median = group_members[midpoint]
804 if num_groups % 2 == 0:
805 median = (median + group_members[midpoint - 1]) / 2
806 self.outf.write("Median members per group: {0}\n\n".format(median))
808 # convert this to the frequency of group membership, i.e. how many
809 # groups have 5 members, how many have 6 members, etc
810 group_freqs = defaultdict(int)
811 for group, num_members in group_assignments.items():
812 group_freqs[num_members] += 1
814 # now squash this down even further, so that we just display the number
815 # of groups that fall into one of the following membership bands
816 bands = [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24),
817 (25, 29), (30, 39), (40, 49), (50, 59), (60, 69), (70, 79),
818 (80, 89), (90, 99), (100, 149), (150, 199), (200, 249),
819 (250, 299), (300, 399), (400, 499), (500, 999), (1000, 1999),
820 (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999),
821 (10000, max_members)]
823 self.outf.write("Members Number of Groups\n")
824 self.outf.write("-------------------------------------------------\n")
826 for band in bands:
827 band_start = band[0]
828 band_end = band[1]
829 if band_start > max_members:
830 break
832 num_groups = self.num_in_range(band_start, band_end, group_freqs)
834 if num_groups != 0:
835 band_str = "{0}-{1}".format(band_start, band_end)
836 self.outf.write("%13s %u\n" % (band_str, num_groups))
838 self.outf.write("\n* Note this does not include nested group memberships\n")
841 class cmd_group_edit(Command):
842 """Modify Group AD object.
844 This command will allow editing of a group account in the Active Directory
845 domain. You will then be able to add or change attributes and their values.
847 The groupname specified on the command is the sAMAccountName.
849 The command may be run from the root userid or another authorized userid.
851 The -H or --URL= option can be used to execute the command against a remote
852 server.
854 Example1:
855 samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
856 -U administrator --password=passw1rd
858 Example1 shows how to edit a groups attributes in the domain against a
859 remote LDAP server.
861 The -H parameter is used to specify the remote target server.
863 Example2:
864 samba-tool group edit Group2
866 Example2 shows how to edit a groups attributes in the domain against a local
867 server.
869 Example3:
870 samba-tool group edit Group3 --editor=nano
872 Example3 shows how to edit a groups attributes in the domain against a local
873 server using the 'nano' editor.
875 synopsis = "%prog <groupname> [options]"
877 takes_options = [
878 Option("-H", "--URL", help="LDB URL for database or target server",
879 type=str, metavar="URL", dest="H"),
880 Option("--editor", help="Editor to use instead of the system default,"
881 " or 'vi' if no system default is set.", type=str),
884 takes_args = ["groupname"]
885 takes_optiongroups = {
886 "sambaopts": options.SambaOptions,
887 "credopts": options.CredentialsOptions,
888 "versionopts": options.VersionOptions,
891 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
892 H=None, editor=None):
893 lp = sambaopts.get_loadparm()
894 creds = credopts.get_credentials(lp, fallback_machine=True)
895 samdb = SamDB(url=H, session_info=system_session(),
896 credentials=creds, lp=lp)
898 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
899 ldb.binary_encode(groupname))
901 domaindn = samdb.domain_dn()
903 try:
904 res = samdb.search(base=domaindn,
905 expression=filter,
906 scope=ldb.SCOPE_SUBTREE)
907 group_dn = res[0].dn
908 except IndexError:
909 raise CommandError('Unable to find group "%s"' % (groupname))
911 if len(res) != 1:
912 raise CommandError('Invalid number of results: for "%s": %d' %
913 ((groupname), len(res)))
915 msg = res[0]
916 result_ldif = common.get_ldif_for_editor(samdb, msg)
918 if editor is None:
919 editor = os.environ.get('EDITOR')
920 if editor is None:
921 editor = 'vi'
923 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
924 t_file.write(get_bytes(result_ldif))
925 t_file.flush()
926 try:
927 check_call([editor, t_file.name])
928 except CalledProcessError as e:
929 raise CalledProcessError("ERROR: ", e)
930 with open(t_file.name) as edited_file:
931 edited_message = edited_file.read()
933 msgs_edited = samdb.parse_ldif(edited_message)
934 msg_edited = next(msgs_edited)[1]
936 res_msg_diff = samdb.msg_diff(msg, msg_edited)
937 if len(res_msg_diff) == 0:
938 self.outf.write("Nothing to do\n")
939 return
941 try:
942 samdb.modify(res_msg_diff)
943 except Exception as e:
944 raise CommandError("Failed to modify group '%s': " % groupname, e)
946 self.outf.write("Modified group '%s' successfully\n" % groupname)
949 class cmd_group_add_unix_attrs(Command):
950 """Add RFC2307 attributes to a group.
952 This command adds Unix attributes to a group account in the Active
953 Directory domain.
954 The groupname specified on the command is the sAMaccountName.
956 Unix (RFC2307) attributes will be added to the group account.
958 Add 'idmap_ldb:use rfc2307 = Yes' to smb.conf to use these attributes for
959 UID/GID mapping.
961 The command may be run from the root userid or another authorized userid.
962 The -H or --URL= option can be used to execute the command against a
963 remote server.
965 Example1:
966 samba-tool group addunixattrs Group1 10000
968 Example1 shows how to add RFC2307 attributes to a domain enabled group
969 account.
971 The groups Unix ID will be set to '10000', provided this ID isn't already
972 in use.
975 synopsis = "%prog <groupname> <gidnumber> [options]"
977 takes_options = [
978 Option("-H", "--URL", help="LDB URL for database or target server",
979 type=str, metavar="URL", dest="H"),
982 takes_args = ["groupname", "gidnumber"]
984 takes_optiongroups = {
985 "sambaopts": options.SambaOptions,
986 "credopts": options.CredentialsOptions,
987 "versionopts": options.VersionOptions,
990 def run(self, groupname, gidnumber, credopts=None, sambaopts=None,
991 versionopts=None, H=None):
993 lp = sambaopts.get_loadparm()
994 creds = credopts.get_credentials(lp)
996 samdb = SamDB(url=H, session_info=system_session(),
997 credentials=creds, lp=lp)
999 domaindn = samdb.domain_dn()
1001 # Check group exists and doesn't have a gidNumber
1002 filter = "(samaccountname={})".format(ldb.binary_encode(groupname))
1003 res = samdb.search(domaindn,
1004 scope=ldb.SCOPE_SUBTREE,
1005 expression=filter)
1006 if (len(res) == 0):
1007 raise CommandError("Unable to find group '{}'".format(groupname))
1009 group_dn = res[0].dn
1011 if "gidNumber" in res[0]:
1012 raise CommandError("Group {} is a Unix group.".format(groupname))
1014 # Check if supplied gidnumber isn't already being used
1015 filter = "(&(objectClass=group)(gidNumber={}))".format(gidnumber)
1016 res = samdb.search(domaindn,
1017 scope=ldb.SCOPE_SUBTREE,
1018 expression=filter)
1019 if (len(res) != 0):
1020 raise CommandError('gidNumber {} already used.'.format(gidnumber))
1022 if not lp.get("idmap_ldb:use rfc2307"):
1023 self.outf.write("You are setting a Unix/RFC2307 GID. "
1024 "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
1025 " in smb.conf to use the attributes for "
1026 "XID/SID-mapping.\n")
1028 group_mod = """
1029 dn: {0}
1030 changetype: modify
1031 add: gidNumber
1032 gidNumber: {1}
1033 """.format(group_dn, gidnumber)
1035 try:
1036 samdb.modify_ldif(group_mod)
1037 except ldb.LdbError as e:
1038 raise CommandError("Failed to modify group '{0}': {1}"
1039 .format(groupname, e))
1041 self.outf.write("Modified Group '{}' successfully\n".format(groupname))
1044 class cmd_group_rename(Command):
1045 """Rename a group and related attributes.
1047 This command allows to set the group's name related attributes. The
1048 group's CN will be renamed automatically.
1050 The group's CN will be the sAMAccountName.
1051 Use the --force-new-cn option to specify the new CN manually and the
1052 --reset-cn to reset this change.
1054 Use an empty attribute value to remove the specified attribute.
1056 The groupname specified on the command is the sAMAccountName.
1058 The command may be run locally from the root userid or another authorized
1059 userid.
1061 The -H or --URL= option can be used to execute the command against a remote
1062 server.
1064 Example1:
1065 samba-tool group rename employees --samaccountname=staff
1067 Example1 shows how to change the samaccountname of a group 'employees' to
1068 'staff'. The CN of the group employees will also be changed to 'staff',
1069 if the previous CN was the previous sAMAccountName.
1071 Example2:
1072 samba-tool group rename employees --mail-address='staff@company.com' \\
1073 -H ldap://samba.samdom.example.com -U administrator
1075 Example2 shows how to rename the mail address of a group 'employees' to
1076 'staff@company.com'.
1077 The -H parameter is used to specify the remote target server.
1080 synopsis = "%prog <groupname> [options]"
1082 takes_options = [
1083 Option("-H", "--URL",
1084 help="LDB URL for database or target server",
1085 type=str, metavar="URL", dest="H"),
1086 Option("--force-new-cn",
1087 help="Specify a new CN (RND) instead of using the sAMAccountName.",
1088 type=str),
1089 Option("--reset-cn",
1090 help="Set the CN (RDN) to the sAMAccountName. Use this option "
1091 "to reset the changes made with the --force-new-cn option.",
1092 action="store_true"),
1093 Option("--mail-address",
1094 help="New mail address",
1095 type=str),
1096 Option("--samaccountname",
1097 help="New account name (sAMAccountName/logon name)",
1098 type=str)
1101 takes_args = ["groupname"]
1102 takes_optiongroups = {
1103 "sambaopts": options.SambaOptions,
1104 "credopts": options.CredentialsOptions,
1105 "versionopts": options.VersionOptions,
1108 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
1109 H=None, mail_address=None, samaccountname=None, force_new_cn=None,
1110 reset_cn=None):
1111 # illegal options
1112 if force_new_cn and reset_cn:
1113 raise CommandError("It is not allowed to specify --force-new-cn "
1114 "together with --reset-cn.")
1115 if force_new_cn == "":
1116 raise CommandError("Failed to rename group - delete protected "
1117 "attribute 'CN'")
1118 if samaccountname == "":
1119 raise CommandError("Failed to rename group - delete protected "
1120 "attribute 'sAMAccountName'")
1122 lp = sambaopts.get_loadparm()
1123 creds = credopts.get_credentials(lp, fallback_machine=True)
1124 samdb = SamDB(url=H, session_info=system_session(),
1125 credentials=creds, lp=lp)
1126 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
1128 filter = ("(&(objectClass=group)(samaccountname=%s))" %
1129 ldb.binary_encode(groupname))
1130 try:
1131 res = samdb.search(base=domain_dn,
1132 scope=ldb.SCOPE_SUBTREE,
1133 expression=filter,
1134 attrs=["sAMAccountName",
1135 "cn",
1136 "mail"]
1138 old_group = res[0]
1139 group_dn = old_group.dn
1140 except IndexError:
1141 raise CommandError('Unable to find group "%s"' % (groupname))
1143 group_parent_dn = group_dn.parent()
1144 old_cn = old_group["cn"][0]
1146 # get the actual and the new group cn and the new dn
1147 if force_new_cn is not None:
1148 new_cn = force_new_cn
1149 elif samaccountname is not None:
1150 new_cn = samaccountname
1151 else:
1152 new_cn = old_group["sAMAccountName"]
1154 # CN must change, if the new CN is different and the old CN is the
1155 # standard CN or the change is forced with force-new-cn or reset-cn
1156 expected_cn = old_group["sAMAccountName"]
1157 must_change_cn = str(old_cn) != str(new_cn) and \
1158 (str(old_cn) == str(expected_cn) or \
1159 reset_cn or bool(force_new_cn))
1161 new_group_dn = ldb.Dn(samdb, "CN=%s" % new_cn)
1162 new_group_dn.add_base(group_parent_dn)
1164 # format given attributes
1165 group_attrs = ldb.Message()
1166 group_attrs.dn = group_dn
1167 samdb.prepare_attr_replace(group_attrs, old_group, "sAMAccountName",
1168 samaccountname)
1169 samdb.prepare_attr_replace(group_attrs, old_group, "mail", mail_address)
1171 group_attributes_changed = len(group_attrs) > 0
1173 # update the group with formatted attributes
1174 samdb.transaction_start()
1175 try:
1176 if group_attributes_changed:
1177 samdb.modify(group_attrs)
1178 if must_change_cn:
1179 samdb.rename(group_dn, new_group_dn)
1180 except Exception as e:
1181 samdb.transaction_cancel()
1182 raise CommandError('Failed to rename group "%s"' % groupname, e)
1183 samdb.transaction_commit()
1185 if must_change_cn:
1186 self.outf.write('Renamed CN of group "%s" from "%s" to "%s" '
1187 'successfully\n' % (groupname, old_cn, new_cn))
1189 if group_attributes_changed:
1190 self.outf.write('Following attributes of group "%s" have been '
1191 'changed successfully:\n' % (groupname))
1192 for attr in group_attrs.keys():
1193 if attr == "dn":
1194 continue
1195 self.outf.write('%s: %s\n' % (attr, group_attrs[attr]
1196 if group_attrs[attr] else '[removed]'))
1198 class cmd_group(SuperCommand):
1199 """Group management."""
1201 subcommands = {}
1202 subcommands["add"] = cmd_group_add()
1203 subcommands["create"] = cmd_group_add()
1204 subcommands["delete"] = cmd_group_delete()
1205 subcommands["edit"] = cmd_group_edit()
1206 subcommands["addmembers"] = cmd_group_add_members()
1207 subcommands["removemembers"] = cmd_group_remove_members()
1208 subcommands["list"] = cmd_group_list()
1209 subcommands["listmembers"] = cmd_group_list_members()
1210 subcommands["move"] = cmd_group_move()
1211 subcommands["show"] = cmd_group_show()
1212 subcommands["stats"] = cmd_group_stats()
1213 subcommands["addunixattrs"] = cmd_group_add_unix_attrs()
1214 subcommands["rename"] = cmd_group_rename()