1 # machine account (computer) management
3 # Copyright Bjoern Baumbch <bb@sernet.de> 2018
5 # based on user management
6 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
7 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 import samba.getopt as options
30 from samba import sd_utils
31 from samba.dcerpc import dnsserver, dnsp, security
32 from samba.dnsserver import ARecord, AAAARecord
33 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
34 from samba.remove_dc import remove_dns_references
35 from samba.auth import system_session
36 from samba.samdb import SamDB
37 from samba.compat import get_bytes
38 from subprocess import check_call, CalledProcessError
49 from samba.netcmd import (
56 def _is_valid_ip(ip_string, address_families=None):
57 """Check ip string is valid address"""
58 # by default, check both ipv4 and ipv6
59 if not address_families:
60 address_families = [socket.AF_INET, socket.AF_INET6]
62 for address_family in address_families:
64 socket.inet_pton(address_family, ip_string)
65 return True # if no error, return directly
67 continue # Otherwise, check next family
71 def _is_valid_ipv4(ip_string):
72 """Check ip string is valid ipv4 address"""
73 return _is_valid_ip(ip_string, address_families=[socket.AF_INET])
76 def _is_valid_ipv6(ip_string):
77 """Check ip string is valid ipv6 address"""
78 return _is_valid_ip(ip_string, address_families=[socket.AF_INET6])
82 samdb, name, dns_conn, change_owner_sd,
83 server, ip_address_list, logger):
84 """Add DNS A or AAAA records while creating computer. """
85 name = name.rstrip('$')
86 client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
87 select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN
88 zone = samdb.domain_dns_name()
90 sd_helper = sd_utils.SDUtils(samdb)
93 buflen, res = dns_conn.DnssrvEnumRecords2(
105 except WERRORError as e:
106 if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
112 for record in rec.records:
113 if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
115 del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
116 del_rec_buf.rec = record
118 dns_conn.DnssrvUpdateRecord2(
127 except WERRORError as e:
128 if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
131 for ip_address in ip_address_list:
132 if _is_valid_ipv6(ip_address):
133 logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % (
134 name, zone, ip_address))
135 rec = AAAARecord(ip_address)
136 elif _is_valid_ipv4(ip_address):
137 logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % (
138 name, zone, ip_address))
139 rec = ARecord(ip_address)
141 raise ValueError('Invalid IP: {}'.format(ip_address))
144 add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
145 add_rec_buf.rec = rec
147 dns_conn.DnssrvUpdateRecord2(
157 if (len(ip_address_list) > 0):
158 domaindns_zone_dn = ldb.Dn(
160 'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
163 dns_a_dn, ldap_record = samdb.dns_lookup(
164 "%s.%s" % (name, zone),
165 dns_partition=domaindns_zone_dn,
168 # Make the DC own the DNS record, not the administrator
169 sd_helper.modify_sd_on_dn(
172 controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
176 class cmd_computer_create(Command):
177 """Create a new computer.
179 This command creates a new computer account in the Active Directory domain.
180 The computername specified on the command is the sAMaccountName without the
181 trailing $ (dollar sign).
183 User accounts may represent physical entities, such as workstations. Computer
184 accounts are also referred to as security principals and are assigned a
185 security identifier (SID).
188 samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \\
189 -Uadministrator%passw1rd
191 Example1 shows how to create a new computer in the domain against a remote LDAP
192 server. The -H parameter is used to specify the remote target server. The -U
193 option is used to pass the userid and password authorized to issue the command
197 sudo samba-tool computer create Computer2
199 Example2 shows how to create a new computer in the domain against the local
200 server. sudo is used so a user may run the command as root.
203 samba-tool computer create Computer3 --computerou='OU=OrgUnit'
205 Example3 shows how to create a new computer in the OrgUnit organizational unit.
208 synopsis = "%prog <computername> [options]"
211 Option("-H", "--URL", help="LDB URL for database or target server",
212 type=str, metavar="URL", dest="H"),
213 Option("--computerou",
214 help=("DN of alternative location (with or without domainDN "
215 "counterpart) to default CN=Computers in which new "
216 "computer object will be created. E.g. 'OU=<OU name>'"),
218 Option("--description", help="Computers's description", type=str),
219 Option("--prepare-oldjoin",
220 help="Prepare enabled machine account for oldjoin mechanism",
221 action="store_true"),
222 Option("--ip-address",
223 dest='ip_address_list',
224 help=("IPv4 address for the computer's A record, or IPv6 "
225 "address for AAAA record, can be provided multiple "
228 Option("--service-principal-name",
229 dest='service_principal_name_list',
230 help=("Computer's Service Principal Name, can be provided "
235 takes_args = ["computername"]
237 takes_optiongroups = {
238 "sambaopts": options.SambaOptions,
239 "credopts": options.CredentialsOptions,
240 "versionopts": options.VersionOptions,
243 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
244 H=None, computerou=None, description=None, prepare_oldjoin=False,
245 ip_address_list=None, service_principal_name_list=None):
247 if ip_address_list is None:
250 if service_principal_name_list is None:
251 service_principal_name_list = []
253 # check each IP address if provided
254 for ip_address in ip_address_list:
255 if not _is_valid_ip(ip_address):
256 raise CommandError('Invalid IP address {}'.format(ip_address))
258 lp = sambaopts.get_loadparm()
259 creds = credopts.get_credentials(lp)
262 samdb = SamDB(url=H, session_info=system_session(),
263 credentials=creds, lp=lp)
264 samdb.newcomputer(computername, computerou=computerou,
265 description=description,
266 prepare_oldjoin=prepare_oldjoin,
267 ip_address_list=ip_address_list,
268 service_principal_name_list=service_principal_name_list,
272 # if ip_address_list provided, then we need to create DNS
273 # records for this computer.
275 hostname = re.sub(r"\$$", "", computername)
276 if hostname.count('$'):
277 raise CommandError('Illegal computername "%s"' % computername)
279 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
280 ldb.binary_encode(hostname))
283 base=samdb.domain_dn(),
284 scope=ldb.SCOPE_SUBTREE,
286 attrs=['primaryGroupID', 'objectSid'])
288 group = recs[0]['primaryGroupID'][0]
289 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
291 dns_conn = dnsserver.dnsserver(
292 "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
295 change_owner_sd = security.descriptor()
296 change_owner_sd.owner_sid = owner
297 change_owner_sd.group_sid = security.dom_sid(
298 "{}-{}".format(samdb.get_domain_sid(), group),
302 samdb, hostname, dns_conn,
303 change_owner_sd, samdb.host_dns_name(),
304 ip_address_list, self.get_logger())
305 except Exception as e:
306 raise CommandError("Failed to create computer '%s': " %
309 self.outf.write("Computer '%s' created successfully\n" % computername)
312 class cmd_computer_delete(Command):
313 """Delete a computer.
315 This command deletes a computer account from the Active Directory domain. The
316 computername specified on the command is the sAMAccountName without the
317 trailing $ (dollar sign).
319 Once the account is deleted, all permissions and memberships associated with
320 that account are deleted. If a new computer account is added with the same name
321 as a previously deleted account name, the new computer does not have the
322 previous permissions. The new account computer will be assigned a new security
323 identifier (SID) and permissions and memberships will have to be added.
325 The command may be run from the root userid or another authorized
326 userid. The -H or --URL= option can be used to execute the command against
330 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \\
331 -Uadministrator%passw1rd
333 Example1 shows how to delete a computer in the domain against a remote LDAP
334 server. The -H parameter is used to specify the remote target server. The
335 --computername= and --password= options are used to pass the computername and
336 password of a computer that exists on the remote server and is authorized to
337 issue the command on that server.
340 sudo samba-tool computer delete Computer2
342 Example2 shows how to delete a computer in the domain against the local server.
343 sudo is used so a computer may run the command as root.
346 synopsis = "%prog <computername> [options]"
349 Option("-H", "--URL", help="LDB URL for database or target server",
350 type=str, metavar="URL", dest="H"),
353 takes_args = ["computername"]
354 takes_optiongroups = {
355 "sambaopts": options.SambaOptions,
356 "credopts": options.CredentialsOptions,
357 "versionopts": options.VersionOptions,
360 def run(self, computername, credopts=None, sambaopts=None,
361 versionopts=None, H=None):
362 lp = sambaopts.get_loadparm()
363 creds = credopts.get_credentials(lp, fallback_machine=True)
365 samdb = SamDB(url=H, session_info=system_session(),
366 credentials=creds, lp=lp)
368 samaccountname = computername
369 if not computername.endswith('$'):
370 samaccountname = "%s$" % computername
372 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
373 (ldb.binary_encode(samaccountname),
374 dsdb.ATYPE_WORKSTATION_TRUST))
376 res = samdb.search(base=samdb.domain_dn(),
377 scope=ldb.SCOPE_SUBTREE,
379 attrs=["userAccountControl", "dNSHostName"])
380 computer_dn = res[0].dn
381 computer_ac = int(res[0]["userAccountControl"][0])
382 if "dNSHostName" in res[0]:
383 computer_dns_host_name = str(res[0]["dNSHostName"][0])
385 computer_dns_host_name = None
387 raise CommandError('Unable to find computer "%s"' % computername)
389 computer_is_workstation = (
390 computer_ac & dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
391 if not computer_is_workstation:
392 raise CommandError('Failed to remove computer "%s": '
393 'Computer is not a workstation - removal denied'
396 samdb.delete(computer_dn)
397 if computer_dns_host_name:
398 remove_dns_references(
399 samdb, self.get_logger(), computer_dns_host_name,
401 except Exception as e:
402 raise CommandError('Failed to remove computer "%s"' %
404 self.outf.write("Deleted computer %s\n" % computername)
407 class cmd_computer_edit(Command):
408 """Modify Computer AD object.
410 This command will allow editing of a computer account in the Active
411 Directory domain. You will then be able to add or change attributes and
414 The computername specified on the command is the sAMaccountName with or
415 without the trailing $ (dollar sign).
417 The command may be run from the root userid or another authorized userid.
419 The -H or --URL= option can be used to execute the command against a remote
423 samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \\
424 -U administrator --password=passw1rd
426 Example1 shows how to edit a computers attributes in the domain against a
429 The -H parameter is used to specify the remote target server.
432 samba-tool computer edit Computer2
434 Example2 shows how to edit a computers attributes in the domain against a
438 samba-tool computer edit Computer3 --editor=nano
440 Example3 shows how to edit a computers attributes in the domain against a
441 local LDAP server using the 'nano' editor.
443 synopsis = "%prog <computername> [options]"
446 Option("-H", "--URL", help="LDB URL for database or target server",
447 type=str, metavar="URL", dest="H"),
448 Option("--editor", help="Editor to use instead of the system default,"
449 " or 'vi' if no system default is set.", type=str),
452 takes_args = ["computername"]
453 takes_optiongroups = {
454 "sambaopts": options.SambaOptions,
455 "credopts": options.CredentialsOptions,
456 "versionopts": options.VersionOptions,
459 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
460 H=None, editor=None):
461 lp = sambaopts.get_loadparm()
462 creds = credopts.get_credentials(lp, fallback_machine=True)
463 samdb = SamDB(url=H, session_info=system_session(),
464 credentials=creds, lp=lp)
466 samaccountname = computername
467 if not computername.endswith('$'):
468 samaccountname = "%s$" % computername
470 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
471 (dsdb.ATYPE_WORKSTATION_TRUST,
472 ldb.binary_encode(samaccountname)))
474 domaindn = samdb.domain_dn()
477 res = samdb.search(base=domaindn,
479 scope=ldb.SCOPE_SUBTREE)
480 computer_dn = res[0].dn
482 raise CommandError('Unable to find computer "%s"' % (computername))
485 raise CommandError('Invalid number of results: for "%s": %d' %
486 ((computername), len(res)))
489 result_ldif = common.get_ldif_for_editor(samdb, msg)
492 editor = os.environ.get('EDITOR')
496 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
497 t_file.write(get_bytes(result_ldif))
500 check_call([editor, t_file.name])
501 except CalledProcessError as e:
502 raise CalledProcessError("ERROR: ", e)
503 with open(t_file.name) as edited_file:
504 edited_message = edited_file.read()
506 msgs_edited = samdb.parse_ldif(edited_message)
507 msg_edited = next(msgs_edited)[1]
509 res_msg_diff = samdb.msg_diff(msg, msg_edited)
510 if len(res_msg_diff) == 0:
511 self.outf.write("Nothing to do\n")
515 samdb.modify(res_msg_diff)
516 except Exception as e:
517 raise CommandError("Failed to modify computer '%s': " %
520 self.outf.write("Modified computer '%s' successfully\n" % computername)
522 class cmd_computer_list(Command):
523 """List all computers."""
525 synopsis = "%prog [options]"
528 Option("-H", "--URL", help="LDB URL for database or target server",
529 type=str, metavar="URL", dest="H"),
532 takes_optiongroups = {
533 "sambaopts": options.SambaOptions,
534 "credopts": options.CredentialsOptions,
535 "versionopts": options.VersionOptions,
538 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
539 lp = sambaopts.get_loadparm()
540 creds = credopts.get_credentials(lp, fallback_machine=True)
542 samdb = SamDB(url=H, session_info=system_session(),
543 credentials=creds, lp=lp)
545 filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
547 domain_dn = samdb.domain_dn()
548 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
550 attrs=["samaccountname"])
555 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
558 class cmd_computer_show(Command):
559 """Display a computer AD object.
561 This command displays a computer account and it's attributes in the Active
563 The computername specified on the command is the sAMAccountName.
565 The command may be run from the root userid or another authorized
568 The -H or --URL= option can be used to execute the command against a remote
572 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\
575 Example1 shows how display a computers attributes in the domain against a
578 The -H parameter is used to specify the remote target server.
581 samba-tool computer show Computer2
583 Example2 shows how to display a computers attributes in the domain against a
587 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
589 Example3 shows how to display a computers objectSid and operatingSystem
592 synopsis = "%prog <computername> [options]"
595 Option("-H", "--URL", help="LDB URL for database or target server",
596 type=str, metavar="URL", dest="H"),
597 Option("--attributes",
598 help=("Comma separated list of attributes, "
599 "which will be printed."),
600 type=str, dest="computer_attrs"),
603 takes_args = ["computername"]
604 takes_optiongroups = {
605 "sambaopts": options.SambaOptions,
606 "credopts": options.CredentialsOptions,
607 "versionopts": options.VersionOptions,
610 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
611 H=None, computer_attrs=None):
613 lp = sambaopts.get_loadparm()
614 creds = credopts.get_credentials(lp, fallback_machine=True)
615 samdb = SamDB(url=H, session_info=system_session(),
616 credentials=creds, lp=lp)
620 attrs = computer_attrs.split(",")
622 samaccountname = computername
623 if not computername.endswith('$'):
624 samaccountname = "%s$" % computername
626 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
627 (dsdb.ATYPE_WORKSTATION_TRUST,
628 ldb.binary_encode(samaccountname)))
630 domaindn = samdb.domain_dn()
633 res = samdb.search(base=domaindn, expression=filter,
634 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
635 computer_dn = res[0].dn
637 raise CommandError('Unable to find computer "%s"' %
641 computer_ldif = common.get_ldif_for_editor(samdb, msg)
642 self.outf.write(computer_ldif)
645 class cmd_computer_move(Command):
646 """Move a computer to an organizational unit/container."""
648 synopsis = "%prog computername <new_ou_dn> [options]"
651 Option("-H", "--URL", help="LDB URL for database or target server",
652 type=str, metavar="URL", dest="H"),
655 takes_args = ["computername", "new_ou_dn"]
656 takes_optiongroups = {
657 "sambaopts": options.SambaOptions,
658 "credopts": options.CredentialsOptions,
659 "versionopts": options.VersionOptions,
662 def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
663 versionopts=None, H=None):
664 lp = sambaopts.get_loadparm()
665 creds = credopts.get_credentials(lp, fallback_machine=True)
666 samdb = SamDB(url=H, session_info=system_session(),
667 credentials=creds, lp=lp)
668 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
670 samaccountname = computername
671 if not computername.endswith('$'):
672 samaccountname = "%s$" % computername
674 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
675 (ldb.binary_encode(samaccountname),
676 dsdb.ATYPE_WORKSTATION_TRUST))
678 res = samdb.search(base=domain_dn,
680 scope=ldb.SCOPE_SUBTREE)
681 computer_dn = res[0].dn
683 raise CommandError('Unable to find computer "%s"' % (computername))
685 full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
686 if not full_new_ou_dn.is_child_of(domain_dn):
687 full_new_ou_dn.add_base(domain_dn)
688 new_computer_dn = ldb.Dn(samdb, str(computer_dn))
689 new_computer_dn.remove_base_components(len(computer_dn) -1)
690 new_computer_dn.add_base(full_new_ou_dn)
692 samdb.rename(computer_dn, new_computer_dn)
693 except Exception as e:
694 raise CommandError('Failed to move computer "%s"' % computername, e)
695 self.outf.write('Moved computer "%s" to "%s"\n' %
696 (computername, new_ou_dn))
699 class cmd_computer(SuperCommand):
700 """Computer management."""
703 subcommands["create"] = cmd_computer_create()
704 subcommands["delete"] = cmd_computer_delete()
705 subcommands["edit"] = cmd_computer_edit()
706 subcommands["list"] = cmd_computer_list()
707 subcommands["show"] = cmd_computer_show()
708 subcommands["move"] = cmd_computer_move()