netcmd: models: update docstring of Computer.find method
[samba.git] / python / samba / netcmd / group.py
1 # Copyright Jelmer Vernooij 2008
2 #
3 # Based on the original in EJS:
4 # Copyright Andrew Tridgell 2005
5 #
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.
10 #
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.
15 #
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/>.
18
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
24
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,
41 )
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
48
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})
56
57
58 class cmd_group_add(Command):
59     """Creates a new AD group.
60
61 This command adds a new Active Directory group.  The groupname specified on the command is a unique sAMAccountName.
62
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.
64
65 Groups may also be used to establish email distribution lists, using --group-type=Distribution.
66
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.
68
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.
70
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.
73
74 Example1:
75 samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group'
76
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.
78
79 Example2:
80 sudo samba-tool group add Group2 --group-type=Distribution
81
82 Example2 adds a new distribution group to the local server.  The command is run under root using the sudo command.
83
84 Example3:
85 samba-tool group add Group3 --nis-domain=samdom --gid-number=12345
86
87 Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature).
88 """
89
90     synopsis = "%prog <groupname> [options]"
91
92     takes_optiongroups = {
93         "sambaopts": options.SambaOptions,
94         "versionopts": options.VersionOptions,
95         "credopts": options.CredentialsOptions,
96     }
97
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),
114     ]
115
116     takes_args = ["groupname"]
117
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):
122
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)
127
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.')
130
131         lp = sambaopts.get_loadparm()
132         creds = credopts.get_credentials(lp, fallback_machine=True)
133
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)
140
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'
157
158             if invalid_option is not None:
159                 raise CommandError(f'Superfluous option --{invalid_option} '
160                                    f'specified with --special')
161
162             if not samdb.am_pdc():
163                 raise CommandError('Adding special groups is only permitted '
164                                    'against the PDC!')
165
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'),
176             }
177
178             special_group = special_groups.get(groupname.lower())
179             if special_group is None:
180                 raise CommandError(f'Unknown special group "{groupname}".')
181
182             groupname, gtype, rid, description = special_group
183             group_type = normalise_int32(gtype)
184
185             group_dn = samdb.get_default_basedn()
186
187             if gtype == GTYPE_SECURITY_GLOBAL_GROUP:
188                 object_sid = security.dom_sid(
189                     f'{samdb.get_domain_sid()}-{rid}')
190                 system_flags = None
191
192                 if not groupou:
193                     group_dn = samdb.get_wellknown_dn(group_dn,
194                                                       DS_GUID_USERS_CONTAINER)
195
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)
201
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}')
209
210             if groupou:
211                 try:
212                     group_dn.add_child(groupou)
213                 except ldb.LdbError:
214                     raise CommandError(f'Invalid group OU "{groupou}"')
215
216             try:
217                 group_dn.add_child(f'CN={groupname}')
218             except ldb.LdbError:
219                 raise CommandError(f'Invalid group name "{groupname}"')
220
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',
229             }
230
231             if system_flags is not None:
232                 msg['systemFlags'] = system_flags
233
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)
246
247                     if len(res) != 1:
248                         raise CommandError(
249                             f'Failed to add group "{groupname}"', e)
250
251                     name = res[0].get('sAMAccountName', idx=0)
252                     if name:
253                         with_name = f' with name "{name}"'
254                     else:
255                         with_name = ''
256
257                     raise CommandError(
258                         f'Failed to add group "{groupname}" - Special group '
259                         f'already exists{with_name} at "{res[0].dn}".')
260
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)
278
279                     if len(res) != 1:
280                         raise CommandError(
281                             f'Failed to add group "{groupname}"', e)
282
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'
288
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.')
298
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')
326
327             return
328
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)
337
338
339 class cmd_group_delete(Command):
340     """Deletes an AD group.
341
342 The command deletes an existing AD group from the Active Directory domain.  The groupname specified on the command is the sAMAccountName.
343
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.
345
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.
347
348 Example1:
349 samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
350
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.
352
353 Example2:
354 sudo samba-tool group delete Group2
355
356 Example2 deletes group Group2 from the local server.  The command is run under root using the sudo command.
357 """
358
359     synopsis = "%prog <groupname> [options]"
360
361     takes_optiongroups = {
362         "sambaopts": options.SambaOptions,
363         "versionopts": options.VersionOptions,
364         "credopts": options.CredentialsOptions,
365     }
366
367     takes_options = [
368         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
369                metavar="URL", dest="H"),
370     ]
371
372     takes_args = ["groupname"]
373
374     def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None):
375
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)
380
381         filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
382                   ldb.binary_encode(groupname))
383
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))
392
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)
399
400
401 class cmd_group_add_members(Command):
402     """Add members to an AD group.
403
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.
405
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.
407
408 The member names specified on the command must be the sAMaccountName.
409
410 Example1:
411 samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
412
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.
414
415 Example2:
416 sudo samba-tool group addmembers supergroup User2
417
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.
419 """
420
421     synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
422
423     takes_optiongroups = {
424         "sambaopts": options.SambaOptions,
425         "versionopts": options.VersionOptions,
426         "credopts": options.CredentialsOptions,
427     }
428
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),
450     ]
451
452     takes_args = ["groupname", "listofmembers?"]
453
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"):
464
465         lp = sambaopts.get_loadparm()
466         creds = credopts.get_credentials(lp, fallback_machine=True)
467
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.')
472
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(',')
482
483             if member_base_dn is not None:
484                 member_base_dn = samdb.normalize_dn_in_domain(member_base_dn)
485
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)
495
496
497 class cmd_group_remove_members(Command):
498     """Remove members from an AD group.
499
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.
501
502 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
503
504 Example1:
505 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
506
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.
508
509 Example2:
510 sudo samba-tool group removemembers supergroup User1
511
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.
513 """
514
515     synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
516
517     takes_optiongroups = {
518         "sambaopts": options.SambaOptions,
519         "versionopts": options.VersionOptions,
520         "credopts": options.CredentialsOptions,
521     }
522
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),
544     ]
545
546     takes_args = ["groupname", "listofmembers?"]
547
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"):
558
559         lp = sambaopts.get_loadparm()
560         creds = credopts.get_credentials(lp, fallback_machine=True)
561
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.')
566
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(',')
576
577             if member_base_dn is not None:
578                 member_base_dn = samdb.normalize_dn_in_domain(member_base_dn)
579
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)
589
590
591 class cmd_group_list(Command):
592     """List all groups."""
593
594     synopsis = "%prog [options]"
595
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."),
609     ]
610
611     takes_optiongroups = {
612         "sambaopts": options.SambaOptions,
613         "credopts": options.CredentialsOptions,
614         "versionopts": options.VersionOptions,
615     }
616
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)
627
628         samdb = SamDB(url=H, session_info=system_session(),
629                       credentials=creds, lp=lp)
630         attrs=["samaccountname"]
631
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
642
643         if verbose:
644             self.outf.write("Group Name                                  Group Type      Group Scope  Members\n")
645             self.outf.write("--------------------------------------------------------------------------------\n")
646
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
673
674                 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
675
676
677 class cmd_group_list_members(Command):
678     """List all members of an AD group.
679
680 This command lists members from an existing Active Directory group. The command accepts one group name.
681
682 Example1:
683 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
684 """
685
686     synopsis = "%prog <groupname> [options]"
687
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.")
703     ]
704
705     takes_optiongroups = {
706         "sambaopts": options.SambaOptions,
707         "credopts": options.CredentialsOptions,
708         "versionopts": options.VersionOptions,
709     }
710
711     takes_args = ["groupname"]
712
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)
724
725         try:
726             samdb = SamDB(url=H, session_info=system_session(),
727                           credentials=creds, lp=lp)
728
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))
738
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)
742
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))
751
752             filter_disabled = ""
753             if hide_disabled is True:
754                 filter_disabled = "(!(userAccountControl:%s:=%u))" % (
755                     ldb.OID_COMPARATOR_AND, UF_ACCOUNTDISABLE)
756
757             filter = "(&(|(primaryGroupID=%s)(memberOf=%s))%s%s)" % (
758                 rid, group_sid_dn, filter_disabled, filter_expires)
759
760             res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
761                                expression=filter,
762                                attrs=["samAccountName", "cn"])
763
764             if (len(res) == 0):
765                 return
766
767             for msg in res:
768                 if full_dn:
769                     self.outf.write("%s\n" % msg.get("dn"))
770                     continue
771
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)
776
777         except Exception as e:
778             raise CommandError('Failed to list members of "%s" group - %s' %
779                                (groupname, e))
780
781
782 class cmd_group_move(Command):
783     """Move a group to an organizational unit/container.
784
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.
790
791     The command may be run from the root userid or another authorized userid.
792
793     The -H or --URL= option can be used to execute the command against a remote
794     server.
795
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
799
800     Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
801     unit on a remote LDAP server.
802
803     The -H parameter is used to specify the remote target server.
804
805     Example2:
806     samba-tool group move Group1 CN=Users
807
808     Example2 shows how to move a group Group1 back into the CN=Users container
809     on the local server.
810     """
811
812     synopsis = "%prog <groupname> <new_parent_dn> [options]"
813
814     takes_options = [
815         Option("-H", "--URL", help="LDB URL for database or target server",
816                type=str, metavar="URL", dest="H"),
817     ]
818
819     takes_args = ["groupname", "new_parent_dn"]
820     takes_optiongroups = {
821         "sambaopts": options.SambaOptions,
822         "credopts": options.CredentialsOptions,
823         "versionopts": options.VersionOptions,
824     }
825
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())
833
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))
843
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))
849
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)
853
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))
860
861
862 class cmd_group_show(Command):
863     """Display a group AD object.
864
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.
868
869 The command may be run from the root userid or another authorized userid.
870
871 The -H or --URL= option can be used to execute the command against a remote
872 server.
873
874 Example1:
875 samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
876     -U administrator --password=passw1rd
877
878 Example1 shows how to display a group's attributes in the domain against a
879 remote LDAP server.
880
881 The -H parameter is used to specify the remote target server.
882
883 Example2:
884 samba-tool group show Group2
885
886 Example2 shows how to display a group's attributes in the domain against a local
887 LDAP server.
888
889 Example3:
890 samba-tool group show Group3 --attributes=member,objectGUID
891
892 Example3 shows how to display a groups objectGUID and member attributes.
893 """
894     synopsis = "%prog <group name> [options]"
895
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"),
903     ]
904
905     takes_args = ["groupname"]
906     takes_optiongroups = {
907         "sambaopts": options.SambaOptions,
908         "credopts": options.CredentialsOptions,
909         "versionopts": options.VersionOptions,
910     }
911
912     def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
913             H=None, group_attrs=None):
914
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)
919
920         attrs = None
921         if group_attrs:
922             attrs = group_attrs.split(",")
923
924         filter = ("(&(objectCategory=group)(sAMAccountName=%s))" %
925                    ldb.binary_encode(groupname))
926
927         domaindn = samdb.domain_dn()
928
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))
935
936         for msg in res:
937             group_ldif = common.get_ldif_for_editor(samdb, msg)
938             self.outf.write(group_ldif)
939
940
941 class cmd_group_stats(Command):
942     """Summary statistics about group memberships."""
943
944     synopsis = "%prog [options]"
945
946     takes_options = [
947         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
948                metavar="URL", dest="H"),
949     ]
950
951     takes_optiongroups = {
952         "sambaopts": options.SambaOptions,
953         "credopts": options.CredentialsOptions,
954         "versionopts": options.VersionOptions,
955     }
956
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
962
963         return total_count
964
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)
968
969         samdb = SamDB(url=H, session_info=system_session(),
970                       credentials=creds, lp=lp)
971
972         domain_dn = samdb.domain_dn()
973         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
974                            expression=("(objectClass=group)"),
975                            attrs=["samaccountname", "member"])
976
977         # first count up how many members each group has
978         group_assignments = {}
979         total_memberships = 0
980
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
986
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)
994
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))
1009
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
1015
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)]
1024
1025         self.outf.write("Members        Number of Groups\n")
1026         self.outf.write("-------------------------------------------------\n")
1027
1028         for band in bands:
1029             band_start = band[0]
1030             band_end = band[1]
1031             if band_start > max_members:
1032                 break
1033
1034             num_groups = self.num_in_range(band_start, band_end, group_freqs)
1035
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))
1039
1040         self.outf.write("\n* Note this does not include nested group memberships\n")
1041
1042
1043 class cmd_group_edit(Command):
1044     """Modify Group AD object.
1045
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.
1048
1049     The groupname specified on the command is the sAMAccountName.
1050
1051     The command may be run from the root userid or another authorized userid.
1052
1053     The -H or --URL= option can be used to execute the command against a remote
1054     server.
1055
1056     Example1:
1057     samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
1058         -U administrator --password=passw1rd
1059
1060     Example1 shows how to edit a groups attributes in the domain against a
1061     remote LDAP server.
1062
1063     The -H parameter is used to specify the remote target server.
1064
1065     Example2:
1066     samba-tool group edit Group2
1067
1068     Example2 shows how to edit a groups attributes in the domain against a local
1069     server.
1070
1071     Example3:
1072     samba-tool group edit Group3 --editor=nano
1073
1074     Example3 shows how to edit a groups attributes in the domain against a local
1075     server using the 'nano' editor.
1076     """
1077     synopsis = "%prog <groupname> [options]"
1078
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),
1084     ]
1085
1086     takes_args = ["groupname"]
1087     takes_optiongroups = {
1088         "sambaopts": options.SambaOptions,
1089         "credopts": options.CredentialsOptions,
1090         "versionopts": options.VersionOptions,
1091     }
1092
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)
1099
1100         filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
1101                   ldb.binary_encode(groupname))
1102
1103         domaindn = samdb.domain_dn()
1104
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))
1112
1113         if len(res) != 1:
1114             raise CommandError('Invalid number of results: for "%s": %d' %
1115                                ((groupname), len(res)))
1116
1117         msg = res[0]
1118         result_ldif = common.get_ldif_for_editor(samdb, msg)
1119
1120         if editor is None:
1121             editor = os.environ.get('EDITOR')
1122             if editor is None:
1123                 editor = 'vi'
1124
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()
1134
1135         msgs_edited = samdb.parse_ldif(edited_message)
1136         msg_edited = next(msgs_edited)[1]
1137
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
1142
1143         try:
1144             samdb.modify(res_msg_diff)
1145         except Exception as e:
1146             raise CommandError("Failed to modify group '%s': " % groupname, e)
1147
1148         self.outf.write("Modified group '%s' successfully\n" % groupname)
1149
1150
1151 class cmd_group_add_unix_attrs(Command):
1152     """Add RFC2307 attributes to a group.
1153
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.
1157
1158 Unix (RFC2307) attributes will be added to the group account.
1159
1160 Add 'idmap_ldb:use rfc2307 = Yes' to smb.conf to use these attributes for
1161 UID/GID mapping.
1162
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.
1166
1167 Example1:
1168 samba-tool group addunixattrs Group1 10000
1169
1170 Example1 shows how to add RFC2307 attributes to a domain enabled group
1171 account.
1172
1173 The groups Unix ID will be set to '10000', provided this ID isn't already
1174 in use.
1175
1176 """
1177     synopsis = "%prog <groupname> <gidnumber> [options]"
1178
1179     takes_options = [
1180         Option("-H", "--URL", help="LDB URL for database or target server",
1181                type=str, metavar="URL", dest="H"),
1182     ]
1183
1184     takes_args = ["groupname", "gidnumber"]
1185
1186     takes_optiongroups = {
1187         "sambaopts": options.SambaOptions,
1188         "credopts": options.CredentialsOptions,
1189         "versionopts": options.VersionOptions,
1190         }
1191
1192     def run(self, groupname, gidnumber, credopts=None, sambaopts=None,
1193             versionopts=None, H=None):
1194
1195         lp = sambaopts.get_loadparm()
1196         creds = credopts.get_credentials(lp)
1197
1198         samdb = SamDB(url=H, session_info=system_session(),
1199                       credentials=creds, lp=lp)
1200
1201         domaindn = samdb.domain_dn()
1202
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))
1210
1211         group_dn = res[0].dn
1212
1213         if "gidNumber" in res[0]:
1214             raise CommandError("Group {} is a Unix group.".format(groupname))
1215
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))
1223
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")
1229
1230         group_mod = """
1231 dn: {0}
1232 changetype: modify
1233 add: gidNumber
1234 gidNumber: {1}
1235 """.format(group_dn, gidnumber)
1236
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))
1242
1243         self.outf.write("Modified Group '{}' successfully\n".format(groupname))
1244
1245
1246 class cmd_group_rename(Command):
1247     """Rename a group and related attributes.
1248
1249     This command allows to set the group's name related attributes. The
1250     group's CN will be renamed automatically.
1251
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.
1255
1256     Use an empty attribute value to remove the specified attribute.
1257
1258     The groupname specified on the command is the sAMAccountName.
1259
1260     The command may be run locally from the root userid or another authorized
1261     userid.
1262
1263     The -H or --URL= option can be used to execute the command against a remote
1264     server.
1265
1266     Example1:
1267     samba-tool group rename employees --samaccountname=staff
1268
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.
1272
1273     Example2:
1274     samba-tool group rename employees --mail-address='staff@company.com' \\
1275         -H ldap://samba.samdom.example.com -U administrator
1276
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.
1280     """
1281
1282     synopsis = "%prog <groupname> [options]"
1283
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)
1301         ]
1302
1303     takes_args = ["groupname"]
1304     takes_optiongroups = {
1305         "sambaopts": options.SambaOptions,
1306         "credopts": options.CredentialsOptions,
1307         "versionopts": options.VersionOptions,
1308     }
1309
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'")
1323
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())
1329
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"]
1339                              )
1340             old_group = res[0]
1341             group_dn = old_group.dn
1342         except IndexError:
1343            raise CommandError('Unable to find group "%s"' % (groupname))
1344
1345         group_parent_dn = group_dn.parent()
1346         old_cn = old_group["cn"][0]
1347
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"]
1355
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))
1362
1363         new_group_dn = ldb.Dn(samdb, "CN=%s" % new_cn)
1364         new_group_dn.add_base(group_parent_dn)
1365
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)
1372
1373         group_attributes_changed = len(group_attrs) > 0
1374
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()
1386
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))
1390
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]'))
1399
1400 class cmd_group(SuperCommand):
1401     """Group management."""
1402
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()