smbd: Ensure we grant owner sid in check_parent_access_fsp()
[Samba.git] / python / samba / netcmd / computer.py
blob1413803cf8a73b8f835390a7b6966f2c51ae8c7c
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
24 import ldb
25 import socket
26 import samba
27 import re
28 import os
29 import tempfile
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.common import get_bytes
38 from subprocess import check_call, CalledProcessError
39 from . import common
41 from samba import (
42 credentials,
43 dsdb,
44 Ldb,
45 werror,
46 WERRORError
49 from samba.netcmd import (
50 Command,
51 CommandError,
52 SuperCommand,
53 Option,
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:
63 try:
64 socket.inet_pton(address_family, ip_string)
65 return True # if no error, return directly
66 except socket.error:
67 continue # Otherwise, check next family
68 return False
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])
81 def add_dns_records(
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()
89 name_found = True
90 sd_helper = sd_utils.SDUtils(samdb)
92 try:
93 buflen, res = dns_conn.DnssrvEnumRecords2(
94 client_version,
96 server,
97 zone,
98 name,
99 None,
100 dnsp.DNS_TYPE_ALL,
101 select_flags,
102 None,
103 None,
105 except WERRORError as e:
106 if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
107 name_found = False
109 if name_found:
110 for rec in res.rec:
111 for record in rec.records:
112 if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
113 # delete record
114 del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
115 del_rec_buf.rec = record
116 try:
117 dns_conn.DnssrvUpdateRecord2(
118 client_version,
120 server,
121 zone,
122 name,
123 None,
124 del_rec_buf,
126 except WERRORError as e:
127 if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
128 raise
130 for ip_address in ip_address_list:
131 if _is_valid_ipv6(ip_address):
132 logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % (
133 name, zone, ip_address))
134 rec = AAAARecord(ip_address)
135 elif _is_valid_ipv4(ip_address):
136 logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % (
137 name, zone, ip_address))
138 rec = ARecord(ip_address)
139 else:
140 raise ValueError('Invalid IP: {}'.format(ip_address))
142 # Add record
143 add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
144 add_rec_buf.rec = rec
146 dns_conn.DnssrvUpdateRecord2(
147 client_version,
149 server,
150 zone,
151 name,
152 add_rec_buf,
153 None,
156 if (len(ip_address_list) > 0):
157 domaindns_zone_dn = ldb.Dn(
158 samdb,
159 'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
162 dns_a_dn, ldap_record = samdb.dns_lookup(
163 "%s.%s" % (name, zone),
164 dns_partition=domaindns_zone_dn,
167 # Make the DC own the DNS record, not the administrator
168 sd_helper.modify_sd_on_dn(
169 dns_a_dn,
170 change_owner_sd,
171 controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
175 class cmd_computer_add(Command):
176 """Add a new computer.
178 This command adds a new computer account to the Active Directory domain.
179 The computername specified on the command is the sAMaccountName without the
180 trailing $ (dollar sign).
182 Computer accounts may represent physical entities, such as workstations. Computer
183 accounts are also referred to as security principals and are assigned a
184 security identifier (SID).
186 Example1:
187 samba-tool computer add Computer1 -H ldap://samba.samdom.example.com \\
188 -Uadministrator%passw1rd
190 Example1 shows how to add a new computer to the domain against a remote LDAP
191 server. The -H parameter is used to specify the remote target server. The -U
192 option is used to pass the userid and password authorized to issue the command
193 remotely.
195 Example2:
196 sudo samba-tool computer add Computer2
198 Example2 shows how to add a new computer to the domain against the local
199 server. sudo is used so a user may run the command as root.
201 Example3:
202 samba-tool computer add Computer3 --computerou='OU=OrgUnit'
204 Example3 shows how to add a new computer in the OrgUnit organizational unit.
207 synopsis = "%prog <computername> [options]"
209 takes_options = [
210 Option("-H", "--URL", help="LDB URL for database or target server",
211 type=str, metavar="URL", dest="H"),
212 Option("--computerou",
213 help=("DN of alternative location (with or without domainDN "
214 "counterpart) to default CN=Computers in which new "
215 "computer object will be created. E.g. 'OU=<OU name>'"),
216 type=str),
217 Option("--description", help="Computer's description", type=str),
218 Option("--prepare-oldjoin",
219 help="Prepare enabled machine account for oldjoin mechanism",
220 action="store_true"),
221 Option("--ip-address",
222 dest='ip_address_list',
223 help=("IPv4 address for the computer's A record, or IPv6 "
224 "address for AAAA record, can be provided multiple "
225 "times"),
226 action='append'),
227 Option("--service-principal-name",
228 dest='service_principal_name_list',
229 help=("Computer's Service Principal Name, can be provided "
230 "multiple times"),
231 action='append')
234 takes_args = ["computername"]
236 takes_optiongroups = {
237 "sambaopts": options.SambaOptions,
238 "credopts": options.CredentialsOptions,
239 "versionopts": options.VersionOptions,
242 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
243 H=None, computerou=None, description=None, prepare_oldjoin=False,
244 ip_address_list=None, service_principal_name_list=None):
246 if ip_address_list is None:
247 ip_address_list = []
249 if service_principal_name_list is None:
250 service_principal_name_list = []
252 # check each IP address if provided
253 for ip_address in ip_address_list:
254 if not _is_valid_ip(ip_address):
255 raise CommandError('Invalid IP address {}'.format(ip_address))
257 lp = sambaopts.get_loadparm()
258 creds = credopts.get_credentials(lp)
260 try:
261 samdb = SamDB(url=H, session_info=system_session(),
262 credentials=creds, lp=lp)
263 samdb.newcomputer(computername, computerou=computerou,
264 description=description,
265 prepare_oldjoin=prepare_oldjoin,
266 ip_address_list=ip_address_list,
267 service_principal_name_list=service_principal_name_list,
270 if ip_address_list:
271 # if ip_address_list provided, then we need to create DNS
272 # records for this computer.
274 hostname = re.sub(r"\$$", "", computername)
275 if hostname.count('$'):
276 raise CommandError('Illegal computername "%s"' % computername)
278 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
279 ldb.binary_encode(hostname))
281 recs = samdb.search(
282 base=samdb.domain_dn(),
283 scope=ldb.SCOPE_SUBTREE,
284 expression=filters,
285 attrs=['primaryGroupID', 'objectSid'])
287 group = recs[0]['primaryGroupID'][0]
288 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
290 dns_conn = dnsserver.dnsserver(
291 "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
292 lp, creds)
294 change_owner_sd = security.descriptor()
295 change_owner_sd.owner_sid = owner
296 change_owner_sd.group_sid = security.dom_sid(
297 "{}-{}".format(samdb.get_domain_sid(), group),
300 add_dns_records(
301 samdb, hostname, dns_conn,
302 change_owner_sd, samdb.host_dns_name(),
303 ip_address_list, self.get_logger())
304 except Exception as e:
305 raise CommandError("Failed to add computer '%s': " %
306 computername, e)
308 self.outf.write("Computer '%s' added successfully\n" % computername)
311 class cmd_computer_delete(Command):
312 """Delete a computer.
314 This command deletes a computer account from the Active Directory domain. The
315 computername specified on the command is the sAMAccountName without the
316 trailing $ (dollar sign).
318 Once the account is deleted, all permissions and memberships associated with
319 that account are deleted. If a new computer account is added with the same name
320 as a previously deleted account name, the new computer does not have the
321 previous permissions. The new account computer will be assigned a new security
322 identifier (SID) and permissions and memberships will have to be added.
324 The command may be run from the root userid or another authorized
325 userid. The -H or --URL= option can be used to execute the command against
326 a remote server.
328 Example1:
329 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \\
330 -Uadministrator%passw1rd
332 Example1 shows how to delete a computer in the domain against a remote LDAP
333 server. The -H parameter is used to specify the remote target server. The
334 --computername= and --password= options are used to pass the computername and
335 password of a computer that exists on the remote server and is authorized to
336 issue the command on that server.
338 Example2:
339 sudo samba-tool computer delete Computer2
341 Example2 shows how to delete a computer in the domain against the local server.
342 sudo is used so a computer may run the command as root.
345 synopsis = "%prog <computername> [options]"
347 takes_options = [
348 Option("-H", "--URL", help="LDB URL for database or target server",
349 type=str, metavar="URL", dest="H"),
352 takes_args = ["computername"]
353 takes_optiongroups = {
354 "sambaopts": options.SambaOptions,
355 "credopts": options.CredentialsOptions,
356 "versionopts": options.VersionOptions,
359 def run(self, computername, credopts=None, sambaopts=None,
360 versionopts=None, H=None):
361 lp = sambaopts.get_loadparm()
362 creds = credopts.get_credentials(lp, fallback_machine=True)
364 samdb = SamDB(url=H, session_info=system_session(),
365 credentials=creds, lp=lp)
367 samaccountname = computername
368 if not computername.endswith('$'):
369 samaccountname = "%s$" % computername
371 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
372 (ldb.binary_encode(samaccountname),
373 dsdb.ATYPE_WORKSTATION_TRUST))
374 try:
375 res = samdb.search(base=samdb.domain_dn(),
376 scope=ldb.SCOPE_SUBTREE,
377 expression=filter,
378 attrs=["userAccountControl", "dNSHostName"])
379 computer_dn = res[0].dn
380 computer_ac = int(res[0]["userAccountControl"][0])
381 if "dNSHostName" in res[0]:
382 computer_dns_host_name = str(res[0]["dNSHostName"][0])
383 else:
384 computer_dns_host_name = None
385 except IndexError:
386 raise CommandError('Unable to find computer "%s"' % computername)
388 computer_is_workstation = (
389 computer_ac & dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
390 if not computer_is_workstation:
391 raise CommandError('Failed to remove computer "%s": '
392 'Computer is not a workstation - removal denied'
393 % computername)
394 try:
395 samdb.delete(computer_dn)
396 if computer_dns_host_name:
397 remove_dns_references(
398 samdb, self.get_logger(), computer_dns_host_name,
399 ignore_no_name=True)
400 except Exception as e:
401 raise CommandError('Failed to remove computer "%s"' %
402 samaccountname, e)
403 self.outf.write("Deleted computer %s\n" % computername)
406 class cmd_computer_edit(Command):
407 """Modify Computer AD object.
409 This command will allow editing of a computer account in the Active
410 Directory domain. You will then be able to add or change attributes and
411 their values.
413 The computername specified on the command is the sAMaccountName with or
414 without the trailing $ (dollar sign).
416 The command may be run from the root userid or another authorized userid.
418 The -H or --URL= option can be used to execute the command against a remote
419 server.
421 Example1:
422 samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \\
423 -U administrator --password=passw1rd
425 Example1 shows how to edit a computers attributes in the domain against a
426 remote LDAP server.
428 The -H parameter is used to specify the remote target server.
430 Example2:
431 samba-tool computer edit Computer2
433 Example2 shows how to edit a computers attributes in the domain against a
434 local LDAP server.
436 Example3:
437 samba-tool computer edit Computer3 --editor=nano
439 Example3 shows how to edit a computers attributes in the domain against a
440 local LDAP server using the 'nano' editor.
442 synopsis = "%prog <computername> [options]"
444 takes_options = [
445 Option("-H", "--URL", help="LDB URL for database or target server",
446 type=str, metavar="URL", dest="H"),
447 Option("--editor", help="Editor to use instead of the system default,"
448 " or 'vi' if no system default is set.", type=str),
451 takes_args = ["computername"]
452 takes_optiongroups = {
453 "sambaopts": options.SambaOptions,
454 "credopts": options.CredentialsOptions,
455 "versionopts": options.VersionOptions,
458 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
459 H=None, editor=None):
460 lp = sambaopts.get_loadparm()
461 creds = credopts.get_credentials(lp, fallback_machine=True)
462 samdb = SamDB(url=H, session_info=system_session(),
463 credentials=creds, lp=lp)
465 samaccountname = computername
466 if not computername.endswith('$'):
467 samaccountname = "%s$" % computername
469 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
470 (dsdb.ATYPE_WORKSTATION_TRUST,
471 ldb.binary_encode(samaccountname)))
473 domaindn = samdb.domain_dn()
475 try:
476 res = samdb.search(base=domaindn,
477 expression=filter,
478 scope=ldb.SCOPE_SUBTREE)
479 computer_dn = res[0].dn
480 except IndexError:
481 raise CommandError('Unable to find computer "%s"' % (computername))
483 if len(res) != 1:
484 raise CommandError('Invalid number of results: for "%s": %d' %
485 ((computername), len(res)))
487 msg = res[0]
488 result_ldif = common.get_ldif_for_editor(samdb, msg)
490 if editor is None:
491 editor = os.environ.get('EDITOR')
492 if editor is None:
493 editor = 'vi'
495 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
496 t_file.write(get_bytes(result_ldif))
497 t_file.flush()
498 try:
499 check_call([editor, t_file.name])
500 except CalledProcessError as e:
501 raise CalledProcessError("ERROR: ", e)
502 with open(t_file.name) as edited_file:
503 edited_message = edited_file.read()
505 msgs_edited = samdb.parse_ldif(edited_message)
506 msg_edited = next(msgs_edited)[1]
508 res_msg_diff = samdb.msg_diff(msg, msg_edited)
509 if len(res_msg_diff) == 0:
510 self.outf.write("Nothing to do\n")
511 return
513 try:
514 samdb.modify(res_msg_diff)
515 except Exception as e:
516 raise CommandError("Failed to modify computer '%s': " %
517 computername, e)
519 self.outf.write("Modified computer '%s' successfully\n" % computername)
521 class cmd_computer_list(Command):
522 """List all computers."""
524 synopsis = "%prog [options]"
526 takes_options = [
527 Option("-H", "--URL", help="LDB URL for database or target server",
528 type=str, metavar="URL", dest="H"),
529 Option("-b", "--base-dn",
530 help="Specify base DN to use",
531 type=str),
532 Option("--full-dn", dest="full_dn",
533 default=False,
534 action="store_true",
535 help="Display DN instead of the sAMAccountName.")
538 takes_optiongroups = {
539 "sambaopts": options.SambaOptions,
540 "credopts": options.CredentialsOptions,
541 "versionopts": options.VersionOptions,
544 def run(self,
545 sambaopts=None,
546 credopts=None,
547 versionopts=None,
548 H=None,
549 base_dn=None,
550 full_dn=False):
551 lp = sambaopts.get_loadparm()
552 creds = credopts.get_credentials(lp, fallback_machine=True)
554 samdb = SamDB(url=H, session_info=system_session(),
555 credentials=creds, lp=lp)
557 filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
559 search_dn = samdb.domain_dn()
560 if base_dn:
561 search_dn = samdb.normalize_dn_in_domain(base_dn)
563 res = samdb.search(search_dn,
564 scope=ldb.SCOPE_SUBTREE,
565 expression=filter,
566 attrs=["samaccountname"])
567 if (len(res) == 0):
568 return
570 for msg in res:
571 if full_dn:
572 self.outf.write("%s\n" % msg.get("dn"))
573 continue
575 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
578 class cmd_computer_show(Command):
579 """Display a computer AD object.
581 This command displays a computer account and it's attributes in the Active
582 Directory domain.
583 The computername specified on the command is the sAMAccountName.
585 The command may be run from the root userid or another authorized
586 userid.
588 The -H or --URL= option can be used to execute the command against a remote
589 server.
591 Example1:
592 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\
593 -U administrator
595 Example1 shows how display a computers attributes in the domain against a
596 remote LDAP server.
598 The -H parameter is used to specify the remote target server.
600 Example2:
601 samba-tool computer show Computer2
603 Example2 shows how to display a computers attributes in the domain against a
604 local LDAP server.
606 Example3:
607 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
609 Example3 shows how to display a computers objectSid and operatingSystem
610 attribute.
612 synopsis = "%prog <computername> [options]"
614 takes_options = [
615 Option("-H", "--URL", help="LDB URL for database or target server",
616 type=str, metavar="URL", dest="H"),
617 Option("--attributes",
618 help=("Comma separated list of attributes, "
619 "which will be printed."),
620 type=str, dest="computer_attrs"),
623 takes_args = ["computername"]
624 takes_optiongroups = {
625 "sambaopts": options.SambaOptions,
626 "credopts": options.CredentialsOptions,
627 "versionopts": options.VersionOptions,
630 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
631 H=None, computer_attrs=None):
633 lp = sambaopts.get_loadparm()
634 creds = credopts.get_credentials(lp, fallback_machine=True)
635 samdb = SamDB(url=H, session_info=system_session(),
636 credentials=creds, lp=lp)
638 attrs = None
639 if computer_attrs:
640 attrs = computer_attrs.split(",")
642 samaccountname = computername
643 if not computername.endswith('$'):
644 samaccountname = "%s$" % computername
646 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
647 (dsdb.ATYPE_WORKSTATION_TRUST,
648 ldb.binary_encode(samaccountname)))
650 domaindn = samdb.domain_dn()
652 try:
653 res = samdb.search(base=domaindn, expression=filter,
654 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
655 computer_dn = res[0].dn
656 except IndexError:
657 raise CommandError('Unable to find computer "%s"' %
658 samaccountname)
660 for msg in res:
661 computer_ldif = common.get_ldif_for_editor(samdb, msg)
662 self.outf.write(computer_ldif)
665 class cmd_computer_move(Command):
666 """Move a computer to an organizational unit/container."""
668 synopsis = "%prog <computername> <new_ou_dn> [options]"
670 takes_options = [
671 Option("-H", "--URL", help="LDB URL for database or target server",
672 type=str, metavar="URL", dest="H"),
675 takes_args = ["computername", "new_ou_dn"]
676 takes_optiongroups = {
677 "sambaopts": options.SambaOptions,
678 "credopts": options.CredentialsOptions,
679 "versionopts": options.VersionOptions,
682 def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
683 versionopts=None, H=None):
684 lp = sambaopts.get_loadparm()
685 creds = credopts.get_credentials(lp, fallback_machine=True)
686 samdb = SamDB(url=H, session_info=system_session(),
687 credentials=creds, lp=lp)
688 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
690 samaccountname = computername
691 if not computername.endswith('$'):
692 samaccountname = "%s$" % computername
694 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
695 (ldb.binary_encode(samaccountname),
696 dsdb.ATYPE_WORKSTATION_TRUST))
697 try:
698 res = samdb.search(base=domain_dn,
699 expression=filter,
700 scope=ldb.SCOPE_SUBTREE)
701 computer_dn = res[0].dn
702 except IndexError:
703 raise CommandError('Unable to find computer "%s"' % (computername))
705 full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
706 if not full_new_ou_dn.is_child_of(domain_dn):
707 full_new_ou_dn.add_base(domain_dn)
708 new_computer_dn = ldb.Dn(samdb, str(computer_dn))
709 new_computer_dn.remove_base_components(len(computer_dn) -1)
710 new_computer_dn.add_base(full_new_ou_dn)
711 try:
712 samdb.rename(computer_dn, new_computer_dn)
713 except Exception as e:
714 raise CommandError('Failed to move computer "%s"' % computername, e)
715 self.outf.write('Moved computer "%s" to "%s"\n' %
716 (computername, new_ou_dn))
719 class cmd_computer(SuperCommand):
720 """Computer management."""
722 subcommands = {}
723 subcommands["add"] = cmd_computer_add()
724 subcommands["create"] = cmd_computer_add()
725 subcommands["delete"] = cmd_computer_delete()
726 subcommands["edit"] = cmd_computer_edit()
727 subcommands["list"] = cmd_computer_list()
728 subcommands["show"] = cmd_computer_show()
729 subcommands["move"] = cmd_computer_move()