1 # Copyright Jelmer Vernooij 2008
3 # Based on the original in EJS:
4 # Copyright Andrew Tridgell 2005
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import samba
.getopt
as options
20 from samba
.netcmd
import Command
, SuperCommand
, CommandError
, Option
22 from samba
.ndr
import ndr_unpack
23 from samba
.dcerpc
import security
25 from samba
.auth
import system_session
26 from samba
.samdb
import SamDB
27 from samba
.dsdb
import (
28 ATYPE_SECURITY_GLOBAL_GROUP
,
29 GTYPE_SECURITY_BUILTIN_LOCAL_GROUP
,
30 GTYPE_SECURITY_DOMAIN_LOCAL_GROUP
,
31 GTYPE_SECURITY_GLOBAL_GROUP
,
32 GTYPE_SECURITY_UNIVERSAL_GROUP
,
33 GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP
,
34 GTYPE_DISTRIBUTION_GLOBAL_GROUP
,
35 GTYPE_DISTRIBUTION_UNIVERSAL_GROUP
,
38 from collections
import defaultdict
39 from subprocess
import check_call
, CalledProcessError
40 from samba
.common
import get_bytes
45 security_group
= dict({"Builtin": GTYPE_SECURITY_BUILTIN_LOCAL_GROUP
,
46 "Domain": GTYPE_SECURITY_DOMAIN_LOCAL_GROUP
,
47 "Global": GTYPE_SECURITY_GLOBAL_GROUP
,
48 "Universal": GTYPE_SECURITY_UNIVERSAL_GROUP
})
49 distribution_group
= dict({"Domain": GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP
,
50 "Global": GTYPE_DISTRIBUTION_GLOBAL_GROUP
,
51 "Universal": GTYPE_DISTRIBUTION_UNIVERSAL_GROUP
})
54 class cmd_group_add(Command
):
55 """Creates a new AD group.
57 This command adds a new Active Directory group. The groupname specified on the command is a unique sAMAccountName.
59 An Active Directory group may contain user and computer accounts as well as other groups. An administrator adds a new group and adds members to that group so they can be managed as a single entity. This helps to simplify security and system administration.
61 Groups may also be used to establish email distribution lists, using --group-type=Distribution.
63 Groups are located in domains in organizational units (OUs). The group's scope is a characteristic of the group that designates the extent to which the group is applied within the domain tree or forest.
65 The group location (OU), type (security or distribution) and scope may all be specified on the samba-tool command when the group is created.
67 The command may be run from the root userid or another authorized userid. The
68 -H or --URL= option can be used to execute the command on a remote server.
71 samba-tool group add Group1 -H ldap://samba.samdom.example.com --description='Simple group'
73 Example1 adds a new group with the name Group1 added to the Users container on a remote LDAP server. The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server. It defaults to the security type and global scope.
76 sudo samba-tool group add Group2 --group-type=Distribution
78 Example2 adds a new distribution group to the local server. The command is run under root using the sudo command.
81 samba-tool group add Group3 --nis-domain=samdom --gid-number=12345
83 Example3 adds a new RFC2307 enabled group for NIS domain samdom and GID 12345 (both options are required to enable this feature).
86 synopsis
= "%prog <groupname> [options]"
88 takes_optiongroups
= {
89 "sambaopts": options
.SambaOptions
,
90 "versionopts": options
.VersionOptions
,
91 "credopts": options
.CredentialsOptions
,
95 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
96 metavar
="URL", dest
="H"),
98 help="Alternative location (without domainDN counterpart) to default CN=Users in which new user object will be created",
100 Option("--group-scope", type="choice", choices
=["Domain", "Global", "Universal"],
101 help="Group scope (Domain | Global | Universal)"),
102 Option("--group-type", type="choice", choices
=["Security", "Distribution"],
103 help="Group type (Security | Distribution)"),
104 Option("--description", help="Group's description", type=str),
105 Option("--mail-address", help="Group's email address", type=str),
106 Option("--notes", help="Groups's notes", type=str),
107 Option("--gid-number", help="Group's Unix/RFC2307 GID number", type=int),
108 Option("--nis-domain", help="SFU30 NIS Domain", type=str),
111 takes_args
= ["groupname"]
113 def run(self
, groupname
, credopts
=None, sambaopts
=None,
114 versionopts
=None, H
=None, groupou
=None, group_scope
=None,
115 group_type
=None, description
=None, mail_address
=None, notes
=None, gid_number
=None, nis_domain
=None):
117 if (group_type
or "Security") == "Security":
118 gtype
= security_group
.get(group_scope
, GTYPE_SECURITY_GLOBAL_GROUP
)
120 gtype
= distribution_group
.get(group_scope
, GTYPE_DISTRIBUTION_GLOBAL_GROUP
)
122 if (gid_number
is None and nis_domain
is not None) or (gid_number
is not None and nis_domain
is None):
123 raise CommandError('Both --gid-number and --nis-domain have to be set for a RFC2307-enabled group. Operation cancelled.')
125 lp
= sambaopts
.get_loadparm()
126 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
129 samdb
= SamDB(url
=H
, session_info
=system_session(),
130 credentials
=creds
, lp
=lp
)
131 samdb
.newgroup(groupname
, groupou
=groupou
, grouptype
=gtype
,
132 description
=description
, mailaddress
=mail_address
, notes
=notes
,
133 gidnumber
=gid_number
, nisdomain
=nis_domain
)
134 except Exception as e
:
135 # FIXME: catch more specific exception
136 raise CommandError('Failed to add group "%s"' % groupname
, e
)
137 self
.outf
.write("Added group %s\n" % groupname
)
140 class cmd_group_delete(Command
):
141 """Deletes an AD group.
143 The command deletes an existing AD group from the Active Directory domain. The groupname specified on the command is the sAMAccountName.
145 Deleting a group is a permanent operation. When a group is deleted, all permissions and rights that users in the group had inherited from the group account are deleted as well.
147 The command may be run from the root userid or another authorized userid. The -H or --URL option can be used to execute the command on a remote server.
150 samba-tool group delete Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
152 Example1 shows how to delete an AD group from a remote LDAP server. The -U parameter is used to pass the userid and password of a user that exists on the remote server and is authorized to issue the command on that server.
155 sudo samba-tool group delete Group2
157 Example2 deletes group Group2 from the local server. The command is run under root using the sudo command.
160 synopsis
= "%prog <groupname> [options]"
162 takes_optiongroups
= {
163 "sambaopts": options
.SambaOptions
,
164 "versionopts": options
.VersionOptions
,
165 "credopts": options
.CredentialsOptions
,
169 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
170 metavar
="URL", dest
="H"),
173 takes_args
= ["groupname"]
175 def run(self
, groupname
, credopts
=None, sambaopts
=None, versionopts
=None, H
=None):
177 lp
= sambaopts
.get_loadparm()
178 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
179 samdb
= SamDB(url
=H
, session_info
=system_session(),
180 credentials
=creds
, lp
=lp
)
182 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
183 ldb
.binary_encode(groupname
))
186 res
= samdb
.search(base
=samdb
.domain_dn(),
187 scope
=ldb
.SCOPE_SUBTREE
,
192 raise CommandError('Unable to find group "%s"' % (groupname
))
195 samdb
.delete(group_dn
)
196 except Exception as e
:
197 # FIXME: catch more specific exception
198 raise CommandError('Failed to remove group "%s"' % groupname
, e
)
199 self
.outf
.write("Deleted group %s\n" % groupname
)
202 class cmd_group_add_members(Command
):
203 """Add members to an AD group.
205 This command adds one or more members to an existing Active Directory group. The command accepts one or more group member names separated by commas. A group member may be a user or computer account or another Active Directory group.
207 When a member is added to a group the member may inherit permissions and rights from the group. Likewise, when permission or rights of a group are changed, the changes may reflect in the members through inheritance.
209 The member names specified on the command must be the sAMaccountName.
212 samba-tool group addmembers supergroup Group1,Group2,User1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
214 Example1 shows how to add two groups, Group1 and Group2 and one user account, User1, to the existing AD group named supergroup. The command will be run on a remote server specified with the -H. The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server.
217 sudo samba-tool group addmembers supergroup User2
219 Example2 shows how to add a single user account, User2, to the supergroup AD group. It uses the sudo command to run as root when issuing the command.
222 synopsis
= "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
224 takes_optiongroups
= {
225 "sambaopts": options
.SambaOptions
,
226 "versionopts": options
.VersionOptions
,
227 "credopts": options
.CredentialsOptions
,
231 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
232 metavar
="URL", dest
="H"),
233 Option("--member-dn",
234 help=("DN of the new group member to be added.\n"
235 "The --object-types option will be ignored."),
238 Option("--object-types",
239 help=("Comma separated list of object types.\n"
240 "The types are used to filter the search for the "
241 "specified members.\n"
242 "Valid values are: user, group, computer, serviceaccount, "
244 "Default: user,group,computer"),
245 default
="user,group,computer",
247 Option("--member-base-dn",
248 help=("Base DN for group member search.\n"
249 "Default is the domain DN."),
253 takes_args
= ["groupname", "listofmembers?"]
264 object_types
="user,group,computer"):
266 lp
= sambaopts
.get_loadparm()
267 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
269 if member_dn
is None and listofmembers
is None:
272 'Either listofmembers or --member-dn must be specified.')
275 samdb
= SamDB(url
=H
, session_info
=system_session(),
276 credentials
=creds
, lp
=lp
)
278 if member_dn
is not None:
279 groupmembers
+= member_dn
280 if listofmembers
is not None:
281 groupmembers
+= listofmembers
.split(',')
282 group_member_types
= object_types
.split(',')
284 if member_base_dn
is not None:
285 member_base_dn
= samdb
.normalize_dn_in_domain(member_base_dn
)
287 samdb
.add_remove_group_members(groupname
, groupmembers
,
288 add_members_operation
=True,
289 member_types
=group_member_types
,
290 member_base_dn
=member_base_dn
)
291 except Exception as e
:
292 # FIXME: catch more specific exception
293 raise CommandError('Failed to add members %r to group "%s" - %s' % (
294 groupmembers
, groupname
, e
))
295 self
.outf
.write("Added members to group %s\n" % groupname
)
298 class cmd_group_remove_members(Command
):
299 """Remove members from an AD group.
301 This command removes one or more members from an existing Active Directory group. The command accepts one or more group member names separated by commas. A group member may be a user or computer account or another Active Directory group that is a member of the group specified on the command.
303 When a member is removed from a group, inherited permissions and rights will no longer apply to the member.
306 samba-tool group removemembers supergroup Group1 -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
308 Example1 shows how to remove Group1 from supergroup. The command will run on the remote server specified on the -H parameter. The -U parameter is used to pass the userid and password of a user authorized to issue the command on the remote server.
311 sudo samba-tool group removemembers supergroup User1
313 Example2 shows how to remove a single user account, User2, from the supergroup AD group. It uses the sudo command to run as root when issuing the command.
316 synopsis
= "%prog <groupname> (<listofmembers>]|--member-dn=<member-dn>) [options]"
318 takes_optiongroups
= {
319 "sambaopts": options
.SambaOptions
,
320 "versionopts": options
.VersionOptions
,
321 "credopts": options
.CredentialsOptions
,
325 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
326 metavar
="URL", dest
="H"),
327 Option("--member-dn",
328 help=("DN of the group member to be removed.\n"
329 "The --object-types option will be ignored."),
332 Option("--object-types",
333 help=("Comma separated list of object types.\n"
334 "The types are used to filter the search for the "
335 "specified members.\n"
336 "Valid values are: user, group, computer, serviceaccount, "
338 "Default: user,group,computer"),
339 default
="user,group,computer",
341 Option("--member-base-dn",
342 help=("Base DN for group member search.\n"
343 "Default is the domain DN."),
347 takes_args
= ["groupname", "listofmembers?"]
358 object_types
="user,group,computer"):
360 lp
= sambaopts
.get_loadparm()
361 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
363 if member_dn
is None and listofmembers
is None:
366 'Either listofmembers or --member-dn must be specified.')
369 samdb
= SamDB(url
=H
, session_info
=system_session(),
370 credentials
=creds
, lp
=lp
)
372 if member_dn
is not None:
373 groupmembers
+= member_dn
374 if listofmembers
is not None:
375 groupmembers
+= listofmembers
.split(',')
376 group_member_types
= object_types
.split(',')
378 if member_base_dn
is not None:
379 member_base_dn
= samdb
.normalize_dn_in_domain(member_base_dn
)
381 samdb
.add_remove_group_members(groupname
,
383 add_members_operation
=False,
384 member_types
=group_member_types
,
385 member_base_dn
=member_base_dn
)
386 except Exception as e
:
387 # FIXME: Catch more specific exception
388 raise CommandError('Failed to remove members %r from group "%s"' % (listofmembers
, groupname
), e
)
389 self
.outf
.write("Removed members from group %s\n" % groupname
)
392 class cmd_group_list(Command
):
393 """List all groups."""
395 synopsis
= "%prog [options]"
398 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
399 metavar
="URL", dest
="H"),
400 Option("-v", "--verbose",
401 help="Verbose output, showing group type and group scope.",
402 action
="store_true"),
403 Option("-b", "--base-dn",
404 help="Specify base DN to use.",
406 Option("--full-dn", dest
="full_dn",
409 help="Display DN instead of the sAMAccountName."),
412 takes_optiongroups
= {
413 "sambaopts": options
.SambaOptions
,
414 "credopts": options
.CredentialsOptions
,
415 "versionopts": options
.VersionOptions
,
426 lp
= sambaopts
.get_loadparm()
427 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
429 samdb
= SamDB(url
=H
, session_info
=system_session(),
430 credentials
=creds
, lp
=lp
)
431 attrs
=["samaccountname"]
434 attrs
+= ["grouptype", "member"]
435 domain_dn
= samdb
.domain_dn()
437 domain_dn
= samdb
.normalize_dn_in_domain(base_dn
)
438 res
= samdb
.search(domain_dn
, scope
=ldb
.SCOPE_SUBTREE
,
439 expression
=("(objectClass=group)"),
445 self
.outf
.write("Group Name Group Type Group Scope Members\n")
446 self
.outf
.write("--------------------------------------------------------------------------------\n")
449 self
.outf
.write("%-44s" % msg
.get("samaccountname", idx
=0))
450 hgtype
= hex(int("%s" % msg
["grouptype"]) & 0x00000000FFFFFFFF)
451 if (hgtype
== hex(int(security_group
.get("Builtin")))):
452 self
.outf
.write("Security Builtin ")
453 elif (hgtype
== hex(int(security_group
.get("Domain")))):
454 self
.outf
.write("Security Domain ")
455 elif (hgtype
== hex(int(security_group
.get("Global")))):
456 self
.outf
.write("Security Global ")
457 elif (hgtype
== hex(int(security_group
.get("Universal")))):
458 self
.outf
.write("Security Universal")
459 elif (hgtype
== hex(int(distribution_group
.get("Global")))):
460 self
.outf
.write("Distribution Global ")
461 elif (hgtype
== hex(int(distribution_group
.get("Domain")))):
462 self
.outf
.write("Distribution Domain ")
463 elif (hgtype
== hex(int(distribution_group
.get("Universal")))):
464 self
.outf
.write("Distribution Universal")
467 num_members
= len(msg
.get("member", default
=[]))
468 self
.outf
.write(" %6u\n" % num_members
)
472 self
.outf
.write("%s\n" % msg
.get("dn"))
475 self
.outf
.write("%s\n" % msg
.get("samaccountname", idx
=0))
478 class cmd_group_list_members(Command
):
479 """List all members of an AD group.
481 This command lists members from an existing Active Directory group. The command accepts one group name.
484 samba-tool group listmembers \"Domain Users\" -H ldap://samba.samdom.example.com -Uadministrator%passw0rd
487 synopsis
= "%prog <groupname> [options]"
490 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
491 metavar
="URL", dest
="H"),
492 Option("--hide-expired",
493 help="Do not list expired group members",
495 action
='store_true'),
496 Option("--hide-disabled",
499 help="Do not list disabled group members"),
500 Option("--full-dn", dest
="full_dn",
503 help="Display DN instead of the sAMAccountName.")
506 takes_optiongroups
= {
507 "sambaopts": options
.SambaOptions
,
508 "credopts": options
.CredentialsOptions
,
509 "versionopts": options
.VersionOptions
,
512 takes_args
= ["groupname"]
523 lp
= sambaopts
.get_loadparm()
524 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
527 samdb
= SamDB(url
=H
, session_info
=system_session(),
528 credentials
=creds
, lp
=lp
)
530 search_filter
= ("(&(objectClass=group)(sAMAccountName=%s))" %
531 ldb
.binary_encode(groupname
))
533 res
= samdb
.search(samdb
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
534 expression
=(search_filter
),
536 group_sid_binary
= res
[0].get('objectSid', idx
=0)
538 raise CommandError('Unable to find group "%s"' % (groupname
))
540 group_sid
= ndr_unpack(security
.dom_sid
, group_sid_binary
)
541 (group_dom_sid
, rid
) = group_sid
.split()
542 group_sid_dn
= "<SID=%s>" % (group_sid
)
545 if hide_expired
is True:
546 current_nttime
= samdb
.get_nttime()
548 "(|(accountExpires=0)(accountExpires>=%u))" % (current_nttime
)
551 if hide_disabled
is True:
552 filter_disabled
= "(!(userAccountControl:%s:=%u))" % (
553 ldb
.OID_COMPARATOR_AND
, UF_ACCOUNTDISABLE
)
555 filter = "(&(|(primaryGroupID=%s)(memberOf=%s))%s%s)" % (
556 rid
, group_sid_dn
, filter_disabled
, filter_expires
)
558 res
= samdb
.search(samdb
.domain_dn(), scope
=ldb
.SCOPE_SUBTREE
,
560 attrs
=["samAccountName", "cn"])
567 self
.outf
.write("%s\n" % msg
.get("dn"))
570 member_name
= msg
.get("samAccountName", idx
=0)
571 if member_name
is None:
572 member_name
= msg
.get("cn", idx
=0)
573 self
.outf
.write("%s\n" % member_name
)
575 except Exception as e
:
576 raise CommandError('Failed to list members of "%s" group - %s' %
580 class cmd_group_move(Command
):
581 """Move a group to an organizational unit/container.
583 This command moves a group object into the specified organizational unit
585 The groupname specified on the command is the sAMAccountName.
586 The name of the organizational unit or container can be specified as a
587 full DN or without the domainDN component.
589 The command may be run from the root userid or another authorized userid.
591 The -H or --URL= option can be used to execute the command against a remote
595 samba-tool group move Group1 'OU=OrgUnit,DC=samdom.DC=example,DC=com' \\
596 -H ldap://samba.samdom.example.com -U administrator
598 Example1 shows how to move a group Group1 into the 'OrgUnit' organizational
599 unit on a remote LDAP server.
601 The -H parameter is used to specify the remote target server.
604 samba-tool group move Group1 CN=Users
606 Example2 shows how to move a group Group1 back into the CN=Users container
610 synopsis
= "%prog <groupname> <new_parent_dn> [options]"
613 Option("-H", "--URL", help="LDB URL for database or target server",
614 type=str, metavar
="URL", dest
="H"),
617 takes_args
= ["groupname", "new_parent_dn"]
618 takes_optiongroups
= {
619 "sambaopts": options
.SambaOptions
,
620 "credopts": options
.CredentialsOptions
,
621 "versionopts": options
.VersionOptions
,
624 def run(self
, groupname
, new_parent_dn
, credopts
=None, sambaopts
=None,
625 versionopts
=None, H
=None):
626 lp
= sambaopts
.get_loadparm()
627 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
628 samdb
= SamDB(url
=H
, session_info
=system_session(),
629 credentials
=creds
, lp
=lp
)
630 domain_dn
= ldb
.Dn(samdb
, samdb
.domain_dn())
632 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
633 ldb
.binary_encode(groupname
))
635 res
= samdb
.search(base
=domain_dn
,
637 scope
=ldb
.SCOPE_SUBTREE
)
640 raise CommandError('Unable to find group "%s"' % (groupname
))
643 full_new_parent_dn
= samdb
.normalize_dn_in_domain(new_parent_dn
)
644 except Exception as e
:
645 raise CommandError('Invalid new_parent_dn "%s": %s' %
646 (new_parent_dn
, e
.message
))
648 full_new_group_dn
= ldb
.Dn(samdb
, str(group_dn
))
649 full_new_group_dn
.remove_base_components(len(group_dn
) - 1)
650 full_new_group_dn
.add_base(full_new_parent_dn
)
653 samdb
.rename(group_dn
, full_new_group_dn
)
654 except Exception as e
:
655 raise CommandError('Failed to move group "%s"' % groupname
, e
)
656 self
.outf
.write('Moved group "%s" into "%s"\n' %
657 (groupname
, full_new_parent_dn
))
660 class cmd_group_show(Command
):
661 """Display a group AD object.
663 This command displays a group object and it's attributes in the Active
665 The group name specified on the command is the sAMAccountName of the group.
667 The command may be run from the root userid or another authorized userid.
669 The -H or --URL= option can be used to execute the command against a remote
673 samba-tool group show Group1 -H ldap://samba.samdom.example.com \\
674 -U administrator --password=passw1rd
676 Example1 shows how to display a group's attributes in the domain against a
679 The -H parameter is used to specify the remote target server.
682 samba-tool group show Group2
684 Example2 shows how to display a group's attributes in the domain against a local
688 samba-tool group show Group3 --attributes=member,objectGUID
690 Example3 shows how to display a groups objectGUID and member attributes.
692 synopsis
= "%prog <group name> [options]"
695 Option("-H", "--URL", help="LDB URL for database or target server",
696 type=str, metavar
="URL", dest
="H"),
697 Option("--attributes",
698 help=("Comma separated list of attributes, "
699 "which will be printed."),
700 type=str, dest
="group_attrs"),
703 takes_args
= ["groupname"]
704 takes_optiongroups
= {
705 "sambaopts": options
.SambaOptions
,
706 "credopts": options
.CredentialsOptions
,
707 "versionopts": options
.VersionOptions
,
710 def run(self
, groupname
, credopts
=None, sambaopts
=None, versionopts
=None,
711 H
=None, group_attrs
=None):
713 lp
= sambaopts
.get_loadparm()
714 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
715 samdb
= SamDB(url
=H
, session_info
=system_session(),
716 credentials
=creds
, lp
=lp
)
720 attrs
= group_attrs
.split(",")
722 filter = ("(&(objectCategory=group)(sAMAccountName=%s))" %
723 ldb
.binary_encode(groupname
))
725 domaindn
= samdb
.domain_dn()
728 res
= samdb
.search(base
=domaindn
, expression
=filter,
729 scope
=ldb
.SCOPE_SUBTREE
, attrs
=attrs
)
732 raise CommandError('Unable to find group "%s"' % (groupname
))
735 group_ldif
= common
.get_ldif_for_editor(samdb
, msg
)
736 self
.outf
.write(group_ldif
)
739 class cmd_group_stats(Command
):
740 """Summary statistics about group memberships."""
742 synopsis
= "%prog [options]"
745 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
746 metavar
="URL", dest
="H"),
749 takes_optiongroups
= {
750 "sambaopts": options
.SambaOptions
,
751 "credopts": options
.CredentialsOptions
,
752 "versionopts": options
.VersionOptions
,
755 def num_in_range(self
, range_min
, range_max
, group_freqs
):
757 for members
, count
in group_freqs
.items():
758 if range_min
<= members
and members
<= range_max
:
763 def run(self
, sambaopts
=None, credopts
=None, versionopts
=None, H
=None):
764 lp
= sambaopts
.get_loadparm()
765 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
767 samdb
= SamDB(url
=H
, session_info
=system_session(),
768 credentials
=creds
, lp
=lp
)
770 domain_dn
= samdb
.domain_dn()
771 res
= samdb
.search(domain_dn
, scope
=ldb
.SCOPE_SUBTREE
,
772 expression
=("(objectClass=group)"),
773 attrs
=["samaccountname", "member"])
775 # first count up how many members each group has
776 group_assignments
= {}
777 total_memberships
= 0
780 name
= str(msg
.get("samaccountname"))
781 num_members
= len(msg
.get("member", default
=[]))
782 group_assignments
[name
] = num_members
783 total_memberships
+= num_members
785 num_groups
= res
.count
786 self
.outf
.write("Group membership statistics*\n")
787 self
.outf
.write("-------------------------------------------------\n")
788 self
.outf
.write("Total groups: {0}\n".format(num_groups
))
789 self
.outf
.write("Total memberships: {0}\n".format(total_memberships
))
790 average
= total_memberships
/ float(num_groups
)
791 self
.outf
.write("Average members per group: %.2f\n" % average
)
793 # find the max and median memberships (note that some default groups
794 # always have zero members, so displaying the min is not very helpful)
795 group_names
= list(group_assignments
.keys())
796 group_members
= list(group_assignments
.values())
797 idx
= group_members
.index(max(group_members
))
798 max_members
= group_members
[idx
]
799 self
.outf
.write("Max members: {0} ({1})\n".format(max_members
,
802 midpoint
= num_groups
// 2
803 median
= group_members
[midpoint
]
804 if num_groups
% 2 == 0:
805 median
= (median
+ group_members
[midpoint
- 1]) / 2
806 self
.outf
.write("Median members per group: {0}\n\n".format(median
))
808 # convert this to the frequency of group membership, i.e. how many
809 # groups have 5 members, how many have 6 members, etc
810 group_freqs
= defaultdict(int)
811 for group
, num_members
in group_assignments
.items():
812 group_freqs
[num_members
] += 1
814 # now squash this down even further, so that we just display the number
815 # of groups that fall into one of the following membership bands
816 bands
= [(0, 1), (2, 4), (5, 9), (10, 14), (15, 19), (20, 24),
817 (25, 29), (30, 39), (40, 49), (50, 59), (60, 69), (70, 79),
818 (80, 89), (90, 99), (100, 149), (150, 199), (200, 249),
819 (250, 299), (300, 399), (400, 499), (500, 999), (1000, 1999),
820 (2000, 2999), (3000, 3999), (4000, 4999), (5000, 9999),
821 (10000, max_members
)]
823 self
.outf
.write("Members Number of Groups\n")
824 self
.outf
.write("-------------------------------------------------\n")
829 if band_start
> max_members
:
832 num_groups
= self
.num_in_range(band_start
, band_end
, group_freqs
)
835 band_str
= "{0}-{1}".format(band_start
, band_end
)
836 self
.outf
.write("%13s %u\n" % (band_str
, num_groups
))
838 self
.outf
.write("\n* Note this does not include nested group memberships\n")
841 class cmd_group_edit(Command
):
842 """Modify Group AD object.
844 This command will allow editing of a group account in the Active Directory
845 domain. You will then be able to add or change attributes and their values.
847 The groupname specified on the command is the sAMAccountName.
849 The command may be run from the root userid or another authorized userid.
851 The -H or --URL= option can be used to execute the command against a remote
855 samba-tool group edit Group1 -H ldap://samba.samdom.example.com \\
856 -U administrator --password=passw1rd
858 Example1 shows how to edit a groups attributes in the domain against a
861 The -H parameter is used to specify the remote target server.
864 samba-tool group edit Group2
866 Example2 shows how to edit a groups attributes in the domain against a local
870 samba-tool group edit Group3 --editor=nano
872 Example3 shows how to edit a groups attributes in the domain against a local
873 server using the 'nano' editor.
875 synopsis
= "%prog <groupname> [options]"
878 Option("-H", "--URL", help="LDB URL for database or target server",
879 type=str, metavar
="URL", dest
="H"),
880 Option("--editor", help="Editor to use instead of the system default,"
881 " or 'vi' if no system default is set.", type=str),
884 takes_args
= ["groupname"]
885 takes_optiongroups
= {
886 "sambaopts": options
.SambaOptions
,
887 "credopts": options
.CredentialsOptions
,
888 "versionopts": options
.VersionOptions
,
891 def run(self
, groupname
, credopts
=None, sambaopts
=None, versionopts
=None,
892 H
=None, editor
=None):
893 lp
= sambaopts
.get_loadparm()
894 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
895 samdb
= SamDB(url
=H
, session_info
=system_session(),
896 credentials
=creds
, lp
=lp
)
898 filter = ("(&(sAMAccountName=%s)(objectClass=group))" %
899 ldb
.binary_encode(groupname
))
901 domaindn
= samdb
.domain_dn()
904 res
= samdb
.search(base
=domaindn
,
906 scope
=ldb
.SCOPE_SUBTREE
)
909 raise CommandError('Unable to find group "%s"' % (groupname
))
912 raise CommandError('Invalid number of results: for "%s": %d' %
913 ((groupname
), len(res
)))
916 result_ldif
= common
.get_ldif_for_editor(samdb
, msg
)
919 editor
= os
.environ
.get('EDITOR')
923 with tempfile
.NamedTemporaryFile(suffix
=".tmp") as t_file
:
924 t_file
.write(get_bytes(result_ldif
))
927 check_call([editor
, t_file
.name
])
928 except CalledProcessError
as e
:
929 raise CalledProcessError("ERROR: ", e
)
930 with
open(t_file
.name
) as edited_file
:
931 edited_message
= edited_file
.read()
933 msgs_edited
= samdb
.parse_ldif(edited_message
)
934 msg_edited
= next(msgs_edited
)[1]
936 res_msg_diff
= samdb
.msg_diff(msg
, msg_edited
)
937 if len(res_msg_diff
) == 0:
938 self
.outf
.write("Nothing to do\n")
942 samdb
.modify(res_msg_diff
)
943 except Exception as e
:
944 raise CommandError("Failed to modify group '%s': " % groupname
, e
)
946 self
.outf
.write("Modified group '%s' successfully\n" % groupname
)
949 class cmd_group_add_unix_attrs(Command
):
950 """Add RFC2307 attributes to a group.
952 This command adds Unix attributes to a group account in the Active
954 The groupname specified on the command is the sAMaccountName.
956 Unix (RFC2307) attributes will be added to the group account.
958 Add 'idmap_ldb:use rfc2307 = Yes' to smb.conf to use these attributes for
961 The command may be run from the root userid or another authorized userid.
962 The -H or --URL= option can be used to execute the command against a
966 samba-tool group addunixattrs Group1 10000
968 Example1 shows how to add RFC2307 attributes to a domain enabled group
971 The groups Unix ID will be set to '10000', provided this ID isn't already
975 synopsis
= "%prog <groupname> <gidnumber> [options]"
978 Option("-H", "--URL", help="LDB URL for database or target server",
979 type=str, metavar
="URL", dest
="H"),
982 takes_args
= ["groupname", "gidnumber"]
984 takes_optiongroups
= {
985 "sambaopts": options
.SambaOptions
,
986 "credopts": options
.CredentialsOptions
,
987 "versionopts": options
.VersionOptions
,
990 def run(self
, groupname
, gidnumber
, credopts
=None, sambaopts
=None,
991 versionopts
=None, H
=None):
993 lp
= sambaopts
.get_loadparm()
994 creds
= credopts
.get_credentials(lp
)
996 samdb
= SamDB(url
=H
, session_info
=system_session(),
997 credentials
=creds
, lp
=lp
)
999 domaindn
= samdb
.domain_dn()
1001 # Check group exists and doesn't have a gidNumber
1002 filter = "(samaccountname={})".format(ldb
.binary_encode(groupname
))
1003 res
= samdb
.search(domaindn
,
1004 scope
=ldb
.SCOPE_SUBTREE
,
1007 raise CommandError("Unable to find group '{}'".format(groupname
))
1009 group_dn
= res
[0].dn
1011 if "gidNumber" in res
[0]:
1012 raise CommandError("Group {} is a Unix group.".format(groupname
))
1014 # Check if supplied gidnumber isn't already being used
1015 filter = "(&(objectClass=group)(gidNumber={}))".format(gidnumber
)
1016 res
= samdb
.search(domaindn
,
1017 scope
=ldb
.SCOPE_SUBTREE
,
1020 raise CommandError('gidNumber {} already used.'.format(gidnumber
))
1022 if not lp
.get("idmap_ldb:use rfc2307"):
1023 self
.outf
.write("You are setting a Unix/RFC2307 GID. "
1024 "You may want to set 'idmap_ldb:use rfc2307 = Yes'"
1025 " in smb.conf to use the attributes for "
1026 "XID/SID-mapping.\n")
1033 """.format(group_dn
, gidnumber
)
1036 samdb
.modify_ldif(group_mod
)
1037 except ldb
.LdbError
as e
:
1038 raise CommandError("Failed to modify group '{0}': {1}"
1039 .format(groupname
, e
))
1041 self
.outf
.write("Modified Group '{}' successfully\n".format(groupname
))
1044 class cmd_group_rename(Command
):
1045 """Rename a group and related attributes.
1047 This command allows to set the group's name related attributes. The
1048 group's CN will be renamed automatically.
1050 The group's CN will be the sAMAccountName.
1051 Use the --force-new-cn option to specify the new CN manually and the
1052 --reset-cn to reset this change.
1054 Use an empty attribute value to remove the specified attribute.
1056 The groupname specified on the command is the sAMAccountName.
1058 The command may be run locally from the root userid or another authorized
1061 The -H or --URL= option can be used to execute the command against a remote
1065 samba-tool group rename employees --samaccountname=staff
1067 Example1 shows how to change the samaccountname of a group 'employees' to
1068 'staff'. The CN of the group employees will also be changed to 'staff',
1069 if the previous CN was the previous sAMAccountName.
1072 samba-tool group rename employees --mail-address='staff@company.com' \\
1073 -H ldap://samba.samdom.example.com -U administrator
1075 Example2 shows how to rename the mail address of a group 'employees' to
1076 'staff@company.com'.
1077 The -H parameter is used to specify the remote target server.
1080 synopsis
= "%prog <groupname> [options]"
1083 Option("-H", "--URL",
1084 help="LDB URL for database or target server",
1085 type=str, metavar
="URL", dest
="H"),
1086 Option("--force-new-cn",
1087 help="Specify a new CN (RND) instead of using the sAMAccountName.",
1089 Option("--reset-cn",
1090 help="Set the CN (RDN) to the sAMAccountName. Use this option "
1091 "to reset the changes made with the --force-new-cn option.",
1092 action
="store_true"),
1093 Option("--mail-address",
1094 help="New mail address",
1096 Option("--samaccountname",
1097 help="New account name (sAMAccountName/logon name)",
1101 takes_args
= ["groupname"]
1102 takes_optiongroups
= {
1103 "sambaopts": options
.SambaOptions
,
1104 "credopts": options
.CredentialsOptions
,
1105 "versionopts": options
.VersionOptions
,
1108 def run(self
, groupname
, credopts
=None, sambaopts
=None, versionopts
=None,
1109 H
=None, mail_address
=None, samaccountname
=None, force_new_cn
=None,
1112 if force_new_cn
and reset_cn
:
1113 raise CommandError("It is not allowed to specify --force-new-cn "
1114 "together with --reset-cn.")
1115 if force_new_cn
== "":
1116 raise CommandError("Failed to rename group - delete protected "
1118 if samaccountname
== "":
1119 raise CommandError("Failed to rename group - delete protected "
1120 "attribute 'sAMAccountName'")
1122 lp
= sambaopts
.get_loadparm()
1123 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
1124 samdb
= SamDB(url
=H
, session_info
=system_session(),
1125 credentials
=creds
, lp
=lp
)
1126 domain_dn
= ldb
.Dn(samdb
, samdb
.domain_dn())
1128 filter = ("(&(objectClass=group)(samaccountname=%s))" %
1129 ldb
.binary_encode(groupname
))
1131 res
= samdb
.search(base
=domain_dn
,
1132 scope
=ldb
.SCOPE_SUBTREE
,
1134 attrs
=["sAMAccountName",
1139 group_dn
= old_group
.dn
1141 raise CommandError('Unable to find group "%s"' % (groupname
))
1143 group_parent_dn
= group_dn
.parent()
1144 old_cn
= old_group
["cn"][0]
1146 # get the actual and the new group cn and the new dn
1147 if force_new_cn
is not None:
1148 new_cn
= force_new_cn
1149 elif samaccountname
is not None:
1150 new_cn
= samaccountname
1152 new_cn
= old_group
["sAMAccountName"]
1154 # CN must change, if the new CN is different and the old CN is the
1155 # standard CN or the change is forced with force-new-cn or reset-cn
1156 expected_cn
= old_group
["sAMAccountName"]
1157 must_change_cn
= str(old_cn
) != str(new_cn
) and \
1158 (str(old_cn
) == str(expected_cn
) or \
1159 reset_cn
or bool(force_new_cn
))
1161 new_group_dn
= ldb
.Dn(samdb
, "CN=%s" % new_cn
)
1162 new_group_dn
.add_base(group_parent_dn
)
1164 # format given attributes
1165 group_attrs
= ldb
.Message()
1166 group_attrs
.dn
= group_dn
1167 samdb
.prepare_attr_replace(group_attrs
, old_group
, "sAMAccountName",
1169 samdb
.prepare_attr_replace(group_attrs
, old_group
, "mail", mail_address
)
1171 group_attributes_changed
= len(group_attrs
) > 0
1173 # update the group with formatted attributes
1174 samdb
.transaction_start()
1176 if group_attributes_changed
:
1177 samdb
.modify(group_attrs
)
1179 samdb
.rename(group_dn
, new_group_dn
)
1180 except Exception as e
:
1181 samdb
.transaction_cancel()
1182 raise CommandError('Failed to rename group "%s"' % groupname
, e
)
1183 samdb
.transaction_commit()
1186 self
.outf
.write('Renamed CN of group "%s" from "%s" to "%s" '
1187 'successfully\n' % (groupname
, old_cn
, new_cn
))
1189 if group_attributes_changed
:
1190 self
.outf
.write('Following attributes of group "%s" have been '
1191 'changed successfully:\n' % (groupname
))
1192 for attr
in group_attrs
.keys():
1195 self
.outf
.write('%s: %s\n' % (attr
, group_attrs
[attr
]
1196 if group_attrs
[attr
] else '[removed]'))
1198 class cmd_group(SuperCommand
):
1199 """Group management."""
1202 subcommands
["add"] = cmd_group_add()
1203 subcommands
["create"] = cmd_group_add()
1204 subcommands
["delete"] = cmd_group_delete()
1205 subcommands
["edit"] = cmd_group_edit()
1206 subcommands
["addmembers"] = cmd_group_add_members()
1207 subcommands
["removemembers"] = cmd_group_remove_members()
1208 subcommands
["list"] = cmd_group_list()
1209 subcommands
["listmembers"] = cmd_group_list_members()
1210 subcommands
["move"] = cmd_group_move()
1211 subcommands
["show"] = cmd_group_show()
1212 subcommands
["stats"] = cmd_group_stats()
1213 subcommands
["addunixattrs"] = cmd_group_add_unix_attrs()
1214 subcommands
["rename"] = cmd_group_rename()