samba-tool group delete: use binary encoded group name
[metze/samba-autobuild/.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_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     GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
30     GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
31     GTYPE_SECURITY_GLOBAL_GROUP,
32     GTYPE_SECURITY_UNIVERSAL_GROUP,
33     GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP,
34     GTYPE_DISTRIBUTION_GLOBAL_GROUP,
35     GTYPE_DISTRIBUTION_UNIVERSAL_GROUP,
36 )
37 from collections import defaultdict
38 from subprocess import check_call, CalledProcessError
39 from samba.compat import get_bytes
40 import os
41 import tempfile
42 from . import common
43
44 security_group = dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP,
45                        "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
46                        "Global": GTYPE_SECURITY_GLOBAL_GROUP,
47                        "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP})
48 distribution_group = dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP,
49                            "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP,
50                            "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP})
51
52
53 class cmd_group_add(Command):
54     """Creates a new AD group.
55
56 This command creates a new Active Directory group.  The groupname specified on the command is a unique sAMAccountName.
57
58 An Active Directory group may contain user and computer accounts as well as other groups.  An administrator creates a group and adds members to that group so they can be managed as a single entity.  This helps to simplify security and system administration.
59
60 Groups may also be used to establish email distribution lists, using --group-type=Distribution.
61
62 Groups are located in domains in organizational units (OUs).  The group's scope is a characteristic of the group that designates the extent to which the group is applied within the domain tree or forest.
63
64 The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created.
65
66 The command may be run from the root userid or another authorized userid.  The
67 -H or --URL= option can be used to execute the command on a remote server.
68
69 Example1:
70 samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group'
71
72 Example1 adds a new group with the name Group1 added to the Users container on a remote LDAP server.  The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server.  It defaults to the security type and global scope.
73
74 Example2:
75 sudo samba-tool group add Group2 --group-type=Distribution
76
77 Example2 adds a new distribution group to the local server.  The command is run under root using the sudo command.
78
79 Example3:
80 samba-tool group add Group3 --nis-domain=samdom --gid-number=12345
81
82 Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature).
83 """
84
85     synopsis = "%prog <groupname> [options]"
86
87     takes_optiongroups = {
88         "sambaopts": options.SambaOptions,
89         "versionopts": options.VersionOptions,
90         "credopts": options.CredentialsOptions,
91     }
92
93     takes_options = [
94         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
95                metavar="URL", dest="H"),
96         Option("--groupou",
97                help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created",
98                type=str),
99         Option("--group-scope", type="choice", choices=["Domain", "Global", "Universal"],
100                help="Group scope (Domain | Global | Universal)"),
101         Option("--group-type", type="choice", choices=["Security", "Distribution"],
102                help="Group type (Security | Distribution)"),
103         Option("--description", help="Group's description", type=str),
104         Option("--mail-address", help="Group's email address", type=str),
105         Option("--notes", help="Groups's notes", type=str),
106         Option("--gid-number", help="Group's Unix/RFC2307 GID number", type=int),
107         Option("--nis-domain", help="SFU30 NIS Domain", type=str),
108     ]
109
110     takes_args = ["groupname"]
111
112     def run(self, groupname, credopts=None, sambaopts=None,
113             versionopts=None, H=None, groupou=None, group_scope=None,
114             group_type=None, description=None, mail_address=None, notes=None, gid_number=None, nis_domain=None):
115
116         if (group_type or "Security") == "Security":
117             gtype = security_group.get(group_scope, GTYPE_SECURITY_GLOBAL_GROUP)
118         else:
119             gtype = distribution_group.get(group_scope, GTYPE_DISTRIBUTION_GLOBAL_GROUP)
120
121         if (gid_number is None and nis_domain is not None) or (gid_number is not None and nis_domain is None):
122             raise CommandError('Both --gid-number and --nis-domain have to be set for a RFC2307-enabled group. Operation cancelled.')
123
124         lp = sambaopts.get_loadparm()
125         creds = credopts.get_credentials(lp, fallback_machine=True)
126
127         try:
128             samdb = SamDB(url=H, session_info=system_session(),
129                           credentials=creds, lp=lp)
130             samdb.newgroup(groupname, groupou=groupou, grouptype=gtype,
131                            description=description, mailaddress=mail_address, notes=notes,
132                            gidnumber=gid_number, nisdomain=nis_domain)
133         except Exception as e:
134             # FIXME: catch more specific exception
135             raise CommandError('Failed to create group "%s"' % groupname, e)
136         self.outf.write("Added group %s\n" % groupname)
137
138
139 class cmd_group_delete(Command):
140     """Deletes an AD group.
141
142 The command deletes an existing AD group from the Active Directory domain.  The groupname specified on the command is the sAMAccountName.
143
144 Deleting a group is a permanent operation.  When a group is deleted, all permissions and rights that users in the group had inherited from the group account are deleted as well.
145
146 The command may be run from the root userid or another authorized userid.  The -H or --URL option can be used to execute the command on a remote server.
147
148 Example1:
149 samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
150
151 Example1 shows how to delete an AD group from a remote LDAP server.  The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server.
152
153 Example2:
154 sudo samba-tool group delete Group2
155
156 Example2 deletes group Group2 from the local server.  The command is run under root using the sudo command.
157 """
158
159     synopsis = "%prog <groupname> [options]"
160
161     takes_optiongroups = {
162         "sambaopts": options.SambaOptions,
163         "versionopts": options.VersionOptions,
164         "credopts": options.CredentialsOptions,
165     }
166
167     takes_options = [
168         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
169                metavar="URL", dest="H"),
170     ]
171
172     takes_args = ["groupname"]
173
174     def run(self, groupname, credopts=None, sambaopts=None, versionopts=None, H=None):
175
176         lp = sambaopts.get_loadparm()
177         creds = credopts.get_credentials(lp, fallback_machine=True)
178         samdb = SamDB(url=H, session_info=system_session(),
179                       credentials=creds, lp=lp)
180
181         filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
182                   ldb.binary_encode(groupname))
183
184         try:
185             res = samdb.search(base=samdb.domain_dn(),
186                                scope=ldb.SCOPE_SUBTREE,
187                                expression=filter,
188                                attrs=["dn"])
189             group_dn = res[0].dn
190         except IndexError:
191             raise CommandError('Unable to find group "%s"' % (groupname))
192
193         try:
194             samdb.delete(group_dn)
195         except Exception as e:
196             # FIXME: catch more specific exception
197             raise CommandError('Failed to remove group "%s"' % groupname, e)
198         self.outf.write("Deleted group %s\n" % groupname)
199
200
201 class cmd_group_add_members(Command):
202     """Add members to an AD group.
203
204 This command adds one or more members to an existing Active Directory group. The command accepts one or more group member names separated by commas.  A group member may be a user or computer account or another Active Directory group.
205
206 When a member is added to a group the member may inherit permissions and rights from the group.  Likewise, when permission or rights of a group are changed, the changes may reflect in the members through inheritance.
207
208 The member names specified on the command must be the sAMaccountName.
209
210 Example1:
211 samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
212
213 Example1 shows how to add two groups, Group1 and Group2 and one user account, User1, to the existing AD group named supergroup.  The command will be run on a remote server specified with the -H.  The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server.
214
215 Example2:
216 sudo samba-tool group addmembers supergroup User2
217
218 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.
219 """
220
221     synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
222
223     takes_optiongroups = {
224         "sambaopts": options.SambaOptions,
225         "versionopts": options.VersionOptions,
226         "credopts": options.CredentialsOptions,
227     }
228
229     takes_options = [
230         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
231                metavar="URL", dest="H"),
232         Option("--member-dn",
233                help=("DN of the new group member to be added.\n"
234                      "The --object-types option will be ignored."),
235                type=str,
236                action="append"),
237         Option("--object-types",
238                help=("Comma separated list of object types.\n"
239                      "The types are used to filter the search for the "
240                      "specified members.\n"
241                      "Valid values are: user, group, computer, serviceaccount, "
242                      "contact and all.\n"
243                      "Default: user,group,computer"),
244                default="user,group,computer",
245                type=str),
246         Option("--member-base-dn",
247                help=("Base DN for group member search.\n"
248                      "Default is the domain DN."),
249                type=str),
250     ]
251
252     takes_args = ["groupname", "listofmembers?"]
253
254     def run(self,
255             groupname,
256             listofmembers=None,
257             credopts=None,
258             sambaopts=None,
259             versionopts=None,
260             H=None,
261             member_base_dn=None,
262             member_dn=None,
263             object_types="user,group,computer"):
264
265         lp = sambaopts.get_loadparm()
266         creds = credopts.get_credentials(lp, fallback_machine=True)
267
268         if member_dn is None and listofmembers is None:
269             self.usage()
270             raise CommandError(
271                 'Either listofmembers or --member-dn must be specified.')
272
273         try:
274             samdb = SamDB(url=H, session_info=system_session(),
275                           credentials=creds, lp=lp)
276             groupmembers = []
277             if member_dn is not None:
278                 groupmembers += member_dn
279             if listofmembers is not None:
280                 groupmembers += listofmembers.split(',')
281             group_member_types = object_types.split(',')
282
283             if member_base_dn is not None:
284                 member_base_dn = samdb.normalize_dn_in_domain(member_base_dn)
285
286             samdb.add_remove_group_members(groupname, groupmembers,
287                                            add_members_operation=True,
288                                            member_types=group_member_types,
289                                            member_base_dn=member_base_dn)
290         except Exception as e:
291             # FIXME: catch more specific exception
292             raise CommandError('Failed to add members %r to group "%s" - %s' % (
293                 groupmembers, groupname, e))
294         self.outf.write("Added members to group %s\n" % groupname)
295
296
297 class cmd_group_remove_members(Command):
298     """Remove members from an AD group.
299
300 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.
301
302 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
303
304 Example1:
305 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
306
307 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.
308
309 Example2:
310 sudo samba-tool group removemembers supergroup User1
311
312 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.
313 """
314
315     synopsis = "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
316
317     takes_optiongroups = {
318         "sambaopts": options.SambaOptions,
319         "versionopts": options.VersionOptions,
320         "credopts": options.CredentialsOptions,
321     }
322
323     takes_options = [
324         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
325                metavar="URL", dest="H"),
326         Option("--member-dn",
327                help=("DN of the group member to be removed.\n"
328                      "The --object-types option will be ignored."),
329                type=str,
330                action="append"),
331         Option("--object-types",
332                help=("Comma separated list of object types.\n"
333                      "The types are used to filter the search for the "
334                      "specified members.\n"
335                      "Valid values are: user, group, computer, serviceaccount, "
336                      "contact and all.\n"
337                      "Default: user,group,computer"),
338                default="user,group,computer",
339                type=str),
340         Option("--member-base-dn",
341                help=("Base DN for group member search.\n"
342                      "Default is the domain DN."),
343                type=str),
344     ]
345
346     takes_args = ["groupname", "listofmembers?"]
347
348     def run(self,
349             groupname,
350             listofmembers=None,
351             credopts=None,
352             sambaopts=None,
353             versionopts=None,
354             H=None,
355             member_base_dn=None,
356             member_dn=None,
357             object_types="user,group,computer"):
358
359         lp = sambaopts.get_loadparm()
360         creds = credopts.get_credentials(lp, fallback_machine=True)
361
362         if member_dn is None and listofmembers is None:
363             self.usage()
364             raise CommandError(
365                 'Either listofmembers or --member-dn must be specified.')
366
367         try:
368             samdb = SamDB(url=H, session_info=system_session(),
369                           credentials=creds, lp=lp)
370             groupmembers = []
371             if member_dn is not None:
372                 groupmembers += member_dn
373             if listofmembers is not None:
374                 groupmembers += listofmembers.split(',')
375             group_member_types = object_types.split(',')
376
377             if member_base_dn is not None:
378                 member_base_dn = samdb.normalize_dn_in_domain(member_base_dn)
379
380             samdb.add_remove_group_members(groupname,
381                                            groupmembers,
382                                            add_members_operation=False,
383                                            member_types=group_member_types,
384                                            member_base_dn=member_base_dn)
385         except Exception as e:
386             # FIXME: Catch more specific exception
387             raise CommandError('Failed to remove members %r from group "%s"' % (listofmembers, groupname), e)
388         self.outf.write("Removed members from group %s\n" % groupname)
389
390
391 class cmd_group_list(Command):
392     """List all groups."""
393
394     synopsis = "%prog [options]"
395
396     takes_options = [
397         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
398                metavar="URL", dest="H"),
399         Option("-v", "--verbose",
400                help="Verbose output, showing group type and group scope.",
401                action="store_true"),
402         Option("-b", "--base-dn",
403                help="Specify base DN to use.",
404                type=str),
405         Option("--full-dn", dest="full_dn",
406                default=False,
407                action='store_true',
408                help="Display DN instead of the sAMAccountName."),
409     ]
410
411     takes_optiongroups = {
412         "sambaopts": options.SambaOptions,
413         "credopts": options.CredentialsOptions,
414         "versionopts": options.VersionOptions,
415     }
416
417     def run(self,
418             sambaopts=None,
419             credopts=None,
420             versionopts=None,
421             H=None,
422             verbose=False,
423             base_dn=None,
424             full_dn=False):
425         lp = sambaopts.get_loadparm()
426         creds = credopts.get_credentials(lp, fallback_machine=True)
427
428         samdb = SamDB(url=H, session_info=system_session(),
429                       credentials=creds, lp=lp)
430         attrs=["samaccountname"]
431
432         if verbose:
433             attrs += ["grouptype", "member"]
434         domain_dn = samdb.domain_dn()
435         if base_dn:
436             domain_dn = samdb.normalize_dn_in_domain(base_dn)
437         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
438                            expression=("(objectClass=group)"),
439                            attrs=attrs)
440         if (len(res) == 0):
441             return
442
443         if verbose:
444             self.outf.write("Group Name                                  Group Type      Group Scope  Members\n")
445             self.outf.write("--------------------------------------------------------------------------------\n")
446
447             for msg in res:
448                 self.outf.write("%-44s" % msg.get("samaccountname", idx=0))
449                 hgtype = hex(int("%s" % msg["grouptype"]) & 0x00000000FFFFFFFF)
450                 if (hgtype == hex(int(security_group.get("Builtin")))):
451                     self.outf.write("Security         Builtin  ")
452                 elif (hgtype == hex(int(security_group.get("Domain")))):
453                     self.outf.write("Security         Domain   ")
454                 elif (hgtype == hex(int(security_group.get("Global")))):
455                     self.outf.write("Security         Global   ")
456                 elif (hgtype == hex(int(security_group.get("Universal")))):
457                     self.outf.write("Security         Universal")
458                 elif (hgtype == hex(int(distribution_group.get("Global")))):
459                     self.outf.write("Distribution     Global   ")
460                 elif (hgtype == hex(int(distribution_group.get("Domain")))):
461                     self.outf.write("Distribution     Domain   ")
462                 elif (hgtype == hex(int(distribution_group.get("Universal")))):
463                     self.outf.write("Distribution     Universal")
464                 else:
465                     self.outf.write("                          ")
466                 num_members = len(msg.get("member", default=[]))
467                 self.outf.write("    %6u\n" % num_members)
468         else:
469             for msg in res:
470                 if full_dn:
471                     self.outf.write("%s\n" % msg.get("dn"))
472                     continue
473
474                 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
475
476
477 class cmd_group_list_members(Command):
478     """List all members of an AD group.
479
480 This command lists members from an existing Active Directory group. The command accepts one group name.
481
482 Example1:
483 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
484 """
485
486     synopsis = "%prog <groupname> [options]"
487
488     takes_options = [
489         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
490                metavar="URL", dest="H"),
491         Option("--full-dn", dest="full_dn",
492                default=False,
493                action='store_true',
494                help="Display DN instead of the sAMAccountName.")
495     ]
496
497     takes_optiongroups = {
498         "sambaopts": options.SambaOptions,
499         "credopts": options.CredentialsOptions,
500         "versionopts": options.VersionOptions,
501     }
502
503     takes_args = ["groupname"]
504
505     def run(self,
506             groupname,
507             credopts=None,
508             sambaopts=None,
509             versionopts=None,
510             H=None,
511             full_dn=False):
512         lp = sambaopts.get_loadparm()
513         creds = credopts.get_credentials(lp, fallback_machine=True)
514
515         try:
516             samdb = SamDB(url=H, session_info=system_session(),
517                           credentials=creds, lp=lp)
518
519             search_filter = ("(&(objectClass=group)(sAMAccountName=%s))" %
520                              ldb.binary_encode(groupname))
521             try:
522                 res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
523                                    expression=(search_filter),
524                                    attrs=["objectSid"])
525                 group_sid_binary = res[0].get('objectSid', idx=0)
526             except IndexError:
527                 raise CommandError('Unable to find group "%s"' % (groupname))
528
529             group_sid = ndr_unpack(security.dom_sid, group_sid_binary)
530             (group_dom_sid, rid) = group_sid.split()
531             group_sid_dn = "<SID=%s>" % (group_sid)
532
533             search_filter = ("(|(primaryGroupID=%s)(memberOf=%s))" %
534                              (rid, group_sid_dn))
535             res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
536                                expression=(search_filter),
537                                attrs=["samAccountName", "cn"])
538
539             if (len(res) == 0):
540                 return
541
542             for msg in res:
543                 if full_dn:
544                     self.outf.write("%s\n" % msg.get("dn"))
545                     continue
546
547                 member_name = msg.get("samAccountName", idx=0)
548                 if member_name is None:
549                     member_name = msg.get("cn", idx=0)
550                 self.outf.write("%s\n" % member_name)
551
552         except Exception as e:
553             raise CommandError('Failed to list members of "%s" group - %s' %
554                                (groupname, e))
555
556
557 class cmd_group_move(Command):
558     """Move a group to an organizational unit/container.
559
560     This command moves a group object into the specified organizational unit
561     or container.
562     The groupname specified on the command is the sAMAccountName.
563     The name of the organizational unit or container can be specified as a
564     full DN or without the domainDN component.
565
566     The command may be run from the root userid or another authorized userid.
567
568     The -H or --URL= option can be used to execute the command against a remote
569     server.
570
571     Example1:
572     samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
573         -H ldap://samba.samdom.example.com -U administrator
574
575     Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
576     unit on a remote LDAP server.
577
578     The -H parameter is used to specify the remote target server.
579
580     Example2:
581     samba-tool group move Group1 CN=Users
582
583     Example2 shows how to move a group Group1 back into the CN=Users container
584     on the local server.
585     """
586
587     synopsis = "%prog <groupname> <new_parent_dn> [options]"
588
589     takes_options = [
590         Option("-H", "--URL", help="LDB URL for database or target server",
591                type=str, metavar="URL", dest="H"),
592     ]
593
594     takes_args = ["groupname", "new_parent_dn"]
595     takes_optiongroups = {
596         "sambaopts": options.SambaOptions,
597         "credopts": options.CredentialsOptions,
598         "versionopts": options.VersionOptions,
599     }
600
601     def run(self, groupname, new_parent_dn, credopts=None, sambaopts=None,
602             versionopts=None, H=None):
603         lp = sambaopts.get_loadparm()
604         creds = credopts.get_credentials(lp, fallback_machine=True)
605         samdb = SamDB(url=H, session_info=system_session(),
606                       credentials=creds, lp=lp)
607         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
608
609         filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
610                   ldb.binary_encode(groupname))
611         try:
612             res = samdb.search(base=domain_dn,
613                                expression=filter,
614                                scope=ldb.SCOPE_SUBTREE)
615             group_dn = res[0].dn
616         except IndexError:
617             raise CommandError('Unable to find group "%s"' % (groupname))
618
619         try:
620             full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
621         except Exception as e:
622             raise CommandError('Invalid new_parent_dn "%s": %s' %
623                                (new_parent_dn, e.message))
624
625         full_new_group_dn = ldb.Dn(samdb, str(group_dn))
626         full_new_group_dn.remove_base_components(len(group_dn) - 1)
627         full_new_group_dn.add_base(full_new_parent_dn)
628
629         try:
630             samdb.rename(group_dn, full_new_group_dn)
631         except Exception as e:
632             raise CommandError('Failed to move group "%s"' % groupname, e)
633         self.outf.write('Moved group "%s" into "%s"\n' %
634                         (groupname, full_new_parent_dn))
635
636
637 class cmd_group_show(Command):
638     """Display a group AD object.
639
640 This command displays a group object and it's attributes in the Active
641 Directory domain.
642 The group name specified on the command is the sAMAccountName of the group.
643
644 The command may be run from the root userid or another authorized userid.
645
646 The -H or --URL= option can be used to execute the command against a remote
647 server.
648
649 Example1:
650 samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
651     -U administrator --password=passw1rd
652
653 Example1 shows how to display a group's attributes in the domain against a
654 remote LDAP server.
655
656 The -H parameter is used to specify the remote target server.
657
658 Example2:
659 samba-tool group show Group2
660
661 Example2 shows how to display a group's attributes in the domain against a local
662 LDAP server.
663
664 Example3:
665 samba-tool group show Group3 --attributes=member,objectGUID
666
667 Example3 shows how to display a groups objectGUID and member attributes.
668 """
669     synopsis = "%prog <group name> [options]"
670
671     takes_options = [
672         Option("-H", "--URL", help="LDB URL for database or target server",
673                type=str, metavar="URL", dest="H"),
674         Option("--attributes",
675                help=("Comma separated list of attributes, "
676                      "which will be printed."),
677                type=str, dest="group_attrs"),
678     ]
679
680     takes_args = ["groupname"]
681     takes_optiongroups = {
682         "sambaopts": options.SambaOptions,
683         "credopts": options.CredentialsOptions,
684         "versionopts": options.VersionOptions,
685     }
686
687     def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
688             H=None, group_attrs=None):
689
690         lp = sambaopts.get_loadparm()
691         creds = credopts.get_credentials(lp, fallback_machine=True)
692         samdb = SamDB(url=H, session_info=system_session(),
693                       credentials=creds, lp=lp)
694
695         attrs = None
696         if group_attrs:
697             attrs = group_attrs.split(",")
698
699         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
700                   (ATYPE_SECURITY_GLOBAL_GROUP,
701                    ldb.binary_encode(groupname)))
702
703         domaindn = samdb.domain_dn()
704
705         try:
706             res = samdb.search(base=domaindn, expression=filter,
707                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
708             user_dn = res[0].dn
709         except IndexError:
710             raise CommandError('Unable to find group "%s"' % (groupname))
711
712         for msg in res:
713             group_ldif = common.get_ldif_for_editor(samdb, msg)
714             self.outf.write(group_ldif)
715
716
717 class cmd_group_stats(Command):
718     """Summary statistics about group memberships."""
719
720     synopsis = "%prog [options]"
721
722     takes_options = [
723         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
724                metavar="URL", dest="H"),
725     ]
726
727     takes_optiongroups = {
728         "sambaopts": options.SambaOptions,
729         "credopts": options.CredentialsOptions,
730         "versionopts": options.VersionOptions,
731     }
732
733     def num_in_range(self, range_min, range_max, group_freqs):
734         total_count = 0
735         for members, count in group_freqs.items():
736             if range_min <= members and members <= range_max:
737                 total_count += count
738
739         return total_count
740
741     def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
742         lp = sambaopts.get_loadparm()
743         creds = credopts.get_credentials(lp, fallback_machine=True)
744
745         samdb = SamDB(url=H, session_info=system_session(),
746                       credentials=creds, lp=lp)
747
748         domain_dn = samdb.domain_dn()
749         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
750                            expression=("(objectClass=group)"),
751                            attrs=["samaccountname", "member"])
752
753         # first count up how many members each group has
754         group_assignments = {}
755         total_memberships = 0
756
757         for msg in res:
758             name = str(msg.get("samaccountname"))
759             num_members = len(msg.get("member", default=[]))
760             group_assignments[name] = num_members
761             total_memberships += num_members
762
763         num_groups = res.count
764         self.outf.write("Group membership statistics*\n")
765         self.outf.write("-------------------------------------------------\n")
766         self.outf.write("Total groups: {0}\n".format(num_groups))
767         self.outf.write("Total memberships: {0}\n".format(total_memberships))
768         average = total_memberships / float(num_groups)
769         self.outf.write("Average members per group: %.2f\n" % average)
770
771         # find the max and median memberships (note that some default groups
772         # always have zero members, so displaying the min is not very helpful)
773         group_names = list(group_assignments.keys())
774         group_members = list(group_assignments.values())
775         idx = group_members.index(max(group_members))
776         max_members = group_members[idx]
777         self.outf.write("Max members: {0} ({1})\n".format(max_members,
778                                                           group_names[idx]))
779         group_members.sort()
780         midpoint = num_groups // 2
781         median = group_members[midpoint]
782         if num_groups % 2 == 0:
783             median = (median + group_members[midpoint - 1]) / 2
784         self.outf.write("Median members per group: {0}\n\n".format(median))
785
786         # convert this to the frequency of group membership, i.e. how many
787         # groups have 5 members, how many have 6 members, etc
788         group_freqs = defaultdict(int)
789         for group, num_members in group_assignments.items():
790             group_freqs[num_members] += 1
791
792         # now squash this down even further, so that we just display the number
793         # of groups that fall into one of the following membership bands
794         bands = [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24),
795                  (25, 29), (30, 39), (40, 49), (50, 59), (60, 69), (70, 79),
796                  (80, 89), (90, 99), (100, 149), (150, 199), (200, 249),
797                  (250, 299), (300, 399), (400, 499), (500, 999), (1000, 1999),
798                  (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999),
799                  (10000, max_members)]
800
801         self.outf.write("Members        Number of Groups\n")
802         self.outf.write("-------------------------------------------------\n")
803
804         for band in bands:
805             band_start = band[0]
806             band_end = band[1]
807             if band_start > max_members:
808                 break
809
810             num_groups = self.num_in_range(band_start, band_end, group_freqs)
811
812             if num_groups != 0:
813                 band_str = "{0}-{1}".format(band_start, band_end)
814                 self.outf.write("%13s  %u\n" % (band_str, num_groups))
815
816         self.outf.write("\n* Note this does not include nested group memberships\n")
817
818
819 class cmd_group_edit(Command):
820     """Modify Group AD object.
821
822     This command will allow editing of a group account in the Active Directory
823     domain. You will then be able to add or change attributes and their values.
824
825     The groupname specified on the command is the sAMAccountName.
826
827     The command may be run from the root userid or another authorized userid.
828
829     The -H or --URL= option can be used to execute the command against a remote
830     server.
831
832     Example1:
833     samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
834         -U administrator --password=passw1rd
835
836     Example1 shows how to edit a groups attributes in the domain against a
837     remote LDAP server.
838
839     The -H parameter is used to specify the remote target server.
840
841     Example2:
842     samba-tool group edit Group2
843
844     Example2 shows how to edit a groups attributes in the domain against a local
845     server.
846
847     Example3:
848     samba-tool group edit Group3 --editor=nano
849
850     Example3 shows how to edit a groups attributes in the domain against a local
851     server using the 'nano' editor.
852     """
853     synopsis = "%prog <groupname> [options]"
854
855     takes_options = [
856         Option("-H", "--URL", help="LDB URL for database or target server",
857                type=str, metavar="URL", dest="H"),
858         Option("--editor", help="Editor to use instead of the system default,"
859                " or 'vi' if no system default is set.", type=str),
860     ]
861
862     takes_args = ["groupname"]
863     takes_optiongroups = {
864         "sambaopts": options.SambaOptions,
865         "credopts": options.CredentialsOptions,
866         "versionopts": options.VersionOptions,
867     }
868
869     def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
870             H=None, editor=None):
871         lp = sambaopts.get_loadparm()
872         creds = credopts.get_credentials(lp, fallback_machine=True)
873         samdb = SamDB(url=H, session_info=system_session(),
874                       credentials=creds, lp=lp)
875
876         filter = ("(&(sAMAccountName=%s)(objectClass=group))" % groupname)
877
878         domaindn = samdb.domain_dn()
879
880         try:
881             res = samdb.search(base=domaindn,
882                                expression=filter,
883                                scope=ldb.SCOPE_SUBTREE)
884             group_dn = res[0].dn
885         except IndexError:
886             raise CommandError('Unable to find group "%s"' % (groupname))
887
888         if len(res) != 1:
889             raise CommandError('Invalid number of results: for "%s": %d' %
890                                ((groupname), len(res)))
891
892         msg = res[0]
893         result_ldif = common.get_ldif_for_editor(samdb, msg)
894
895         if editor is None:
896             editor = os.environ.get('EDITOR')
897             if editor is None:
898                 editor = 'vi'
899
900         with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
901             t_file.write(get_bytes(result_ldif))
902             t_file.flush()
903             try:
904                 check_call([editor, t_file.name])
905             except CalledProcessError as e:
906                 raise CalledProcessError("ERROR: ", e)
907             with open(t_file.name) as edited_file:
908                 edited_message = edited_file.read()
909
910         msgs_edited = samdb.parse_ldif(edited_message)
911         msg_edited = next(msgs_edited)[1]
912
913         res_msg_diff = samdb.msg_diff(msg, msg_edited)
914         if len(res_msg_diff) == 0:
915             self.outf.write("Nothing to do\n")
916             return
917
918         try:
919             samdb.modify(res_msg_diff)
920         except Exception as e:
921             raise CommandError("Failed to modify group '%s': " % groupname, e)
922
923         self.outf.write("Modified group '%s' successfully\n" % groupname)
924
925
926 class cmd_group_add_unix_attrs(Command):
927     """Add RFC2307 attributes to a group.
928
929 This command adds Unix attributes to a group account in the Active
930 Directory domain.
931 The groupname specified on the command is the sAMaccountName.
932
933 Unix (RFC2307) attributes will be added to the group account.
934
935 Add 'idmap_ldb:use rfc2307 = Yes' to smb.conf to use these attributes for
936 UID/GID mapping.
937
938 The command may be run from the root userid or another authorized userid.
939 The -H or --URL= option can be used to execute the command against a
940 remote server.
941
942 Example1:
943 samba-tool group addunixattrs Group1 10000
944
945 Example1 shows how to add RFC2307 attributes to a domain enabled group
946 account.
947
948 The groups Unix ID will be set to '10000', provided this ID isn't already
949 in use.
950
951 """
952     synopsis = "%prog <groupname> <gidnumber> [options]"
953
954     takes_options = [
955         Option("-H", "--URL", help="LDB URL for database or target server",
956                type=str, metavar="URL", dest="H"),
957     ]
958
959     takes_args = ["groupname", "gidnumber"]
960
961     takes_optiongroups = {
962         "sambaopts": options.SambaOptions,
963         "credopts": options.CredentialsOptions,
964         "versionopts": options.VersionOptions,
965         }
966
967     def run(self, groupname, gidnumber, credopts=None, sambaopts=None,
968             versionopts=None, H=None):
969
970         lp = sambaopts.get_loadparm()
971         creds = credopts.get_credentials(lp)
972
973         samdb = SamDB(url=H, session_info=system_session(),
974                       credentials=creds, lp=lp)
975
976         domaindn = samdb.domain_dn()
977
978         # Check group exists and doesn't have a gidNumber
979         filter = "(samaccountname={})".format(ldb.binary_encode(groupname))
980         res = samdb.search(domaindn,
981                            scope=ldb.SCOPE_SUBTREE,
982                            expression=filter)
983         if (len(res) == 0):
984             raise CommandError("Unable to find group '{}'".format(groupname))
985
986         group_dn = res[0].dn
987
988         if "gidNumber" in res[0]:
989             raise CommandError("Group {} is a Unix group.".format(groupname))
990
991         # Check if supplied gidnumber isn't already being used
992         filter = "(&(objectClass=group)(gidNumber={}))".format(gidnumber)
993         res = samdb.search(domaindn,
994                            scope=ldb.SCOPE_SUBTREE,
995                            expression=filter)
996         if (len(res) != 0):
997             raise CommandError('gidNumber {} already used.'.format(gidnumber))
998
999         if not lp.get("idmap_ldb:use rfc2307"):
1000             self.outf.write("You are setting a Unix/RFC2307 GID. "
1001                             "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
1002                             " in smb.conf to use the attributes for "
1003                             "XID/SID-mapping.\n")
1004
1005         group_mod = """
1006 dn: {0}
1007 changetype: modify
1008 add: gidNumber
1009 gidNumber: {1}
1010 """.format(group_dn, gidnumber)
1011
1012         try:
1013             samdb.modify_ldif(group_mod)
1014         except ldb.LdbError as e:
1015             raise CommandError("Failed to modify group '{0}': {1}"
1016                                .format(groupname, e))
1017
1018         self.outf.write("Modified Group '{}' successfully\n".format(groupname))
1019
1020
1021 class cmd_group(SuperCommand):
1022     """Group management."""
1023
1024     subcommands = {}
1025     subcommands["add"] = cmd_group_add()
1026     subcommands["delete"] = cmd_group_delete()
1027     subcommands["edit"] = cmd_group_edit()
1028     subcommands["addmembers"] = cmd_group_add_members()
1029     subcommands["removemembers"] = cmd_group_remove_members()
1030     subcommands["list"] = cmd_group_list()
1031     subcommands["listmembers"] = cmd_group_list_members()
1032     subcommands["move"] = cmd_group_move()
1033     subcommands["show"] = cmd_group_show()
1034     subcommands["stats"] = cmd_group_stats()
1035     subcommands["addunixattrs"] = cmd_group_add_unix_attrs()