samba-tool {user,group,computer,contact} show: avoid base64 encoded strings if possible
[samba.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 from . import common
40
41 from samba import (
42     credentials,
43     dsdb,
44     Ldb,
45     werror,
46     WERRORError
47 )
48
49 from samba.netcmd import (
50     Command,
51     CommandError,
52     SuperCommand,
53     Option,
54 )
55
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]
61
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
69
70
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])
74
75
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])
79
80
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)
91
92     try:
93         buflen, res = dns_conn.DnssrvEnumRecords2(
94             client_version,
95             0,
96             server,
97             zone,
98             name,
99             None,
100             dnsp.DNS_TYPE_ALL,
101             select_flags,
102             None,
103             None,
104         )
105     except WERRORError as e:
106         if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
107             name_found = False
108             pass
109
110     if name_found:
111         for rec in res.rec:
112             for record in rec.records:
113                 if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
114                     # delete record
115                     del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
116                     del_rec_buf.rec = record
117                     try:
118                         dns_conn.DnssrvUpdateRecord2(
119                             client_version,
120                             0,
121                             server,
122                             zone,
123                             name,
124                             None,
125                             del_rec_buf,
126                         )
127                     except WERRORError as e:
128                         if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
129                             raise
130
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)
140         else:
141             raise ValueError('Invalid IP: {}'.format(ip_address))
142
143         # Add record
144         add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
145         add_rec_buf.rec = rec
146
147         dns_conn.DnssrvUpdateRecord2(
148             client_version,
149             0,
150             server,
151             zone,
152             name,
153             add_rec_buf,
154             None,
155         )
156
157     if (len(ip_address_list) > 0):
158         domaindns_zone_dn = ldb.Dn(
159             samdb,
160             'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
161         )
162
163         dns_a_dn, ldap_record = samdb.dns_lookup(
164             "%s.%s" % (name, zone),
165             dns_partition=domaindns_zone_dn,
166         )
167
168         # Make the DC own the DNS record, not the administrator
169         sd_helper.modify_sd_on_dn(
170             dns_a_dn,
171             change_owner_sd,
172             controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
173         )
174
175
176 class cmd_computer_create(Command):
177     """Create a new computer.
178
179 This command creates a new computer account in the Active Directory domain.
180 The computername specified on the command is the sAMaccountName without the
181 trailing $ (dollar sign).
182
183 User 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).
186
187 Example1:
188 samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \\
189     -Uadministrator%passw1rd
190
191 Example1 shows how to create a new computer in 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
194 remotely.
195
196 Example2:
197 sudo samba-tool computer create Computer2
198
199 Example2 shows how to create a new computer in the domain against the local
200 server. sudo is used so a user may run the command as root.
201
202 Example3:
203 samba-tool computer create Computer3 --computerou='OU=OrgUnit'
204
205 Example3 shows how to create a new computer in the OrgUnit organizational unit.
206
207 """
208     synopsis = "%prog <computername> [options]"
209
210     takes_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>'"),
217                type=str),
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 "
226                      "times"),
227                action='append'),
228         Option("--service-principal-name",
229                dest='service_principal_name_list',
230                help=("Computer's Service Principal Name, can be provided "
231                      "multiple times"),
232                action='append')
233     ]
234
235     takes_args = ["computername"]
236
237     takes_optiongroups = {
238         "sambaopts": options.SambaOptions,
239         "credopts": options.CredentialsOptions,
240         "versionopts": options.VersionOptions,
241     }
242
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):
246
247         if ip_address_list is None:
248             ip_address_list = []
249
250         if service_principal_name_list is None:
251             service_principal_name_list = []
252
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))
257
258         lp = sambaopts.get_loadparm()
259         creds = credopts.get_credentials(lp)
260
261         try:
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,
269                               )
270
271             if ip_address_list:
272                 # if ip_address_list provided, then we need to create DNS
273                 # records for this computer.
274
275                 hostname = re.sub(r"\$$", "", computername)
276                 if hostname.count('$'):
277                     raise CommandError('Illegal computername "%s"' % computername)
278
279                 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
280                     ldb.binary_encode(hostname))
281
282                 recs = samdb.search(
283                     base=samdb.domain_dn(),
284                     scope=ldb.SCOPE_SUBTREE,
285                     expression=filters,
286                     attrs=['primaryGroupID', 'objectSid'])
287
288                 group = recs[0]['primaryGroupID'][0]
289                 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
290
291                 dns_conn = dnsserver.dnsserver(
292                     "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
293                     lp, creds)
294
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),
299                 )
300
301                 add_dns_records(
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 create computer '%s': " %
307                                computername, e)
308
309         self.outf.write("Computer '%s' created successfully\n" % computername)
310
311
312 class cmd_computer_delete(Command):
313     """Delete a computer.
314
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).
318
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.
324
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
327 a remote server.
328
329 Example1:
330 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \\
331     -Uadministrator%passw1rd
332
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.
338
339 Example2:
340 sudo samba-tool computer delete Computer2
341
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.
344
345 """
346     synopsis = "%prog <computername> [options]"
347
348     takes_options = [
349         Option("-H", "--URL", help="LDB URL for database or target server",
350                type=str, metavar="URL", dest="H"),
351     ]
352
353     takes_args = ["computername"]
354     takes_optiongroups = {
355         "sambaopts": options.SambaOptions,
356         "credopts": options.CredentialsOptions,
357         "versionopts": options.VersionOptions,
358     }
359
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)
364
365         samdb = SamDB(url=H, session_info=system_session(),
366                       credentials=creds, lp=lp)
367
368         samaccountname = computername
369         if not computername.endswith('$'):
370             samaccountname = "%s$" % computername
371
372         filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
373                   (ldb.binary_encode(samaccountname),
374                    dsdb.ATYPE_WORKSTATION_TRUST))
375         try:
376             res = samdb.search(base=samdb.domain_dn(),
377                                scope=ldb.SCOPE_SUBTREE,
378                                expression=filter,
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])
384             else:
385                 computer_dns_host_name = None
386         except IndexError:
387             raise CommandError('Unable to find computer "%s"' % computername)
388
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'
394                                % computername)
395         try:
396             samdb.delete(computer_dn)
397             if computer_dns_host_name:
398                 remove_dns_references(
399                     samdb, self.get_logger(), computer_dns_host_name,
400                     ignore_no_name=True)
401         except Exception as e:
402             raise CommandError('Failed to remove computer "%s"' %
403                                samaccountname, e)
404         self.outf.write("Deleted computer %s\n" % computername)
405
406
407 class cmd_computer_edit(Command):
408     """Modify Computer AD object.
409
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
412     their values.
413
414     The computername specified on the command is the sAMaccountName with or
415     without the trailing $ (dollar sign).
416
417     The command may be run from the root userid or another authorized userid.
418
419     The -H or --URL= option can be used to execute the command against a remote
420     server.
421
422     Example1:
423     samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \\
424         -U administrator --password=passw1rd
425
426     Example1 shows how to edit a computers attributes in the domain against a
427     remote LDAP server.
428
429     The -H parameter is used to specify the remote target server.
430
431     Example2:
432     samba-tool computer edit Computer2
433
434     Example2 shows how to edit a computers attributes in the domain against a
435     local LDAP server.
436
437     Example3:
438     samba-tool computer edit Computer3 --editor=nano
439
440     Example3 shows how to edit a computers attributes in the domain against a
441     local LDAP server using the 'nano' editor.
442     """
443     synopsis = "%prog <computername> [options]"
444
445     takes_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),
450     ]
451
452     takes_args = ["computername"]
453     takes_optiongroups = {
454         "sambaopts": options.SambaOptions,
455         "credopts": options.CredentialsOptions,
456         "versionopts": options.VersionOptions,
457     }
458
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)
465
466         samaccountname = computername
467         if not computername.endswith('$'):
468             samaccountname = "%s$" % computername
469
470         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
471                   (dsdb.ATYPE_WORKSTATION_TRUST,
472                    ldb.binary_encode(samaccountname)))
473
474         domaindn = samdb.domain_dn()
475
476         try:
477             res = samdb.search(base=domaindn,
478                                expression=filter,
479                                scope=ldb.SCOPE_SUBTREE)
480             computer_dn = res[0].dn
481         except IndexError:
482             raise CommandError('Unable to find computer "%s"' % (computername))
483
484         if len(res) != 1:
485             raise CommandError('Invalid number of results: for "%s": %d' %
486                                ((computername), len(res)))
487
488         msg = res[0]
489         result_ldif = common.get_ldif_for_editor(samdb, msg)
490
491         if editor is None:
492             editor = os.environ.get('EDITOR')
493             if editor is None:
494                 editor = 'vi'
495
496         with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
497             t_file.write(get_bytes(result_ldif))
498             t_file.flush()
499             try:
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()
505
506         msgs_edited = samdb.parse_ldif(edited_message)
507         msg_edited = next(msgs_edited)[1]
508
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")
512             return
513
514         try:
515             samdb.modify(res_msg_diff)
516         except Exception as e:
517             raise CommandError("Failed to modify computer '%s': " %
518                                (computername, e))
519
520         self.outf.write("Modified computer '%s' successfully\n" % computername)
521
522 class cmd_computer_list(Command):
523     """List all computers."""
524
525     synopsis = "%prog [options]"
526
527     takes_options = [
528         Option("-H", "--URL", help="LDB URL for database or target server",
529                type=str, metavar="URL", dest="H"),
530     ]
531
532     takes_optiongroups = {
533         "sambaopts": options.SambaOptions,
534         "credopts": options.CredentialsOptions,
535         "versionopts": options.VersionOptions,
536     }
537
538     def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
539         lp = sambaopts.get_loadparm()
540         creds = credopts.get_credentials(lp, fallback_machine=True)
541
542         samdb = SamDB(url=H, session_info=system_session(),
543                       credentials=creds, lp=lp)
544
545         filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
546
547         domain_dn = samdb.domain_dn()
548         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
549                            expression=filter,
550                            attrs=["samaccountname"])
551         if (len(res) == 0):
552             return
553
554         for msg in res:
555             self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
556
557
558 class cmd_computer_show(Command):
559     """Display a computer AD object.
560
561 This command displays a computer account and it's attributes in the Active
562 Directory domain.
563 The computername specified on the command is the sAMAccountName.
564
565 The command may be run from the root userid or another authorized
566 userid.
567
568 The -H or --URL= option can be used to execute the command against a remote
569 server.
570
571 Example1:
572 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\
573     -U administrator
574
575 Example1 shows how display a computers attributes in the domain against a
576 remote LDAP server.
577
578 The -H parameter is used to specify the remote target server.
579
580 Example2:
581 samba-tool computer show Computer2
582
583 Example2 shows how to display a computers attributes in the domain against a
584 local LDAP server.
585
586 Example3:
587 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
588
589 Example3 shows how to display a computers objectSid and operatingSystem
590 attribute.
591 """
592     synopsis = "%prog <computername> [options]"
593
594     takes_options = [
595         Option("-H", "--URL", help="LDB URL for database or target server",
596                type=str, metavar="URL", dest="H"),
597         Option("--attributes",
598                help=("Comma separated list of attributes, "
599                      "which will be printed."),
600                type=str, dest="computer_attrs"),
601     ]
602
603     takes_args = ["computername"]
604     takes_optiongroups = {
605         "sambaopts": options.SambaOptions,
606         "credopts": options.CredentialsOptions,
607         "versionopts": options.VersionOptions,
608     }
609
610     def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
611             H=None, computer_attrs=None):
612
613         lp = sambaopts.get_loadparm()
614         creds = credopts.get_credentials(lp, fallback_machine=True)
615         samdb = SamDB(url=H, session_info=system_session(),
616                       credentials=creds, lp=lp)
617
618         attrs = None
619         if computer_attrs:
620             attrs = computer_attrs.split(",")
621
622         samaccountname = computername
623         if not computername.endswith('$'):
624             samaccountname = "%s$" % computername
625
626         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
627                   (dsdb.ATYPE_WORKSTATION_TRUST,
628                    ldb.binary_encode(samaccountname)))
629
630         domaindn = samdb.domain_dn()
631
632         try:
633             res = samdb.search(base=domaindn, expression=filter,
634                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
635             computer_dn = res[0].dn
636         except IndexError:
637             raise CommandError('Unable to find computer "%s"' %
638                                samaccountname)
639
640         for msg in res:
641             computer_ldif = common.get_ldif_for_editor(samdb, msg)
642             self.outf.write(computer_ldif)
643
644
645 class cmd_computer_move(Command):
646     """Move a computer to an organizational unit/container."""
647
648     synopsis = "%prog computername <new_ou_dn> [options]"
649
650     takes_options = [
651         Option("-H", "--URL", help="LDB URL for database or target server",
652                type=str, metavar="URL", dest="H"),
653     ]
654
655     takes_args = ["computername", "new_ou_dn"]
656     takes_optiongroups = {
657         "sambaopts": options.SambaOptions,
658         "credopts": options.CredentialsOptions,
659         "versionopts": options.VersionOptions,
660     }
661
662     def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
663             versionopts=None, H=None):
664         lp = sambaopts.get_loadparm()
665         creds = credopts.get_credentials(lp, fallback_machine=True)
666         samdb = SamDB(url=H, session_info=system_session(),
667                       credentials=creds, lp=lp)
668         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
669
670         samaccountname = computername
671         if not computername.endswith('$'):
672             samaccountname = "%s$" % computername
673
674         filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
675                   (ldb.binary_encode(samaccountname),
676                    dsdb.ATYPE_WORKSTATION_TRUST))
677         try:
678             res = samdb.search(base=domain_dn,
679                                expression=filter,
680                                scope=ldb.SCOPE_SUBTREE)
681             computer_dn = res[0].dn
682         except IndexError:
683             raise CommandError('Unable to find computer "%s"' % (computername))
684
685         full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
686         if not full_new_ou_dn.is_child_of(domain_dn):
687             full_new_ou_dn.add_base(domain_dn)
688         new_computer_dn = ldb.Dn(samdb, str(computer_dn))
689         new_computer_dn.remove_base_components(len(computer_dn) -1)
690         new_computer_dn.add_base(full_new_ou_dn)
691         try:
692             samdb.rename(computer_dn, new_computer_dn)
693         except Exception as e:
694             raise CommandError('Failed to move computer "%s"' % computername, e)
695         self.outf.write('Moved computer "%s" to "%s"\n' %
696                         (computername, new_ou_dn))
697
698
699 class cmd_computer(SuperCommand):
700     """Computer management."""
701
702     subcommands = {}
703     subcommands["create"] = cmd_computer_create()
704     subcommands["delete"] = cmd_computer_delete()
705     subcommands["edit"] = cmd_computer_edit()
706     subcommands["list"] = cmd_computer_list()
707     subcommands["show"] = cmd_computer_show()
708     subcommands["move"] = cmd_computer_move()