python/samba/netcmd: changes for samab.tests.samba_tool.computer
[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 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 = str(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 not computer_is_workstation:
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
439 class cmd_computer_show(Command):
440     """Display a computer AD object.
441
442 This command displays a computer account and it's attributes in the Active
443 Directory domain.
444 The computername specified on the command is the sAMAccountName.
445
446 The command may be run from the root userid or another authorized
447 userid.
448
449 The -H or --URL= option can be used to execute the command against a remote
450 server.
451
452 Example1:
453 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \
454     -U administrator
455
456 Example1 shows how display a computers attributes in the domain against a
457 remote LDAP server.
458
459 The -H parameter is used to specify the remote target server.
460
461 Example2:
462 samba-tool computer show Computer2
463
464 Example2 shows how to display a computers attributes in the domain against a
465 local LDAP server.
466
467 Example3:
468 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
469
470 Example3 shows how to display a computers objectSid and operatingSystem
471 attribute.
472 """
473     synopsis = "%prog <computername> [options]"
474
475     takes_options = [
476         Option("-H", "--URL", help="LDB URL for database or target server",
477                type=str, metavar="URL", dest="H"),
478         Option("--attributes",
479                help=("Comma separated list of attributes, "
480                      "which will be printed."),
481                type=str, dest="computer_attrs"),
482     ]
483
484     takes_args = ["computername"]
485     takes_optiongroups = {
486         "sambaopts": options.SambaOptions,
487         "credopts": options.CredentialsOptions,
488         "versionopts": options.VersionOptions,
489     }
490
491     def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
492             H=None, computer_attrs=None):
493
494         lp = sambaopts.get_loadparm()
495         creds = credopts.get_credentials(lp, fallback_machine=True)
496         samdb = SamDB(url=H, session_info=system_session(),
497                       credentials=creds, lp=lp)
498
499         attrs = None
500         if computer_attrs:
501             attrs = computer_attrs.split(",")
502
503         samaccountname = computername
504         if not computername.endswith('$'):
505             samaccountname = "%s$" % computername
506
507         filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
508                   (dsdb.ATYPE_WORKSTATION_TRUST,
509                    ldb.binary_encode(samaccountname)))
510
511         domaindn = samdb.domain_dn()
512
513         try:
514             res = samdb.search(base=domaindn, expression=filter,
515                                scope=ldb.SCOPE_SUBTREE, attrs=attrs)
516             computer_dn = res[0].dn
517         except IndexError:
518             raise CommandError('Unable to find computer "%s"' %
519                                samaccountname)
520
521         for msg in res:
522             computer_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
523             self.outf.write(computer_ldif)
524
525
526 class cmd_computer_move(Command):
527     """Move a computer to an organizational unit/container."""
528
529     synopsis = "%prog computername <new_ou_dn> [options]"
530
531     takes_options = [
532         Option("-H", "--URL", help="LDB URL for database or target server",
533                type=str, metavar="URL", dest="H"),
534     ]
535
536     takes_args = ["computername", "new_ou_dn"]
537     takes_optiongroups = {
538         "sambaopts": options.SambaOptions,
539         "credopts": options.CredentialsOptions,
540         "versionopts": options.VersionOptions,
541     }
542
543     def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
544             versionopts=None, H=None):
545         lp = sambaopts.get_loadparm()
546         creds = credopts.get_credentials(lp, fallback_machine=True)
547         samdb = SamDB(url=H, session_info=system_session(),
548                       credentials=creds, lp=lp)
549         domain_dn = ldb.Dn(samdb, samdb.domain_dn())
550
551         samaccountname = computername
552         if not computername.endswith('$'):
553             samaccountname = "%s$" % computername
554
555         filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
556                   (ldb.binary_encode(samaccountname),
557                    dsdb.ATYPE_WORKSTATION_TRUST))
558         try:
559             res = samdb.search(base=domain_dn,
560                                expression=filter,
561                                scope=ldb.SCOPE_SUBTREE)
562             computer_dn = res[0].dn
563         except IndexError:
564             raise CommandError('Unable to find computer "%s"' % (computername))
565
566         full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
567         if not full_new_ou_dn.is_child_of(domain_dn):
568             full_new_ou_dn.add_base(domain_dn)
569         new_computer_dn = ldb.Dn(samdb, str(computer_dn))
570         new_computer_dn.remove_base_components(len(computer_dn) -1)
571         new_computer_dn.add_base(full_new_ou_dn)
572         try:
573             samdb.rename(computer_dn, new_computer_dn)
574         except Exception as e:
575             raise CommandError('Failed to move computer "%s"' % computername, e)
576         self.outf.write('Moved computer "%s" to "%s"\n' %
577                         (computername, new_ou_dn))
578
579
580 class cmd_computer(SuperCommand):
581     """Computer management."""
582
583     subcommands = {}
584     subcommands["create"] = cmd_computer_create()
585     subcommands["delete"] = cmd_computer_delete()
586     subcommands["list"] = cmd_computer_list()
587     subcommands["show"] = cmd_computer_show()
588     subcommands["move"] = cmd_computer_move()