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
28 from samba import sd_utils
29 from samba.dcerpc import dnsserver, dnsp, security
30 from samba.dnsserver import ARecord, AAAARecord
31 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
32 from samba.remove_dc import remove_dns_references
33 from samba.auth import system_session
34 from samba.samdb import SamDB
44 from samba.netcmd import (
52 def _is_valid_ip(ip_string, address_families=None):
53 """Check ip string is valid address"""
54 # by default, check both ipv4 and ipv6
55 if not address_families:
56 address_families = [socket.AF_INET, socket.AF_INET6]
58 for address_family in address_families:
60 socket.inet_pton(address_family, ip_string)
61 return True # if no error, return directly
63 continue # Otherwise, check next family
67 def _is_valid_ipv4(ip_string):
68 """Check ip string is valid ipv4 address"""
69 return _is_valid_ip(ip_string, address_families=[socket.AF_INET])
72 def _is_valid_ipv6(ip_string):
73 """Check ip string is valid ipv6 address"""
74 return _is_valid_ip(ip_string, address_families=[socket.AF_INET6])
78 samdb, name, dns_conn, change_owner_sd,
79 server, ip_address_list, logger):
80 """Add DNS A or AAAA records while creating computer. """
81 name = name.rstrip('$')
82 client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
83 select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN
84 zone = samdb.domain_dns_name()
86 sd_helper = sd_utils.SDUtils(samdb)
89 buflen, res = dns_conn.DnssrvEnumRecords2(
101 except WERRORError as e:
102 if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
108 for record in rec.records:
109 if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
111 del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
112 del_rec_buf.rec = record
114 dns_conn.DnssrvUpdateRecord2(
123 except WERRORError as e:
124 if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
127 for ip_address in ip_address_list:
128 if _is_valid_ipv6(ip_address):
129 logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % (
130 name, zone, ip_address))
131 rec = AAAARecord(ip_address)
132 elif _is_valid_ipv4(ip_address):
133 logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % (
134 name, zone, ip_address))
135 rec = ARecord(ip_address)
137 raise ValueError('Invalid IP: {}'.format(ip_address))
140 add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
141 add_rec_buf.rec = rec
143 dns_conn.DnssrvUpdateRecord2(
153 if (len(ip_address_list) > 0):
154 domaindns_zone_dn = ldb.Dn(
156 'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
159 dns_a_dn, ldap_record = samdb.dns_lookup(
160 "%s.%s" % (name, zone),
161 dns_partition=domaindns_zone_dn,
164 # Make the DC own the DNS record, not the administrator
165 sd_helper.modify_sd_on_dn(
168 controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
172 class cmd_computer_create(Command):
173 """Create a new computer.
175 This command creates a new computer account in the Active Directory domain.
176 The computername specified on the command is the sAMaccountName without the
177 trailing $ (dollar sign).
179 User accounts may represent physical entities, such as workstations. Computer
180 accounts are also referred to as security principals and are assigned a
181 security identifier (SID).
184 samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \
185 -Uadministrator%passw1rd
187 Example1 shows how to create a new computer in the domain against a remote LDAP
188 server. The -H parameter is used to specify the remote target server. The -U
189 option is used to pass the userid and password authorized to issue the command
193 sudo samba-tool computer create Computer2
195 Example2 shows how to create a new computer in the domain against the local
196 server. sudo is used so a user may run the command as root.
199 samba-tool computer create Computer3 --computerou='OU=OrgUnit'
201 Example3 shows how to create a new computer in the OrgUnit organizational unit.
204 synopsis = "%prog <computername> [options]"
207 Option("-H", "--URL", help="LDB URL for database or target server",
208 type=str, metavar="URL", dest="H"),
209 Option("--computerou",
210 help=("DN of alternative location (with or without domainDN "
211 "counterpart) to default CN=Computers in which new "
212 "computer object will be created. E.g. 'OU=<OU name>'"),
214 Option("--description", help="Computers's description", type=str),
215 Option("--prepare-oldjoin",
216 help="Prepare enabled machine account for oldjoin mechanism",
217 action="store_true"),
218 Option("--ip-address",
219 dest='ip_address_list',
220 help=("IPv4 address for the computer's A record, or IPv6 "
221 "address for AAAA record, can be provided multiple "
224 Option("--service-principal-name",
225 dest='service_principal_name_list',
226 help=("Computer's Service Principal Name, can be provided "
231 takes_args = ["computername"]
233 takes_optiongroups = {
234 "sambaopts": options.SambaOptions,
235 "credopts": options.CredentialsOptions,
236 "versionopts": options.VersionOptions,
239 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
240 H=None, computerou=None, description=None, prepare_oldjoin=False,
241 ip_address_list=None, service_principal_name_list=None):
243 if ip_address_list is None:
246 if service_principal_name_list is None:
247 service_principal_name_list = []
249 # check each IP address if provided
250 for ip_address in ip_address_list:
251 if not _is_valid_ip(ip_address):
252 raise CommandError('Invalid IP address {}'.format(ip_address))
254 lp = sambaopts.get_loadparm()
255 creds = credopts.get_credentials(lp)
258 samdb = SamDB(url=H, session_info=system_session(),
259 credentials=creds, lp=lp)
260 samdb.newcomputer(computername, computerou=computerou,
261 description=description,
262 prepare_oldjoin=prepare_oldjoin,
263 ip_address_list=ip_address_list,
264 service_principal_name_list=service_principal_name_list,
268 # if ip_address_list provided, then we need to create DNS
269 # records for this computer.
271 hostname = re.sub(r"\$$", "", computername)
272 if hostname.count('$'):
273 raise CommandError('Illegal computername "%s"' % computername)
275 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
276 ldb.binary_encode(hostname))
279 base=samdb.domain_dn(),
280 scope=ldb.SCOPE_SUBTREE,
282 attrs=['primaryGroupID', 'objectSid'])
284 group = recs[0]['primaryGroupID'][0]
285 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
287 dns_conn = dnsserver.dnsserver(
288 "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
291 change_owner_sd = security.descriptor()
292 change_owner_sd.owner_sid = owner
293 change_owner_sd.group_sid = security.dom_sid(
294 "{}-{}".format(samdb.get_domain_sid(), group),
298 samdb, hostname, dns_conn,
299 change_owner_sd, samdb.host_dns_name(),
300 ip_address_list, self.get_logger())
301 except Exception as e:
302 raise CommandError("Failed to create computer '%s': " %
305 self.outf.write("Computer '%s' created successfully\n" % computername)
308 class cmd_computer_delete(Command):
309 """Delete a computer.
311 This command deletes a computer account from the Active Directory domain. The
312 computername specified on the command is the sAMAccountName without the
313 trailing $ (dollar sign).
315 Once the account is deleted, all permissions and memberships associated with
316 that account are deleted. If a new computer account is added with the same name
317 as a previously deleted account name, the new computer does not have the
318 previous permissions. The new account computer will be assigned a new security
319 identifier (SID) and permissions and memberships will have to be added.
321 The command may be run from the root userid or another authorized
322 userid. The -H or --URL= option can be used to execute the command against
326 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \
327 -Uadministrator%passw1rd
329 Example1 shows how to delete a computer in the domain against a remote LDAP
330 server. The -H parameter is used to specify the remote target server. The
331 --computername= and --password= options are used to pass the computername and
332 password of a computer that exists on the remote server and is authorized to
333 issue the command on that server.
336 sudo samba-tool computer delete Computer2
338 Example2 shows how to delete a computer in the domain against the local server.
339 sudo is used so a computer may run the command as root.
342 synopsis = "%prog <computername> [options]"
345 Option("-H", "--URL", help="LDB URL for database or target server",
346 type=str, metavar="URL", dest="H"),
349 takes_args = ["computername"]
350 takes_optiongroups = {
351 "sambaopts": options.SambaOptions,
352 "credopts": options.CredentialsOptions,
353 "versionopts": options.VersionOptions,
356 def run(self, computername, credopts=None, sambaopts=None,
357 versionopts=None, H=None):
358 lp = sambaopts.get_loadparm()
359 creds = credopts.get_credentials(lp, fallback_machine=True)
361 samdb = SamDB(url=H, session_info=system_session(),
362 credentials=creds, lp=lp)
364 samaccountname = computername
365 if not computername.endswith('$'):
366 samaccountname = "%s$" % computername
368 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
369 (ldb.binary_encode(samaccountname),
370 dsdb.ATYPE_WORKSTATION_TRUST))
372 res = samdb.search(base=samdb.domain_dn(),
373 scope=ldb.SCOPE_SUBTREE,
375 attrs=["userAccountControl", "dNSHostName"])
376 computer_dn = res[0].dn
377 computer_ac = int(res[0]["userAccountControl"][0])
378 if "dNSHostName" in res[0]:
379 computer_dns_host_name = res[0]["dNSHostName"][0]
381 computer_dns_host_name = None
383 raise CommandError('Unable to find computer "%s"' % computername)
385 computer_is_workstation = (
386 computer_ac & dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
387 if computer_is_workstation == False:
388 raise CommandError('Failed to remove computer "%s": '
389 'Computer is not a workstation - removal denied'
392 samdb.delete(computer_dn)
393 if computer_dns_host_name:
394 remove_dns_references(
395 samdb, self.get_logger(), computer_dns_host_name,
397 except Exception as e:
398 raise CommandError('Failed to remove computer "%s"' %
400 self.outf.write("Deleted computer %s\n" % computername)
403 class cmd_computer_list(Command):
404 """List all computers."""
406 synopsis = "%prog [options]"
409 Option("-H", "--URL", help="LDB URL for database or target server",
410 type=str, metavar="URL", dest="H"),
413 takes_optiongroups = {
414 "sambaopts": options.SambaOptions,
415 "credopts": options.CredentialsOptions,
416 "versionopts": options.VersionOptions,
419 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
420 lp = sambaopts.get_loadparm()
421 creds = credopts.get_credentials(lp, fallback_machine=True)
423 samdb = SamDB(url=H, session_info=system_session(),
424 credentials=creds, lp=lp)
426 filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
428 domain_dn = samdb.domain_dn()
429 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
431 attrs=["samaccountname"])
436 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
439 class cmd_computer_show(Command):
440 """Display a computer AD object.
442 This command displays a computer account and it's attributes in the Active
444 The computername specified on the command is the sAMAccountName.
446 The command may be run from the root userid or another authorized
449 The -H or --URL= option can be used to execute the command against a remote
453 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \
456 Example1 shows how display a computers attributes in the domain against a
459 The -H parameter is used to specify the remote target server.
462 samba-tool computer show Computer2
464 Example2 shows how to display a computers attributes in the domain against a
468 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
470 Example3 shows how to display a computers objectSid and operatingSystem
473 synopsis = "%prog <computername> [options]"
476 Option("-H", "--URL", help="LDB URL for database or target server",
477 type=str, metavar="URL", dest="H"),
478 Option("--attributes",
479 help=("Comma separated list of attributes, "
480 "which will be printed."),
481 type=str, dest="computer_attrs"),
484 takes_args = ["computername"]
485 takes_optiongroups = {
486 "sambaopts": options.SambaOptions,
487 "credopts": options.CredentialsOptions,
488 "versionopts": options.VersionOptions,
491 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
492 H=None, computer_attrs=None):
494 lp = sambaopts.get_loadparm()
495 creds = credopts.get_credentials(lp, fallback_machine=True)
496 samdb = SamDB(url=H, session_info=system_session(),
497 credentials=creds, lp=lp)
501 attrs = computer_attrs.split(",")
503 samaccountname = computername
504 if not computername.endswith('$'):
505 samaccountname = "%s$" % computername
507 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
508 (dsdb.ATYPE_WORKSTATION_TRUST,
509 ldb.binary_encode(samaccountname)))
511 domaindn = samdb.domain_dn()
514 res = samdb.search(base=domaindn, expression=filter,
515 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
516 computer_dn = res[0].dn
518 raise CommandError('Unable to find computer "%s"' %
522 computer_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
523 self.outf.write(computer_ldif)
526 class cmd_computer_move(Command):
527 """Move a computer to an organizational unit/container."""
529 synopsis = "%prog computername <new_ou_dn> [options]"
532 Option("-H", "--URL", help="LDB URL for database or target server",
533 type=str, metavar="URL", dest="H"),
536 takes_args = ["computername", "new_ou_dn"]
537 takes_optiongroups = {
538 "sambaopts": options.SambaOptions,
539 "credopts": options.CredentialsOptions,
540 "versionopts": options.VersionOptions,
543 def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
544 versionopts=None, H=None):
545 lp = sambaopts.get_loadparm()
546 creds = credopts.get_credentials(lp, fallback_machine=True)
547 samdb = SamDB(url=H, session_info=system_session(),
548 credentials=creds, lp=lp)
549 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
551 samaccountname = computername
552 if not computername.endswith('$'):
553 samaccountname = "%s$" % computername
555 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
556 (ldb.binary_encode(samaccountname),
557 dsdb.ATYPE_WORKSTATION_TRUST))
559 res = samdb.search(base=domain_dn,
561 scope=ldb.SCOPE_SUBTREE)
562 computer_dn = res[0].dn
564 raise CommandError('Unable to find computer "%s"' % (computername))
566 full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
567 if not full_new_ou_dn.is_child_of(domain_dn):
568 full_new_ou_dn.add_base(domain_dn)
569 new_computer_dn = ldb.Dn(samdb, str(computer_dn))
570 new_computer_dn.remove_base_components(len(computer_dn) -1)
571 new_computer_dn.add_base(full_new_ou_dn)
573 samdb.rename(computer_dn, new_computer_dn)
574 except Exception as e:
575 raise CommandError('Failed to move computer "%s"' % computername, e)
576 self.outf.write('Moved computer "%s" to "%s"\n' %
577 (computername, new_ou_dn))
580 class cmd_computer(SuperCommand):
581 """Computer management."""
584 subcommands["create"] = cmd_computer_create()
585 subcommands["delete"] = cmd_computer_delete()
586 subcommands["list"] = cmd_computer_list()
587 subcommands["show"] = cmd_computer_show()
588 subcommands["move"] = cmd_computer_move()