ldb: Free memory when repacking database
[garming/samba-autobuild/.git] / python / samba / netcmd / computer.py
1 # machine account (computer) management
2 #
3 # Copyright Bjoern Baumbch <bb@sernet.de> 2018
4 #
5 # based on user management
6 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
7 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
8 #
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.
13 #
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.
18 #
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/>.
21 #
22
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.compat import get_bytes
38 from subprocess import check_call, CalledProcessError
39
40 from samba import (
41     credentials,
42     dsdb,
43     Ldb,
44     werror,
45     WERRORError
46 )
47
48 from samba.netcmd import (
49     Command,
50     CommandError,
51     SuperCommand,
52     Option,
53 )
54
55 def _is_valid_ip(ip_string, address_families=None):
56     """Check ip string is valid address"""
57     # by default, check both ipv4 and ipv6
58     if not address_families:
59         address_families = [socket.AF_INET, socket.AF_INET6]
60
61     for address_family in address_families:
62         try:
63             socket.inet_pton(address_family, ip_string)
64             return True  # if no error, return directly
65         except socket.error:
66             continue  # Otherwise, check next family
67     return False
68
69
70 def _is_valid_ipv4(ip_string):
71     """Check ip string is valid ipv4 address"""
72     return _is_valid_ip(ip_string, address_families=[socket.AF_INET])
73
74
75 def _is_valid_ipv6(ip_string):
76     """Check ip string is valid ipv6 address"""
77     return _is_valid_ip(ip_string, address_families=[socket.AF_INET6])
78
79
80 def add_dns_records(
81         samdb, name, dns_conn, change_owner_sd,
82         server, ip_address_list, logger):
83     """Add DNS A or AAAA records while creating computer. """
84     name = name.rstrip('$')
85     client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
86     select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN
87     zone = samdb.domain_dns_name()
88     name_found = True
89     sd_helper = sd_utils.SDUtils(samdb)
90
91     try:
92         buflen, res = dns_conn.DnssrvEnumRecords2(
93             client_version,
94             0,
95             server,
96             zone,
97             name,
98             None,
99             dnsp.DNS_TYPE_ALL,
100             select_flags,
101             None,
102             None,
103         )
104     except WERRORError as e:
105         if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
106             name_found = False
107             pass
108
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,
119                             0,
120                             server,
121                             zone,
122                             name,
123                             None,
124                             del_rec_buf,
125                         )
126                     except WERRORError as e:
127                         if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
128                             raise
129
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))
141
142         # Add record
143         add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
144         add_rec_buf.rec = rec
145
146         dns_conn.DnssrvUpdateRecord2(
147             client_version,
148             0,
149             server,
150             zone,
151             name,
152             add_rec_buf,
153             None,
154         )
155
156     if (len(ip_address_list) > 0):
157         domaindns_zone_dn = ldb.Dn(
158             samdb,
159             'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
160         )
161
162         dns_a_dn, ldap_record = samdb.dns_lookup(
163             "%s.%s" % (name, zone),
164             dns_partition=domaindns_zone_dn,
165         )
166
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)],
172         )
173
174
175 class cmd_computer_create(Command):
176     """Create a new computer.
177
178 This command creates a new computer account in the Active Directory domain.
179 The computername specified on the command is the sAMaccountName without the
180 trailing $ (dollar sign).
181
182 User 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).
185
186 Example1:
187 samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \\
188     -Uadministrator%passw1rd
189
190 Example1 shows how to create a new computer in 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.
194
195 Example2:
196 sudo samba-tool computer create Computer2
197
198 Example2 shows how to create a new computer in the domain against the local
199 server. sudo is used so a user may run the command as root.
200
201 Example3:
202 samba-tool computer create Computer3 --computerou='OU=OrgUnit'
203
204 Example3 shows how to create a new computer in the OrgUnit organizational unit.
205
206 """
207     synopsis = "%prog <computername> [options]"
208
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="Computers'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')
232     ]
233
234     takes_args = ["computername"]
235
236     takes_optiongroups = {
237         "sambaopts": options.SambaOptions,
238         "credopts": options.CredentialsOptions,
239         "versionopts": options.VersionOptions,
240     }
241
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):
245
246         if ip_address_list is None:
247             ip_address_list = []
248
249         if service_principal_name_list is None:
250             service_principal_name_list = []
251
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))
256
257         lp = sambaopts.get_loadparm()
258         creds = credopts.get_credentials(lp)
259
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,
268                               )
269
270             if ip_address_list:
271                 # if ip_address_list provided, then we need to create DNS
272                 # records for this computer.
273
274                 hostname = re.sub(r"\$$", "", computername)
275                 if hostname.count('$'):
276                     raise CommandError('Illegal computername "%s"' % computername)
277
278                 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
279                     ldb.binary_encode(hostname))
280
281                 recs = samdb.search(
282                     base=samdb.domain_dn(),
283                     scope=ldb.SCOPE_SUBTREE,
284                     expression=filters,
285                     attrs=['primaryGroupID', 'objectSid'])
286
287                 group = recs[0]['primaryGroupID'][0]
288                 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
289
290                 dns_conn = dnsserver.dnsserver(
291                     "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
292                     lp, creds)
293
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),
298                 )
299
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 create computer '%s': " %
306                                computername, e)
307
308         self.outf.write("Computer '%s' created successfully\n" % computername)
309
310
311 class cmd_computer_delete(Command):
312     """Delete a computer.
313
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).
317
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.
323
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.
327
328 Example1:
329 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \\
330     -Uadministrator%passw1rd
331
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.
337
338 Example2:
339 sudo samba-tool computer delete Computer2
340
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.
343
344 """
345     synopsis = "%prog <computername> [options]"
346
347     takes_options = [
348         Option("-H", "--URL", help="LDB URL for database or target server",
349                type=str, metavar="URL", dest="H"),
350     ]
351
352     takes_args = ["computername"]
353     takes_optiongroups = {
354         "sambaopts": options.SambaOptions,
355         "credopts": options.CredentialsOptions,
356         "versionopts": options.VersionOptions,
357     }
358
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)
363
364         samdb = SamDB(url=H, session_info=system_session(),
365                       credentials=creds, lp=lp)
366
367         samaccountname = computername
368         if not computername.endswith('$'):
369             samaccountname = "%s$" % computername
370
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)
387
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)
404
405
406 class cmd_computer_edit(Command):
407     """Modify Computer AD object.
408
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.
412
413     The computername specified on the command is the sAMaccountName with or
414     without the trailing $ (dollar sign).
415
416     The command may be run from the root userid or another authorized userid.
417
418     The -H or --URL= option can be used to execute the command against a remote
419     server.
420
421     Example1:
422     samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \\
423         -U administrator --password=passw1rd
424
425     Example1 shows how to edit a computers attributes in the domain against a
426     remote LDAP server.
427
428     The -H parameter is used to specify the remote target server.
429
430     Example2:
431     samba-tool computer edit Computer2
432
433     Example2 shows how to edit a computers attributes in the domain against a
434     local LDAP server.
435
436     Example3:
437     samba-tool computer edit Computer3 --editor=nano
438
439     Example3 shows how to edit a computers attributes in the domain against a
440     local LDAP server using the 'nano' editor.
441     """
442     synopsis = "%prog <computername> [options]"
443
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),
449     ]
450
451     takes_args = ["computername"]
452     takes_optiongroups = {
453         "sambaopts": options.SambaOptions,
454         "credopts": options.CredentialsOptions,
455         "versionopts": options.VersionOptions,
456     }
457
458     def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
459             H=None, editor=None):
460         from . import common
461
462         lp = sambaopts.get_loadparm()
463         creds = credopts.get_credentials(lp, fallback_machine=True)
464         samdb = SamDB(url=H, session_info=system_session(),
465                       credentials=creds, lp=lp)
466
467         samaccountname = computername
468         if not computername.endswith('$'):
469             samaccountname = "%s$" % computername
470
471         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
472                   (dsdb.ATYPE_WORKSTATION_TRUST,
473                    ldb.binary_encode(samaccountname)))
474
475         domaindn = samdb.domain_dn()
476
477         try:
478             res = samdb.search(base=domaindn,
479                                expression=filter,
480                                scope=ldb.SCOPE_SUBTREE)
481             computer_dn = res[0].dn
482         except IndexError:
483             raise CommandError('Unable to find computer "%s"' % (computername))
484
485         if len(res) != 1:
486             raise CommandError('Invalid number of results: for "%s": %d' %
487                                ((computername), len(res)))
488
489         msg = res[0]
490         result_ldif = common.get_ldif_for_editor(samdb, msg)
491
492         if editor is None:
493             editor = os.environ.get('EDITOR')
494             if editor is None:
495                 editor = 'vi'
496
497         with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
498             t_file.write(get_bytes(result_ldif))
499             t_file.flush()
500             try:
501                 check_call([editor, t_file.name])
502             except CalledProcessError as e:
503                 raise CalledProcessError("ERROR: ", e)
504             with open(t_file.name) as edited_file:
505                 edited_message = edited_file.read()
506
507         msgs_edited = samdb.parse_ldif(edited_message)
508         msg_edited = next(msgs_edited)[1]
509
510         res_msg_diff = samdb.msg_diff(msg, msg_edited)
511         if len(res_msg_diff) == 0:
512             self.outf.write("Nothing to do\n")
513             return
514
515         try:
516             samdb.modify(res_msg_diff)
517         except Exception as e:
518             raise CommandError("Failed to modify computer '%s': " %
519                                (computername, e))
520
521         self.outf.write("Modified computer '%s' successfully\n" % computername)
522
523 class cmd_computer_list(Command):
524     """List all computers."""
525
526     synopsis = "%prog [options]"
527
528     takes_options = [
529         Option("-H", "--URL", help="LDB URL for database or target server",
530                type=str, metavar="URL", dest="H"),
531     ]
532
533     takes_optiongroups = {
534         "sambaopts": options.SambaOptions,
535         "credopts": options.CredentialsOptions,
536         "versionopts": options.VersionOptions,
537     }
538
539     def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
540         lp = sambaopts.get_loadparm()
541         creds = credopts.get_credentials(lp, fallback_machine=True)
542
543         samdb = SamDB(url=H, session_info=system_session(),
544                       credentials=creds, lp=lp)
545
546         filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
547
548         domain_dn = samdb.domain_dn()
549         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
550                            expression=filter,
551                            attrs=["samaccountname"])
552         if (len(res) == 0):
553             return
554
555         for msg in res:
556             self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
557
558
559 class cmd_computer_show(Command):
560     """Display a computer AD object.
561
562 This command displays a computer account and it's attributes in the Active
563 Directory domain.
564 The computername specified on the command is the sAMAccountName.
565
566 The command may be run from the root userid or another authorized
567 userid.
568
569 The -H or --URL= option can be used to execute the command against a remote
570 server.
571
572 Example1:
573 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\
574     -U administrator
575
576 Example1 shows how display a computers attributes in the domain against a
577 remote LDAP server.
578
579 The -H parameter is used to specify the remote target server.
580
581 Example2:
582 samba-tool computer show Computer2
583
584 Example2 shows how to display a computers attributes in the domain against a
585 local LDAP server.
586
587 Example3:
588 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
589
590 Example3 shows how to display a computers objectSid and operatingSystem
591 attribute.
592 """
593     synopsis = "%prog <computername> [options]"
594
595     takes_options = [
596         Option("-H", "--URL", help="LDB URL for database or target server",
597                type=str, metavar="URL", dest="H"),
598         Option("--attributes",
599                help=("Comma separated list of attributes, "
600                      "which will be printed."),
601                type=str, dest="computer_attrs"),
602     ]
603
604     takes_args = ["computername"]
605     takes_optiongroups = {
606         "sambaopts": options.SambaOptions,
607         "credopts": options.CredentialsOptions,
608         "versionopts": options.VersionOptions,
609     }
610
611     def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
612             H=None, computer_attrs=None):
613
614         lp = sambaopts.get_loadparm()
615         creds = credopts.get_credentials(lp, fallback_machine=True)
616         samdb = SamDB(url=H, session_info=system_session(),
617                       credentials=creds, lp=lp)
618
619         attrs = None
620         if computer_attrs:
621             attrs = computer_attrs.split(",")
622
623         samaccountname = computername
624         if not computername.endswith('$'):
625             samaccountname = "%s$" % computername
626
627         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
628                   (dsdb.ATYPE_WORKSTATION_TRUST,
629                    ldb.binary_encode(samaccountname)))
630
631         domaindn = samdb.domain_dn()
632
633         try:
634             res = samdb.search(base=domaindn, expression=filter,
635                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
636             computer_dn = res[0].dn
637         except IndexError:
638             raise CommandError('Unable to find computer "%s"' %
639                                samaccountname)
640
641         for msg in res:
642             computer_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
643             self.outf.write(computer_ldif)
644
645
646 class cmd_computer_move(Command):
647     """Move a computer to an organizational unit/container."""
648
649     synopsis = "%prog computername <new_ou_dn> [options]"
650
651     takes_options = [
652         Option("-H", "--URL", help="LDB URL for database or target server",
653                type=str, metavar="URL", dest="H"),
654     ]
655
656     takes_args = ["computername", "new_ou_dn"]
657     takes_optiongroups = {
658         "sambaopts": options.SambaOptions,
659         "credopts": options.CredentialsOptions,
660         "versionopts": options.VersionOptions,
661     }
662
663     def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
664             versionopts=None, H=None):
665         lp = sambaopts.get_loadparm()
666         creds = credopts.get_credentials(lp, fallback_machine=True)
667         samdb = SamDB(url=H, session_info=system_session(),
668                       credentials=creds, lp=lp)
669         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
670
671         samaccountname = computername
672         if not computername.endswith('$'):
673             samaccountname = "%s$" % computername
674
675         filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
676                   (ldb.binary_encode(samaccountname),
677                    dsdb.ATYPE_WORKSTATION_TRUST))
678         try:
679             res = samdb.search(base=domain_dn,
680                                expression=filter,
681                                scope=ldb.SCOPE_SUBTREE)
682             computer_dn = res[0].dn
683         except IndexError:
684             raise CommandError('Unable to find computer "%s"' % (computername))
685
686         full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
687         if not full_new_ou_dn.is_child_of(domain_dn):
688             full_new_ou_dn.add_base(domain_dn)
689         new_computer_dn = ldb.Dn(samdb, str(computer_dn))
690         new_computer_dn.remove_base_components(len(computer_dn) -1)
691         new_computer_dn.add_base(full_new_ou_dn)
692         try:
693             samdb.rename(computer_dn, new_computer_dn)
694         except Exception as e:
695             raise CommandError('Failed to move computer "%s"' % computername, e)
696         self.outf.write('Moved computer "%s" to "%s"\n' %
697                         (computername, new_ou_dn))
698
699
700 class cmd_computer(SuperCommand):
701     """Computer management."""
702
703     subcommands = {}
704     subcommands["create"] = cmd_computer_create()
705     subcommands["delete"] = cmd_computer_delete()
706     subcommands["edit"] = cmd_computer_edit()
707     subcommands["list"] = cmd_computer_list()
708     subcommands["show"] = cmd_computer_show()
709     subcommands["move"] = cmd_computer_move()