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
.common
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_add(Command
):
177 """Add a new computer.
179 This command adds a new computer account to the Active Directory domain.
180 The computername specified on the command is the sAMaccountName without the
181 trailing $ (dollar sign).
183 Computer 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 add Computer1 -H ldap://samba.samdom.example.com \\
189 -Uadministrator%passw1rd
191 Example1 shows how to add a new computer to 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 add Computer2
199 Example2 shows how to add a new computer to the domain against the local
200 server. sudo is used so a user may run the command as root.
203 samba-tool computer add Computer3 --computerou='OU=OrgUnit'
205 Example3 shows how to add 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 add computer '%s': " %
309 self
.outf
.write("Computer '%s' added 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"),
530 Option("-b", "--base-dn",
531 help="Specify base DN to use",
533 Option("--full-dn", dest
="full_dn",
536 help="Display DN instead of the sAMAccountName.")
539 takes_optiongroups
= {
540 "sambaopts": options
.SambaOptions
,
541 "credopts": options
.CredentialsOptions
,
542 "versionopts": options
.VersionOptions
,
552 lp
= sambaopts
.get_loadparm()
553 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
555 samdb
= SamDB(url
=H
, session_info
=system_session(),
556 credentials
=creds
, lp
=lp
)
558 filter = "(sAMAccountType=%u)" % (dsdb
.ATYPE_WORKSTATION_TRUST
)
560 search_dn
= samdb
.domain_dn()
562 search_dn
= samdb
.normalize_dn_in_domain(base_dn
)
564 res
= samdb
.search(search_dn
,
565 scope
=ldb
.SCOPE_SUBTREE
,
567 attrs
=["samaccountname"])
573 self
.outf
.write("%s\n" % msg
.get("dn"))
576 self
.outf
.write("%s\n" % msg
.get("samaccountname", idx
=0))
579 class cmd_computer_show(Command
):
580 """Display a computer AD object.
582 This command displays a computer account and it's attributes in the Active
584 The computername specified on the command is the sAMAccountName.
586 The command may be run from the root userid or another authorized
589 The -H or --URL= option can be used to execute the command against a remote
593 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\
596 Example1 shows how display a computers attributes in the domain against a
599 The -H parameter is used to specify the remote target server.
602 samba-tool computer show Computer2
604 Example2 shows how to display a computers attributes in the domain against a
608 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
610 Example3 shows how to display a computers objectSid and operatingSystem
613 synopsis
= "%prog <computername> [options]"
616 Option("-H", "--URL", help="LDB URL for database or target server",
617 type=str, metavar
="URL", dest
="H"),
618 Option("--attributes",
619 help=("Comma separated list of attributes, "
620 "which will be printed."),
621 type=str, dest
="computer_attrs"),
624 takes_args
= ["computername"]
625 takes_optiongroups
= {
626 "sambaopts": options
.SambaOptions
,
627 "credopts": options
.CredentialsOptions
,
628 "versionopts": options
.VersionOptions
,
631 def run(self
, computername
, credopts
=None, sambaopts
=None, versionopts
=None,
632 H
=None, computer_attrs
=None):
634 lp
= sambaopts
.get_loadparm()
635 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
636 samdb
= SamDB(url
=H
, session_info
=system_session(),
637 credentials
=creds
, lp
=lp
)
641 attrs
= computer_attrs
.split(",")
643 samaccountname
= computername
644 if not computername
.endswith('$'):
645 samaccountname
= "%s$" % computername
647 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
648 (dsdb
.ATYPE_WORKSTATION_TRUST
,
649 ldb
.binary_encode(samaccountname
)))
651 domaindn
= samdb
.domain_dn()
654 res
= samdb
.search(base
=domaindn
, expression
=filter,
655 scope
=ldb
.SCOPE_SUBTREE
, attrs
=attrs
)
656 computer_dn
= res
[0].dn
658 raise CommandError('Unable to find computer "%s"' %
662 computer_ldif
= common
.get_ldif_for_editor(samdb
, msg
)
663 self
.outf
.write(computer_ldif
)
666 class cmd_computer_move(Command
):
667 """Move a computer to an organizational unit/container."""
669 synopsis
= "%prog <computername> <new_ou_dn> [options]"
672 Option("-H", "--URL", help="LDB URL for database or target server",
673 type=str, metavar
="URL", dest
="H"),
676 takes_args
= ["computername", "new_ou_dn"]
677 takes_optiongroups
= {
678 "sambaopts": options
.SambaOptions
,
679 "credopts": options
.CredentialsOptions
,
680 "versionopts": options
.VersionOptions
,
683 def run(self
, computername
, new_ou_dn
, credopts
=None, sambaopts
=None,
684 versionopts
=None, H
=None):
685 lp
= sambaopts
.get_loadparm()
686 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
687 samdb
= SamDB(url
=H
, session_info
=system_session(),
688 credentials
=creds
, lp
=lp
)
689 domain_dn
= ldb
.Dn(samdb
, samdb
.domain_dn())
691 samaccountname
= computername
692 if not computername
.endswith('$'):
693 samaccountname
= "%s$" % computername
695 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
696 (ldb
.binary_encode(samaccountname
),
697 dsdb
.ATYPE_WORKSTATION_TRUST
))
699 res
= samdb
.search(base
=domain_dn
,
701 scope
=ldb
.SCOPE_SUBTREE
)
702 computer_dn
= res
[0].dn
704 raise CommandError('Unable to find computer "%s"' % (computername
))
706 full_new_ou_dn
= ldb
.Dn(samdb
, new_ou_dn
)
707 if not full_new_ou_dn
.is_child_of(domain_dn
):
708 full_new_ou_dn
.add_base(domain_dn
)
709 new_computer_dn
= ldb
.Dn(samdb
, str(computer_dn
))
710 new_computer_dn
.remove_base_components(len(computer_dn
) -1)
711 new_computer_dn
.add_base(full_new_ou_dn
)
713 samdb
.rename(computer_dn
, new_computer_dn
)
714 except Exception as e
:
715 raise CommandError('Failed to move computer "%s"' % computername
, e
)
716 self
.outf
.write('Moved computer "%s" to "%s"\n' %
717 (computername
, new_ou_dn
))
720 class cmd_computer(SuperCommand
):
721 """Computer management."""
724 subcommands
["add"] = cmd_computer_add()
725 subcommands
["create"] = cmd_computer_add()
726 subcommands
["delete"] = cmd_computer_delete()
727 subcommands
["edit"] = cmd_computer_edit()
728 subcommands
["list"] = cmd_computer_list()
729 subcommands
["show"] = cmd_computer_show()
730 subcommands
["move"] = cmd_computer_move()