# machine account (computer) management # # Copyright Bjoern Baumbch 2018 # # based on user management # Copyright Jelmer Vernooij 2010 # Copyright Theresa Halloran 2011 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import samba.getopt as options import ldb import socket import samba import re import os import tempfile from samba import sd_utils from samba.dcerpc import dnsserver, dnsp, security from samba.dnsserver import ARecord, AAAARecord from samba.ndr import ndr_unpack, ndr_pack, ndr_print from samba.remove_dc import remove_dns_references from samba.auth import system_session from samba.samdb import SamDB from samba.common import get_bytes from subprocess import check_call, CalledProcessError from . import common from samba import ( credentials, dsdb, Ldb, werror, WERRORError ) from samba.netcmd import ( Command, CommandError, SuperCommand, Option, ) def _is_valid_ip(ip_string, address_families=None): """Check ip string is valid address""" # by default, check both ipv4 and ipv6 if not address_families: address_families = [socket.AF_INET, socket.AF_INET6] for address_family in address_families: try: socket.inet_pton(address_family, ip_string) return True # if no error, return directly except socket.error: continue # Otherwise, check next family return False def _is_valid_ipv4(ip_string): """Check ip string is valid ipv4 address""" return _is_valid_ip(ip_string, address_families=[socket.AF_INET]) def _is_valid_ipv6(ip_string): """Check ip string is valid ipv6 address""" return _is_valid_ip(ip_string, address_families=[socket.AF_INET6]) def add_dns_records( samdb, name, dns_conn, change_owner_sd, server, ip_address_list, logger): """Add DNS A or AAAA records while creating computer. """ name = name.rstrip('$') client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN zone = samdb.domain_dns_name() name_found = True sd_helper = sd_utils.SDUtils(samdb) try: buflen, res = dns_conn.DnssrvEnumRecords2( client_version, 0, server, zone, name, None, dnsp.DNS_TYPE_ALL, select_flags, None, None, ) except WERRORError as e: if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST: name_found = False pass if name_found: for rec in res.rec: for record in rec.records: if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA: # delete record del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() del_rec_buf.rec = record try: dns_conn.DnssrvUpdateRecord2( client_version, 0, server, zone, name, None, del_rec_buf, ) except WERRORError as e: if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST: raise for ip_address in ip_address_list: if _is_valid_ipv6(ip_address): logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % ( name, zone, ip_address)) rec = AAAARecord(ip_address) elif _is_valid_ipv4(ip_address): logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % ( name, zone, ip_address)) rec = ARecord(ip_address) else: raise ValueError('Invalid IP: {}'.format(ip_address)) # Add record add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF() add_rec_buf.rec = rec dns_conn.DnssrvUpdateRecord2( client_version, 0, server, zone, name, add_rec_buf, None, ) if (len(ip_address_list) > 0): domaindns_zone_dn = ldb.Dn( samdb, 'DC=DomainDnsZones,%s' % samdb.get_default_basedn(), ) dns_a_dn, ldap_record = samdb.dns_lookup( "%s.%s" % (name, zone), dns_partition=domaindns_zone_dn, ) # Make the DC own the DNS record, not the administrator sd_helper.modify_sd_on_dn( dns_a_dn, change_owner_sd, controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)], ) class cmd_computer_add(Command): """Add a new computer. This command adds a new computer account to the Active Directory domain. The computername specified on the command is the sAMaccountName without the trailing $ (dollar sign). Computer accounts may represent physical entities, such as workstations. Computer accounts are also referred to as security principals and are assigned a security identifier (SID). Example1: samba-tool computer add Computer1 -H ldap://samba.samdom.example.com \\ -Uadministrator%passw1rd Example1 shows how to add a new computer to the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The -U option is used to pass the userid and password authorized to issue the command remotely. Example2: sudo samba-tool computer add Computer2 Example2 shows how to add a new computer to the domain against the local server. sudo is used so a user may run the command as root. Example3: samba-tool computer add Computer3 --computerou='OU=OrgUnit' Example3 shows how to add a new computer in the OrgUnit organizational unit. """ synopsis = "%prog [options]" takes_options = [ Option("-H", "--URL", help="LDB URL for database or target server", type=str, metavar="URL", dest="H"), Option("--computerou", help=("DN of alternative location (with or without domainDN " "counterpart) to default CN=Computers in which new " "computer object will be created. E.g. 'OU='"), type=str), Option("--description", help="Computers's description", type=str), Option("--prepare-oldjoin", help="Prepare enabled machine account for oldjoin mechanism", action="store_true"), Option("--ip-address", dest='ip_address_list', help=("IPv4 address for the computer's A record, or IPv6 " "address for AAAA record, can be provided multiple " "times"), action='append'), Option("--service-principal-name", dest='service_principal_name_list', help=("Computer's Service Principal Name, can be provided " "multiple times"), action='append') ] takes_args = ["computername"] takes_optiongroups = { "sambaopts": options.SambaOptions, "credopts": options.CredentialsOptions, "versionopts": options.VersionOptions, } def run(self, computername, credopts=None, sambaopts=None, versionopts=None, H=None, computerou=None, description=None, prepare_oldjoin=False, ip_address_list=None, service_principal_name_list=None): if ip_address_list is None: ip_address_list = [] if service_principal_name_list is None: service_principal_name_list = [] # check each IP address if provided for ip_address in ip_address_list: if not _is_valid_ip(ip_address): raise CommandError('Invalid IP address {}'.format(ip_address)) lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp) try: samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) samdb.newcomputer(computername, computerou=computerou, description=description, prepare_oldjoin=prepare_oldjoin, ip_address_list=ip_address_list, service_principal_name_list=service_principal_name_list, ) if ip_address_list: # if ip_address_list provided, then we need to create DNS # records for this computer. hostname = re.sub(r"\$$", "", computername) if hostname.count('$'): raise CommandError('Illegal computername "%s"' % computername) filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format( ldb.binary_encode(hostname)) recs = samdb.search( base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE, expression=filters, attrs=['primaryGroupID', 'objectSid']) group = recs[0]['primaryGroupID'][0] owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0]) dns_conn = dnsserver.dnsserver( "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()), lp, creds) change_owner_sd = security.descriptor() change_owner_sd.owner_sid = owner change_owner_sd.group_sid = security.dom_sid( "{}-{}".format(samdb.get_domain_sid(), group), ) add_dns_records( samdb, hostname, dns_conn, change_owner_sd, samdb.host_dns_name(), ip_address_list, self.get_logger()) except Exception as e: raise CommandError("Failed to add computer '%s': " % computername, e) self.outf.write("Computer '%s' added successfully\n" % computername) class cmd_computer_delete(Command): """Delete a computer. This command deletes a computer account from the Active Directory domain. The computername specified on the command is the sAMAccountName without the trailing $ (dollar sign). Once the account is deleted, all permissions and memberships associated with that account are deleted. If a new computer account is added with the same name as a previously deleted account name, the new computer does not have the previous permissions. The new account computer will be assigned a new security identifier (SID) and permissions and memberships will have to be added. 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 against a remote server. Example1: samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \\ -Uadministrator%passw1rd Example1 shows how to delete a computer in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. The --computername= and --password= options are used to pass the computername and password of a computer that exists on the remote server and is authorized to issue the command on that server. Example2: sudo samba-tool computer delete Computer2 Example2 shows how to delete a computer in the domain against the local server. sudo is used so a computer may run the command as root. """ synopsis = "%prog [options]" takes_options = [ Option("-H", "--URL", help="LDB URL for database or target server", type=str, metavar="URL", dest="H"), ] takes_args = ["computername"] takes_optiongroups = { "sambaopts": options.SambaOptions, "credopts": options.CredentialsOptions, "versionopts": options.VersionOptions, } def run(self, computername, credopts=None, sambaopts=None, versionopts=None, H=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp, fallback_machine=True) samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) samaccountname = computername if not computername.endswith('$'): samaccountname = "%s$" % computername filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" % (ldb.binary_encode(samaccountname), dsdb.ATYPE_WORKSTATION_TRUST)) try: res = samdb.search(base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE, expression=filter, attrs=["userAccountControl", "dNSHostName"]) computer_dn = res[0].dn computer_ac = int(res[0]["userAccountControl"][0]) if "dNSHostName" in res[0]: computer_dns_host_name = str(res[0]["dNSHostName"][0]) else: computer_dns_host_name = None except IndexError: raise CommandError('Unable to find computer "%s"' % computername) computer_is_workstation = ( computer_ac & dsdb.UF_WORKSTATION_TRUST_ACCOUNT) if not computer_is_workstation: raise CommandError('Failed to remove computer "%s": ' 'Computer is not a workstation - removal denied' % computername) try: samdb.delete(computer_dn) if computer_dns_host_name: remove_dns_references( samdb, self.get_logger(), computer_dns_host_name, ignore_no_name=True) except Exception as e: raise CommandError('Failed to remove computer "%s"' % samaccountname, e) self.outf.write("Deleted computer %s\n" % computername) class cmd_computer_edit(Command): """Modify Computer AD object. This command will allow editing of a computer account in the Active Directory domain. You will then be able to add or change attributes and their values. The computername specified on the command is the sAMaccountName with or without the trailing $ (dollar sign). 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 against a remote server. Example1: samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \\ -U administrator --password=passw1rd Example1 shows how to edit a computers attributes in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. Example2: samba-tool computer edit Computer2 Example2 shows how to edit a computers attributes in the domain against a local LDAP server. Example3: samba-tool computer edit Computer3 --editor=nano Example3 shows how to edit a computers attributes in the domain against a local LDAP server using the 'nano' editor. """ synopsis = "%prog [options]" takes_options = [ Option("-H", "--URL", help="LDB URL for database or target server", type=str, metavar="URL", dest="H"), Option("--editor", help="Editor to use instead of the system default," " or 'vi' if no system default is set.", type=str), ] takes_args = ["computername"] takes_optiongroups = { "sambaopts": options.SambaOptions, "credopts": options.CredentialsOptions, "versionopts": options.VersionOptions, } def run(self, computername, credopts=None, sambaopts=None, versionopts=None, H=None, editor=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp, fallback_machine=True) samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) samaccountname = computername if not computername.endswith('$'): samaccountname = "%s$" % computername filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" % (dsdb.ATYPE_WORKSTATION_TRUST, ldb.binary_encode(samaccountname))) domaindn = samdb.domain_dn() try: res = samdb.search(base=domaindn, expression=filter, scope=ldb.SCOPE_SUBTREE) computer_dn = res[0].dn except IndexError: raise CommandError('Unable to find computer "%s"' % (computername)) if len(res) != 1: raise CommandError('Invalid number of results: for "%s": %d' % ((computername), len(res))) msg = res[0] result_ldif = common.get_ldif_for_editor(samdb, msg) if editor is None: editor = os.environ.get('EDITOR') if editor is None: editor = 'vi' with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file: t_file.write(get_bytes(result_ldif)) t_file.flush() try: check_call([editor, t_file.name]) except CalledProcessError as e: raise CalledProcessError("ERROR: ", e) with open(t_file.name) as edited_file: edited_message = edited_file.read() msgs_edited = samdb.parse_ldif(edited_message) msg_edited = next(msgs_edited)[1] res_msg_diff = samdb.msg_diff(msg, msg_edited) if len(res_msg_diff) == 0: self.outf.write("Nothing to do\n") return try: samdb.modify(res_msg_diff) except Exception as e: raise CommandError("Failed to modify computer '%s': " % (computername, e)) self.outf.write("Modified computer '%s' successfully\n" % computername) class cmd_computer_list(Command): """List all computers.""" synopsis = "%prog [options]" takes_options = [ Option("-H", "--URL", help="LDB URL for database or target server", type=str, metavar="URL", dest="H"), Option("-b", "--base-dn", help="Specify base DN to use", type=str), Option("--full-dn", dest="full_dn", default=False, action="store_true", help="Display DN instead of the sAMAccountName.") ] takes_optiongroups = { "sambaopts": options.SambaOptions, "credopts": options.CredentialsOptions, "versionopts": options.VersionOptions, } def run(self, sambaopts=None, credopts=None, versionopts=None, H=None, base_dn=None, full_dn=False): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp, fallback_machine=True) samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST) search_dn = samdb.domain_dn() if base_dn: search_dn = samdb.normalize_dn_in_domain(base_dn) res = samdb.search(search_dn, scope=ldb.SCOPE_SUBTREE, expression=filter, attrs=["samaccountname"]) if (len(res) == 0): return for msg in res: if full_dn: self.outf.write("%s\n" % msg.get("dn")) continue self.outf.write("%s\n" % msg.get("samaccountname", idx=0)) class cmd_computer_show(Command): """Display a computer AD object. This command displays a computer account and it's attributes in the Active Directory domain. The computername specified on the command is the sAMAccountName. 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 against a remote server. Example1: samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\ -U administrator Example1 shows how display a computers attributes in the domain against a remote LDAP server. The -H parameter is used to specify the remote target server. Example2: samba-tool computer show Computer2 Example2 shows how to display a computers attributes in the domain against a local LDAP server. Example3: samba-tool computer show Computer2 --attributes=objectSid,operatingSystem Example3 shows how to display a computers objectSid and operatingSystem attribute. """ synopsis = "%prog [options]" takes_options = [ Option("-H", "--URL", help="LDB URL for database or target server", type=str, metavar="URL", dest="H"), Option("--attributes", help=("Comma separated list of attributes, " "which will be printed."), type=str, dest="computer_attrs"), ] takes_args = ["computername"] takes_optiongroups = { "sambaopts": options.SambaOptions, "credopts": options.CredentialsOptions, "versionopts": options.VersionOptions, } def run(self, computername, credopts=None, sambaopts=None, versionopts=None, H=None, computer_attrs=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp, fallback_machine=True) samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) attrs = None if computer_attrs: attrs = computer_attrs.split(",") samaccountname = computername if not computername.endswith('$'): samaccountname = "%s$" % computername filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" % (dsdb.ATYPE_WORKSTATION_TRUST, ldb.binary_encode(samaccountname))) domaindn = samdb.domain_dn() try: res = samdb.search(base=domaindn, expression=filter, scope=ldb.SCOPE_SUBTREE, attrs=attrs) computer_dn = res[0].dn except IndexError: raise CommandError('Unable to find computer "%s"' % samaccountname) for msg in res: computer_ldif = common.get_ldif_for_editor(samdb, msg) self.outf.write(computer_ldif) class cmd_computer_move(Command): """Move a computer to an organizational unit/container.""" synopsis = "%prog [options]" takes_options = [ Option("-H", "--URL", help="LDB URL for database or target server", type=str, metavar="URL", dest="H"), ] takes_args = ["computername", "new_ou_dn"] takes_optiongroups = { "sambaopts": options.SambaOptions, "credopts": options.CredentialsOptions, "versionopts": options.VersionOptions, } def run(self, computername, new_ou_dn, credopts=None, sambaopts=None, versionopts=None, H=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp, fallback_machine=True) samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) domain_dn = ldb.Dn(samdb, samdb.domain_dn()) samaccountname = computername if not computername.endswith('$'): samaccountname = "%s$" % computername filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" % (ldb.binary_encode(samaccountname), dsdb.ATYPE_WORKSTATION_TRUST)) try: res = samdb.search(base=domain_dn, expression=filter, scope=ldb.SCOPE_SUBTREE) computer_dn = res[0].dn except IndexError: raise CommandError('Unable to find computer "%s"' % (computername)) full_new_ou_dn = ldb.Dn(samdb, new_ou_dn) if not full_new_ou_dn.is_child_of(domain_dn): full_new_ou_dn.add_base(domain_dn) new_computer_dn = ldb.Dn(samdb, str(computer_dn)) new_computer_dn.remove_base_components(len(computer_dn) -1) new_computer_dn.add_base(full_new_ou_dn) try: samdb.rename(computer_dn, new_computer_dn) except Exception as e: raise CommandError('Failed to move computer "%s"' % computername, e) self.outf.write('Moved computer "%s" to "%s"\n' % (computername, new_ou_dn)) class cmd_computer(SuperCommand): """Computer management.""" subcommands = {} subcommands["add"] = cmd_computer_add() subcommands["create"] = cmd_computer_add() subcommands["delete"] = cmd_computer_delete() subcommands["edit"] = cmd_computer_edit() subcommands["list"] = cmd_computer_list() subcommands["show"] = cmd_computer_show() subcommands["move"] = cmd_computer_move()