PEP8: fix E128: continuation line under-indented for visual indent
[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 from samba import sd_utils
29 from samba.dcerpc import dnsserver, dnsp, security
30 from samba.dnsserver import ARecord, AAAARecord
31 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
32 from samba.remove_dc import remove_dns_references
33 from samba.auth import system_session
34 from samba.samdb import SamDB
35
36 from samba import (
37     credentials,
38     dsdb,
39     Ldb,
40     werror,
41     WERRORError
42 )
43
44 from samba.netcmd import (
45     Command,
46     CommandError,
47     SuperCommand,
48     Option,
49 )
50
51
52 def _is_valid_ip(ip_string, address_families=None):
53     """Check ip string is valid address"""
54     # by default, check both ipv4 and ipv6
55     if not address_families:
56         address_families = [socket.AF_INET, socket.AF_INET6]
57
58     for address_family in address_families:
59         try:
60             socket.inet_pton(address_family, ip_string)
61             return True  # if no error, return directly
62         except socket.error:
63             continue  # Otherwise, check next family
64     return False
65
66
67 def _is_valid_ipv4(ip_string):
68     """Check ip string is valid ipv4 address"""
69     return _is_valid_ip(ip_string, address_families=[socket.AF_INET])
70
71
72 def _is_valid_ipv6(ip_string):
73     """Check ip string is valid ipv6 address"""
74     return _is_valid_ip(ip_string, address_families=[socket.AF_INET6])
75
76
77 def add_dns_records(
78         samdb, name, dns_conn, change_owner_sd,
79         server, ip_address_list, logger):
80     """Add DNS A or AAAA records while creating computer. """
81     name = name.rstrip('$')
82     client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
83     select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN
84     zone = samdb.domain_dns_name()
85     name_found = True
86     sd_helper = sd_utils.SDUtils(samdb)
87
88     try:
89         buflen, res = dns_conn.DnssrvEnumRecords2(
90             client_version,
91             0,
92             server,
93             zone,
94             name,
95             None,
96             dnsp.DNS_TYPE_ALL,
97             select_flags,
98             None,
99             None,
100         )
101     except WERRORError as e:
102         if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
103             name_found = False
104             pass
105
106     if name_found:
107         for rec in res.rec:
108             for record in rec.records:
109                 if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
110                     # delete record
111                     del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
112                     del_rec_buf.rec = record
113                     try:
114                         dns_conn.DnssrvUpdateRecord2(
115                             client_version,
116                             0,
117                             server,
118                             zone,
119                             name,
120                             None,
121                             del_rec_buf,
122                         )
123                     except WERRORError as e:
124                         if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
125                             raise
126
127     for ip_address in ip_address_list:
128         if _is_valid_ipv6(ip_address):
129             logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % (
130                 name, zone, ip_address))
131             rec = AAAARecord(ip_address)
132         elif _is_valid_ipv4(ip_address):
133             logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % (
134                 name, zone, ip_address))
135             rec = ARecord(ip_address)
136         else:
137             raise ValueError('Invalid IP: {}'.format(ip_address))
138
139         # Add record
140         add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
141         add_rec_buf.rec = rec
142
143         dns_conn.DnssrvUpdateRecord2(
144             client_version,
145             0,
146             server,
147             zone,
148             name,
149             add_rec_buf,
150             None,
151         )
152
153     if (len(ip_address_list) > 0):
154         domaindns_zone_dn = ldb.Dn(
155             samdb,
156             'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
157         )
158
159         dns_a_dn, ldap_record = samdb.dns_lookup(
160             "%s.%s" % (name, zone),
161             dns_partition=domaindns_zone_dn,
162         )
163
164         # Make the DC own the DNS record, not the administrator
165         sd_helper.modify_sd_on_dn(
166             dns_a_dn,
167             change_owner_sd,
168             controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
169         )
170
171
172 class cmd_computer_create(Command):
173     """Create a new computer.
174
175 This command creates a new computer account in the Active Directory domain.
176 The computername specified on the command is the sAMaccountName without the
177 trailing $ (dollar sign).
178
179 User accounts may represent physical entities, such as workstations. Computer
180 accounts are also referred to as security principals and are assigned a
181 security identifier (SID).
182
183 Example1:
184 samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \
185     -Uadministrator%passw1rd
186
187 Example1 shows how to create a new computer in the domain against a remote LDAP
188 server. The -H parameter is used to specify the remote target server. The -U
189 option is used to pass the userid and password authorized to issue the command
190 remotely.
191
192 Example2:
193 sudo samba-tool computer create Computer2
194
195 Example2 shows how to create a new computer in the domain against the local
196 server. sudo is used so a user may run the command as root.
197
198 Example3:
199 samba-tool computer create Computer3 --computerou='OU=OrgUnit'
200
201 Example3 shows how to create a new computer in the OrgUnit organizational unit.
202
203 """
204     synopsis = "%prog <computername> [options]"
205
206     takes_options = [
207         Option("-H", "--URL", help="LDB URL for database or target server",
208                type=str, metavar="URL", dest="H"),
209         Option("--computerou",
210                help=("DN of alternative location (with or without domainDN "
211                      "counterpart) to default CN=Computers in which new "
212                      "computer object will be created. E.g. 'OU=<OU name>'"),
213                type=str),
214         Option("--description", help="Computers's description", type=str),
215         Option("--prepare-oldjoin",
216                help="Prepare enabled machine account for oldjoin mechanism",
217                action="store_true"),
218         Option("--ip-address",
219                dest='ip_address_list',
220                help=("IPv4 address for the computer's A record, or IPv6 "
221                      "address for AAAA record, can be provided multiple "
222                      "times"),
223                action='append'),
224         Option("--service-principal-name",
225                dest='service_principal_name_list',
226                help=("Computer's Service Principal Name, can be provided "
227                      "multiple times"),
228                action='append')
229     ]
230
231     takes_args = ["computername"]
232
233     takes_optiongroups = {
234         "sambaopts": options.SambaOptions,
235         "credopts": options.CredentialsOptions,
236         "versionopts": options.VersionOptions,
237     }
238
239     def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
240             H=None, computerou=None, description=None, prepare_oldjoin=False,
241             ip_address_list=None, service_principal_name_list=None):
242
243         if ip_address_list is None:
244             ip_address_list = []
245
246         if service_principal_name_list is None:
247             service_principal_name_list = []
248
249         # check each IP address if provided
250         for ip_address in ip_address_list:
251             if not _is_valid_ip(ip_address):
252                 raise CommandError('Invalid IP address {}'.format(ip_address))
253
254         lp = sambaopts.get_loadparm()
255         creds = credopts.get_credentials(lp)
256
257         try:
258             samdb = SamDB(url=H, session_info=system_session(),
259                           credentials=creds, lp=lp)
260             samdb.newcomputer(computername, computerou=computerou,
261                               description=description,
262                               prepare_oldjoin=prepare_oldjoin,
263                               ip_address_list=ip_address_list,
264                               service_principal_name_list=service_principal_name_list,
265                               )
266
267             if ip_address_list:
268                 # if ip_address_list provided, then we need to create DNS
269                 # records for this computer.
270
271                 hostname = re.sub(r"\$$", "", computername)
272                 if hostname.count('$'):
273                     raise CommandError('Illegal computername "%s"' % computername)
274
275                 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
276                     ldb.binary_encode(hostname))
277
278                 recs = samdb.search(
279                     base=samdb.domain_dn(),
280                     scope=ldb.SCOPE_SUBTREE,
281                     expression=filters,
282                     attrs=['primaryGroupID', 'objectSid'])
283
284                 group = recs[0]['primaryGroupID'][0]
285                 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
286
287                 dns_conn = dnsserver.dnsserver(
288                     "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
289                     lp, creds)
290
291                 change_owner_sd = security.descriptor()
292                 change_owner_sd.owner_sid = owner
293                 change_owner_sd.group_sid = security.dom_sid(
294                     "{}-{}".format(samdb.get_domain_sid(), group),
295                 )
296
297                 add_dns_records(
298                     samdb, hostname, dns_conn,
299                     change_owner_sd, samdb.host_dns_name(),
300                     ip_address_list, self.get_logger())
301         except Exception as e:
302             raise CommandError("Failed to create computer '%s': " %
303                                computername, e)
304
305         self.outf.write("Computer '%s' created successfully\n" % computername)
306
307
308 class cmd_computer_delete(Command):
309     """Delete a computer.
310
311 This command deletes a computer account from the Active Directory domain. The
312 computername specified on the command is the sAMAccountName without the
313 trailing $ (dollar sign).
314
315 Once the account is deleted, all permissions and memberships associated with
316 that account are deleted. If a new computer account is added with the same name
317 as a previously deleted account name, the new computer does not have the
318 previous permissions. The new account computer will be assigned a new security
319 identifier (SID) and permissions and memberships will have to be added.
320
321 The command may be run from the root userid or another authorized
322 userid. The -H or --URL= option can be used to execute the command against
323 a remote server.
324
325 Example1:
326 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \
327     -Uadministrator%passw1rd
328
329 Example1 shows how to delete a computer in the domain against a remote LDAP
330 server. The -H parameter is used to specify the remote target server. The
331 --computername= and --password= options are used to pass the computername and
332 password of a computer that exists on the remote server and is authorized to
333 issue the command on that server.
334
335 Example2:
336 sudo samba-tool computer delete Computer2
337
338 Example2 shows how to delete a computer in the domain against the local server.
339 sudo is used so a computer may run the command as root.
340
341 """
342     synopsis = "%prog <computername> [options]"
343
344     takes_options = [
345         Option("-H", "--URL", help="LDB URL for database or target server",
346                type=str, metavar="URL", dest="H"),
347     ]
348
349     takes_args = ["computername"]
350     takes_optiongroups = {
351         "sambaopts": options.SambaOptions,
352         "credopts": options.CredentialsOptions,
353         "versionopts": options.VersionOptions,
354     }
355
356     def run(self, computername, credopts=None, sambaopts=None,
357             versionopts=None, H=None):
358         lp = sambaopts.get_loadparm()
359         creds = credopts.get_credentials(lp, fallback_machine=True)
360
361         samdb = SamDB(url=H, session_info=system_session(),
362                       credentials=creds, lp=lp)
363
364         samaccountname = computername
365         if not computername.endswith('$'):
366             samaccountname = "%s$" % computername
367
368         filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
369                   (ldb.binary_encode(samaccountname),
370                    dsdb.ATYPE_WORKSTATION_TRUST))
371         try:
372             res = samdb.search(base=samdb.domain_dn(),
373                                scope=ldb.SCOPE_SUBTREE,
374                                expression=filter,
375                                attrs=["userAccountControl", "dNSHostName"])
376             computer_dn = res[0].dn
377             computer_ac = int(res[0]["userAccountControl"][0])
378             if "dNSHostName" in res[0]:
379                 computer_dns_host_name = res[0]["dNSHostName"][0]
380             else:
381                 computer_dns_host_name = None
382         except IndexError:
383             raise CommandError('Unable to find computer "%s"' % computername)
384
385         computer_is_workstation = (
386             computer_ac & dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
387         if computer_is_workstation == False:
388             raise CommandError('Failed to remove computer "%s": '
389                                'Computer is not a workstation - removal denied'
390                                % computername)
391         try:
392             samdb.delete(computer_dn)
393             if computer_dns_host_name:
394                 remove_dns_references(
395                     samdb, self.get_logger(), computer_dns_host_name,
396                     ignore_no_name=True)
397         except Exception as e:
398             raise CommandError('Failed to remove computer "%s"' %
399                                samaccountname, e)
400         self.outf.write("Deleted computer %s\n" % computername)
401
402
403 class cmd_computer_list(Command):
404     """List all computers."""
405
406     synopsis = "%prog [options]"
407
408     takes_options = [
409         Option("-H", "--URL", help="LDB URL for database or target server",
410                type=str, metavar="URL", dest="H"),
411     ]
412
413     takes_optiongroups = {
414         "sambaopts": options.SambaOptions,
415         "credopts": options.CredentialsOptions,
416         "versionopts": options.VersionOptions,
417     }
418
419     def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
420         lp = sambaopts.get_loadparm()
421         creds = credopts.get_credentials(lp, fallback_machine=True)
422
423         samdb = SamDB(url=H, session_info=system_session(),
424                       credentials=creds, lp=lp)
425
426         filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
427
428         domain_dn = samdb.domain_dn()
429         res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
430                            expression=filter,
431                            attrs=["samaccountname"])
432         if (len(res) == 0):
433             return
434
435         for msg in res:
436             self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
437
438 class cmd_computer_show(Command):
439     """Display a computer AD object.
440
441 This command displays a computer account and it's attributes in the Active
442 Directory domain.
443 The computername specified on the command is the sAMAccountName.
444
445 The command may be run from the root userid or another authorized
446 userid.
447
448 The -H or --URL= option can be used to execute the command against a remote
449 server.
450
451 Example1:
452 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \
453     -U administrator
454
455 Example1 shows how display a computers attributes in the domain against a
456 remote LDAP server.
457
458 The -H parameter is used to specify the remote target server.
459
460 Example2:
461 samba-tool computer show Computer2
462
463 Example2 shows how to display a computers attributes in the domain against a
464 local LDAP server.
465
466 Example3:
467 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
468
469 Example3 shows how to display a computers objectSid and operatingSystem
470 attribute.
471 """
472     synopsis = "%prog <computername> [options]"
473
474     takes_options = [
475         Option("-H", "--URL", help="LDB URL for database or target server",
476                type=str, metavar="URL", dest="H"),
477         Option("--attributes",
478                help=("Comma separated list of attributes, "
479                      "which will be printed."),
480                type=str, dest="computer_attrs"),
481     ]
482
483     takes_args = ["computername"]
484     takes_optiongroups = {
485         "sambaopts": options.SambaOptions,
486         "credopts": options.CredentialsOptions,
487         "versionopts": options.VersionOptions,
488     }
489
490     def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
491             H=None, computer_attrs=None):
492
493         lp = sambaopts.get_loadparm()
494         creds = credopts.get_credentials(lp, fallback_machine=True)
495         samdb = SamDB(url=H, session_info=system_session(),
496                       credentials=creds, lp=lp)
497
498         attrs = None
499         if computer_attrs:
500             attrs = computer_attrs.split(",")
501
502         samaccountname = computername
503         if not computername.endswith('$'):
504             samaccountname = "%s$" % computername
505
506         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
507                   (dsdb.ATYPE_WORKSTATION_TRUST,
508                    ldb.binary_encode(samaccountname)))
509
510         domaindn = samdb.domain_dn()
511
512         try:
513             res = samdb.search(base=domaindn, expression=filter,
514                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
515             computer_dn = res[0].dn
516         except IndexError:
517             raise CommandError('Unable to find computer "%s"' %
518                                samaccountname)
519
520         for msg in res:
521             computer_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
522             self.outf.write(computer_ldif)
523
524 class cmd_computer_move(Command):
525     """Move a computer to an organizational unit/container."""
526
527     synopsis = "%prog computername <new_ou_dn> [options]"
528
529     takes_options = [
530         Option("-H", "--URL", help="LDB URL for database or target server",
531                type=str, metavar="URL", dest="H"),
532     ]
533
534     takes_args = [ "computername", "new_ou_dn" ]
535     takes_optiongroups = {
536         "sambaopts": options.SambaOptions,
537         "credopts": options.CredentialsOptions,
538         "versionopts": options.VersionOptions,
539     }
540
541     def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
542             versionopts=None, H=None):
543         lp = sambaopts.get_loadparm()
544         creds = credopts.get_credentials(lp, fallback_machine=True)
545         samdb = SamDB(url=H, session_info=system_session(),
546                       credentials=creds, lp=lp)
547         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
548
549         samaccountname = computername
550         if not computername.endswith('$'):
551             samaccountname = "%s$" % computername
552
553         filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
554                   (ldb.binary_encode(samaccountname),
555                    dsdb.ATYPE_WORKSTATION_TRUST))
556         try:
557             res = samdb.search(base=domain_dn,
558                                expression=filter,
559                                scope=ldb.SCOPE_SUBTREE)
560             computer_dn = res[0].dn
561         except IndexError:
562             raise CommandError('Unable to find computer "%s"' % (computername))
563
564         full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
565         if not full_new_ou_dn.is_child_of(domain_dn):
566             full_new_ou_dn.add_base(domain_dn)
567         new_computer_dn = ldb.Dn(samdb, str(computer_dn))
568         new_computer_dn.remove_base_components(len(computer_dn)-1)
569         new_computer_dn.add_base(full_new_ou_dn)
570         try:
571             samdb.rename(computer_dn, new_computer_dn)
572         except Exception as e:
573             raise CommandError('Failed to move computer "%s"' % computername, e)
574         self.outf.write('Moved computer "%s" to "%s"\n' %
575                         (computername, new_ou_dn))
576
577
578 class cmd_computer(SuperCommand):
579     """Computer management."""
580
581     subcommands = {}
582     subcommands["create"] = cmd_computer_create()
583     subcommands["delete"] = cmd_computer_delete()
584     subcommands["list"] = cmd_computer_list()
585     subcommands["show"] = cmd_computer_show()
586     subcommands["move"] = cmd_computer_move()