samba-tool: add --full-dn option to group listmembers command
[metze/samba/wip.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                   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> [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     ]
233
234     takes_args = ["groupname", "listofmembers"]
235
236     def run(self, groupname, listofmembers, credopts=None, sambaopts=None,
237             versionopts=None, H=None):
238
239         lp = sambaopts.get_loadparm()
240         creds = credopts.get_credentials(lp, fallback_machine=True)
241
242         try:
243             samdb = SamDB(url=H, session_info=system_session(),
244                           credentials=creds, lp=lp)
245             groupmembers = listofmembers.split(',')
246             samdb.add_remove_group_members(groupname, groupmembers,
247                                            add_members_operation=True)
248         except Exception as e:
249             # FIXME: catch more specific exception
250             raise CommandError('Failed to add members "%s" to group "%s"' % (
251                 listofmembers, groupname), e)
252         self.outf.write("Added members to group %s\n" % groupname)
253
254
255 class cmd_group_remove_members(Command):
256     """Remove members from an AD group.
257
258 This command removes one or more members from an existing Active Directory group.  The command accepts one or more group member names separated by commas.  A group member may be a user or computer account or another Active Directory group that is a member of the group specified on the command.
259
260 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
261
262 Example1:
263 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
264
265 Example1 shows how to remove Group1 from supergroup.  The command will run on the remote server specified on the -H parameter.  The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server.
266
267 Example2:
268 sudo samba-tool group removemembers supergroup User1
269
270 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.
271 """
272
273     synopsis = "%prog <groupname> <listofmembers> [options]"
274
275     takes_optiongroups = {
276         "sambaopts": options.SambaOptions,
277         "versionopts": options.VersionOptions,
278         "credopts": options.CredentialsOptions,
279     }
280
281     takes_options = [
282         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
283                metavar="URL", dest="H"),
284     ]
285
286     takes_args = ["groupname", "listofmembers"]
287
288     def run(self, groupname, listofmembers, credopts=None, sambaopts=None,
289             versionopts=None, H=None):
290
291         lp = sambaopts.get_loadparm()
292         creds = credopts.get_credentials(lp, fallback_machine=True)
293
294         try:
295             samdb = SamDB(url=H, session_info=system_session(),
296                           credentials=creds, lp=lp)
297             samdb.add_remove_group_members(groupname, listofmembers.split(","),
298                                            add_members_operation=False)
299         except Exception as e:
300             # FIXME: Catch more specific exception
301             raise CommandError('Failed to remove members "%s" from group "%s"' % (listofmembers, groupname), e)
302         self.outf.write("Removed members from group %s\n" % groupname)
303
304
305 class cmd_group_list(Command):
306     """List all groups."""
307
308     synopsis = "%prog [options]"
309
310     takes_options = [
311         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
312                metavar="URL", dest="H"),
313         Option("-v", "--verbose",
314                help="Verbose output, showing group type and group scope.",
315                action="store_true"),
316         Option("--full-dn", dest="full_dn",
317                default=False,
318                action='store_true',
319                help="Display DN instead of the sAMAccountName."),
320     ]
321
322     takes_optiongroups = {
323         "sambaopts": options.SambaOptions,
324         "credopts": options.CredentialsOptions,
325         "versionopts": options.VersionOptions,
326     }
327
328     def run(self,
329             sambaopts=None,
330             credopts=None,
331             versionopts=None,
332             H=None,
333             verbose=False,
334             full_dn=False):
335         lp = sambaopts.get_loadparm()
336         creds = credopts.get_credentials(lp, fallback_machine=True)
337
338         samdb = SamDB(url=H, session_info=system_session(),
339                       credentials=creds, lp=lp)
340         attrs=["samaccountname"]
341
342         if verbose:
343             attrs += ["grouptype", "member"]
344         domain_dn = samdb.domain_dn()
345         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
346                            expression=("(objectClass=group)"),
347                            attrs=attrs)
348         if (len(res) == 0):
349             return
350
351         if verbose:
352             self.outf.write("Group Name                                  Group Type      Group Scope  Members\n")
353             self.outf.write("--------------------------------------------------------------------------------\n")
354
355             for msg in res:
356                 self.outf.write("%-44s" % msg.get("samaccountname", idx=0))
357                 hgtype = hex(int("%s" % msg["grouptype"]) & 0x00000000FFFFFFFF)
358                 if (hgtype == hex(int(security_group.get("Builtin")))):
359                     self.outf.write("Security         Builtin  ")
360                 elif (hgtype == hex(int(security_group.get("Domain")))):
361                     self.outf.write("Security         Domain   ")
362                 elif (hgtype == hex(int(security_group.get("Global")))):
363                     self.outf.write("Security         Global   ")
364                 elif (hgtype == hex(int(security_group.get("Universal")))):
365                     self.outf.write("Security         Universal")
366                 elif (hgtype == hex(int(distribution_group.get("Global")))):
367                     self.outf.write("Distribution     Global   ")
368                 elif (hgtype == hex(int(distribution_group.get("Domain")))):
369                     self.outf.write("Distribution     Domain   ")
370                 elif (hgtype == hex(int(distribution_group.get("Universal")))):
371                     self.outf.write("Distribution     Universal")
372                 else:
373                     self.outf.write("                          ")
374                 num_members = len(msg.get("member", default=[]))
375                 self.outf.write("    %6u\n" % num_members)
376         else:
377             for msg in res:
378                 if full_dn:
379                     self.outf.write("%s\n" % msg.get("dn"))
380                     continue
381
382                 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
383
384
385 class cmd_group_list_members(Command):
386     """List all members of an AD group.
387
388 This command lists members from an existing Active Directory group. The command accepts one group name.
389
390 Example1:
391 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
392 """
393
394     synopsis = "%prog <groupname> [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("--full-dn", dest="full_dn",
400                default=False,
401                action='store_true',
402                help="Display DN instead of the sAMAccountName.")
403     ]
404
405     takes_optiongroups = {
406         "sambaopts": options.SambaOptions,
407         "credopts": options.CredentialsOptions,
408         "versionopts": options.VersionOptions,
409     }
410
411     takes_args = ["groupname"]
412
413     def run(self,
414             groupname,
415             credopts=None,
416             sambaopts=None,
417             versionopts=None,
418             H=None,
419             full_dn=False):
420         lp = sambaopts.get_loadparm()
421         creds = credopts.get_credentials(lp, fallback_machine=True)
422
423         try:
424             samdb = SamDB(url=H, session_info=system_session(),
425                           credentials=creds, lp=lp)
426
427             search_filter = "(&(objectClass=group)(samaccountname=%s))" % groupname
428             res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
429                                expression=(search_filter),
430                                attrs=["objectSid"])
431
432             if (len(res) != 1):
433                 return
434
435             group_dn = res[0].get('dn', idx=0)
436             object_sid = res[0].get('objectSid', idx=0)
437
438             object_sid = ndr_unpack(security.dom_sid, object_sid)
439             (group_dom_sid, rid) = object_sid.split()
440
441             search_filter = "(|(primaryGroupID=%s)(memberOf=%s))" % (rid, group_dn)
442             res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
443                                expression=(search_filter),
444                                attrs=["samAccountName", "cn"])
445
446             if (len(res) == 0):
447                 return
448
449             for msg in res:
450                 if full_dn:
451                     self.outf.write("%s\n" % msg.get("dn"))
452                     continue
453
454                 member_name = msg.get("samAccountName", idx=0)
455                 if member_name is None:
456                     member_name = msg.get("cn", idx=0)
457                 self.outf.write("%s\n" % member_name)
458
459         except Exception as e:
460             raise CommandError('Failed to list members of "%s" group ' % groupname, e)
461
462
463 class cmd_group_move(Command):
464     """Move a group to an organizational unit/container.
465
466     This command moves a group object into the specified organizational unit
467     or container.
468     The groupname specified on the command is the sAMAccountName.
469     The name of the organizational unit or container can be specified as a
470     full DN or without the domainDN component.
471
472     The command may be run from the root userid or another authorized userid.
473
474     The -H or --URL= option can be used to execute the command against a remote
475     server.
476
477     Example1:
478     samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
479         -H ldap://samba.samdom.example.com -U administrator
480
481     Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
482     unit on a remote LDAP server.
483
484     The -H parameter is used to specify the remote target server.
485
486     Example2:
487     samba-tool group move Group1 CN=Users
488
489     Example2 shows how to move a group Group1 back into the CN=Users container
490     on the local server.
491     """
492
493     synopsis = "%prog <groupname> <new_parent_dn> [options]"
494
495     takes_options = [
496         Option("-H", "--URL", help="LDB URL for database or target server",
497                type=str, metavar="URL", dest="H"),
498     ]
499
500     takes_args = ["groupname", "new_parent_dn"]
501     takes_optiongroups = {
502         "sambaopts": options.SambaOptions,
503         "credopts": options.CredentialsOptions,
504         "versionopts": options.VersionOptions,
505     }
506
507     def run(self, groupname, new_parent_dn, credopts=None, sambaopts=None,
508             versionopts=None, H=None):
509         lp = sambaopts.get_loadparm()
510         creds = credopts.get_credentials(lp, fallback_machine=True)
511         samdb = SamDB(url=H, session_info=system_session(),
512                       credentials=creds, lp=lp)
513         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
514
515         filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
516                   groupname)
517         try:
518             res = samdb.search(base=domain_dn,
519                                expression=filter,
520                                scope=ldb.SCOPE_SUBTREE)
521             group_dn = res[0].dn
522         except IndexError:
523             raise CommandError('Unable to find group "%s"' % (groupname))
524
525         try:
526             full_new_parent_dn = samdb.normalize_dn_in_domain(new_parent_dn)
527         except Exception as e:
528             raise CommandError('Invalid new_parent_dn "%s": %s' %
529                                (new_parent_dn, e.message))
530
531         full_new_group_dn = ldb.Dn(samdb, str(group_dn))
532         full_new_group_dn.remove_base_components(len(group_dn) - 1)
533         full_new_group_dn.add_base(full_new_parent_dn)
534
535         try:
536             samdb.rename(group_dn, full_new_group_dn)
537         except Exception as e:
538             raise CommandError('Failed to move group "%s"' % groupname, e)
539         self.outf.write('Moved group "%s" into "%s"\n' %
540                         (groupname, full_new_parent_dn))
541
542
543 class cmd_group_show(Command):
544     """Display a group AD object.
545
546 This command displays a group object and it's attributes in the Active
547 Directory domain.
548 The group name specified on the command is the sAMAccountName of the group.
549
550 The command may be run from the root userid or another authorized userid.
551
552 The -H or --URL= option can be used to execute the command against a remote
553 server.
554
555 Example1:
556 samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
557     -U administrator --password=passw1rd
558
559 Example1 shows how to display a group's attributes in the domain against a
560 remote LDAP server.
561
562 The -H parameter is used to specify the remote target server.
563
564 Example2:
565 samba-tool group show Group2
566
567 Example2 shows how to display a group's attributes in the domain against a local
568 LDAP server.
569
570 Example3:
571 samba-tool group show Group3 --attributes=member,objectGUID
572
573 Example3 shows how to display a groups objectGUID and member attributes.
574 """
575     synopsis = "%prog <group name> [options]"
576
577     takes_options = [
578         Option("-H", "--URL", help="LDB URL for database or target server",
579                type=str, metavar="URL", dest="H"),
580         Option("--attributes",
581                help=("Comma separated list of attributes, "
582                      "which will be printed."),
583                type=str, dest="group_attrs"),
584     ]
585
586     takes_args = ["groupname"]
587     takes_optiongroups = {
588         "sambaopts": options.SambaOptions,
589         "credopts": options.CredentialsOptions,
590         "versionopts": options.VersionOptions,
591     }
592
593     def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
594             H=None, group_attrs=None):
595
596         lp = sambaopts.get_loadparm()
597         creds = credopts.get_credentials(lp, fallback_machine=True)
598         samdb = SamDB(url=H, session_info=system_session(),
599                       credentials=creds, lp=lp)
600
601         attrs = None
602         if group_attrs:
603             attrs = group_attrs.split(",")
604
605         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
606                   (ATYPE_SECURITY_GLOBAL_GROUP,
607                    ldb.binary_encode(groupname)))
608
609         domaindn = samdb.domain_dn()
610
611         try:
612             res = samdb.search(base=domaindn, expression=filter,
613                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
614             user_dn = res[0].dn
615         except IndexError:
616             raise CommandError('Unable to find group "%s"' % (groupname))
617
618         for msg in res:
619             group_ldif = common.get_ldif_for_editor(samdb, msg)
620             self.outf.write(group_ldif)
621
622
623 class cmd_group_stats(Command):
624     """Summary statistics about group memberships."""
625
626     synopsis = "%prog [options]"
627
628     takes_options = [
629         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
630                metavar="URL", dest="H"),
631     ]
632
633     takes_optiongroups = {
634         "sambaopts": options.SambaOptions,
635         "credopts": options.CredentialsOptions,
636         "versionopts": options.VersionOptions,
637     }
638
639     def num_in_range(self, range_min, range_max, group_freqs):
640         total_count = 0
641         for members, count in group_freqs.items():
642             if range_min <= members and members <= range_max:
643                 total_count += count
644
645         return total_count
646
647     def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
648         lp = sambaopts.get_loadparm()
649         creds = credopts.get_credentials(lp, fallback_machine=True)
650
651         samdb = SamDB(url=H, session_info=system_session(),
652                       credentials=creds, lp=lp)
653
654         domain_dn = samdb.domain_dn()
655         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
656                            expression=("(objectClass=group)"),
657                            attrs=["samaccountname", "member"])
658
659         # first count up how many members each group has
660         group_assignments = {}
661         total_memberships = 0
662
663         for msg in res:
664             name = str(msg.get("samaccountname"))
665             num_members = len(msg.get("member", default=[]))
666             group_assignments[name] = num_members
667             total_memberships += num_members
668
669         num_groups = res.count
670         self.outf.write("Group membership statistics*\n")
671         self.outf.write("-------------------------------------------------\n")
672         self.outf.write("Total groups: {0}\n".format(num_groups))
673         self.outf.write("Total memberships: {0}\n".format(total_memberships))
674         average = total_memberships / float(num_groups)
675         self.outf.write("Average members per group: %.2f\n" % average)
676
677         # find the max and median memberships (note that some default groups
678         # always have zero members, so displaying the min is not very helpful)
679         group_names = list(group_assignments.keys())
680         group_members = list(group_assignments.values())
681         idx = group_members.index(max(group_members))
682         max_members = group_members[idx]
683         self.outf.write("Max members: {0} ({1})\n".format(max_members,
684                                                           group_names[idx]))
685         group_members.sort()
686         midpoint = num_groups // 2
687         median = group_members[midpoint]
688         if num_groups % 2 == 0:
689             median = (median + group_members[midpoint - 1]) / 2
690         self.outf.write("Median members per group: {0}\n\n".format(median))
691
692         # convert this to the frequency of group membership, i.e. how many
693         # groups have 5 members, how many have 6 members, etc
694         group_freqs = defaultdict(int)
695         for group, num_members in group_assignments.items():
696             group_freqs[num_members] += 1
697
698         # now squash this down even further, so that we just display the number
699         # of groups that fall into one of the following membership bands
700         bands = [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24),
701                  (25, 29), (30, 39), (40, 49), (50, 59), (60, 69), (70, 79),
702                  (80, 89), (90, 99), (100, 149), (150, 199), (200, 249),
703                  (250, 299), (300, 399), (400, 499), (500, 999), (1000, 1999),
704                  (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999),
705                  (10000, max_members)]
706
707         self.outf.write("Members        Number of Groups\n")
708         self.outf.write("-------------------------------------------------\n")
709
710         for band in bands:
711             band_start = band[0]
712             band_end = band[1]
713             if band_start > max_members:
714                 break
715
716             num_groups = self.num_in_range(band_start, band_end, group_freqs)
717
718             if num_groups != 0:
719                 band_str = "{0}-{1}".format(band_start, band_end)
720                 self.outf.write("%13s  %u\n" % (band_str, num_groups))
721
722         self.outf.write("\n* Note this does not include nested group memberships\n")
723
724
725 class cmd_group_edit(Command):
726     """Modify Group AD object.
727
728     This command will allow editing of a group account in the Active Directory
729     domain. You will then be able to add or change attributes and their values.
730
731     The groupname specified on the command is the sAMAccountName.
732
733     The command may be run from the root userid or another authorized userid.
734
735     The -H or --URL= option can be used to execute the command against a remote
736     server.
737
738     Example1:
739     samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
740         -U administrator --password=passw1rd
741
742     Example1 shows how to edit a groups attributes in the domain against a
743     remote LDAP server.
744
745     The -H parameter is used to specify the remote target server.
746
747     Example2:
748     samba-tool group edit Group2
749
750     Example2 shows how to edit a groups attributes in the domain against a local
751     server.
752
753     Example3:
754     samba-tool group edit Group3 --editor=nano
755
756     Example3 shows how to edit a groups attributes in the domain against a local
757     server using the 'nano' editor.
758     """
759     synopsis = "%prog <groupname> [options]"
760
761     takes_options = [
762         Option("-H", "--URL", help="LDB URL for database or target server",
763                type=str, metavar="URL", dest="H"),
764         Option("--editor", help="Editor to use instead of the system default,"
765                " or 'vi' if no system default is set.", type=str),
766     ]
767
768     takes_args = ["groupname"]
769     takes_optiongroups = {
770         "sambaopts": options.SambaOptions,
771         "credopts": options.CredentialsOptions,
772         "versionopts": options.VersionOptions,
773     }
774
775     def run(self, groupname, credopts=None, sambaopts=None, versionopts=None,
776             H=None, editor=None):
777         lp = sambaopts.get_loadparm()
778         creds = credopts.get_credentials(lp, fallback_machine=True)
779         samdb = SamDB(url=H, session_info=system_session(),
780                       credentials=creds, lp=lp)
781
782         filter = ("(&(sAMAccountName=%s)(objectClass=group))" % groupname)
783
784         domaindn = samdb.domain_dn()
785
786         try:
787             res = samdb.search(base=domaindn,
788                                expression=filter,
789                                scope=ldb.SCOPE_SUBTREE)
790             group_dn = res[0].dn
791         except IndexError:
792             raise CommandError('Unable to find group "%s"' % (groupname))
793
794         if len(res) != 1:
795             raise CommandError('Invalid number of results: for "%s": %d' %
796                                ((groupname), len(res)))
797
798         msg = res[0]
799         result_ldif = common.get_ldif_for_editor(samdb, msg)
800
801         if editor is None:
802             editor = os.environ.get('EDITOR')
803             if editor is None:
804                 editor = 'vi'
805
806         with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
807             t_file.write(get_bytes(result_ldif))
808             t_file.flush()
809             try:
810                 check_call([editor, t_file.name])
811             except CalledProcessError as e:
812                 raise CalledProcessError("ERROR: ", e)
813             with open(t_file.name) as edited_file:
814                 edited_message = edited_file.read()
815
816         msgs_edited = samdb.parse_ldif(edited_message)
817         msg_edited = next(msgs_edited)[1]
818
819         res_msg_diff = samdb.msg_diff(msg, msg_edited)
820         if len(res_msg_diff) == 0:
821             self.outf.write("Nothing to do\n")
822             return
823
824         try:
825             samdb.modify(res_msg_diff)
826         except Exception as e:
827             raise CommandError("Failed to modify group '%s': " % groupname, e)
828
829         self.outf.write("Modified group '%s' successfully\n" % groupname)
830
831
832 class cmd_group_add_unix_attrs(Command):
833     """Add RFC2307 attributes to a group.
834
835 This command adds Unix attributes to a group account in the Active
836 Directory domain.
837 The groupname specified on the command is the sAMaccountName.
838
839 Unix (RFC2307) attributes will be added to the group account.
840
841 Add 'idmap_ldb:use rfc2307 = Yes' to smb.conf to use these attributes for
842 UID/GID mapping.
843
844 The command may be run from the root userid or another authorized userid.
845 The -H or --URL= option can be used to execute the command against a
846 remote server.
847
848 Example1:
849 samba-tool group addunixattrs Group1 10000
850
851 Example1 shows how to add RFC2307 attributes to a domain enabled group
852 account.
853
854 The groups Unix ID will be set to '10000', provided this ID isn't already
855 in use.
856
857 """
858     synopsis = "%prog <groupname> <gidnumber> [options]"
859
860     takes_options = [
861         Option("-H", "--URL", help="LDB URL for database or target server",
862                type=str, metavar="URL", dest="H"),
863     ]
864
865     takes_args = ["groupname", "gidnumber"]
866
867     takes_optiongroups = {
868         "sambaopts": options.SambaOptions,
869         "credopts": options.CredentialsOptions,
870         "versionopts": options.VersionOptions,
871         }
872
873     def run(self, groupname, gidnumber, credopts=None, sambaopts=None,
874             versionopts=None, H=None):
875
876         lp = sambaopts.get_loadparm()
877         creds = credopts.get_credentials(lp)
878
879         samdb = SamDB(url=H, session_info=system_session(),
880                       credentials=creds, lp=lp)
881
882         domaindn = samdb.domain_dn()
883
884         # Check group exists and doesn't have a gidNumber
885         filter = "(samaccountname={})".format(ldb.binary_encode(groupname))
886         res = samdb.search(domaindn,
887                            scope=ldb.SCOPE_SUBTREE,
888                            expression=filter)
889         if (len(res) == 0):
890             raise CommandError("Unable to find group '{}'".format(groupname))
891
892         group_dn = res[0].dn
893
894         if "gidNumber" in res[0]:
895             raise CommandError("Group {} is a Unix group.".format(groupname))
896
897         # Check if supplied gidnumber isn't already being used
898         filter = "(&(objectClass=group)(gidNumber={}))".format(gidnumber)
899         res = samdb.search(domaindn,
900                            scope=ldb.SCOPE_SUBTREE,
901                            expression=filter)
902         if (len(res) != 0):
903             raise CommandError('gidNumber {} already used.'.format(gidnumber))
904
905         if not lp.get("idmap_ldb:use rfc2307"):
906             self.outf.write("You are setting a Unix/RFC2307 GID. "
907                             "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
908                             " in smb.conf to use the attributes for "
909                             "XID/SID-mapping.\n")
910
911         group_mod = """
912 dn: {0}
913 changetype: modify
914 add: gidNumber
915 gidNumber: {1}
916 """.format(group_dn, gidnumber)
917
918         try:
919             samdb.modify_ldif(group_mod)
920         except ldb.LdbError as e:
921             raise CommandError("Failed to modify group '{0}': {1}"
922                                .format(groupname, e))
923
924         self.outf.write("Modified Group '{}' successfully\n".format(groupname))
925
926
927 class cmd_group(SuperCommand):
928     """Group management."""
929
930     subcommands = {}
931     subcommands["add"] = cmd_group_add()
932     subcommands["delete"] = cmd_group_delete()
933     subcommands["edit"] = cmd_group_edit()
934     subcommands["addmembers"] = cmd_group_add_members()
935     subcommands["removemembers"] = cmd_group_remove_members()
936     subcommands["list"] = cmd_group_list()
937     subcommands["listmembers"] = cmd_group_list_members()
938     subcommands["move"] = cmd_group_move()
939     subcommands["show"] = cmd_group_show()
940     subcommands["stats"] = cmd_group_stats()
941     subcommands["addunixattrs"] = cmd_group_add_unix_attrs()