ldb:kv_index: use subtransaction_cancel in transaction_cancel
[Samba.git] / python / samba / netcmd / group.py
bloba705560225330e04cc17fab675c469a634ce06f3
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_pack, ndr_unpack
23 from samba.dcerpc import security
25 from samba.auth import system_session
26 from samba.samdb import SamDB
27 from samba.dsdb import (
28 ATYPE_SECURITY_GLOBAL_GROUP,
29 DS_GUID_USERS_CONTAINER,
30 GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
31 GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
32 GTYPE_SECURITY_GLOBAL_GROUP,
33 GTYPE_SECURITY_UNIVERSAL_GROUP,
34 GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP,
35 GTYPE_DISTRIBUTION_GLOBAL_GROUP,
36 GTYPE_DISTRIBUTION_UNIVERSAL_GROUP,
37 SYSTEM_FLAG_DISALLOW_DELETE,
38 SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE,
39 SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME,
40 UF_ACCOUNTDISABLE,
42 from collections import defaultdict
43 from subprocess import check_call, CalledProcessError
44 from samba.common import get_bytes, normalise_int32
45 import os
46 import tempfile
47 from . import common
49 security_group = dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
50 "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
51 "Global": GTYPE_SECURITY_GLOBAL_GROUP,
52 "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP})
53 distribution_group = dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP,
54 "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP,
55 "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP})
58 class cmd_group_add(Command):
59 """Creates a new AD group.
61 This command adds a new Active Directory group. The groupname specified on the command is a unique sAMAccountName.
63 An Active Directory group may contain user and computer accounts as well as other groups. An administrator adds a new group and adds members to that group so they can be managed as a single entity. This helps to simplify security and system administration.
65 Groups may also be used to establish email distribution lists, using --group-type=Distribution.
67 Groups are located in domains in organizational units (OUs). The group's scope is a characteristic of the group that designates the extent to which the group is applied within the domain tree or forest.
69 The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created.
71 The command may be run from the root userid or another authorized userid. The
72 -H or --URL= option can be used to execute the command on a remote server.
74 Example1:
75 samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group'
77 Example1 adds a new group with the name Group1 added to the Users container on a remote LDAP server. The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server. It defaults to the security type and global scope.
79 Example2:
80 sudo samba-tool group add Group2 --group-type=Distribution
82 Example2 adds a new distribution group to the local server. The command is run under root using the sudo command.
84 Example3:
85 samba-tool group add Group3 --nis-domain=samdom --gid-number=12345
87 Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature).
88 """
90 synopsis = "%prog <groupname> [options]"
92 takes_optiongroups = {
93 "sambaopts": options.SambaOptions,
94 "versionopts": options.VersionOptions,
95 "credopts": options.CredentialsOptions,
98 takes_options = [
99 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
100 metavar="URL", dest="H"),
101 Option("--groupou",
102 help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created",
103 type=str),
104 Option("--group-scope", type="choice", choices=["Domain", "Global", "Universal"],
105 help="Group scope (Domain | Global | Universal)"),
106 Option("--group-type", type="choice", choices=["Security", "Distribution"],
107 help="Group type (Security | Distribution)"),
108 Option("--description", help="Group's description", type=str),
109 Option("--mail-address", help="Group's email address", type=str),
110 Option("--notes", help="Group's notes", type=str),
111 Option("--gid-number", help="Group's Unix/RFC2307 GID number", type=int),
112 Option("--nis-domain", help="SFU30 NIS Domain", type=str),
113 Option("--special", help="Add a special predefined group", action="store_true", default=False),
116 takes_args = ["groupname"]
118 def run(self, groupname, credopts=None, sambaopts=None,
119 versionopts=None, H=None, groupou=None, group_scope=None,
120 group_type=None, description=None, mail_address=None, notes=None, gid_number=None, nis_domain=None,
121 special=False):
123 if (group_type or "Security") == "Security":
124 gtype = security_group.get(group_scope, GTYPE_SECURITY_GLOBAL_GROUP)
125 else:
126 gtype = distribution_group.get(group_scope, GTYPE_DISTRIBUTION_GLOBAL_GROUP)
128 if (gid_number is None and nis_domain is not None) or (gid_number is not None and nis_domain is None):
129 raise CommandError('Both --gid-number and --nis-domain have to be set for a RFC2307-enabled group. Operation cancelled.')
131 lp = sambaopts.get_loadparm()
132 creds = credopts.get_credentials(lp, fallback_machine=True)
134 try:
135 samdb = SamDB(url=H, session_info=system_session(),
136 credentials=creds, lp=lp)
137 except Exception as e:
138 # FIXME: catch more specific exception
139 raise CommandError(f'Failed to add group "{groupname}"', e)
141 if special:
142 invalid_option = None
143 if group_scope is not None:
144 invalid_option = 'group-scope'
145 elif group_type is not None:
146 invalid_option = 'group-type'
147 elif description is not None:
148 invalid_option = 'description'
149 elif mail_address is not None:
150 invalid_option = 'mail-address'
151 elif notes is not None:
152 invalid_option = 'notes'
153 elif gid_number is not None:
154 invalid_option = 'gid-number'
155 elif nis_domain is not None:
156 invalid_option = 'nis-domain'
158 if invalid_option is not None:
159 raise CommandError(f'Superfluous option --{invalid_option} '
160 f'specified with --special')
162 if not samdb.am_pdc():
163 raise CommandError('Adding special groups is only permitted '
164 'against the PDC!')
166 special_groups = {
167 # On Windows, this group is added automatically when the PDC
168 # role is held by a DC running Windows Server 2012 R2 or later.
169 # https://docs.microsoft.com/en-us/windows-server/security/credentials-protection-and-management/protected-users-security-group#BKMK_Requirements
170 'Protected Users'.lower(): (
171 'Protected Users',
172 GTYPE_SECURITY_GLOBAL_GROUP,
173 security.DOMAIN_RID_PROTECTED_USERS,
174 'Members of this group are afforded additional '
175 'protections against authentication security threats'),
178 special_group = special_groups.get(groupname.lower())
179 if special_group is None:
180 raise CommandError(f'Unknown special group "{groupname}".')
182 groupname, gtype, rid, description = special_group
183 group_type = normalise_int32(gtype)
185 group_dn = samdb.get_default_basedn()
187 if gtype == GTYPE_SECURITY_GLOBAL_GROUP:
188 object_sid = security.dom_sid(
189 f'{samdb.get_domain_sid()}-{rid}')
190 system_flags = None
192 if not groupou:
193 group_dn = samdb.get_wellknown_dn(group_dn,
194 DS_GUID_USERS_CONTAINER)
196 elif gtype == GTYPE_SECURITY_BUILTIN_LOCAL_GROUP:
197 object_sid = security.dom_sid(f'S-1-5-32-{rid}')
198 system_flags = (SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE |
199 SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME |
200 SYSTEM_FLAG_DISALLOW_DELETE)
202 if not groupou:
203 try:
204 group_dn.add_child('CN=Builtin')
205 except ldb.LdbError:
206 raise RuntimeError('Error getting Builtin objects DN')
207 else:
208 raise RuntimeError(f'Unknown group type {gtype}')
210 if groupou:
211 try:
212 group_dn.add_child(groupou)
213 except ldb.LdbError:
214 raise CommandError(f'Invalid group OU "{groupou}"')
216 try:
217 group_dn.add_child(f'CN={groupname}')
218 except ldb.LdbError:
219 raise CommandError(f'Invalid group name "{groupname}"')
221 msg = {
222 'dn': group_dn,
223 'sAMAccountName': groupname,
224 'objectClass': 'group',
225 'groupType': group_type,
226 'description': description,
227 'objectSid': ndr_pack(object_sid),
228 'isCriticalSystemObject': 'TRUE',
231 if system_flags is not None:
232 msg['systemFlags'] = system_flags
234 try:
235 samdb.add(msg, controls=['relax:0'])
236 except ldb.LdbError as e:
237 num, estr = e.args
238 if num == ldb.ERR_CONSTRAINT_VIOLATION:
239 try:
240 res = samdb.search(
241 expression=f'(objectSid={object_sid})',
242 attrs=['sAMAccountName'])
243 except ldb.LdbError:
244 raise CommandError(
245 f'Failed to add group "{groupname}"', e)
247 if len(res) != 1:
248 raise CommandError(
249 f'Failed to add group "{groupname}"', e)
251 name = res[0].get('sAMAccountName', idx=0)
252 if name:
253 with_name = f' with name "{name}"'
254 else:
255 with_name = ''
257 raise CommandError(
258 f'Failed to add group "{groupname}" - Special group '
259 f'already exists{with_name} at "{res[0].dn}".')
261 elif num == ldb.ERR_ENTRY_ALREADY_EXISTS:
262 try:
263 res = samdb.search(base=group_dn,
264 scope=ldb.SCOPE_BASE,
265 attrs=['sAMAccountName',
266 'objectSid',
267 'groupType'])
268 except ldb.LdbError:
269 try:
270 res = samdb.search(
271 expression=f'(sAMAccountName={groupname})',
272 attrs=['sAMAccountName',
273 'objectSid',
274 'groupType'])
275 except ldb.LdbError:
276 raise CommandError(
277 f'Failed to add group "{groupname}"', e)
279 if len(res) != 1:
280 raise CommandError(
281 f'Failed to add group "{groupname}"', e)
283 got_name = res[0].get('sAMAccountName', idx=0)
284 if got_name:
285 named = f'named "{got_name}"'
286 else:
287 named = 'with no name'
289 got_group_type = res[0].get('groupType',
290 idx=0).decode('utf-8')
291 if group_type != got_group_type:
292 raise CommandError(
293 f'Failed to add group "{groupname}" - An object '
294 f'{named} at "{res[0].dn}" already exists, but it '
295 f'is not a security group. Rename or remove this '
296 f'existing object before attempting to add this '
297 f'special group.')
299 sid = res[0].get('objectSid', idx=0)
300 if sid is None:
301 raise CommandError(
302 f'Failed to add group "{groupname}" - A security '
303 f'group {named} at "{res[0].dn}" already exists, '
304 f'but it lacks a SID. Rename or remove this '
305 f'existing object before attempting to add this '
306 f'special group.')
307 else:
308 sid = ndr_unpack(security.dom_sid, sid)
309 if sid == object_sid:
310 raise CommandError(
311 f'Failed to add group "{groupname}" - The '
312 f'security group {named} at "{res[0].dn}" '
313 f'already exists.')
314 else:
315 raise CommandError(
316 f'Failed to add group "{groupname}" - A '
317 f'security group {named} at "{res[0].dn}" '
318 f'already exists, but it has the wrong SID, '
319 f'and will not function as expected. Rename '
320 f'or remove this existing object before '
321 f'attempting to add this special group.')
322 else:
323 raise CommandError(f'Failed to add group "{groupname}"', e)
324 else:
325 self.outf.write(f'Added group {groupname}\n')
327 return
329 try:
330 samdb.newgroup(groupname, groupou=groupou, grouptype=gtype,
331 description=description, mailaddress=mail_address, notes=notes,
332 gidnumber=gid_number, nisdomain=nis_domain)
333 except Exception as e:
334 # FIXME: catch more specific exception
335 raise CommandError('Failed to add group "%s"' % groupname, e)
336 self.outf.write("Added group %s\n" % groupname)
339 class cmd_group_delete(Command):
340 """Deletes an AD group.
342 The command deletes an existing AD group from the Active Directory domain. The groupname specified on the command is the sAMAccountName.
344 Deleting a group is a permanent operation. When a group is deleted, all permissions and rights that users in the group had inherited from the group account are deleted as well.
346 The command may be run from the root userid or another authorized userid. The -H or --URL option can be used to execute the command on a remote server.
348 Example1:
349 samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
351 Example1 shows how to delete an AD group from a remote LDAP server. The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server.
353 Example2:
354 sudo samba-tool group delete Group2
356 Example2 deletes group Group2 from the local server. The command is run under root using the sudo command.
359 synopsis = "%prog <groupname> [options]"
361 takes_optiongroups = {
362 "sambaopts": options.SambaOptions,
363 "versionopts": options.VersionOptions,
364 "credopts": options.CredentialsOptions,
367 takes_options = [
368 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
369 metavar="URL", dest="H"),
372 takes_args = ["groupname"]
374 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None):
376 lp = sambaopts.get_loadparm()
377 creds = credopts.get_credentials(lp, fallback_machine=True)
378 samdb = SamDB(url=H, session_info=system_session(),
379 credentials=creds, lp=lp)
381 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
382 ldb.binary_encode(groupname))
384 try:
385 res = samdb.search(base=samdb.domain_dn(),
386 scope=ldb.SCOPE_SUBTREE,
387 expression=filter,
388 attrs=["dn"])
389 group_dn = res[0].dn
390 except IndexError:
391 raise CommandError('Unable to find group "%s"' % (groupname))
393 try:
394 samdb.delete(group_dn)
395 except Exception as e:
396 # FIXME: catch more specific exception
397 raise CommandError('Failed to remove group "%s"' % groupname, e)
398 self.outf.write("Deleted group %s\n" % groupname)
401 class cmd_group_add_members(Command):
402 """Add members to an AD group.
404 This command adds one or more members to an existing Active Directory group. The command accepts one or more group member names separated by commas. A group member may be a user or computer account or another Active Directory group.
406 When a member is added to a group the member may inherit permissions and rights from the group. Likewise, when permission or rights of a group are changed, the changes may reflect in the members through inheritance.
408 The member names specified on the command must be the sAMaccountName.
410 Example1:
411 samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
413 Example1 shows how to add two groups, Group1 and Group2 and one user account, User1, to the existing AD group named supergroup. The command will be run on a remote server specified with the -H. The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server.
415 Example2:
416 sudo samba-tool group addmembers supergroup User2
418 Example2 shows how to add a single user account, User2, to the supergroup AD group. It uses the sudo command to run as root when issuing the command.
421 synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
423 takes_optiongroups = {
424 "sambaopts": options.SambaOptions,
425 "versionopts": options.VersionOptions,
426 "credopts": options.CredentialsOptions,
429 takes_options = [
430 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
431 metavar="URL", dest="H"),
432 Option("--member-dn",
433 help=("DN of the new group member to be added.\n"
434 "The --object-types option will be ignored."),
435 type=str,
436 action="append"),
437 Option("--object-types",
438 help=("Comma separated list of object types.\n"
439 "The types are used to filter the search for the "
440 "specified members.\n"
441 "Valid values are: user, group, computer, serviceaccount, "
442 "contact and all.\n"
443 "Default: user,group,computer"),
444 default="user,group,computer",
445 type=str),
446 Option("--member-base-dn",
447 help=("Base DN for group member search.\n"
448 "Default is the domain DN."),
449 type=str),
452 takes_args = ["groupname", "listofmembers?"]
454 def run(self,
455 groupname,
456 listofmembers=None,
457 credopts=None,
458 sambaopts=None,
459 versionopts=None,
460 H=None,
461 member_base_dn=None,
462 member_dn=None,
463 object_types="user,group,computer"):
465 lp = sambaopts.get_loadparm()
466 creds = credopts.get_credentials(lp, fallback_machine=True)
468 if member_dn is None and listofmembers is None:
469 self.usage()
470 raise CommandError(
471 'Either listofmembers or --member-dn must be specified.')
473 try:
474 samdb = SamDB(url=H, session_info=system_session(),
475 credentials=creds, lp=lp)
476 groupmembers = []
477 if member_dn is not None:
478 groupmembers += member_dn
479 if listofmembers is not None:
480 groupmembers += listofmembers.split(',')
481 group_member_types = object_types.split(',')
483 if member_base_dn is not None:
484 member_base_dn = samdb.normalize_dn_in_domain(member_base_dn)
486 samdb.add_remove_group_members(groupname, groupmembers,
487 add_members_operation=True,
488 member_types=group_member_types,
489 member_base_dn=member_base_dn)
490 except Exception as e:
491 # FIXME: catch more specific exception
492 raise CommandError('Failed to add members %r to group "%s" - %s' % (
493 groupmembers, groupname, e))
494 self.outf.write("Added members to group %s\n" % groupname)
497 class cmd_group_remove_members(Command):
498 """Remove members from an AD group.
500 This command removes one or more members from an existing Active Directory group. The command accepts one or more group member names separated by commas. A group member may be a user or computer account or another Active Directory group that is a member of the group specified on the command.
502 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
504 Example1:
505 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
507 Example1 shows how to remove Group1 from supergroup. The command will run on the remote server specified on the -H parameter. The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server.
509 Example2:
510 sudo samba-tool group removemembers supergroup User1
512 Example2 shows how to remove a single user account, User2, from the supergroup AD group. It uses the sudo command to run as root when issuing the command.
515 synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
517 takes_optiongroups = {
518 "sambaopts": options.SambaOptions,
519 "versionopts": options.VersionOptions,
520 "credopts": options.CredentialsOptions,
523 takes_options = [
524 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
525 metavar="URL", dest="H"),
526 Option("--member-dn",
527 help=("DN of the group member to be removed.\n"
528 "The --object-types option will be ignored."),
529 type=str,
530 action="append"),
531 Option("--object-types",
532 help=("Comma separated list of object types.\n"
533 "The types are used to filter the search for the "
534 "specified members.\n"
535 "Valid values are: user, group, computer, serviceaccount, "
536 "contact and all.\n"
537 "Default: user,group,computer"),
538 default="user,group,computer",
539 type=str),
540 Option("--member-base-dn",
541 help=("Base DN for group member search.\n"
542 "Default is the domain DN."),
543 type=str),
546 takes_args = ["groupname", "listofmembers?"]
548 def run(self,
549 groupname,
550 listofmembers=None,
551 credopts=None,
552 sambaopts=None,
553 versionopts=None,
554 H=None,
555 member_base_dn=None,
556 member_dn=None,
557 object_types="user,group,computer"):
559 lp = sambaopts.get_loadparm()
560 creds = credopts.get_credentials(lp, fallback_machine=True)
562 if member_dn is None and listofmembers is None:
563 self.usage()
564 raise CommandError(
565 'Either listofmembers or --member-dn must be specified.')
567 try:
568 samdb = SamDB(url=H, session_info=system_session(),
569 credentials=creds, lp=lp)
570 groupmembers = []
571 if member_dn is not None:
572 groupmembers += member_dn
573 if listofmembers is not None:
574 groupmembers += listofmembers.split(',')
575 group_member_types = object_types.split(',')
577 if member_base_dn is not None:
578 member_base_dn = samdb.normalize_dn_in_domain(member_base_dn)
580 samdb.add_remove_group_members(groupname,
581 groupmembers,
582 add_members_operation=False,
583 member_types=group_member_types,
584 member_base_dn=member_base_dn)
585 except Exception as e:
586 # FIXME: Catch more specific exception
587 raise CommandError('Failed to remove members %r from group "%s"' % (listofmembers, groupname), e)
588 self.outf.write("Removed members from group %s\n" % groupname)
591 class cmd_group_list(Command):
592 """List all groups."""
594 synopsis = "%prog [options]"
596 takes_options = [
597 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
598 metavar="URL", dest="H"),
599 Option("-v", "--verbose",
600 help="Verbose output, showing group type and group scope.",
601 action="store_true"),
602 Option("-b", "--base-dn",
603 help="Specify base DN to use.",
604 type=str),
605 Option("--full-dn", dest="full_dn",
606 default=False,
607 action='store_true',
608 help="Display DN instead of the sAMAccountName."),
611 takes_optiongroups = {
612 "sambaopts": options.SambaOptions,
613 "credopts": options.CredentialsOptions,
614 "versionopts": options.VersionOptions,
617 def run(self,
618 sambaopts=None,
619 credopts=None,
620 versionopts=None,
621 H=None,
622 verbose=False,
623 base_dn=None,
624 full_dn=False):
625 lp = sambaopts.get_loadparm()
626 creds = credopts.get_credentials(lp, fallback_machine=True)
628 samdb = SamDB(url=H, session_info=system_session(),
629 credentials=creds, lp=lp)
630 attrs=["samaccountname"]
632 if verbose:
633 attrs += ["grouptype", "member"]
634 domain_dn = samdb.domain_dn()
635 if base_dn:
636 domain_dn = samdb.normalize_dn_in_domain(base_dn)
637 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
638 expression=("(objectClass=group)"),
639 attrs=attrs)
640 if (len(res) == 0):
641 return
643 if verbose:
644 self.outf.write("Group Name Group Type Group Scope Members\n")
645 self.outf.write("--------------------------------------------------------------------------------\n")
647 for msg in res:
648 self.outf.write("%-44s" % msg.get("samaccountname", idx=0))
649 hgtype = hex(int("%s" % msg["grouptype"]) & 0x00000000FFFFFFFF)
650 if (hgtype == hex(int(security_group.get("Builtin")))):
651 self.outf.write("Security Builtin ")
652 elif (hgtype == hex(int(security_group.get("Domain")))):
653 self.outf.write("Security Domain ")
654 elif (hgtype == hex(int(security_group.get("Global")))):
655 self.outf.write("Security Global ")
656 elif (hgtype == hex(int(security_group.get("Universal")))):
657 self.outf.write("Security Universal")
658 elif (hgtype == hex(int(distribution_group.get("Global")))):
659 self.outf.write("Distribution Global ")
660 elif (hgtype == hex(int(distribution_group.get("Domain")))):
661 self.outf.write("Distribution Domain ")
662 elif (hgtype == hex(int(distribution_group.get("Universal")))):
663 self.outf.write("Distribution Universal")
664 else:
665 self.outf.write(" ")
666 num_members = len(msg.get("member", default=[]))
667 self.outf.write(" %6u\n" % num_members)
668 else:
669 for msg in res:
670 if full_dn:
671 self.outf.write("%s\n" % msg.get("dn"))
672 continue
674 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
677 class cmd_group_list_members(Command):
678 """List all members of an AD group.
680 This command lists members from an existing Active Directory group. The command accepts one group name.
682 Example1:
683 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
686 synopsis = "%prog <groupname> [options]"
688 takes_options = [
689 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
690 metavar="URL", dest="H"),
691 Option("--hide-expired",
692 help="Do not list expired group members",
693 default=False,
694 action='store_true'),
695 Option("--hide-disabled",
696 default=False,
697 action='store_true',
698 help="Do not list disabled group members"),
699 Option("--full-dn", dest="full_dn",
700 default=False,
701 action='store_true',
702 help="Display DN instead of the sAMAccountName.")
705 takes_optiongroups = {
706 "sambaopts": options.SambaOptions,
707 "credopts": options.CredentialsOptions,
708 "versionopts": options.VersionOptions,
711 takes_args = ["groupname"]
713 def run(self,
714 groupname,
715 credopts=None,
716 sambaopts=None,
717 versionopts=None,
718 H=None,
719 hide_expired=False,
720 hide_disabled=False,
721 full_dn=False):
722 lp = sambaopts.get_loadparm()
723 creds = credopts.get_credentials(lp, fallback_machine=True)
725 try:
726 samdb = SamDB(url=H, session_info=system_session(),
727 credentials=creds, lp=lp)
729 search_filter = ("(&(objectClass=group)(sAMAccountName=%s))" %
730 ldb.binary_encode(groupname))
731 try:
732 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
733 expression=(search_filter),
734 attrs=["objectSid"])
735 group_sid_binary = res[0].get('objectSid', idx=0)
736 except IndexError:
737 raise CommandError('Unable to find group "%s"' % (groupname))
739 group_sid = ndr_unpack(security.dom_sid, group_sid_binary)
740 (group_dom_sid, rid) = group_sid.split()
741 group_sid_dn = "<SID=%s>" % (group_sid)
743 filter_expires = ""
744 if hide_expired is True:
745 current_nttime = samdb.get_nttime()
746 filter_expires = ("(|"
747 "(!(accountExpires=*))"
748 "(accountExpires=0)"
749 "(accountExpires>=%u)"
750 ")" % (current_nttime))
752 filter_disabled = ""
753 if hide_disabled is True:
754 filter_disabled = "(!(userAccountControl:%s:=%u))" % (
755 ldb.OID_COMPARATOR_AND, UF_ACCOUNTDISABLE)
757 filter = "(&(|(primaryGroupID=%s)(memberOf=%s))%s%s)" % (
758 rid, group_sid_dn, filter_disabled, filter_expires)
760 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
761 expression=filter,
762 attrs=["samAccountName", "cn"])
764 if (len(res) == 0):
765 return
767 for msg in res:
768 if full_dn:
769 self.outf.write("%s\n" % msg.get("dn"))
770 continue
772 member_name = msg.get("samAccountName", idx=0)
773 if member_name is None:
774 member_name = msg.get("cn", idx=0)
775 self.outf.write("%s\n" % member_name)
777 except Exception as e:
778 raise CommandError('Failed to list members of "%s" group - %s' %
779 (groupname, e))
782 class cmd_group_move(Command):
783 """Move a group to an organizational unit/container.
785 This command moves a group object into the specified organizational unit
786 or container.
787 The groupname specified on the command is the sAMAccountName.
788 The name of the organizational unit or container can be specified as a
789 full DN or without the domainDN component.
791 The command may be run from the root userid or another authorized userid.
793 The -H or --URL= option can be used to execute the command against a remote
794 server.
796 Example1:
797 samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
798 -H ldap://samba.samdom.example.com -U administrator
800 Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
801 unit on a remote LDAP server.
803 The -H parameter is used to specify the remote target server.
805 Example2:
806 samba-tool group move Group1 CN=Users
808 Example2 shows how to move a group Group1 back into the CN=Users container
809 on the local server.
812 synopsis = "%prog <groupname> <new_parent_dn> [options]"
814 takes_options = [
815 Option("-H", "--URL", help="LDB URL for database or target server",
816 type=str, metavar="URL", dest="H"),
819 takes_args = ["groupname", "new_parent_dn"]
820 takes_optiongroups = {
821 "sambaopts": options.SambaOptions,
822 "credopts": options.CredentialsOptions,
823 "versionopts": options.VersionOptions,
826 def run(self, groupname, new_parent_dn, credopts=None, sambaopts=None,
827 versionopts=None, H=None):
828 lp = sambaopts.get_loadparm()
829 creds = credopts.get_credentials(lp, fallback_machine=True)
830 samdb = SamDB(url=H, session_info=system_session(),
831 credentials=creds, lp=lp)
832 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
834 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
835 ldb.binary_encode(groupname))
836 try:
837 res = samdb.search(base=domain_dn,
838 expression=filter,
839 scope=ldb.SCOPE_SUBTREE)
840 group_dn = res[0].dn
841 except IndexError:
842 raise CommandError('Unable to find group "%s"' % (groupname))
844 try:
845 full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
846 except Exception as e:
847 raise CommandError('Invalid new_parent_dn "%s": %s' %
848 (new_parent_dn, e.message))
850 full_new_group_dn = ldb.Dn(samdb, str(group_dn))
851 full_new_group_dn.remove_base_components(len(group_dn) - 1)
852 full_new_group_dn.add_base(full_new_parent_dn)
854 try:
855 samdb.rename(group_dn, full_new_group_dn)
856 except Exception as e:
857 raise CommandError('Failed to move group "%s"' % groupname, e)
858 self.outf.write('Moved group "%s" into "%s"\n' %
859 (groupname, full_new_parent_dn))
862 class cmd_group_show(Command):
863 """Display a group AD object.
865 This command displays a group object and it's attributes in the Active
866 Directory domain.
867 The group name specified on the command is the sAMAccountName of the group.
869 The command may be run from the root userid or another authorized userid.
871 The -H or --URL= option can be used to execute the command against a remote
872 server.
874 Example1:
875 samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
876 -U administrator --password=passw1rd
878 Example1 shows how to display a group's attributes in the domain against a
879 remote LDAP server.
881 The -H parameter is used to specify the remote target server.
883 Example2:
884 samba-tool group show Group2
886 Example2 shows how to display a group's attributes in the domain against a local
887 LDAP server.
889 Example3:
890 samba-tool group show Group3 --attributes=member,objectGUID
892 Example3 shows how to display a groups objectGUID and member attributes.
894 synopsis = "%prog <group name> [options]"
896 takes_options = [
897 Option("-H", "--URL", help="LDB URL for database or target server",
898 type=str, metavar="URL", dest="H"),
899 Option("--attributes",
900 help=("Comma separated list of attributes, "
901 "which will be printed."),
902 type=str, dest="group_attrs"),
905 takes_args = ["groupname"]
906 takes_optiongroups = {
907 "sambaopts": options.SambaOptions,
908 "credopts": options.CredentialsOptions,
909 "versionopts": options.VersionOptions,
912 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
913 H=None, group_attrs=None):
915 lp = sambaopts.get_loadparm()
916 creds = credopts.get_credentials(lp, fallback_machine=True)
917 samdb = SamDB(url=H, session_info=system_session(),
918 credentials=creds, lp=lp)
920 attrs = None
921 if group_attrs:
922 attrs = group_attrs.split(",")
924 filter = ("(&(objectCategory=group)(sAMAccountName=%s))" %
925 ldb.binary_encode(groupname))
927 domaindn = samdb.domain_dn()
929 try:
930 res = samdb.search(base=domaindn, expression=filter,
931 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
932 user_dn = res[0].dn
933 except IndexError:
934 raise CommandError('Unable to find group "%s"' % (groupname))
936 for msg in res:
937 group_ldif = common.get_ldif_for_editor(samdb, msg)
938 self.outf.write(group_ldif)
941 class cmd_group_stats(Command):
942 """Summary statistics about group memberships."""
944 synopsis = "%prog [options]"
946 takes_options = [
947 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
948 metavar="URL", dest="H"),
951 takes_optiongroups = {
952 "sambaopts": options.SambaOptions,
953 "credopts": options.CredentialsOptions,
954 "versionopts": options.VersionOptions,
957 def num_in_range(self, range_min, range_max, group_freqs):
958 total_count = 0
959 for members, count in group_freqs.items():
960 if range_min <= members and members <= range_max:
961 total_count += count
963 return total_count
965 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
966 lp = sambaopts.get_loadparm()
967 creds = credopts.get_credentials(lp, fallback_machine=True)
969 samdb = SamDB(url=H, session_info=system_session(),
970 credentials=creds, lp=lp)
972 domain_dn = samdb.domain_dn()
973 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
974 expression=("(objectClass=group)"),
975 attrs=["samaccountname", "member"])
977 # first count up how many members each group has
978 group_assignments = {}
979 total_memberships = 0
981 for msg in res:
982 name = str(msg.get("samaccountname"))
983 num_members = len(msg.get("member", default=[]))
984 group_assignments[name] = num_members
985 total_memberships += num_members
987 num_groups = res.count
988 self.outf.write("Group membership statistics*\n")
989 self.outf.write("-------------------------------------------------\n")
990 self.outf.write("Total groups: {0}\n".format(num_groups))
991 self.outf.write("Total memberships: {0}\n".format(total_memberships))
992 average = total_memberships / float(num_groups)
993 self.outf.write("Average members per group: %.2f\n" % average)
995 # find the max and median memberships (note that some default groups
996 # always have zero members, so displaying the min is not very helpful)
997 group_names = list(group_assignments.keys())
998 group_members = list(group_assignments.values())
999 idx = group_members.index(max(group_members))
1000 max_members = group_members[idx]
1001 self.outf.write("Max members: {0} ({1})\n".format(max_members,
1002 group_names[idx]))
1003 group_members.sort()
1004 midpoint = num_groups // 2
1005 median = group_members[midpoint]
1006 if num_groups % 2 == 0:
1007 median = (median + group_members[midpoint - 1]) / 2
1008 self.outf.write("Median members per group: {0}\n\n".format(median))
1010 # convert this to the frequency of group membership, i.e. how many
1011 # groups have 5 members, how many have 6 members, etc
1012 group_freqs = defaultdict(int)
1013 for group, num_members in group_assignments.items():
1014 group_freqs[num_members] += 1
1016 # now squash this down even further, so that we just display the number
1017 # of groups that fall into one of the following membership bands
1018 bands = [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24),
1019 (25, 29), (30, 39), (40, 49), (50, 59), (60, 69), (70, 79),
1020 (80, 89), (90, 99), (100, 149), (150, 199), (200, 249),
1021 (250, 299), (300, 399), (400, 499), (500, 999), (1000, 1999),
1022 (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999),
1023 (10000, max_members)]
1025 self.outf.write("Members Number of Groups\n")
1026 self.outf.write("-------------------------------------------------\n")
1028 for band in bands:
1029 band_start = band[0]
1030 band_end = band[1]
1031 if band_start > max_members:
1032 break
1034 num_groups = self.num_in_range(band_start, band_end, group_freqs)
1036 if num_groups != 0:
1037 band_str = "{0}-{1}".format(band_start, band_end)
1038 self.outf.write("%13s %u\n" % (band_str, num_groups))
1040 self.outf.write("\n* Note this does not include nested group memberships\n")
1043 class cmd_group_edit(Command):
1044 """Modify Group AD object.
1046 This command will allow editing of a group account in the Active Directory
1047 domain. You will then be able to add or change attributes and their values.
1049 The groupname specified on the command is the sAMAccountName.
1051 The command may be run from the root userid or another authorized userid.
1053 The -H or --URL= option can be used to execute the command against a remote
1054 server.
1056 Example1:
1057 samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
1058 -U administrator --password=passw1rd
1060 Example1 shows how to edit a groups attributes in the domain against a
1061 remote LDAP server.
1063 The -H parameter is used to specify the remote target server.
1065 Example2:
1066 samba-tool group edit Group2
1068 Example2 shows how to edit a groups attributes in the domain against a local
1069 server.
1071 Example3:
1072 samba-tool group edit Group3 --editor=nano
1074 Example3 shows how to edit a groups attributes in the domain against a local
1075 server using the 'nano' editor.
1077 synopsis = "%prog <groupname> [options]"
1079 takes_options = [
1080 Option("-H", "--URL", help="LDB URL for database or target server",
1081 type=str, metavar="URL", dest="H"),
1082 Option("--editor", help="Editor to use instead of the system default,"
1083 " or 'vi' if no system default is set.", type=str),
1086 takes_args = ["groupname"]
1087 takes_optiongroups = {
1088 "sambaopts": options.SambaOptions,
1089 "credopts": options.CredentialsOptions,
1090 "versionopts": options.VersionOptions,
1093 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
1094 H=None, editor=None):
1095 lp = sambaopts.get_loadparm()
1096 creds = credopts.get_credentials(lp, fallback_machine=True)
1097 samdb = SamDB(url=H, session_info=system_session(),
1098 credentials=creds, lp=lp)
1100 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
1101 ldb.binary_encode(groupname))
1103 domaindn = samdb.domain_dn()
1105 try:
1106 res = samdb.search(base=domaindn,
1107 expression=filter,
1108 scope=ldb.SCOPE_SUBTREE)
1109 group_dn = res[0].dn
1110 except IndexError:
1111 raise CommandError('Unable to find group "%s"' % (groupname))
1113 if len(res) != 1:
1114 raise CommandError('Invalid number of results: for "%s": %d' %
1115 ((groupname), len(res)))
1117 msg = res[0]
1118 result_ldif = common.get_ldif_for_editor(samdb, msg)
1120 if editor is None:
1121 editor = os.environ.get('EDITOR')
1122 if editor is None:
1123 editor = 'vi'
1125 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
1126 t_file.write(get_bytes(result_ldif))
1127 t_file.flush()
1128 try:
1129 check_call([editor, t_file.name])
1130 except CalledProcessError as e:
1131 raise CalledProcessError("ERROR: ", e)
1132 with open(t_file.name) as edited_file:
1133 edited_message = edited_file.read()
1135 msgs_edited = samdb.parse_ldif(edited_message)
1136 msg_edited = next(msgs_edited)[1]
1138 res_msg_diff = samdb.msg_diff(msg, msg_edited)
1139 if len(res_msg_diff) == 0:
1140 self.outf.write("Nothing to do\n")
1141 return
1143 try:
1144 samdb.modify(res_msg_diff)
1145 except Exception as e:
1146 raise CommandError("Failed to modify group '%s': " % groupname, e)
1148 self.outf.write("Modified group '%s' successfully\n" % groupname)
1151 class cmd_group_add_unix_attrs(Command):
1152 """Add RFC2307 attributes to a group.
1154 This command adds Unix attributes to a group account in the Active
1155 Directory domain.
1156 The groupname specified on the command is the sAMaccountName.
1158 Unix (RFC2307) attributes will be added to the group account.
1160 Add 'idmap_ldb:use rfc2307 = Yes' to smb.conf to use these attributes for
1161 UID/GID mapping.
1163 The command may be run from the root userid or another authorized userid.
1164 The -H or --URL= option can be used to execute the command against a
1165 remote server.
1167 Example1:
1168 samba-tool group addunixattrs Group1 10000
1170 Example1 shows how to add RFC2307 attributes to a domain enabled group
1171 account.
1173 The groups Unix ID will be set to '10000', provided this ID isn't already
1174 in use.
1177 synopsis = "%prog <groupname> <gidnumber> [options]"
1179 takes_options = [
1180 Option("-H", "--URL", help="LDB URL for database or target server",
1181 type=str, metavar="URL", dest="H"),
1184 takes_args = ["groupname", "gidnumber"]
1186 takes_optiongroups = {
1187 "sambaopts": options.SambaOptions,
1188 "credopts": options.CredentialsOptions,
1189 "versionopts": options.VersionOptions,
1192 def run(self, groupname, gidnumber, credopts=None, sambaopts=None,
1193 versionopts=None, H=None):
1195 lp = sambaopts.get_loadparm()
1196 creds = credopts.get_credentials(lp)
1198 samdb = SamDB(url=H, session_info=system_session(),
1199 credentials=creds, lp=lp)
1201 domaindn = samdb.domain_dn()
1203 # Check group exists and doesn't have a gidNumber
1204 filter = "(samaccountname={})".format(ldb.binary_encode(groupname))
1205 res = samdb.search(domaindn,
1206 scope=ldb.SCOPE_SUBTREE,
1207 expression=filter)
1208 if (len(res) == 0):
1209 raise CommandError("Unable to find group '{}'".format(groupname))
1211 group_dn = res[0].dn
1213 if "gidNumber" in res[0]:
1214 raise CommandError("Group {} is a Unix group.".format(groupname))
1216 # Check if supplied gidnumber isn't already being used
1217 filter = "(&(objectClass=group)(gidNumber={}))".format(gidnumber)
1218 res = samdb.search(domaindn,
1219 scope=ldb.SCOPE_SUBTREE,
1220 expression=filter)
1221 if (len(res) != 0):
1222 raise CommandError('gidNumber {} already used.'.format(gidnumber))
1224 if not lp.get("idmap_ldb:use rfc2307"):
1225 self.outf.write("You are setting a Unix/RFC2307 GID. "
1226 "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
1227 " in smb.conf to use the attributes for "
1228 "XID/SID-mapping.\n")
1230 group_mod = """
1231 dn: {0}
1232 changetype: modify
1233 add: gidNumber
1234 gidNumber: {1}
1235 """.format(group_dn, gidnumber)
1237 try:
1238 samdb.modify_ldif(group_mod)
1239 except ldb.LdbError as e:
1240 raise CommandError("Failed to modify group '{0}': {1}"
1241 .format(groupname, e))
1243 self.outf.write("Modified Group '{}' successfully\n".format(groupname))
1246 class cmd_group_rename(Command):
1247 """Rename a group and related attributes.
1249 This command allows to set the group's name related attributes. The
1250 group's CN will be renamed automatically.
1252 The group's CN will be the sAMAccountName.
1253 Use the --force-new-cn option to specify the new CN manually and the
1254 --reset-cn to reset this change.
1256 Use an empty attribute value to remove the specified attribute.
1258 The groupname specified on the command is the sAMAccountName.
1260 The command may be run locally from the root userid or another authorized
1261 userid.
1263 The -H or --URL= option can be used to execute the command against a remote
1264 server.
1266 Example1:
1267 samba-tool group rename employees --samaccountname=staff
1269 Example1 shows how to change the samaccountname of a group 'employees' to
1270 'staff'. The CN of the group employees will also be changed to 'staff',
1271 if the previous CN was the previous sAMAccountName.
1273 Example2:
1274 samba-tool group rename employees --mail-address='staff@company.com' \\
1275 -H ldap://samba.samdom.example.com -U administrator
1277 Example2 shows how to rename the mail address of a group 'employees' to
1278 'staff@company.com'.
1279 The -H parameter is used to specify the remote target server.
1282 synopsis = "%prog <groupname> [options]"
1284 takes_options = [
1285 Option("-H", "--URL",
1286 help="LDB URL for database or target server",
1287 type=str, metavar="URL", dest="H"),
1288 Option("--force-new-cn",
1289 help="Specify a new CN (RND) instead of using the sAMAccountName.",
1290 type=str),
1291 Option("--reset-cn",
1292 help="Set the CN (RDN) to the sAMAccountName. Use this option "
1293 "to reset the changes made with the --force-new-cn option.",
1294 action="store_true"),
1295 Option("--mail-address",
1296 help="New mail address",
1297 type=str),
1298 Option("--samaccountname",
1299 help="New account name (sAMAccountName/logon name)",
1300 type=str)
1303 takes_args = ["groupname"]
1304 takes_optiongroups = {
1305 "sambaopts": options.SambaOptions,
1306 "credopts": options.CredentialsOptions,
1307 "versionopts": options.VersionOptions,
1310 def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
1311 H=None, mail_address=None, samaccountname=None, force_new_cn=None,
1312 reset_cn=None):
1313 # illegal options
1314 if force_new_cn and reset_cn:
1315 raise CommandError("It is not allowed to specify --force-new-cn "
1316 "together with --reset-cn.")
1317 if force_new_cn == "":
1318 raise CommandError("Failed to rename group - delete protected "
1319 "attribute 'CN'")
1320 if samaccountname == "":
1321 raise CommandError("Failed to rename group - delete protected "
1322 "attribute 'sAMAccountName'")
1324 lp = sambaopts.get_loadparm()
1325 creds = credopts.get_credentials(lp, fallback_machine=True)
1326 samdb = SamDB(url=H, session_info=system_session(),
1327 credentials=creds, lp=lp)
1328 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
1330 filter = ("(&(objectClass=group)(samaccountname=%s))" %
1331 ldb.binary_encode(groupname))
1332 try:
1333 res = samdb.search(base=domain_dn,
1334 scope=ldb.SCOPE_SUBTREE,
1335 expression=filter,
1336 attrs=["sAMAccountName",
1337 "cn",
1338 "mail"]
1340 old_group = res[0]
1341 group_dn = old_group.dn
1342 except IndexError:
1343 raise CommandError('Unable to find group "%s"' % (groupname))
1345 group_parent_dn = group_dn.parent()
1346 old_cn = old_group["cn"][0]
1348 # get the actual and the new group cn and the new dn
1349 if force_new_cn is not None:
1350 new_cn = force_new_cn
1351 elif samaccountname is not None:
1352 new_cn = samaccountname
1353 else:
1354 new_cn = old_group["sAMAccountName"]
1356 # CN must change, if the new CN is different and the old CN is the
1357 # standard CN or the change is forced with force-new-cn or reset-cn
1358 expected_cn = old_group["sAMAccountName"]
1359 must_change_cn = str(old_cn) != str(new_cn) and \
1360 (str(old_cn) == str(expected_cn) or \
1361 reset_cn or bool(force_new_cn))
1363 new_group_dn = ldb.Dn(samdb, "CN=%s" % new_cn)
1364 new_group_dn.add_base(group_parent_dn)
1366 # format given attributes
1367 group_attrs = ldb.Message()
1368 group_attrs.dn = group_dn
1369 samdb.prepare_attr_replace(group_attrs, old_group, "sAMAccountName",
1370 samaccountname)
1371 samdb.prepare_attr_replace(group_attrs, old_group, "mail", mail_address)
1373 group_attributes_changed = len(group_attrs) > 0
1375 # update the group with formatted attributes
1376 samdb.transaction_start()
1377 try:
1378 if group_attributes_changed:
1379 samdb.modify(group_attrs)
1380 if must_change_cn:
1381 samdb.rename(group_dn, new_group_dn)
1382 except Exception as e:
1383 samdb.transaction_cancel()
1384 raise CommandError('Failed to rename group "%s"' % groupname, e)
1385 samdb.transaction_commit()
1387 if must_change_cn:
1388 self.outf.write('Renamed CN of group "%s" from "%s" to "%s" '
1389 'successfully\n' % (groupname, old_cn, new_cn))
1391 if group_attributes_changed:
1392 self.outf.write('Following attributes of group "%s" have been '
1393 'changed successfully:\n' % (groupname))
1394 for attr in group_attrs.keys():
1395 if attr == "dn":
1396 continue
1397 self.outf.write('%s: %s\n' % (attr, group_attrs[attr]
1398 if group_attrs[attr] else '[removed]'))
1400 class cmd_group(SuperCommand):
1401 """Group management."""
1403 subcommands = {}
1404 subcommands["add"] = cmd_group_add()
1405 subcommands["create"] = cmd_group_add()
1406 subcommands["delete"] = cmd_group_delete()
1407 subcommands["edit"] = cmd_group_edit()
1408 subcommands["addmembers"] = cmd_group_add_members()
1409 subcommands["removemembers"] = cmd_group_remove_members()
1410 subcommands["list"] = cmd_group_list()
1411 subcommands["listmembers"] = cmd_group_list_members()
1412 subcommands["move"] = cmd_group_move()
1413 subcommands["show"] = cmd_group_show()
1414 subcommands["stats"] = cmd_group_stats()
1415 subcommands["addunixattrs"] = cmd_group_add_unix_attrs()
1416 subcommands["rename"] = cmd_group_rename()