domain.py 196 KB
Newer Older
1
2
3
4
# domain management
#
# Copyright Matthias Dieter Wallnoefer 2009
# Copyright Andrew Kroeger 2009
5
# Copyright Jelmer Vernooij 2007-2012
6
# Copyright Giampaolo Lauria 2011
7
# Copyright Matthieu Patou <mat@matws.net> 2011
8
# Copyright Andrew Bartlett 2008-2015
9
# Copyright Stefan Metzmacher 2012
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

25
from __future__ import print_function
26
from __future__ import division
27
28
import samba.getopt as options
import ldb
29
import os
30
import sys
31
32
import ctypes
import random
33
34
import tempfile
import logging
35
import subprocess
36
import time
37
import shutil
38
from samba import ntstatus
39
from samba import NTSTATUSError
40
from samba import werror
41
from getpass import getpass
42
from samba.net import Net, LIBNET_JOIN_AUTOMATIC
43
import samba.ntacls
44
from samba.join import join_RODC, join_DC, join_subdomain
45
from samba.auth import system_session
46
from samba.samdb import SamDB, get_default_backend_store
47
from samba.ndr import ndr_pack, ndr_print
48
from samba.dcerpc import drsuapi
49
50
51
from samba.dcerpc import drsblobs
from samba.dcerpc import lsa
from samba.dcerpc import netlogon
52
from samba.dcerpc import security
53
from samba.dcerpc import nbt
54
from samba.dcerpc import misc
55
56
57
58
59
60
from samba.dcerpc.samr import DOMAIN_PASSWORD_COMPLEX, DOMAIN_PASSWORD_STORE_CLEARTEXT
from samba.netcmd import (
    Command,
    CommandError,
    SuperCommand,
    Option
61
)
62
from samba.netcmd.fsmo import get_fsmo_roleowner
63
from samba.netcmd.common import netcmd_get_domain_infos_via_cldap
64
65
66
from samba.samba3 import Samba3
from samba.samba3 import param as s3param
from samba.upgrade import upgrade_from_samba3
67
from samba.drs_utils import drsuapi_connect
68
from samba import remove_dc, arcfour_encrypt, string_to_byte_array
69

70
71
72
73
74
75
from samba.dsdb import (
    DS_DOMAIN_FUNCTION_2000,
    DS_DOMAIN_FUNCTION_2003,
    DS_DOMAIN_FUNCTION_2003_MIXED,
    DS_DOMAIN_FUNCTION_2008,
    DS_DOMAIN_FUNCTION_2008_R2,
76
77
    DS_DOMAIN_FUNCTION_2012,
    DS_DOMAIN_FUNCTION_2012_R2,
78
79
80
81
    DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL,
    DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL,
    UF_WORKSTATION_TRUST_ACCOUNT,
    UF_SERVER_TRUST_ACCOUNT,
82
83
    UF_TRUSTED_FOR_DELEGATION,
    UF_PARTIAL_SECRETS_ACCOUNT
84
)
85

86
87
from samba.provision import (
    provision,
88
    ProvisioningError,
89
90
    DEFAULT_MIN_PWD_LENGTH,
    setup_path
91
)
92
93

from samba.provision.common import (
94
95
    FILL_FULL,
    FILL_NT4SYNC,
96
97
    FILL_DRS
)
98

99
from samba.netcmd.pso import cmd_domain_passwordsettings_pso
100
from samba.netcmd.domain_backup import cmd_domain_backup
101

102
from samba.compat import binary_type
103
from samba.compat import get_string
104

105
string_version_to_constant = {
106
    "2008_R2": DS_DOMAIN_FUNCTION_2008_R2,
107
108
109
110
    "2012": DS_DOMAIN_FUNCTION_2012,
    "2012_R2": DS_DOMAIN_FUNCTION_2012_R2,
}

111
common_provision_join_options = [
112
113
    Option("--machinepass", type="string", metavar="PASSWORD",
           help="choose machine password (otherwise random)"),
114
115
116
    Option("--plaintext-secrets", action="store_true",
           help="Store secret/sensitive values as plain text on disk" +
           "(default is to encrypt secret/ensitive values)"),
117
118
119
120
    Option("--backend-store", type="choice", metavar="BACKENDSTORE",
           choices=["tdb", "mdb"],
           help="Specify the database backend to be used "
           "(default is %s)" % get_default_backend_store()),
121
    Option("--targetdir", metavar="DIR",
122
           help="Set target directory (where to store provision)", type=str),
123
    Option("-q", "--quiet", help="Be quiet", action="store_true"),
124
125
]

126
127
128
129
130
131
132
133
134
135
136
137
common_join_options = [
    Option("--server", help="DC to join", type=str),
    Option("--site", help="site to join", type=str),
    Option("--domain-critical-only",
           help="only replicate critical domain objects",
           action="store_true"),
    Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND",
           choices=["SAMBA_INTERNAL", "BIND9_DLZ", "NONE"],
           help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), "
           "BIND9_DLZ uses samba4 AD to store zone information, "
           "NONE skips the DNS setup entirely (this DC will not be a DNS server)",
           default="SAMBA_INTERNAL"),
138
    Option("-v", "--verbose", help="Be verbose", action="store_true")
139
140
]

141
common_ntvfs_options = [
142
143
    Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)",
           action="store_true")
144
145
]

146

147
def get_testparm_var(testparm, smbconf, varname):
148
149
150
151
    errfile = open(os.devnull, 'w')
    p = subprocess.Popen([testparm, '-s', '-l',
                          '--parameter-name=%s' % varname, smbconf],
                         stdout=subprocess.PIPE, stderr=errfile)
152
    (out, err) = p.communicate()
153
    errfile.close()
154
    lines = out.split(b'\n')
155
    if lines:
156
        return get_string(lines[0]).strip()
157
    return ""
158

159

160
try:
161
    import samba.dckeytab
162
except ImportError:
163
    cmd_domain_export_keytab = None
164
else:
165
166
    class cmd_domain_export_keytab(Command):
        """Dump Kerberos keys of the domain into a keytab."""
167

168
        synopsis = "%prog <keytab> [options]"
169

170
171
172
173
        takes_optiongroups = {
            "sambaopts": options.SambaOptions,
            "credopts": options.CredentialsOptions,
            "versionopts": options.VersionOptions,
174
        }
175

176
177
        takes_options = [
            Option("--principal", help="extract only this principal", type=str),
178
        ]
179

180
        takes_args = ["keytab"]
181

182
183
184
185
        def run(self, keytab, credopts=None, sambaopts=None, versionopts=None, principal=None):
            lp = sambaopts.get_loadparm()
            net = Net(None, lp)
            net.export_keytab(keytab=keytab, principal=principal)
186

187

188
class cmd_domain_info(Command):
189
    """Print basic info about a domain and the DC passed as parameter."""
190

191
    synopsis = "%prog <ip_address> [options]"
192
193

    takes_options = [
194
    ]
195

196
197
198
199
    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "credopts": options.CredentialsOptions,
        "versionopts": options.VersionOptions,
200
    }
201

202
203
204
205
    takes_args = ["address"]

    def run(self, address, credopts=None, sambaopts=None, versionopts=None):
        lp = sambaopts.get_loadparm()
206
207
208
209
        try:
            res = netcmd_get_domain_infos_via_cldap(lp, None, address)
        except RuntimeError:
            raise CommandError("Invalid IP address '" + address + "'!")
210
211
212
213
214
215
216
217
        self.outf.write("Forest           : %s\n" % res.forest)
        self.outf.write("Domain           : %s\n" % res.dns_domain)
        self.outf.write("Netbios domain   : %s\n" % res.domain_name)
        self.outf.write("DC name          : %s\n" % res.pdc_dns_name)
        self.outf.write("DC netbios name  : %s\n" % res.pdc_name)
        self.outf.write("Server site      : %s\n" % res.server_site)
        self.outf.write("Client site      : %s\n" % res.client_site)

218

219
class cmd_domain_provision(Command):
220
    """Provision a domain."""
221

222
    synopsis = "%prog [options]"
223
224
225
226
227
228
229

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
    }

    takes_options = [
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
        Option("--interactive", help="Ask for names", action="store_true"),
        Option("--domain", type="string", metavar="DOMAIN",
               help="NetBIOS domain name to use"),
        Option("--domain-guid", type="string", metavar="GUID",
               help="set domainguid (otherwise random)"),
        Option("--domain-sid", type="string", metavar="SID",
               help="set domainsid (otherwise random)"),
        Option("--ntds-guid", type="string", metavar="GUID",
               help="set NTDS object GUID (otherwise random)"),
        Option("--invocationid", type="string", metavar="GUID",
               help="set invocationid (otherwise random)"),
        Option("--host-name", type="string", metavar="HOSTNAME",
               help="set hostname"),
        Option("--host-ip", type="string", metavar="IPADDRESS",
               help="set IPv4 ipaddress"),
        Option("--host-ip6", type="string", metavar="IP6ADDRESS",
               help="set IPv6 ipaddress"),
        Option("--site", type="string", metavar="SITENAME",
               help="set site name"),
        Option("--adminpass", type="string", metavar="PASSWORD",
               help="choose admin password (otherwise random)"),
        Option("--krbtgtpass", type="string", metavar="PASSWORD",
               help="choose krbtgt password (otherwise random)"),
        Option("--dns-backend", type="choice", metavar="NAMESERVER-BACKEND",
               choices=["SAMBA_INTERNAL", "BIND9_FLATFILE", "BIND9_DLZ", "NONE"],
               help="The DNS server backend. SAMBA_INTERNAL is the builtin name server (default), "
               "BIND9_FLATFILE uses bind9 text database to store zone information, "
               "BIND9_DLZ uses samba4 AD to store zone information, "
               "NONE skips the DNS setup entirely (not recommended)",
               default="SAMBA_INTERNAL"),
        Option("--dnspass", type="string", metavar="PASSWORD",
               help="choose dns password (otherwise random)"),
        Option("--root", type="string", metavar="USERNAME",
               help="choose 'root' unix username"),
        Option("--nobody", type="string", metavar="USERNAME",
               help="choose 'nobody' user"),
        Option("--users", type="string", metavar="GROUPNAME",
               help="choose 'users' group"),
        Option("--blank", action="store_true",
               help="do not add users or groups, just the structure"),
        Option("--server-role", type="choice", metavar="ROLE",
               choices=["domain controller", "dc", "member server", "member", "standalone"],
               help="The server role (domain controller | dc | member server | member | standalone). Default is dc.",
               default="domain controller"),
        Option("--function-level", type="choice", metavar="FOR-FUN-LEVEL",
               choices=["2000", "2003", "2008", "2008_R2"],
               help="The domain and forest function level (2000 | 2003 | 2008 | 2008_R2 - always native). Default is (Windows) 2008_R2 Native.",
               default="2008_R2"),
        Option("--base-schema", type="choice", metavar="BASE-SCHEMA",
               choices=["2008_R2", "2008_R2_old", "2012", "2012_R2"],
               help="The base schema files to use. Default is (Windows) 2008_R2.",
               default="2008_R2"),
        Option("--next-rid", type="int", metavar="NEXTRID", default=1000,
               help="The initial nextRid value (only needed for upgrades).  Default is 1000."),
        Option("--partitions-only",
               help="Configure Samba's partitions, but do not modify them (ie, join a BDC)", action="store_true"),
        Option("--use-rfc2307", action="store_true", help="Use AD to store posix attributes (default = no)"),
287
    ]
288
289

    openldap_options = [
290
291
292
293
294
295
        Option("--ldapadminpass", type="string", metavar="PASSWORD",
               help="choose password to set between Samba and its LDAP backend (otherwise random)"),
        Option("--ldap-backend-type", type="choice", metavar="LDAP-BACKEND-TYPE",
               help="Test initialisation support for unsupported LDAP backend type (fedora-ds or openldap) DO NOT USE",
               choices=["fedora-ds", "openldap"]),
        Option("--ol-mmr-urls", type="string", metavar="LDAPSERVER",
296
               help="List of LDAP-URLS [ ldap://<FQHN>:<PORT>/  (where <PORT> has to be different than 389!) ] separated with comma (\",\") for use with OpenLDAP-MMR (Multi-Master-Replication), e.g.: \"ldap://s4dc1:9000,ldap://s4dc2:9000\""),
297
298
299
300
301
302
303
304
        Option("--ldap-dryrun-mode", help="Configure LDAP backend, but do not run any binaries and exit early.  Used only for the test environment.  DO NOT USE",
               action="store_true"),
        Option("--slapd-path", type="string", metavar="SLAPD-PATH",
               help="Path to slapd for LDAP backend [e.g.:'/usr/local/libexec/slapd']. Required for Setup with LDAP-Backend. OpenLDAP Version >= 2.4.17 should be used."),
        Option("--ldap-backend-extra-port", type="int", metavar="LDAP-BACKEND-EXTRA-PORT", help="Additional TCP port for LDAP backend server (to use for replication)"),
        Option("--ldap-backend-forced-uri", type="string", metavar="LDAP-BACKEND-FORCED-URI",
               help="Force the LDAP backend connection to be to a particular URI.  Use this ONLY for 'existing' backends, or when debugging the interaction with the LDAP backend and you need to intercept the LDA"),
        Option("--ldap-backend-nosync", help="Configure LDAP backend not to call fsync() (for performance in test environments)", action="store_true"),
305
    ]
306

307
    ntvfs_options = [
308
        Option("--use-xattrs", type="choice", choices=["yes", "no", "auto"],
309
310
311
312
313
               metavar="[yes|no|auto]",
               help="Define if we should use the native fs capabilities or a tdb file for "
               "storing attributes likes ntacl when --use-ntvfs is set. "
               "auto tries to make an inteligent guess based on the user rights and system capabilities",
               default="auto")
314
315
    ]

316
317
    takes_options.extend(common_provision_join_options)

318
319
320
    if os.getenv('TEST_LDAP', "no") == "yes":
        takes_options.extend(openldap_options)

321
    if samba.is_ntvfs_fileserver_built():
322
323
        takes_options.extend(common_ntvfs_options)
        takes_options.extend(ntvfs_options)
324

325
326
    takes_args = []

327
    def run(self, sambaopts=None, versionopts=None,
328
329
330
331
332
333
334
335
336
337
            interactive=None,
            domain=None,
            domain_guid=None,
            domain_sid=None,
            ntds_guid=None,
            invocationid=None,
            host_name=None,
            host_ip=None,
            host_ip6=None,
            adminpass=None,
338
            site=None,
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
            krbtgtpass=None,
            machinepass=None,
            dns_backend=None,
            dns_forwarder=None,
            dnspass=None,
            ldapadminpass=None,
            root=None,
            nobody=None,
            users=None,
            quiet=None,
            blank=None,
            ldap_backend_type=None,
            server_role=None,
            function_level=None,
            next_rid=None,
            partitions_only=None,
            targetdir=None,
            ol_mmr_urls=None,
357
            use_xattrs="auto",
358
            slapd_path=None,
359
            use_ntvfs=False,
360
361
362
363
            use_rfc2307=None,
            ldap_backend_nosync=None,
            ldap_backend_extra_port=None,
            ldap_backend_forced_uri=None,
364
            ldap_dryrun_mode=None,
365
            base_schema=None,
366
367
            plaintext_secrets=False,
            backend_store=None):
368

369
        self.logger = self.get_logger(name="provision", quiet=quiet)
370
371
372
373

        lp = sambaopts.get_loadparm()
        smbconf = lp.configfile

374
375
        if dns_forwarder is not None:
            suggested_forwarder = dns_forwarder
376
        else:
377
            suggested_forwarder = self._get_nameserver_ip()
378
379
            if suggested_forwarder is None:
                suggested_forwarder = "none"
380

381
382
383
384
385
386
387
388
389
        if len(self.raw_argv) == 1:
            interactive = True

        if interactive:
            from getpass import getpass
            import socket

            def ask(prompt, default=None):
                if default is not None:
390
                    print("%s [%s]: " % (prompt, default), end=' ')
391
                else:
392
                    print("%s: " % (prompt,), end=' ')
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
                return sys.stdin.readline().rstrip("\n") or default

            try:
                default = socket.getfqdn().split(".", 1)[1].upper()
            except IndexError:
                default = None
            realm = ask("Realm", default)
            if realm in (None, ""):
                raise CommandError("No realm set!")

            try:
                default = realm.split(".")[0]
            except IndexError:
                default = None
            domain = ask("Domain", default)
            if domain is None:
                raise CommandError("No domain set!")

            server_role = ask("Server Role (dc, member, standalone)", "dc")

413
            dns_backend = ask("DNS backend (SAMBA_INTERNAL, BIND9_FLATFILE, BIND9_DLZ, NONE)", "SAMBA_INTERNAL")
414
415
416
            if dns_backend in (None, ''):
                raise CommandError("No DNS backend set!")

417
418
419
420
421
422
            if dns_backend == "SAMBA_INTERNAL":
                dns_forwarder = ask("DNS forwarder IP address (write 'none' to disable forwarding)", suggested_forwarder)
                if dns_forwarder.lower() in (None, 'none'):
                    suggested_forwarder = None
                    dns_forwarder = None

423
424
            while True:
                adminpassplain = getpass("Administrator password: ")
425
426
427
                issue = self._adminpass_issue(adminpassplain)
                if issue:
                    self.errf.write("%s.\n" % issue)
428
429
430
                else:
                    adminpassverify = getpass("Retype password: ")
                    if not adminpassplain == adminpassverify:
431
                        self.errf.write("Sorry, passwords do not match.\n")
432
433
434
435
436
437
438
439
440
441
442
                    else:
                        adminpass = adminpassplain
                        break

        else:
            realm = sambaopts._lp.get('realm')
            if realm is None:
                raise CommandError("No realm set!")
            if domain is None:
                raise CommandError("No domain set!")

443
444
445
446
447
        if adminpass:
            issue = self._adminpass_issue(adminpass)
            if issue:
                raise CommandError(issue)
        else:
448
            self.logger.info("Administrator password will be set randomly!")
449
450
451
452
453
454
455
456
457
458

        if function_level == "2000":
            dom_for_fun_level = DS_DOMAIN_FUNCTION_2000
        elif function_level == "2003":
            dom_for_fun_level = DS_DOMAIN_FUNCTION_2003
        elif function_level == "2008":
            dom_for_fun_level = DS_DOMAIN_FUNCTION_2008
        elif function_level == "2008_R2":
            dom_for_fun_level = DS_DOMAIN_FUNCTION_2008_R2

459
460
461
        if dns_backend == "SAMBA_INTERNAL" and dns_forwarder is None:
            dns_forwarder = suggested_forwarder

462
463
464
465
466
467
468
469
470
471
472
473
474
475
        samdb_fill = FILL_FULL
        if blank:
            samdb_fill = FILL_NT4SYNC
        elif partitions_only:
            samdb_fill = FILL_DRS

        if targetdir is not None:
            if not os.path.isdir(targetdir):
                os.mkdir(targetdir)

        eadb = True

        if use_xattrs == "yes":
            eadb = False
476
        elif use_xattrs == "auto" and use_ntvfs == False:
477
            eadb = False
478
        elif use_ntvfs == False:
479
480
            raise CommandError("--use-xattrs=no requires --use-ntvfs (not supported for production use).  "
                               "Please re-run with --use-xattrs omitted.")
481
482
483
484
485
486
487
488
489
490
491
        elif use_xattrs == "auto" and not lp.get("posix:eadb"):
            if targetdir:
                file = tempfile.NamedTemporaryFile(dir=os.path.abspath(targetdir))
            else:
                file = tempfile.NamedTemporaryFile(dir=os.path.abspath(os.path.dirname(lp.get("private dir"))))
            try:
                try:
                    samba.ntacls.setntacl(lp, file.name,
                                          "O:S-1-5-32G:S-1-5-32", "S-1-5-32", "native")
                    eadb = False
                except Exception:
492
                    self.logger.info("You are not root or your system does not support xattr, using tdb backend for attributes. ")
493
494
495
496
            finally:
                file.close()

        if eadb:
497
            self.logger.info("not using extended attributes to store ACLs and other metadata. If you intend to use this provision in production, rerun the script as root on a system supporting xattrs.")
498
        if ldap_backend_type == "existing":
499
500
            if ldap_backend_forced_uri is not None:
                self.logger.warn("You have specified to use an existing LDAP server as the backend, please make sure an LDAP server is running at %s" % ldap_backend_forced_uri)
501
            else:
502
                self.logger.info("You have specified to use an existing LDAP server as the backend, please make sure an LDAP server is running at the default location")
503
504
        else:
            if ldap_backend_forced_uri is not None:
505
                self.logger.warn("You have specified to use an fixed URI %s for connecting to your LDAP server backend.  This is NOT RECOMMENDED, as our default communiation over ldapi:// is more secure and much less")
506

507
508
509
        if domain_sid is not None:
            domain_sid = security.dom_sid(domain_sid)

510
        session = system_session()
511
512
        if backend_store is None:
            backend_store = get_default_backend_store()
513
        try:
514
            result = provision(self.logger,
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
                               session, smbconf=smbconf, targetdir=targetdir,
                               samdb_fill=samdb_fill, realm=realm, domain=domain,
                               domainguid=domain_guid, domainsid=domain_sid,
                               hostname=host_name,
                               hostip=host_ip, hostip6=host_ip6,
                               sitename=site, ntdsguid=ntds_guid,
                               invocationid=invocationid, adminpass=adminpass,
                               krbtgtpass=krbtgtpass, machinepass=machinepass,
                               dns_backend=dns_backend, dns_forwarder=dns_forwarder,
                               dnspass=dnspass, root=root, nobody=nobody,
                               users=users,
                               serverrole=server_role, dom_for_fun_level=dom_for_fun_level,
                               backend_type=ldap_backend_type,
                               ldapadminpass=ldapadminpass, ol_mmr_urls=ol_mmr_urls, slapd_path=slapd_path,
                               useeadb=eadb, next_rid=next_rid, lp=lp, use_ntvfs=use_ntvfs,
                               use_rfc2307=use_rfc2307, skip_sysvolacl=False,
                               ldap_backend_extra_port=ldap_backend_extra_port,
                               ldap_backend_forced_uri=ldap_backend_forced_uri,
                               nosync=ldap_backend_nosync, ldap_dryrun_mode=ldap_dryrun_mode,
                               base_schema=base_schema,
                               plaintext_secrets=plaintext_secrets,
                               backend_store=backend_store)
537

538
        except ProvisioningError as e:
539
540
            raise CommandError("Provision failed", e)

541
542
543
        result.report_logger(self.logger)

    def _get_nameserver_ip(self):
544
        """Grab the nameserver IP address from /etc/resolv.conf."""
545
        from os import path
546
        RESOLV_CONF = "/etc/resolv.conf"
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565

        if not path.isfile(RESOLV_CONF):
            self.logger.warning("Failed to locate %s" % RESOLV_CONF)
            return None

        handle = None
        try:
            handle = open(RESOLV_CONF, 'r')
            for line in handle:
                if not line.startswith('nameserver'):
                    continue
                # we want the last non-space continuous string of the line
                return line.strip().split()[-1]
        finally:
            if handle is not None:
                handle.close()

        self.logger.warning("No nameserver found in %s" % RESOLV_CONF)

566
567
568
    def _adminpass_issue(self, adminpass):
        """Returns error string for a bad administrator password,
        or None if acceptable"""
569
570
571
        if isinstance(adminpass, binary_type):
            adminpass = adminpass.decode('utf8')
        if len(adminpass) < DEFAULT_MIN_PWD_LENGTH:
572
573
574
575
576
577
578
579
580
            return "Administrator password does not meet the default minimum" \
                " password length requirement (%d characters)" \
                % DEFAULT_MIN_PWD_LENGTH
        elif not samba.check_password_quality(adminpass):
            return "Administrator password does not meet the default" \
                " quality standards"
        else:
            return None

581

582
class cmd_domain_dcpromo(Command):
583
    """Promote an existing domain member or NT4 PDC to an AD DC."""
584
585
586
587
588
589
590
591
592

    synopsis = "%prog <dnsdomain> [DC|RODC] [options]"

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

593
594
    takes_options = []
    takes_options.extend(common_join_options)
595

596
597
    takes_options.extend(common_provision_join_options)

598
    if samba.is_ntvfs_fileserver_built():
599
        takes_options.extend(common_ntvfs_options)
600

601
602
603
604
605
    takes_args = ["domain", "role?"]

    def run(self, domain, role=None, sambaopts=None, credopts=None,
            versionopts=None, server=None, site=None, targetdir=None,
            domain_critical_only=False, parent_domain=None, machinepass=None,
606
            use_ntvfs=False, dns_backend=None,
607
608
            quiet=False, verbose=False, plaintext_secrets=False,
            backend_store=None):
609
610
611
612
        lp = sambaopts.get_loadparm()
        creds = credopts.get_credentials(lp)
        net = Net(creds, lp, server=credopts.ipaddress)

613
        logger = self.get_logger(verbose=verbose, quiet=quiet)
614

615
616
        netbios_name = lp.get("netbios name")

617
        if role is not None:
618
619
620
            role = role.upper()

        if role == "DC":
621
            join_DC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
622
623
                    site=site, netbios_name=netbios_name, targetdir=targetdir,
                    domain_critical_only=domain_critical_only,
624
625
                    machinepass=machinepass, use_ntvfs=use_ntvfs,
                    dns_backend=dns_backend,
626
627
                    promote_existing=True, plaintext_secrets=plaintext_secrets,
                    backend_store=backend_store)
628
        elif role == "RODC":
629
            join_RODC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
630
631
632
                      site=site, netbios_name=netbios_name, targetdir=targetdir,
                      domain_critical_only=domain_critical_only,
                      machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend,
633
634
                      promote_existing=True, plaintext_secrets=plaintext_secrets,
                      backend_store=backend_store)
635
636
637
638
        else:
            raise CommandError("Invalid role '%s' (possible values: DC, RODC)" % role)


639
class cmd_domain_join(Command):
640
    """Join domain as either member or backup domain controller."""
641

642
    synopsis = "%prog <dnsdomain> [DC|RODC|MEMBER|SUBDOMAIN] [options]"
643

644
645
646
647
648
649
    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

650
    takes_options = [
651
        Option("--parent-domain", help="parent domain to create subdomain under", type=str),
652
653
        Option("--adminpass", type="string", metavar="PASSWORD",
               help="choose adminstrator password when joining as a subdomain (otherwise random)"),
654
    ]
655

656
657
658
659
    ntvfs_options = [
        Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)",
               action="store_true")
    ]
660
    takes_options.extend(common_join_options)
661
662
    takes_options.extend(common_provision_join_options)

663
664
665
    if samba.is_ntvfs_fileserver_built():
        takes_options.extend(ntvfs_options)

666
667
668
    takes_args = ["domain", "role?"]

    def run(self, domain, role=None, sambaopts=None, credopts=None,
669
            versionopts=None, server=None, site=None, targetdir=None,
670
            domain_critical_only=False, parent_domain=None, machinepass=None,
671
            use_ntvfs=False, dns_backend=None, adminpass=None,
672
673
674
            quiet=False, verbose=False,
            plaintext_secrets=False,
            backend_store=None):
675
676
677
678
        lp = sambaopts.get_loadparm()
        creds = credopts.get_credentials(lp)
        net = Net(creds, lp, server=credopts.ipaddress)

679
        logger = self.get_logger(verbose=verbose, quiet=quiet)
680

681
682
        netbios_name = lp.get("netbios name")

683
        if role is not None:
684
685
686
            role = role.upper()

        if role is None or role == "MEMBER":
687
688
689
            (join_password, sid, domain_name) = net.join_member(
                domain, netbios_name, LIBNET_JOIN_AUTOMATIC,
                machinepass=machinepass)
690

691
            self.errf.write("Joined domain %s (%s)\n" % (domain_name, sid))
692
        elif role == "DC":
693
            join_DC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
694
                    site=site, netbios_name=netbios_name, targetdir=targetdir,
695
                    domain_critical_only=domain_critical_only,
696
697
                    machinepass=machinepass, use_ntvfs=use_ntvfs,
                    dns_backend=dns_backend,
698
699
                    plaintext_secrets=plaintext_secrets,
                    backend_store=backend_store)
700
        elif role == "RODC":
701
            join_RODC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
702
                      site=site, netbios_name=netbios_name, targetdir=targetdir,
703
                      domain_critical_only=domain_critical_only,
704
                      machinepass=machinepass, use_ntvfs=use_ntvfs,
705
                      dns_backend=dns_backend,
706
707
                      plaintext_secrets=plaintext_secrets,
                      backend_store=backend_store)
708
        elif role == "SUBDOMAIN":
709
710
711
            if not adminpass:
                logger.info("Administrator password will be set randomly!")

712
713
714
            netbios_domain = lp.get("workgroup")
            if parent_domain is None:
                parent_domain = ".".join(domain.split(".")[1:])
715
            join_subdomain(logger=logger, server=server, creds=creds, lp=lp, dnsdomain=domain,
716
717
718
719
                           parent_domain=parent_domain, site=site,
                           netbios_name=netbios_name, netbios_domain=netbios_domain,
                           targetdir=targetdir, machinepass=machinepass,
                           use_ntvfs=use_ntvfs, dns_backend=dns_backend,
720
                           adminpass=adminpass,
721
722
                           plaintext_secrets=plaintext_secrets,
                           backend_store=backend_store)
723
        else:
724
            raise CommandError("Invalid role '%s' (possible values: MEMBER, DC, RODC, SUBDOMAIN)" % role)
725
726


727
class cmd_domain_demote(Command):
728
    """Demote ourselves from the role of Domain Controller."""
729
730
731
732

    synopsis = "%prog [options]"

    takes_options = [
733
734
735
        Option("--server", help="writable DC to write demotion changes on", type=str),
        Option("-H", "--URL", help="LDB URL for database or target server", type=str,
               metavar="URL", dest="H"),
736
737
        Option("--remove-other-dead-server", help="Dead DC (name or NTDS GUID) "
               "to remove ALL references to (rather than this DC)", type=str),
738
        Option("-q", "--quiet", help="Be quiet", action="store_true"),
739
        Option("-v", "--verbose", help="Be verbose", action="store_true"),
740
    ]
741

742
743
744
745
    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "credopts": options.CredentialsOptions,
        "versionopts": options.VersionOptions,
746
    }
747
748

    def run(self, sambaopts=None, credopts=None,
749
            versionopts=None, server=None,
750
751
            remove_other_dead_server=None, H=None,
            verbose=False, quiet=False):
752
753
754
755
        lp = sambaopts.get_loadparm()
        creds = credopts.get_credentials(lp)
        net = Net(creds, lp, server=credopts.ipaddress)

756
        logger = self.get_logger(verbose=verbose, quiet=quiet)
757

758
759
760
761
762
763
764
        if remove_other_dead_server is not None:
            if server is not None:
                samdb = SamDB(url="ldap://%s" % server,
                              session_info=system_session(),
                              credentials=creds, lp=lp)
            else:
                samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp)
765
            try:
766
                remove_dc.remove_dc(samdb, logger, remove_other_dead_server)
767
768
            except remove_dc.DemoteException as err:
                raise CommandError("Demote failed: %s" % err)
769
770
            return

771
        netbios_name = lp.get("netbios name")
772
        samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp)
773
774
775
776
777
778
        if not server:
            res = samdb.search(expression='(&(objectClass=computer)(serverReferenceBL=*))', attrs=["dnsHostName", "name"])
            if (len(res) == 0):
                raise CommandError("Unable to search for servers")

            if (len(res) == 1):
Joe Guo's avatar
Joe Guo committed
779
                raise CommandError("You are the last server in the domain")
780
781
782
783
784
785
786
787

            server = None
            for e in res:
                if str(e["name"]).lower() != netbios_name.lower():
                    server = e["dnsHostName"]
                    break

        ntds_guid = samdb.get_ntds_GUID()
788
        msg = samdb.search(base=str(samdb.get_config_basedn()),
789
790
                           scope=ldb.SCOPE_SUBTREE, expression="(objectGUID=%s)" % ntds_guid,
                           attrs=['options'])
791
792
793
        if len(msg) == 0 or "options" not in msg[0]:
            raise CommandError("Failed to find options on %s" % ntds_guid)

794
        ntds_dn = msg[0].dn
795
796
        dsa_options = int(str(msg[0]['options']))

797
        res = samdb.search(expression="(fSMORoleOwner=%s)" % str(ntds_dn),
798
                           controls=["search_options:1:2"])
799
800

        if len(res) != 0:
801
802
803
804
            raise CommandError("Current DC is still the owner of %d role(s), "
                               "use the role command to transfer roles to "
                               "another DC" %
                               len(res))
805

806
807
        self.errf.write("Using %s as partner server for the demotion\n" %
                        server)
808
809
        (drsuapiBind, drsuapi_handle, supportedExtensions) = drsuapi_connect(server, lp, creds)

810
        self.errf.write("Deactivating inbound replication\n")
811

812
813
        nmsg = ldb.Message()
        nmsg.dn = msg[0].dn
814

815
        if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
816
817
818
            dsa_options |= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
            nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
            samdb.modify(nmsg)
819

820
821
            self.errf.write("Asking partner server %s to synchronize from us\n"
                            % server)
822
            for part in (samdb.get_schema_basedn(),
823
824
                         samdb.get_config_basedn(),
                         samdb.get_root_basedn()):
825
826
827
828
                nc = drsuapi.DsReplicaObjectIdentifier()
                nc.dn = str(part)

                req1 = drsuapi.DsReplicaSyncRequest1()
829
                req1.naming_context = nc
830
831
832
                req1.options = drsuapi.DRSUAPI_DRS_WRIT_REP
                req1.source_dsa_guid = misc.GUID(ntds_guid)

833
                try:
834
                    drsuapiBind.DsReplicaSync(drsuapi_handle, 1, req1)
835
836
                except RuntimeError as e1:
                    (werr, string) = e1.args
837
                    if werr == werror.WERR_DS_DRA_NO_REPLICA:
838
839
840
                        pass
                    else:
                        self.errf.write(
841
842
                            "Error while replicating out last local changes from '%s' for demotion, "
                            "re-enabling inbound replication\n" % part)
843
844
845
                        dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
                        nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
                        samdb.modify(nmsg)
846
                        raise CommandError("Error while sending a DsReplicaSync for partition '%s'" % str(part), string)
847
848
        try:
            remote_samdb = SamDB(url="ldap://%s" % server,
849
850
                                 session_info=system_session(),
                                 credentials=creds, lp=lp)
851

852
            self.errf.write("Changing userControl and container\n")
853
            res = remote_samdb.search(base=str(remote_samdb.domain_dn()),
854
                                      expression="(&(objectClass=user)(sAMAccountName=%s$))" %
855
                                      netbios_name.upper(),
856
                                      attrs=["userAccountControl"])
857
858
859
            dc_dn = res[0].dn
            uac = int(str(res[0]["userAccountControl"]))

860
        except Exception as e:
861
862
863
864
865
866
            if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                self.errf.write(
                    "Error while demoting, re-enabling inbound replication\n")
                dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
                nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
                samdb.modify(nmsg)
867
            raise CommandError("Error while changing account control", e)
868
869

        if (len(res) != 1):
870
871
872
873
874
875
            if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                self.errf.write(
                    "Error while demoting, re-enabling inbound replication")
                dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
                nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
                samdb.modify(nmsg)
876
877
878
879
880
            raise CommandError("Unable to find object with samaccountName = %s$"
                               " in the remote dc" % netbios_name.upper())

        olduac = uac

881
882
883
        uac &= ~(UF_SERVER_TRUST_ACCOUNT |
                 UF_TRUSTED_FOR_DELEGATION |
                 UF_PARTIAL_SECRETS_ACCOUNT)
884
885
886
887
888
889
        uac |= UF_WORKSTATION_TRUST_ACCOUNT

        msg = ldb.Message()
        msg.dn = dc_dn

        msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
890
891
                                                       ldb.FLAG_MOD_REPLACE,
                                                       "userAccountControl")
892
893
        try:
            remote_samdb.modify(msg)
894
        except Exception as e:
895
896
897
898
899
900
            if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                self.errf.write(
                    "Error while demoting, re-enabling inbound replication")
                dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
                nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
                samdb.modify(nmsg)
901
902
903
904

            raise CommandError("Error while changing account control", e)

        parent = msg.dn.parent()
905
906
907
        dc_name = res[0].dn.get_rdn_value()
        rdn = "CN=%s" % dc_name

908
909
        # Let's move to the Computer container
        i = 0
910
        newrdn = str(rdn)
911

912
        computer_dn = ldb.Dn(remote_samdb, "CN=Computers,%s" % str(remote_samdb.domain_dn()))
913
914
915
916
        res = remote_samdb.search(base=computer_dn, expression=rdn, scope=ldb.SCOPE_ONELEVEL)

        if (len(res) != 0):
            res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i),
917
                                      scope=ldb.SCOPE_ONELEVEL)
918
919
920
            while(len(res) != 0 and i < 100):
                i = i + 1
                res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i),
921
                                          scope=ldb.SCOPE_ONELEVEL)
922
923

            if i == 100:
924
925
926
927
928
929
                if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                    self.errf.write(
                        "Error while demoting, re-enabling inbound replication\n")
                    dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
                    nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
                    samdb.modify(nmsg)
930
931
932
933
934

                msg = ldb.Message()
                msg.dn = dc_dn

                msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
935
936
                                                               ldb.FLAG_MOD_REPLACE,
                                                               "userAccountControl")
937
938
939
940

                remote_samdb.modify(msg)

                raise CommandError("Unable to find a slot for renaming %s,"
941
942
                                   " all names from %s-1 to %s-%d seemed used" %
                                   (str(dc_dn), rdn, rdn, i - 9))
943
944
945
946
947
948

            newrdn = "%s-%d" % (rdn, i)

        try:
            newdn = ldb.Dn(remote_samdb, "%s,%s" % (newrdn, str(computer_dn)))
            remote_samdb.rename(dc_dn, newdn)
949
        except Exception as e:
950
951
952
953
954
955
            if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
                self.errf.write(
                    "Error while demoting, re-enabling inbound replication\n")
                dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
                nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
                samdb.modify(nmsg)
956
957
958
959
960

            msg = ldb.Message()
            msg.dn = dc_dn

            msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
961
962
                                                           ldb.FLAG_MOD_REPLACE,
                                                           "userAccountControl")
963
964
965
966
967
968
969
970

            remote_samdb.modify(msg)
            raise CommandError("Error while renaming %s to %s" % (str(dc_dn), str(newdn)), e)

        server_dsa_dn = samdb.get_serverName()
        domain = remote_samdb.get_root_basedn()

        try:
Andrew Bartlett's avatar