summaryrefslogtreecommitdiffstats
path: root/python/samba/netcmd/domain
diff options
context:
space:
mode:
Diffstat (limited to 'python/samba/netcmd/domain')
-rw-r--r--python/samba/netcmd/domain/__init__.py73
-rw-r--r--python/samba/netcmd/domain/auth/__init__.py35
-rw-r--r--python/samba/netcmd/domain/auth/policy.py685
-rw-r--r--python/samba/netcmd/domain/auth/silo.py402
-rw-r--r--python/samba/netcmd/domain/auth/silo_member.py201
-rw-r--r--python/samba/netcmd/domain/backup.py1256
-rw-r--r--python/samba/netcmd/domain/claim/__init__.py35
-rw-r--r--python/samba/netcmd/domain/claim/claim_type.py361
-rw-r--r--python/samba/netcmd/domain/claim/value_type.py105
-rw-r--r--python/samba/netcmd/domain/classicupgrade.py189
-rw-r--r--python/samba/netcmd/domain/common.py64
-rw-r--r--python/samba/netcmd/domain/dcpromo.py90
-rw-r--r--python/samba/netcmd/domain/demote.py335
-rw-r--r--python/samba/netcmd/domain/functional_prep.py145
-rw-r--r--python/samba/netcmd/domain/info.py58
-rw-r--r--python/samba/netcmd/domain/join.py146
-rw-r--r--python/samba/netcmd/domain/keytab.py55
-rw-r--r--python/samba/netcmd/domain/leave.py59
-rw-r--r--python/samba/netcmd/domain/level.py250
-rw-r--r--python/samba/netcmd/domain/models/__init__.py32
-rw-r--r--python/samba/netcmd/domain/models/auth_policy.py109
-rw-r--r--python/samba/netcmd/domain/models/auth_silo.py104
-rw-r--r--python/samba/netcmd/domain/models/claim_type.py58
-rw-r--r--python/samba/netcmd/domain/models/exceptions.py64
-rw-r--r--python/samba/netcmd/domain/models/fields.py507
-rw-r--r--python/samba/netcmd/domain/models/group.py42
-rw-r--r--python/samba/netcmd/domain/models/model.py426
-rw-r--r--python/samba/netcmd/domain/models/query.py81
-rw-r--r--python/samba/netcmd/domain/models/schema.py124
-rw-r--r--python/samba/netcmd/domain/models/site.py47
-rw-r--r--python/samba/netcmd/domain/models/subnet.py45
-rw-r--r--python/samba/netcmd/domain/models/user.py75
-rw-r--r--python/samba/netcmd/domain/models/value_type.py96
-rw-r--r--python/samba/netcmd/domain/passwordsettings.py316
-rw-r--r--python/samba/netcmd/domain/provision.py405
-rw-r--r--python/samba/netcmd/domain/samba3upgrade.py34
-rw-r--r--python/samba/netcmd/domain/schemaupgrade.py350
-rw-r--r--python/samba/netcmd/domain/tombstones.py116
-rw-r--r--python/samba/netcmd/domain/trust.py2338
39 files changed, 9913 insertions, 0 deletions
diff --git a/python/samba/netcmd/domain/__init__.py b/python/samba/netcmd/domain/__init__.py
new file mode 100644
index 0000000..1c527f1
--- /dev/null
+++ b/python/samba/netcmd/domain/__init__.py
@@ -0,0 +1,73 @@
+# domain management
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+from samba import is_ad_dc_built
+from samba.netcmd import SuperCommand
+
+from .auth import cmd_domain_auth
+from .backup import cmd_domain_backup
+from .claim import cmd_domain_claim
+from .classicupgrade import cmd_domain_classicupgrade
+from .common import (common_join_options, common_ntvfs_options,
+ common_provision_join_options)
+from .dcpromo import cmd_domain_dcpromo
+from .demote import cmd_domain_demote
+from .functional_prep import cmd_domain_functional_prep
+from .info import cmd_domain_info
+from .join import cmd_domain_join
+from .keytab import cmd_domain_export_keytab
+from .leave import cmd_domain_leave
+from .level import cmd_domain_level
+from .passwordsettings import cmd_domain_passwordsettings
+from .provision import cmd_domain_provision
+from .samba3upgrade import cmd_domain_samba3upgrade
+from .schemaupgrade import cmd_domain_schema_upgrade
+from .tombstones import cmd_domain_tombstones
+from .trust import cmd_domain_trust
+
+
+class cmd_domain(SuperCommand):
+ """Domain management."""
+
+ subcommands = {}
+ if cmd_domain_export_keytab is not None:
+ subcommands["exportkeytab"] = cmd_domain_export_keytab()
+ subcommands["info"] = cmd_domain_info()
+ subcommands["join"] = cmd_domain_join()
+ subcommands["leave"] = cmd_domain_leave()
+ subcommands["claim"] = cmd_domain_claim()
+ subcommands["auth"] = cmd_domain_auth()
+ if is_ad_dc_built():
+ subcommands["demote"] = cmd_domain_demote()
+ subcommands["provision"] = cmd_domain_provision()
+ subcommands["dcpromo"] = cmd_domain_dcpromo()
+ subcommands["level"] = cmd_domain_level()
+ subcommands["passwordsettings"] = cmd_domain_passwordsettings()
+ subcommands["classicupgrade"] = cmd_domain_classicupgrade()
+ subcommands["samba3upgrade"] = cmd_domain_samba3upgrade()
+ subcommands["trust"] = cmd_domain_trust()
+ subcommands["tombstones"] = cmd_domain_tombstones()
+ subcommands["schemaupgrade"] = cmd_domain_schema_upgrade()
+ subcommands["functionalprep"] = cmd_domain_functional_prep()
+ subcommands["backup"] = cmd_domain_backup()
diff --git a/python/samba/netcmd/domain/auth/__init__.py b/python/samba/netcmd/domain/auth/__init__.py
new file mode 100644
index 0000000..fd74f3e
--- /dev/null
+++ b/python/samba/netcmd/domain/auth/__init__.py
@@ -0,0 +1,35 @@
+# Unix SMB/CIFS implementation.
+#
+# authentication silos
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from samba.netcmd import SuperCommand
+
+from .policy import cmd_domain_auth_policy
+from .silo import cmd_domain_auth_silo
+
+
+class cmd_domain_auth(SuperCommand):
+ """Manage authentication silos and policies on the domain."""
+
+ subcommands = {
+ "policy": cmd_domain_auth_policy(),
+ "silo": cmd_domain_auth_silo(),
+ }
diff --git a/python/samba/netcmd/domain/auth/policy.py b/python/samba/netcmd/domain/auth/policy.py
new file mode 100644
index 0000000..de9ce4b
--- /dev/null
+++ b/python/samba/netcmd/domain/auth/policy.py
@@ -0,0 +1,685 @@
+# Unix SMB/CIFS implementation.
+#
+# authentication silos - authentication policy management
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+import samba.getopt as options
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.netcmd.domain.models import AuthenticationPolicy,\
+ AuthenticationSilo, Group
+from samba.netcmd.domain.models.auth_policy import MIN_TGT_LIFETIME,\
+ MAX_TGT_LIFETIME, StrongNTLMPolicy
+from samba.netcmd.domain.models.exceptions import ModelError
+from samba.netcmd.validators import Range
+
+
+def check_similar_args(option, args):
+ """Helper method for checking similar mutually exclusive args.
+
+ Example: --user-allowed-to-authenticate-from and
+ --user-allowed-to-authenticate-from-device-silo
+ """
+ num = sum(arg is not None for arg in args)
+ if num > 1:
+ raise CommandError(f"{option} argument repeated {num} times.")
+
+
+class UserOptions(options.OptionGroup):
+ """User options used by policy create and policy modify commands."""
+
+ def __init__(self, parser):
+ super().__init__(parser, "User Options")
+
+ self.add_option("--user-tgt-lifetime-mins",
+ help="Ticket-Granting-Ticket lifetime for user accounts.",
+ dest="tgt_lifetime", type=int, action="callback",
+ callback=self.set_option,
+ validators=[Range(min=MIN_TGT_LIFETIME, max=MAX_TGT_LIFETIME)])
+ self.add_option("--user-allow-ntlm-auth",
+ help="Allow NTLM network authentication despite the fact that the user "
+ "is restricted to selected devices.",
+ dest="allow_ntlm_auth", default=False,
+ action="callback", callback=self.set_option)
+ self.add_option("--user-allowed-to-authenticate-from",
+ help="SDDL Rules setting which device the user is allowed to authenticate from.",
+ type=str, dest="allowed_to_authenticate_from",
+ action="callback", callback=self.set_option,
+ metavar="SDDL")
+ self.add_option("--user-allowed-to-authenticate-from-device-silo",
+ help="To authenticate, the user must log in from a device in SILO.",
+ type=str, dest="allowed_to_authenticate_from_device_silo",
+ action="callback", callback=self.set_option,
+ metavar="SILO")
+ self.add_option("--user-allowed-to-authenticate-from-device-group",
+ help="To authenticate, the user must log in from a device in GROUP.",
+ type=str, dest="allowed_to_authenticate_from_device_group",
+ action="callback", callback=self.set_option,
+ metavar="GROUP")
+ self.add_option("--user-allowed-to-authenticate-to",
+ help="A target service, on a user account, requires the connecting user to match SDDL",
+ type=str, dest="allowed_to_authenticate_to",
+ action="callback", callback=self.set_option,
+ metavar="SDDL")
+ self.add_option("--user-allowed-to-authenticate-to-by-group",
+ help="A target service, on a user account, requires the connecting user to be in GROUP",
+ type=str, dest="allowed_to_authenticate_to_by_group",
+ action="callback", callback=self.set_option,
+ metavar="GROUP")
+ self.add_option("--user-allowed-to-authenticate-to-by-silo",
+ help="A target service, on a user account, requires the connecting user to be in SILO",
+ type=str, dest="allowed_to_authenticate_to_by_silo",
+ action="callback", callback=self.set_option,
+ metavar="SILO")
+
+
+class ServiceOptions(options.OptionGroup):
+ """Service options used by policy create and policy modify commands."""
+
+ def __init__(self, parser):
+ super().__init__(parser, "Service Options")
+
+ self.add_option("--service-tgt-lifetime-mins",
+ help="Ticket-Granting-Ticket lifetime for service accounts.",
+ dest="tgt_lifetime", type=int, action="callback",
+ callback=self.set_option,
+ validators=[Range(min=MIN_TGT_LIFETIME, max=MAX_TGT_LIFETIME)])
+ self.add_option("--service-allow-ntlm-auth",
+ help="Allow NTLM network authentication despite "
+ "the fact that the service account "
+ "is restricted to selected devices.",
+ dest="allow_ntlm_auth", default=False,
+ action="callback", callback=self.set_option)
+ self.add_option("--service-allowed-to-authenticate-from",
+ help="SDDL Rules setting which device the "
+ "service account is allowed to authenticate from.",
+ type=str, dest="allowed_to_authenticate_from",
+ action="callback", callback=self.set_option,
+ metavar="SDDL")
+ self.add_option("--service-allowed-to-authenticate-from-device-silo",
+ help="To authenticate, the service must authenticate on a device in SILO.",
+ type=str, dest="allowed_to_authenticate_from_device_silo",
+ action="callback", callback=self.set_option,
+ metavar="SILO")
+ self.add_option("--service-allowed-to-authenticate-from-device-group",
+ help="To authenticate, the service must authenticate on a device in GROUP.",
+ type=str, dest="allowed_to_authenticate_from_device_group",
+ action="callback", callback=self.set_option,
+ metavar="GROUP")
+ self.add_option("--service-allowed-to-authenticate-to",
+ help="The target service requires the connecting user to match SDDL",
+ type=str, dest="allowed_to_authenticate_to",
+ action="callback", callback=self.set_option,
+ metavar="SDDL")
+ self.add_option("--service-allowed-to-authenticate-to-by-group",
+ help="The target service requires the connecting user to be in GROUP",
+ type=str, dest="allowed_to_authenticate_to_by_group",
+ action="callback", callback=self.set_option,
+ metavar="GROUP")
+ self.add_option("--service-allowed-to-authenticate-to-by-silo",
+ help="The target service requires the connecting user to be in SILO",
+ type=str, dest="allowed_to_authenticate_to_by_silo",
+ action="callback", callback=self.set_option,
+ metavar="SILO")
+
+
+class ComputerOptions(options.OptionGroup):
+ """Computer options used by policy create and policy modify commands."""
+
+ def __init__(self, parser):
+ super().__init__(parser, "Computer Options")
+
+ self.add_option("--computer-tgt-lifetime-mins",
+ help="Ticket-Granting-Ticket lifetime for computer accounts.",
+ dest="tgt_lifetime", type=int, action="callback",
+ callback=self.set_option,
+ validators=[Range(min=MIN_TGT_LIFETIME, max=MAX_TGT_LIFETIME)])
+ self.add_option("--computer-allowed-to-authenticate-to",
+ help="The computer account (server, workstation) service requires the connecting user to match SDDL",
+ type=str, dest="allowed_to_authenticate_to",
+ action="callback", callback=self.set_option,
+ metavar="SDDL")
+ self.add_option("--computer-allowed-to-authenticate-to-by-group",
+ help="The computer account (server, workstation) service requires the connecting user to be in GROUP",
+ type=str, dest="allowed_to_authenticate_to_by_group",
+ action="callback", callback=self.set_option,
+ metavar="GROUP")
+ self.add_option("--computer-allowed-to-authenticate-to-by-silo",
+ help="The computer account (server, workstation) service requires the connecting user to be in SILO",
+ type=str, dest="allowed_to_authenticate_to_by_silo",
+ action="callback", callback=self.set_option,
+ metavar="SILO")
+
+
+class cmd_domain_auth_policy_list(Command):
+ """List authentication policies on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--json", help="Output results in JSON format.",
+ dest="output_format", action="store_const", const="json"),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ output_format=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ # Authentication policies grouped by cn.
+ try:
+ policies = {policy.cn: policy.as_dict()
+ for policy in AuthenticationPolicy.query(ldb)}
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Using json output format gives more detail.
+ if output_format == "json":
+ self.print_json(policies)
+ else:
+ for policy in policies.keys():
+ self.outf.write(f"{policy}\n")
+
+
+class cmd_domain_auth_policy_view(Command):
+ """View an authentication policy on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name",
+ help="Name of authentication policy to view (required).",
+ dest="name", action="store", type=str, required=True),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, name=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ policy = AuthenticationPolicy.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if authentication policy exists first.
+ if policy is None:
+ raise CommandError(f"Authentication policy {name} not found.")
+
+ # Display policy as JSON.
+ self.print_json(policy.as_dict())
+
+
+class cmd_domain_auth_policy_create(Command):
+ """Create an authentication policy on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ "useropts": UserOptions,
+ "serviceopts": ServiceOptions,
+ "computeropts": ComputerOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Name of authentication policy (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--description",
+ help="Optional description for authentication policy.",
+ dest="description", action="store", type=str),
+ Option("--protect",
+ help="Protect authentication silo from accidental deletion.",
+ dest="protect", action="store_true"),
+ Option("--unprotect",
+ help="Unprotect authentication silo from accidental deletion.",
+ dest="unprotect", action="store_true"),
+ Option("--audit",
+ help="Only audit authentication policy.",
+ dest="audit", action="store_true"),
+ Option("--enforce",
+ help="Enforce authentication policy.",
+ dest="enforce", action="store_true"),
+ Option("--strong-ntlm-policy",
+ help=f"Strong NTLM Policy ({StrongNTLMPolicy.choices_str()}).",
+ dest="strong_ntlm_policy", type="choice", action="store",
+ choices=StrongNTLMPolicy.get_choices(),
+ default="Disabled"),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, useropts=None,
+ serviceopts=None, computeropts=None, name=None, description=None,
+ protect=None, unprotect=None, audit=None, enforce=None,
+ strong_ntlm_policy=None):
+
+ if protect and unprotect:
+ raise CommandError("--protect and --unprotect cannot be used together.")
+ if audit and enforce:
+ raise CommandError("--audit and --enforce cannot be used together.")
+
+ # Check for repeated, similar arguments.
+ check_similar_args("--user-allowed-to-authenticate-from",
+ [useropts.allowed_to_authenticate_from,
+ useropts.allowed_to_authenticate_from_device_group,
+ useropts.allowed_to_authenticate_from_device_silo])
+ check_similar_args("--user-allowed-to-authenticate-to",
+ [useropts.allowed_to_authenticate_to,
+ useropts.allowed_to_authenticate_to_by_group,
+ useropts.allowed_to_authenticate_to_by_silo])
+ check_similar_args("--service-allowed-to-authenticate-from",
+ [serviceopts.allowed_to_authenticate_from,
+ serviceopts.allowed_to_authenticate_from_device_group,
+ serviceopts.allowed_to_authenticate_from_device_silo])
+ check_similar_args("--service-allowed-to-authenticate-to",
+ [serviceopts.allowed_to_authenticate_to,
+ serviceopts.allowed_to_authenticate_to_by_group,
+ serviceopts.allowed_to_authenticate_to_by_silo])
+ check_similar_args("--computer-allowed-to-authenticate-to",
+ [computeropts.allowed_to_authenticate_to,
+ computeropts.allowed_to_authenticate_to_by_group,
+ computeropts.allowed_to_authenticate_to_by_silo])
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ # Generate SDDL for authenticating users from a device in a group
+ if useropts.allowed_to_authenticate_from_device_group:
+ group = Group.get(
+ ldb, cn=useropts.allowed_to_authenticate_from_device_group)
+ useropts.allowed_to_authenticate_from = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating users from a device in a silo
+ if useropts.allowed_to_authenticate_from_device_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=useropts.allowed_to_authenticate_from_device_silo)
+ useropts.allowed_to_authenticate_from = silo.get_authentication_sddl()
+
+ # Generate SDDL for authenticating user accounts to a group
+ if useropts.allowed_to_authenticate_to_by_group:
+ group = Group.get(
+ ldb, cn=useropts.allowed_to_authenticate_to_by_group)
+ useropts.allowed_to_authenticate_to = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating user accounts to a silo
+ if useropts.allowed_to_authenticate_to_by_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=useropts.allowed_to_authenticate_to_by_silo)
+ useropts.allowed_to_authenticate_to = silo.get_authentication_sddl()
+
+ # Generate SDDL for authenticating service accounts from a device in a group
+ if serviceopts.allowed_to_authenticate_from_device_group:
+ group = Group.get(
+ ldb, cn=serviceopts.allowed_to_authenticate_from_device_group)
+ serviceopts.allowed_to_authenticate_from = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating service accounts from a device in a silo
+ if serviceopts.allowed_to_authenticate_from_device_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=serviceopts.allowed_to_authenticate_from_device_silo)
+ serviceopts.allowed_to_authenticate_from = silo.get_authentication_sddl()
+
+ # Generate SDDL for authenticating service accounts to a group
+ if serviceopts.allowed_to_authenticate_to_by_group:
+ group = Group.get(
+ ldb, cn=serviceopts.allowed_to_authenticate_to_by_group)
+ serviceopts.allowed_to_authenticate_to = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating service accounts to a silo
+ if serviceopts.allowed_to_authenticate_to_by_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=serviceopts.allowed_to_authenticate_to_by_silo)
+ serviceopts.allowed_to_authenticate_to = silo.get_authentication_sddl()
+
+ # Generate SDDL for authenticating computer accounts to a group
+ if computeropts.allowed_to_authenticate_to_by_group:
+ group = Group.get(
+ ldb, cn=computeropts.allowed_to_authenticate_to_by_group)
+ computeropts.allowed_to_authenticate_to = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating computer accounts to a silo
+ if computeropts.allowed_to_authenticate_to_by_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=computeropts.allowed_to_authenticate_to_by_silo)
+ computeropts.allowed_to_authenticate_to = silo.get_authentication_sddl()
+
+ try:
+ policy = AuthenticationPolicy.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Make sure authentication policy doesn't already exist.
+ if policy is not None:
+ raise CommandError(f"Authentication policy {name} already exists.")
+
+ # New policy object.
+ policy = AuthenticationPolicy(
+ cn=name,
+ description=description,
+ strong_ntlm_policy=StrongNTLMPolicy[strong_ntlm_policy.upper()],
+ user_allow_ntlm_auth=useropts.allow_ntlm_auth,
+ user_tgt_lifetime=useropts.tgt_lifetime,
+ user_allowed_to_authenticate_from=useropts.allowed_to_authenticate_from,
+ user_allowed_to_authenticate_to=useropts.allowed_to_authenticate_to,
+ service_allow_ntlm_auth=serviceopts.allow_ntlm_auth,
+ service_tgt_lifetime=serviceopts.tgt_lifetime,
+ service_allowed_to_authenticate_from=serviceopts.allowed_to_authenticate_from,
+ service_allowed_to_authenticate_to=serviceopts.allowed_to_authenticate_to,
+ computer_tgt_lifetime=computeropts.tgt_lifetime,
+ computer_allowed_to_authenticate_to=computeropts.allowed_to_authenticate_to,
+ )
+
+ # Either --enforce will be set or --audit but never both.
+ # The default if both are missing is enforce=True.
+ if enforce is not None:
+ policy.enforced = enforce
+ else:
+ policy.enforced = not audit
+
+ # Create policy.
+ try:
+ policy.save(ldb)
+
+ if protect:
+ policy.protect(ldb)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Authentication policy created successfully.
+ self.outf.write(f"Created authentication policy: {name}\n")
+
+
+class cmd_domain_auth_policy_modify(Command):
+ """Modify authentication policies on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ "useropts": UserOptions,
+ "serviceopts": ServiceOptions,
+ "computeropts": ComputerOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Name of authentication policy (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--description",
+ help="Optional description for authentication policy.",
+ dest="description", action="store", type=str),
+ Option("--protect",
+ help="Protect authentication silo from accidental deletion.",
+ dest="protect", action="store_true"),
+ Option("--unprotect",
+ help="Unprotect authentication silo from accidental deletion.",
+ dest="unprotect", action="store_true"),
+ Option("--audit",
+ help="Only audit authentication policy.",
+ dest="audit", action="store_true"),
+ Option("--enforce",
+ help="Enforce authentication policy.",
+ dest="enforce", action="store_true"),
+ Option("--strong-ntlm-policy",
+ help=f"Strong NTLM Policy ({StrongNTLMPolicy.choices_str()}).",
+ dest="strong_ntlm_policy", type="choice", action="store",
+ choices=StrongNTLMPolicy.get_choices()),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, useropts=None,
+ serviceopts=None, computeropts=None, name=None, description=None,
+ protect=None, unprotect=None, audit=None, enforce=None,
+ strong_ntlm_policy=None):
+
+ if protect and unprotect:
+ raise CommandError("--protect and --unprotect cannot be used together.")
+ if audit and enforce:
+ raise CommandError("--audit and --enforce cannot be used together.")
+
+ # Check for repeated, similar arguments.
+ check_similar_args("--user-allowed-to-authenticate-from",
+ [useropts.allowed_to_authenticate_from,
+ useropts.allowed_to_authenticate_from_device_group,
+ useropts.allowed_to_authenticate_from_device_silo])
+ check_similar_args("--user-allowed-to-authenticate-to",
+ [useropts.allowed_to_authenticate_to,
+ useropts.allowed_to_authenticate_to_by_group,
+ useropts.allowed_to_authenticate_to_by_silo])
+ check_similar_args("--service-allowed-to-authenticate-from",
+ [serviceopts.allowed_to_authenticate_from,
+ serviceopts.allowed_to_authenticate_from_device_group,
+ serviceopts.allowed_to_authenticate_from_device_silo])
+ check_similar_args("--service-allowed-to-authenticate-to",
+ [serviceopts.allowed_to_authenticate_to,
+ serviceopts.allowed_to_authenticate_to_by_group,
+ serviceopts.allowed_to_authenticate_to_by_silo])
+ check_similar_args("--computer-allowed-to-authenticate-to",
+ [computeropts.allowed_to_authenticate_to,
+ computeropts.allowed_to_authenticate_to_by_group,
+ computeropts.allowed_to_authenticate_to_by_silo])
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ # Generate SDDL for authenticating users from a device in a group
+ if useropts.allowed_to_authenticate_from_device_group:
+ group = Group.get(
+ ldb, cn=useropts.allowed_to_authenticate_from_device_group)
+ useropts.allowed_to_authenticate_from = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating users from a device in a silo
+ if useropts.allowed_to_authenticate_from_device_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=useropts.allowed_to_authenticate_from_device_silo)
+ useropts.allowed_to_authenticate_from = silo.get_authentication_sddl()
+
+ # Generate SDDL for authenticating user accounts to a group
+ if useropts.allowed_to_authenticate_to_by_group:
+ group = Group.get(
+ ldb, cn=useropts.allowed_to_authenticate_to_by_group)
+ useropts.allowed_to_authenticate_to = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating user accounts to a silo
+ if useropts.allowed_to_authenticate_to_by_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=useropts.allowed_to_authenticate_to_by_silo)
+ useropts.allowed_to_authenticate_to = silo.get_authentication_sddl()
+
+ # Generate SDDL for authenticating users from a device a device in a group
+ if serviceopts.allowed_to_authenticate_from_device_group:
+ group = Group.get(
+ ldb, cn=serviceopts.allowed_to_authenticate_from_device_group)
+ serviceopts.allowed_to_authenticate_from = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating service accounts from a device in a silo
+ if serviceopts.allowed_to_authenticate_from_device_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=serviceopts.allowed_to_authenticate_from_device_silo)
+ serviceopts.allowed_to_authenticate_from = silo.get_authentication_sddl()
+
+ # Generate SDDL for authenticating service accounts to a group
+ if serviceopts.allowed_to_authenticate_to_by_group:
+ group = Group.get(
+ ldb, cn=serviceopts.allowed_to_authenticate_to_by_group)
+ serviceopts.allowed_to_authenticate_to = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating service accounts to a silo
+ if serviceopts.allowed_to_authenticate_to_by_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=serviceopts.allowed_to_authenticate_to_by_silo)
+ serviceopts.allowed_to_authenticate_to = silo.get_authentication_sddl()
+
+ # Generate SDDL for authenticating computer accounts to a group
+ if computeropts.allowed_to_authenticate_to_by_group:
+ group = Group.get(
+ ldb, cn=computeropts.allowed_to_authenticate_to_by_group)
+ computeropts.allowed_to_authenticate_to = group.get_authentication_sddl()
+
+ # Generate SDDL for authenticating computer accounts to a silo
+ if computeropts.allowed_to_authenticate_to_by_silo:
+ silo = AuthenticationSilo.get(
+ ldb, cn=computeropts.allowed_to_authenticate_to_by_silo)
+ computeropts.allowed_to_authenticate_to = silo.get_authentication_sddl()
+
+ try:
+ policy = AuthenticationPolicy.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if authentication policy exists.
+ if policy is None:
+ raise CommandError(f"Authentication policy {name} not found.")
+
+ # Either --enforce will be set or --audit but never both.
+ if enforce:
+ policy.enforced = True
+ elif audit:
+ policy.enforced = False
+
+ # Update the description.
+ if description is not None:
+ policy.description = description
+
+ # User sign on
+ ###############
+
+ if strong_ntlm_policy is not None:
+ policy.strong_ntlm_policy = \
+ StrongNTLMPolicy[strong_ntlm_policy.upper()]
+
+ if useropts.tgt_lifetime is not None:
+ policy.user_tgt_lifetime = useropts.tgt_lifetime
+
+ if useropts.allowed_to_authenticate_from is not None:
+ policy.user_allowed_to_authenticate_from = \
+ useropts.allowed_to_authenticate_from
+
+ if useropts.allowed_to_authenticate_to is not None:
+ policy.user_allowed_to_authenticate_to = \
+ useropts.allowed_to_authenticate_to
+
+ # Service sign on
+ ##################
+
+ if serviceopts.tgt_lifetime is not None:
+ policy.service_tgt_lifetime = serviceopts.tgt_lifetime
+
+ if serviceopts.allowed_to_authenticate_from is not None:
+ policy.service_allowed_to_authenticate_from = \
+ serviceopts.allowed_to_authenticate_from
+
+ if serviceopts.allowed_to_authenticate_to is not None:
+ policy.service_allowed_to_authenticate_to = \
+ serviceopts.allowed_to_authenticate_to
+
+ # Computer
+ ###########
+
+ if computeropts.tgt_lifetime is not None:
+ policy.computer_tgt_lifetime = computeropts.tgt_lifetime
+
+ if computeropts.allowed_to_authenticate_to is not None:
+ policy.computer_allowed_to_authenticate_to = \
+ computeropts.allowed_to_authenticate_to
+
+ # Update policy.
+ try:
+ policy.save(ldb)
+
+ if protect:
+ policy.protect(ldb)
+ elif unprotect:
+ policy.unprotect(ldb)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Authentication policy updated successfully.
+ self.outf.write(f"Updated authentication policy: {name}\n")
+
+
+class cmd_domain_auth_policy_delete(Command):
+ """Delete authentication policies on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Name of authentication policy (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--force", help="Force delete protected authentication policy.",
+ dest="force", action="store_true")
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, name=None,
+ force=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ policy = AuthenticationPolicy.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if authentication policy exists first.
+ if policy is None:
+ raise CommandError(f"Authentication policy {name} not found.")
+
+ # Delete item, --force removes delete protection first.
+ try:
+ if force:
+ policy.unprotect(ldb)
+
+ policy.delete(ldb)
+ except ModelError as e:
+ if not force:
+ raise CommandError(
+ f"{e}\nTry --force to delete protected authentication policies.")
+ else:
+ raise CommandError(e)
+
+ # Authentication policy deleted successfully.
+ self.outf.write(f"Deleted authentication policy: {name}\n")
+
+
+class cmd_domain_auth_policy(SuperCommand):
+ """Manage authentication policies on the domain."""
+
+ subcommands = {
+ "list": cmd_domain_auth_policy_list(),
+ "view": cmd_domain_auth_policy_view(),
+ "create": cmd_domain_auth_policy_create(),
+ "modify": cmd_domain_auth_policy_modify(),
+ "delete": cmd_domain_auth_policy_delete(),
+ }
diff --git a/python/samba/netcmd/domain/auth/silo.py b/python/samba/netcmd/domain/auth/silo.py
new file mode 100644
index 0000000..2e27761
--- /dev/null
+++ b/python/samba/netcmd/domain/auth/silo.py
@@ -0,0 +1,402 @@
+# Unix SMB/CIFS implementation.
+#
+# authentication silos - authentication silo management
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+import samba.getopt as options
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.netcmd.domain.models import AuthenticationPolicy, AuthenticationSilo
+from samba.netcmd.domain.models.exceptions import ModelError
+
+from .silo_member import cmd_domain_auth_silo_member
+
+
+class cmd_domain_auth_silo_list(Command):
+ """List authentication silos on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--json", help="Output results in JSON format.",
+ dest="output_format", action="store_const", const="json"),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ output_format=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ # Authentication silos grouped by cn.
+ try:
+ silos = {silo.cn: silo.as_dict()
+ for silo in AuthenticationSilo.query(ldb)}
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Using json output format gives more detail.
+ if output_format == "json":
+ self.print_json(silos)
+ else:
+ for silo in silos.keys():
+ self.outf.write(f"{silo}\n")
+
+
+class cmd_domain_auth_silo_view(Command):
+ """View an authentication silo on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name",
+ help="Name of authentication silo to view (required).",
+ dest="name", action="store", type=str, required=True),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, name=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ silo = AuthenticationSilo.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if silo exists first.
+ if silo is None:
+ raise CommandError(f"Authentication silo {name} not found.")
+
+ # Display silo as JSON.
+ self.print_json(silo.as_dict())
+
+
+class cmd_domain_auth_silo_create(Command):
+ """Create a new authentication silo on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Name of authentication silo (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--description",
+ help="Optional description for authentication silo.",
+ dest="description", action="store", type=str),
+ Option("--user-authentication-policy",
+ help="User account authentication policy.",
+ dest="user_authentication_policy", action="store", type=str,
+ metavar="USER_POLICY"),
+ Option("--service-authentication-policy",
+ help="Managed service account authentication policy.",
+ dest="service_authentication_policy", action="store", type=str,
+ metavar="SERVICE_POLICY"),
+ Option("--computer-authentication-policy",
+ help="Computer authentication policy.",
+ dest="computer_authentication_policy", action="store", type=str,
+ metavar="COMPUTER_POLICY"),
+ Option("--protect",
+ help="Protect authentication silo from accidental deletion.",
+ dest="protect", action="store_true"),
+ Option("--unprotect",
+ help="Unprotect authentication silo from accidental deletion.",
+ dest="unprotect", action="store_true"),
+ Option("--audit",
+ help="Only audit silo policies.",
+ dest="audit", action="store_true"),
+ Option("--enforce",
+ help="Enforce silo policies.",
+ dest="enforce", action="store_true")
+ ]
+
+ @staticmethod
+ def get_policy(ldb, name):
+ """Helper function to fetch auth policy or raise CommandError.
+
+ :param ldb: Ldb connection
+ :param name: Either the DN or name of authentication policy
+ """
+ try:
+ return AuthenticationPolicy.lookup(ldb, name)
+ except (LookupError, ValueError) as e:
+ raise CommandError(e)
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ name=None, description=None,
+ user_authentication_policy=None,
+ service_authentication_policy=None,
+ computer_authentication_policy=None,
+ protect=None, unprotect=None,
+ audit=None, enforce=None):
+
+ if protect and unprotect:
+ raise CommandError("--protect and --unprotect cannot be used together.")
+ if audit and enforce:
+ raise CommandError("--audit and --enforce cannot be used together.")
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ silo = AuthenticationSilo.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Make sure silo doesn't already exist.
+ if silo is not None:
+ raise CommandError(f"Authentication silo {name} already exists.")
+
+ # New silo object.
+ silo = AuthenticationSilo(cn=name, description=description)
+
+ # Set user policy
+ if user_authentication_policy:
+ silo.user_authentication_policy = \
+ self.get_policy(ldb, user_authentication_policy).dn
+
+ # Set service policy
+ if service_authentication_policy:
+ silo.service_authentication_policy = \
+ self.get_policy(ldb, service_authentication_policy).dn
+
+ # Set computer policy
+ if computer_authentication_policy:
+ silo.computer_authentication_policy = \
+ self.get_policy(ldb, computer_authentication_policy).dn
+
+ # Either --enforce will be set or --audit but never both.
+ # The default if both are missing is enforce=True.
+ if enforce is not None:
+ silo.enforced = enforce
+ else:
+ silo.enforced = not audit
+
+ # Create silo
+ try:
+ silo.save(ldb)
+
+ if protect:
+ silo.protect(ldb)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Authentication silo created successfully.
+ self.outf.write(f"Created authentication silo: {name}\n")
+
+
+class cmd_domain_auth_silo_modify(Command):
+ """Modify an authentication silo on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Name of authentication silo (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--description",
+ help="Optional description for authentication silo.",
+ dest="description", action="store", type=str),
+ Option("--user-authentication-policy",
+ help="User account authentication policy.",
+ dest="user_authentication_policy", action="store", type=str,
+ metavar="USER_POLICY"),
+ Option("--service-authentication-policy",
+ help="Managed service account authentication policy.",
+ dest="service_authentication_policy", action="store", type=str,
+ metavar="SERVICE_POLICY"),
+ Option("--computer-authentication-policy",
+ help="Computer authentication policy.",
+ dest="computer_authentication_policy", action="store", type=str,
+ metavar="COMPUTER_POLICY"),
+ Option("--protect",
+ help="Protect authentication silo from accidental deletion.",
+ dest="protect", action="store_true"),
+ Option("--unprotect",
+ help="Unprotect authentication silo from accidental deletion.",
+ dest="unprotect", action="store_true"),
+ Option("--audit",
+ help="Only audit silo policies.",
+ dest="audit", action="store_true"),
+ Option("--enforce",
+ help="Enforce silo policies.",
+ dest="enforce", action="store_true")
+ ]
+
+ @staticmethod
+ def get_policy(ldb, name):
+ """Helper function to fetch auth policy or raise CommandError.
+
+ :param ldb: Ldb connection
+ :param name: Either the DN or name of authentication policy
+ """
+ try:
+ return AuthenticationPolicy.lookup(ldb, name)
+ except (LookupError, ModelError, ValueError) as e:
+ raise CommandError(e)
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ name=None, description=None,
+ user_authentication_policy=None,
+ service_authentication_policy=None,
+ computer_authentication_policy=None,
+ protect=None, unprotect=None,
+ audit=None, enforce=None):
+
+ if audit and enforce:
+ raise CommandError("--audit and --enforce cannot be used together.")
+ if protect and unprotect:
+ raise CommandError("--protect and --unprotect cannot be used together.")
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ silo = AuthenticationSilo.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if silo exists first.
+ if silo is None:
+ raise CommandError(f"Authentication silo {name} not found.")
+
+ # Either --enforce will be set or --audit but never both.
+ if enforce:
+ silo.enforced = True
+ elif audit:
+ silo.enforced = False
+
+ # Update the description.
+ if description is not None:
+ silo.description = description
+
+ # Set or unset user policy.
+ if user_authentication_policy == "":
+ silo.user_authentication_policy = None
+ elif user_authentication_policy:
+ silo.user_authentication_policy = \
+ self.get_policy(ldb, user_authentication_policy).dn
+
+ # Set or unset service policy.
+ if service_authentication_policy == "":
+ silo.service_authentication_policy = None
+ elif service_authentication_policy:
+ silo.service_authentication_policy = \
+ self.get_policy(ldb, service_authentication_policy).dn
+
+ # Set or unset computer policy.
+ if computer_authentication_policy == "":
+ silo.computer_authentication_policy = None
+ elif computer_authentication_policy:
+ silo.computer_authentication_policy = \
+ self.get_policy(ldb, computer_authentication_policy).dn
+
+ # Update silo
+ try:
+ silo.save(ldb)
+
+ if protect:
+ silo.protect(ldb)
+ elif unprotect:
+ silo.unprotect(ldb)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Silo updated successfully.
+ self.outf.write(f"Updated authentication silo: {name}\n")
+
+
+class cmd_domain_auth_silo_delete(Command):
+ """Delete an authentication silo on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Name of authentication silo (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--force", help="Force delete protected authentication silo.",
+ dest="force", action="store_true")
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, name=None,
+ force=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ silo = AuthenticationSilo.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if silo exists first.
+ if silo is None:
+ raise CommandError(f"Authentication silo {name} not found.")
+
+ # Delete silo
+ try:
+ if force:
+ silo.unprotect(ldb)
+
+ silo.delete(ldb)
+ except ModelError as e:
+ if not force:
+ raise CommandError(
+ f"{e}\nTry --force to delete protected authentication silos.")
+ else:
+ raise CommandError(e)
+
+ # Authentication silo deleted successfully.
+ self.outf.write(f"Deleted authentication silo: {name}\n")
+
+
+class cmd_domain_auth_silo(SuperCommand):
+ """Manage authentication silos on the domain."""
+
+ subcommands = {
+ "list": cmd_domain_auth_silo_list(),
+ "view": cmd_domain_auth_silo_view(),
+ "create": cmd_domain_auth_silo_create(),
+ "modify": cmd_domain_auth_silo_modify(),
+ "delete": cmd_domain_auth_silo_delete(),
+ "member": cmd_domain_auth_silo_member(),
+ }
diff --git a/python/samba/netcmd/domain/auth/silo_member.py b/python/samba/netcmd/domain/auth/silo_member.py
new file mode 100644
index 0000000..9b41400
--- /dev/null
+++ b/python/samba/netcmd/domain/auth/silo_member.py
@@ -0,0 +1,201 @@
+# Unix SMB/CIFS implementation.
+#
+# authentication silos - silo member management
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+import samba.getopt as options
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.netcmd.domain.models import AuthenticationSilo, User
+from samba.netcmd.domain.models.exceptions import ModelError
+
+
+class cmd_domain_auth_silo_member_grant(Command):
+ """Grant a member access to an authentication silo."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name",
+ help="Name of authentication silo (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--member",
+ help="Member to grant access to the silo (DN or account name).",
+ dest="member", action="store", type=str, required=True),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ name=None, member=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ silo = AuthenticationSilo.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if authentication silo exists first.
+ if silo is None:
+ raise CommandError(f"Authentication silo {name} not found.")
+
+ try:
+ user = User.find(ldb, member)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Ensure the user actually exists first.
+ if user is None:
+ raise CommandError(f"User {member} not found.")
+
+ # Grant access to member.
+ try:
+ silo.grant(ldb, user)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Display silo assigned status.
+ if user.assigned_silo and user.assigned_silo == silo.dn:
+ status = "assigned"
+ else:
+ status = "unassigned"
+
+ print(f"User {user} granted access to the authentication silo {name} ({status}).",
+ file=self.outf)
+
+
+class cmd_domain_auth_silo_member_list(Command):
+ """List all members in the authentication silo."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name",
+ help="Name of authentication silo (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--json", help="Output results in JSON format.",
+ dest="output_format", action="store_const", const="json"),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ name=None, output_format=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ silo = AuthenticationSilo.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if authentication silo exists first.
+ if silo is None:
+ raise CommandError(f"Authentication silo {name} not found.")
+
+ # Fetch all members.
+ try:
+ members = [User.get(ldb, dn=dn) for dn in silo.members]
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Using json output format gives more detail.
+ if output_format == "json":
+ self.print_json([member.as_dict() for member in members])
+ else:
+ for member in members:
+ print(member.dn, file=self.outf)
+
+
+class cmd_domain_auth_silo_member_revoke(Command):
+ """Revoke a member from an authentication silo."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name",
+ help="Name of authentication silo (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--member",
+ help="Member to revoke from the silo (DN or account name).",
+ dest="member", action="store", type=str, required=True),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ name=None, member=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ silo = AuthenticationSilo.get(ldb, cn=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if authentication silo exists first.
+ if silo is None:
+ raise CommandError(f"Authentication silo {name} not found.")
+
+ try:
+ user = User.find(ldb, member)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Ensure the user actually exists first.
+ if user is None:
+ raise CommandError(f"User {member} not found.")
+
+ # Revoke member access.
+ try:
+ silo.revoke(ldb, user)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Display silo assigned status.
+ if user.assigned_silo and user.assigned_silo == silo.dn:
+ status = "assigned"
+ else:
+ status = "unassigned"
+
+ print(f"User {user} revoked from the authentication silo {name} ({status}).",
+ file=self.outf)
+
+
+class cmd_domain_auth_silo_member(SuperCommand):
+ """Manage members in an authentication silo."""
+
+ subcommands = {
+ "grant": cmd_domain_auth_silo_member_grant(),
+ "list": cmd_domain_auth_silo_member_list(),
+ "revoke": cmd_domain_auth_silo_member_revoke(),
+ }
diff --git a/python/samba/netcmd/domain/backup.py b/python/samba/netcmd/domain/backup.py
new file mode 100644
index 0000000..fc7ff53
--- /dev/null
+++ b/python/samba/netcmd/domain/backup.py
@@ -0,0 +1,1256 @@
+# domain_backup
+#
+# Copyright Andrew Bartlett <abartlet@samba.org>
+#
+# 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/>.
+#
+import datetime
+import os
+import sys
+import logging
+import shutil
+import tempfile
+import samba
+import tdb
+import samba.getopt as options
+from samba.samdb import SamDB, get_default_backend_store
+import ldb
+from ldb import LdbError
+from samba.samba3 import libsmb_samba_internal as libsmb
+from samba.samba3 import param as s3param
+from samba.ntacls import backup_online, backup_restore, backup_offline
+from samba.auth import system_session
+from samba.join import DCJoinContext, join_clone, DCCloneAndRenameContext
+from samba.dcerpc.security import dom_sid
+from samba.netcmd import Option, CommandError
+from samba.dcerpc import misc, security, drsblobs
+from samba import Ldb
+from samba.netcmd.fsmo import cmd_fsmo_seize
+from samba.provision import make_smbconf, DEFAULTSITE
+from samba.upgradehelpers import update_krbtgt_account_password
+from samba.remove_dc import remove_dc
+from samba.provision import secretsdb_self_join
+from samba.dbchecker import dbcheck
+import re
+from samba.provision import guess_names, determine_host_ip, determine_host_ip6
+from samba.provision.sambadns import (fill_dns_data_partitions,
+ get_dnsadmins_sid,
+ get_domainguid)
+from samba.tdb_util import tdb_copy
+from samba.mdb_util import mdb_copy
+import errno
+from subprocess import CalledProcessError
+from samba import sites
+from samba.dsdb import _dsdb_load_udv_v2
+from samba.ndr import ndr_pack
+from samba.credentials import SMB_SIGNING_REQUIRED
+from samba import safe_tarfile as tarfile
+
+
+# work out a SID (based on a free RID) to use when the domain gets restored.
+# This ensures that the restored DC's SID won't clash with any other RIDs
+# already in use in the domain
+def get_sid_for_restore(samdb, logger):
+ # Allocate a new RID without modifying the database. This should be safe,
+ # because we acquire the RID master role after creating an account using
+ # this RID during the restore process. Acquiring the RID master role
+ # creates a new RID pool which we will fetch RIDs from, so we shouldn't get
+ # duplicates.
+ try:
+ rid = samdb.next_free_rid()
+ except LdbError as err:
+ logger.info("A SID could not be allocated for restoring the domain. "
+ "Either no RID Set was found on this DC, "
+ "or the RID Set was not usable.")
+ logger.info("To initialise this DC's RID pools, obtain a RID Set from "
+ "this domain's RID master, or run samba-tool dbcheck "
+ "to fix the existing RID Set.")
+ raise CommandError("Cannot create backup", err)
+
+ # Construct full SID
+ sid = dom_sid(samdb.get_domain_sid())
+ sid_for_restore = str(sid) + '-' + str(rid)
+
+ # Confirm the SID is not already in use
+ try:
+ res = samdb.search(scope=ldb.SCOPE_BASE,
+ base='<SID=%s>' % sid_for_restore,
+ attrs=[],
+ controls=['show_deleted:1',
+ 'show_recycled:1'])
+ if len(res) != 1:
+ # This case makes no sense, but neither does a corrupt RID set
+ raise CommandError("Cannot create backup - "
+ "this DC's RID pool is corrupt, "
+ "the next SID (%s) appears to be in use." %
+ sid_for_restore)
+ raise CommandError("Cannot create backup - "
+ "this DC's RID pool is corrupt, "
+ "the next SID %s points to existing object %s. "
+ "Please run samba-tool dbcheck on the source DC." %
+ (sid_for_restore, res[0].dn))
+ except ldb.LdbError as e:
+ (enum, emsg) = e.args
+ if enum != ldb.ERR_NO_SUCH_OBJECT:
+ # We want NO_SUCH_OBJECT, anything else is a serious issue
+ raise
+
+ return str(sid) + '-' + str(rid)
+
+
+def smb_sysvol_conn(server, lp, creds):
+ """Returns an SMB connection to the sysvol share on the DC"""
+ # the SMB bindings rely on having a s3 loadparm
+ s3_lp = s3param.get_context()
+ s3_lp.load(lp.configfile)
+
+ # Force signing for the connection
+ saved_signing_state = creds.get_smb_signing()
+ creds.set_smb_signing(SMB_SIGNING_REQUIRED)
+ conn = libsmb.Conn(server, "sysvol", lp=s3_lp, creds=creds)
+ # Reset signing state
+ creds.set_smb_signing(saved_signing_state)
+ return conn
+
+
+def get_timestamp():
+ return datetime.datetime.now().isoformat().replace(':', '-')
+
+
+def backup_filepath(targetdir, name, time_str):
+ filename = 'samba-backup-%s-%s.tar.bz2' % (name, time_str)
+ return os.path.join(targetdir, filename)
+
+
+def create_backup_tar(logger, tmpdir, backup_filepath):
+ # Adds everything in the tmpdir into a new tar file
+ logger.info("Creating backup file %s..." % backup_filepath)
+ tf = tarfile.open(backup_filepath, 'w:bz2')
+ tf.add(tmpdir, arcname='./')
+ tf.close()
+
+
+def create_log_file(targetdir, lp, backup_type, server, include_secrets,
+ extra_info=None):
+ # create a summary file about the backup, which will get included in the
+ # tar file. This makes it easy for users to see what the backup involved,
+ # without having to untar the DB and interrogate it
+ f = open(os.path.join(targetdir, "backup.txt"), 'w')
+ try:
+ time_str = datetime.datetime.now().strftime('%Y-%b-%d %H:%M:%S')
+ f.write("Backup created %s\n" % time_str)
+ f.write("Using samba-tool version: %s\n" % lp.get('server string'))
+ f.write("Domain %s backup, using DC '%s'\n" % (backup_type, server))
+ f.write("Backup for domain %s (NetBIOS), %s (DNS realm)\n" %
+ (lp.get('workgroup'), lp.get('realm').lower()))
+ f.write("Backup contains domain secrets: %s\n" % str(include_secrets))
+ if extra_info:
+ f.write("%s\n" % extra_info)
+ finally:
+ f.close()
+
+
+# Add a backup-specific marker to the DB with info that we'll use during
+# the restore process
+def add_backup_marker(samdb, marker, value):
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
+ m[marker] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, marker)
+ samdb.modify(m)
+
+
+def check_targetdir(logger, targetdir):
+ if targetdir is None:
+ raise CommandError('Target directory required')
+
+ if not os.path.exists(targetdir):
+ logger.info('Creating targetdir %s...' % targetdir)
+ os.makedirs(targetdir)
+ elif not os.path.isdir(targetdir):
+ raise CommandError("%s is not a directory" % targetdir)
+
+
+# For '--no-secrets' backups, this sets the Administrator user's password to a
+# randomly-generated value. This is similar to the provision behaviour
+def set_admin_password(logger, samdb):
+ """Sets a randomly generated password for the backup DB's admin user"""
+
+ # match the admin user by RID
+ domainsid = samdb.get_domain_sid()
+ match_admin = "(objectsid=%s-%s)" % (domainsid,
+ security.DOMAIN_RID_ADMINISTRATOR)
+ search_expr = "(&(objectClass=user)%s)" % (match_admin,)
+
+ # retrieve the admin username (just in case it's been renamed)
+ res = samdb.search(base=samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
+ expression=search_expr)
+ username = str(res[0]['samaccountname'])
+
+ adminpass = samba.generate_random_password(12, 32)
+ logger.info("Setting %s password in backup to: %s" % (username, adminpass))
+ logger.info("Run 'samba-tool user setpassword %s' after restoring DB" %
+ username)
+ samdb.setpassword(search_expr, adminpass, force_change_at_next_login=False,
+ username=username)
+
+
+class cmd_domain_backup_online(samba.netcmd.Command):
+ """Copy a running DC's current DB into a backup tar file.
+
+ Takes a backup copy of the current domain from a running DC. If the domain
+ were to undergo a catastrophic failure, then the backup file can be used to
+ recover the domain. The backup created is similar to the DB that a new DC
+ would receive when it joins the domain.
+
+ Note that:
+ - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
+ and fix any errors it reports.
+ - all the domain's secrets are included in the backup file.
+ - although the DB contents can be untarred and examined manually, you need
+ to run 'samba-tool domain backup restore' before you can start a Samba DC
+ from the backup file."""
+
+ synopsis = "%prog --server=<DC-to-backup> --targetdir=<output-dir>"
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = [
+ Option("--server", help="The DC to backup", type=str),
+ Option("--targetdir", type=str,
+ help="Directory to write the backup file to"),
+ Option("--no-secrets", action="store_true", default=False,
+ help="Exclude secret values from the backup created"),
+ 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()),
+ ]
+
+ def run(self, sambaopts=None, credopts=None, server=None, targetdir=None,
+ no_secrets=False, backend_store=None):
+ logger = self.get_logger()
+ logger.setLevel(logging.DEBUG)
+
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+
+ # Make sure we have all the required args.
+ if server is None:
+ raise CommandError('Server required')
+
+ check_targetdir(logger, targetdir)
+
+ tmpdir = tempfile.mkdtemp(dir=targetdir)
+
+ # Run a clone join on the remote
+ include_secrets = not no_secrets
+ try:
+ ctx = join_clone(logger=logger, creds=creds, lp=lp,
+ include_secrets=include_secrets, server=server,
+ dns_backend='SAMBA_INTERNAL', targetdir=tmpdir,
+ backend_store=backend_store)
+
+ # get the paths used for the clone, then drop the old samdb connection
+ paths = ctx.paths
+ del ctx
+
+ # Get a free RID to use as the new DC's SID (when it gets restored)
+ remote_sam = SamDB(url='ldap://' + server, credentials=creds,
+ session_info=system_session(), lp=lp)
+ new_sid = get_sid_for_restore(remote_sam, logger)
+ realm = remote_sam.domain_dns_name()
+
+ # Grab the remote DC's sysvol files and bundle them into a tar file
+ logger.info("Backing up sysvol files (via SMB)...")
+ sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
+ smb_conn = smb_sysvol_conn(server, lp, creds)
+ backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
+
+ # remove the default sysvol files created by the clone (we want to
+ # make sure we restore the sysvol.tar.gz files instead)
+ shutil.rmtree(paths.sysvol)
+
+ # Edit the downloaded sam.ldb to mark it as a backup
+ samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
+ flags=ldb.FLG_DONT_CREATE_DB)
+ time_str = get_timestamp()
+ add_backup_marker(samdb, "backupDate", time_str)
+ add_backup_marker(samdb, "sidForRestore", new_sid)
+ add_backup_marker(samdb, "backupType", "online")
+
+ # ensure the admin user always has a password set (same as provision)
+ if no_secrets:
+ set_admin_password(logger, samdb)
+
+ # Add everything in the tmpdir to the backup tar file
+ backup_file = backup_filepath(targetdir, realm, time_str)
+ create_log_file(tmpdir, lp, "online", server, include_secrets)
+ create_backup_tar(logger, tmpdir, backup_file)
+ finally:
+ shutil.rmtree(tmpdir)
+
+
+class cmd_domain_backup_restore(cmd_fsmo_seize):
+ """Restore the domain's DB from a backup-file.
+
+ This restores a previously backed up copy of the domain's DB on a new DC.
+
+ Note that the restored DB will not contain the original DC that the backup
+ was taken from (or any other DCs in the original domain). Only the new DC
+ (specified by --newservername) will be present in the restored DB.
+
+ Samba can then be started against the restored DB. Any existing DCs for the
+ domain should be shutdown before the new DC is started. Other DCs can then
+ be joined to the new DC to recover the network.
+
+ Note that this command should be run as the root user - it will fail
+ otherwise."""
+
+ synopsis = ("%prog --backup-file=<tar-file> --targetdir=<output-dir> "
+ "--newservername=<DC-name>")
+ takes_options = [
+ Option("--backup-file", help="Path to backup file", type=str),
+ Option("--targetdir", help="Path to write to", type=str),
+ Option("--newservername", help="Name for new server", type=str),
+ Option("--host-ip", type="string", metavar="IPADDRESS",
+ help="set IPv4 ipaddress"),
+ Option("--host-ip6", type="string", metavar="IP6ADDRESS",
+ help="set IPv6 ipaddress"),
+ Option("--site", help="Site to add the new server in", type=str),
+ ]
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ def register_dns_zone(self, logger, samdb, lp, ntdsguid, host_ip,
+ host_ip6, site):
+ """
+ Registers the new realm's DNS objects when a renamed domain backup
+ is restored.
+ """
+ names = guess_names(lp)
+ domaindn = names.domaindn
+ forestdn = samdb.get_root_basedn().get_linearized()
+ dnsdomain = names.dnsdomain.lower()
+ dnsforest = dnsdomain
+ hostname = names.netbiosname.lower()
+ domainsid = dom_sid(samdb.get_domain_sid())
+ dnsadmins_sid = get_dnsadmins_sid(samdb, domaindn)
+ domainguid = get_domainguid(samdb, domaindn)
+
+ # work out the IP address to use for the new DC's DNS records
+ host_ip = determine_host_ip(logger, lp, host_ip)
+ host_ip6 = determine_host_ip6(logger, lp, host_ip6)
+
+ if host_ip is None and host_ip6 is None:
+ raise CommandError('Please specify a host-ip for the new server')
+
+ logger.info("DNS realm was renamed to %s" % dnsdomain)
+ logger.info("Populating DNS partitions for new realm...")
+
+ # Add the DNS objects for the new realm (note: the backup clone already
+ # has the root server objects, so don't add them again)
+ fill_dns_data_partitions(samdb, domainsid, site, domaindn,
+ forestdn, dnsdomain, dnsforest, hostname,
+ host_ip, host_ip6, domainguid, ntdsguid,
+ dnsadmins_sid, add_root=False)
+
+ def fix_old_dc_references(self, samdb):
+ """Fixes attributes that reference the old/removed DCs"""
+
+ # we just want to fix up DB problems here that were introduced by us
+ # removing the old DCs. We restrict what we fix up so that the restored
+ # DB matches the backed-up DB as close as possible. (There may be other
+ # DB issues inherited from the backed-up DC, but it's not our place to
+ # silently try to fix them here).
+ samdb.transaction_start()
+ chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
+ in_transaction=True)
+
+ # fix up stale references to the old DC
+ setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
+ attrs = ['lastKnownParent', 'interSiteTopologyGenerator']
+
+ # fix-up stale one-way links that point to the old DC
+ setattr(chk, 'remove_plausible_deleted_DN_links', 'ALL')
+ attrs += ['msDS-NC-Replica-Locations']
+
+ cross_ncs_ctrl = 'search_options:1:2'
+ controls = ['show_deleted:1', cross_ncs_ctrl]
+ chk.check_database(controls=controls, attrs=attrs)
+ samdb.transaction_commit()
+
+ def create_default_site(self, samdb, logger):
+ """Creates the default site, if it doesn't already exist"""
+
+ sitename = DEFAULTSITE
+ search_expr = "(&(cn={0})(objectclass=site))".format(sitename)
+ res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
+ expression=search_expr)
+
+ if len(res) == 0:
+ logger.info("Creating default site '{0}'".format(sitename))
+ sites.create_site(samdb, samdb.get_config_basedn(), sitename)
+
+ return sitename
+
+ def remove_backup_markers(self, samdb):
+ """Remove DB markers added by the backup process"""
+
+ # check what markers we need to remove (this may vary)
+ markers = ['sidForRestore', 'backupRename', 'backupDate', 'backupType']
+ res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+ scope=ldb.SCOPE_BASE,
+ attrs=markers)
+
+ # remove any markers that exist in the DB
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, "@SAMBA_DSDB")
+
+ for attr in markers:
+ if attr in res[0]:
+ m[attr] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, attr)
+
+ samdb.modify(m)
+
+ def get_backup_type(self, samdb):
+ res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+ scope=ldb.SCOPE_BASE,
+ attrs=['backupRename', 'backupType'])
+
+ # note that the backupType marker won't exist on backups created on
+ # v4.9. However, we can still infer the type, as only rename and
+ # online backups are supported on v4.9
+ if 'backupType' in res[0]:
+ backup_type = str(res[0]['backupType'])
+ elif 'backupRename' in res[0]:
+ backup_type = "rename"
+ else:
+ backup_type = "online"
+
+ return backup_type
+
+ def save_uptodate_vectors(self, samdb, partitions):
+ """Ensures the UTDV used by DRS is correct after an offline backup"""
+ for nc in partitions:
+ # load the replUpToDateVector we *should* have
+ utdv = _dsdb_load_udv_v2(samdb, nc)
+
+ # convert it to NDR format and write it into the DB
+ utdv_blob = drsblobs.replUpToDateVectorBlob()
+ utdv_blob.version = 2
+ utdv_blob.ctr.cursors = utdv
+ utdv_blob.ctr.count = len(utdv)
+ new_value = ndr_pack(utdv_blob)
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, nc)
+ m["replUpToDateVector"] = ldb.MessageElement(new_value,
+ ldb.FLAG_MOD_REPLACE,
+ "replUpToDateVector")
+ samdb.modify(m)
+
+ def run(self, sambaopts=None, credopts=None, backup_file=None,
+ targetdir=None, newservername=None, host_ip=None, host_ip6=None,
+ site=None):
+ if not (backup_file and os.path.exists(backup_file)):
+ raise CommandError('Backup file not found.')
+ if targetdir is None:
+ raise CommandError('Please specify a target directory')
+ # allow restoredc to install into a directory prepopulated by selftest
+ if (os.path.exists(targetdir) and os.listdir(targetdir) and
+ os.environ.get('SAMBA_SELFTEST') != '1'):
+ raise CommandError('Target directory is not empty')
+ if not newservername:
+ raise CommandError('Server name required')
+
+ logger = logging.getLogger()
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ # ldapcmp prefers the server's netBIOS name in upper-case
+ newservername = newservername.upper()
+
+ # extract the backup .tar to a temp directory
+ targetdir = os.path.abspath(targetdir)
+ tf = tarfile.open(backup_file)
+ tf.extractall(targetdir)
+ tf.close()
+
+ # use the smb.conf that got backed up, by default (save what was
+ # actually backed up, before we mess with it)
+ smbconf = os.path.join(targetdir, 'etc', 'smb.conf')
+ shutil.copyfile(smbconf, smbconf + ".orig")
+
+ # if a smb.conf was specified on the cmd line, then use that instead
+ cli_smbconf = sambaopts.get_loadparm_path()
+ if cli_smbconf:
+ logger.info("Using %s as restored domain's smb.conf" % cli_smbconf)
+ shutil.copyfile(cli_smbconf, smbconf)
+
+ lp = samba.param.LoadParm()
+ lp.load(smbconf)
+
+ # open a DB connection to the restored DB
+ private_dir = os.path.join(targetdir, 'private')
+ samdb_path = os.path.join(private_dir, 'sam.ldb')
+ samdb = SamDB(url=samdb_path, session_info=system_session(), lp=lp,
+ flags=ldb.FLG_DONT_CREATE_DB)
+ backup_type = self.get_backup_type(samdb)
+
+ if site is None:
+ # There's no great way to work out the correct site to add the
+ # restored DC to. By default, add it to Default-First-Site-Name,
+ # creating the site if it doesn't already exist
+ site = self.create_default_site(samdb, logger)
+ logger.info("Adding new DC to site '{0}'".format(site))
+
+ # read the naming contexts out of the DB
+ res = samdb.search(base="", scope=ldb.SCOPE_BASE,
+ attrs=['namingContexts'])
+ ncs = [str(r) for r in res[0].get('namingContexts')]
+
+ # for offline backups we need to make sure the upToDateness info
+ # contains the invocation-ID and highest-USN of the DC we backed up.
+ # Otherwise replication propagation dampening won't correctly filter
+ # objects created by that DC
+ if backup_type == "offline":
+ self.save_uptodate_vectors(samdb, ncs)
+
+ # Create account using the join_add_objects function in the join object
+ # We need namingContexts, account control flags, and the sid saved by
+ # the backup process.
+ creds = credopts.get_credentials(lp)
+ ctx = DCJoinContext(logger, creds=creds, lp=lp, site=site,
+ forced_local_samdb=samdb,
+ netbios_name=newservername)
+ ctx.nc_list = ncs
+ ctx.full_nc_list = ncs
+ ctx.userAccountControl = (samba.dsdb.UF_SERVER_TRUST_ACCOUNT |
+ samba.dsdb.UF_TRUSTED_FOR_DELEGATION)
+
+ # rewrite the smb.conf to make sure it uses the new targetdir settings.
+ # (This doesn't update all filepaths in a customized config, but it
+ # corrects the same paths that get set by a new provision)
+ logger.info('Updating basic smb.conf settings...')
+ make_smbconf(smbconf, newservername, ctx.domain_name,
+ ctx.realm, targetdir, lp=lp,
+ serverrole="active directory domain controller")
+
+ # Get the SID saved by the backup process and create account
+ res = samdb.search(base=ldb.Dn(samdb, "@SAMBA_DSDB"),
+ scope=ldb.SCOPE_BASE,
+ attrs=['sidForRestore'])
+ sid = res[0].get('sidForRestore')[0]
+ logger.info('Creating account with SID: ' + str(sid))
+ try:
+ ctx.join_add_objects(specified_sid=dom_sid(str(sid)))
+ except LdbError as e:
+ (enum, emsg) = e.args
+ if enum != ldb.ERR_CONSTRAINT_VIOLATION:
+ raise
+
+ dup_res = []
+ try:
+ dup_res = samdb.search(base=ldb.Dn(samdb, "<SID=%s>" % sid),
+ scope=ldb.SCOPE_BASE,
+ attrs=['objectGUID'],
+ controls=["show_deleted:0",
+ "show_recycled:0"])
+ except LdbError as dup_e:
+ (dup_enum, _) = dup_e.args
+ if dup_enum != ldb.ERR_NO_SUCH_OBJECT:
+ raise
+
+ if (len(dup_res) != 1):
+ raise
+
+ objectguid = samdb.schema_format_value("objectGUID",
+ dup_res[0]["objectGUID"][0])
+ objectguid = objectguid.decode('utf-8')
+ logger.error("The RID Pool on the source DC for the backup in %s "
+ "may be corrupt "
+ "or in conflict with SIDs already allocated "
+ "in the domain. " % backup_file)
+ logger.error("Running 'samba-tool dbcheck' on the source "
+ "DC (and obtaining a new backup) may correct the issue.")
+ logger.error("Alternatively please obtain a new backup "
+ "against a different DC.")
+ logger.error("The SID we wish to use (%s) is recorded in "
+ "@SAMBA_DSDB as the sidForRestore attribute."
+ % sid)
+
+ raise CommandError("Domain restore failed because there "
+ "is already an existing object (%s) "
+ "with SID %s and objectGUID %s. "
+ "This conflicts with "
+ "the new DC account we want to add "
+ "for the restored domain. " % (
+ dup_res[0].dn, sid, objectguid))
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, '@ROOTDSE')
+ ntds_guid = str(ctx.ntds_guid)
+ m["dsServiceName"] = ldb.MessageElement("<GUID=%s>" % ntds_guid,
+ ldb.FLAG_MOD_REPLACE,
+ "dsServiceName")
+ samdb.modify(m)
+
+ # if we renamed the backed-up domain, then we need to add the DNS
+ # objects for the new realm (we do this in the restore, now that we
+ # know the new DC's IP address)
+ if backup_type == "rename":
+ self.register_dns_zone(logger, samdb, lp, ctx.ntds_guid,
+ host_ip, host_ip6, site)
+
+ secrets_path = os.path.join(private_dir, 'secrets.ldb')
+ secrets_ldb = Ldb(secrets_path, session_info=system_session(), lp=lp,
+ flags=ldb.FLG_DONT_CREATE_DB)
+ secretsdb_self_join(secrets_ldb, domain=ctx.domain_name,
+ realm=ctx.realm, dnsdomain=ctx.dnsdomain,
+ netbiosname=ctx.myname, domainsid=ctx.domsid,
+ machinepass=ctx.acct_pass,
+ key_version_number=ctx.key_version_number,
+ secure_channel_type=misc.SEC_CHAN_BDC)
+
+ # Seize DNS roles
+ domain_dn = samdb.domain_dn()
+ forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
+ dns_roles = [("domaindns", domain_dn),
+ ("forestdns", forest_dn)]
+ for role, dn in dns_roles:
+ if dn in ncs:
+ self.seize_dns_role(role, samdb, None, None, None, force=True)
+
+ # Seize other roles
+ for role in ['rid', 'pdc', 'naming', 'infrastructure', 'schema']:
+ self.seize_role(role, samdb, force=True)
+
+ # Get all DCs and remove them (this ensures these DCs cannot
+ # replicate because they will not have a password)
+ search_expr = "(&(objectClass=Server)(serverReference=*))"
+ res = samdb.search(samdb.get_config_basedn(), scope=ldb.SCOPE_SUBTREE,
+ expression=search_expr)
+ for m in res:
+ cn = str(m.get('cn')[0])
+ if cn != newservername:
+ remove_dc(samdb, logger, cn)
+
+ # Remove the repsFrom and repsTo from each NC to ensure we do
+ # not try (and fail) to talk to the old DCs
+ for nc in ncs:
+ msg = ldb.Message()
+ msg.dn = ldb.Dn(samdb, nc)
+
+ msg["repsFrom"] = ldb.MessageElement([],
+ ldb.FLAG_MOD_REPLACE,
+ "repsFrom")
+ msg["repsTo"] = ldb.MessageElement([],
+ ldb.FLAG_MOD_REPLACE,
+ "repsTo")
+ samdb.modify(msg)
+
+ # Update the krbtgt passwords twice, ensuring no tickets from
+ # the old domain are valid
+ update_krbtgt_account_password(samdb)
+ update_krbtgt_account_password(samdb)
+
+ # restore the sysvol directory from the backup tar file, including the
+ # original NTACLs. Note that the backup_restore() will fail if not root
+ sysvol_tar = os.path.join(targetdir, 'sysvol.tar.gz')
+ dest_sysvol_dir = lp.get('path', 'sysvol')
+ if not os.path.exists(dest_sysvol_dir):
+ os.makedirs(dest_sysvol_dir)
+ backup_restore(sysvol_tar, dest_sysvol_dir, samdb, smbconf)
+ os.remove(sysvol_tar)
+
+ # fix up any stale links to the old DCs we just removed
+ logger.info("Fixing up any remaining references to the old DCs...")
+ self.fix_old_dc_references(samdb)
+
+ # Remove DB markers added by the backup process
+ self.remove_backup_markers(samdb)
+
+ logger.info("Backup file successfully restored to %s" % targetdir)
+ logger.info("Please check the smb.conf settings are correct before "
+ "starting samba.")
+
+
+class cmd_domain_backup_rename(samba.netcmd.Command):
+ """Copy a running DC's DB to backup file, renaming the domain in the process.
+
+ Where <new-domain> is the new domain's NetBIOS name, and <new-dnsrealm> is
+ the new domain's realm in DNS form.
+
+ This is similar to 'samba-tool backup online' in that it clones the DB of a
+ running DC. However, this option also renames all the domain entries in the
+ DB. Renaming the domain makes it possible to restore and start a new Samba
+ DC without it interfering with the existing Samba domain. In other words,
+ you could use this option to clone your production samba domain and restore
+ it to a separate pre-production environment that won't overlap or interfere
+ with the existing production Samba domain.
+
+ Note that:
+ - it's recommended to run 'samba-tool dbcheck' before taking a backup-file
+ and fix any errors it reports.
+ - all the domain's secrets are included in the backup file.
+ - although the DB contents can be untarred and examined manually, you need
+ to run 'samba-tool domain backup restore' before you can start a Samba DC
+ from the backup file.
+ - GPO and sysvol information will still refer to the old realm and will
+ need to be updated manually.
+ - if you specify 'keep-dns-realm', then the DNS records will need updating
+ in order to work (they will still refer to the old DC's IP instead of the
+ new DC's address).
+ - we recommend that you only use this option if you know what you're doing.
+ """
+
+ synopsis = ("%prog <new-domain> <new-dnsrealm> --server=<DC-to-backup> "
+ "--targetdir=<output-dir>")
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = [
+ Option("--server", help="The DC to backup", type=str),
+ Option("--targetdir", help="Directory to write the backup file",
+ type=str),
+ Option("--keep-dns-realm", action="store_true", default=False,
+ help="Retain the DNS entries for the old realm in the backup"),
+ Option("--no-secrets", action="store_true", default=False,
+ help="Exclude secret values from the backup created"),
+ 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()),
+ ]
+
+ takes_args = ["new_domain_name", "new_dns_realm"]
+
+ def update_dns_root(self, logger, samdb, old_realm, delete_old_dns):
+ """Updates dnsRoot for the partition objects to reflect the rename"""
+
+ # lookup the crossRef objects that hold the old realm's dnsRoot
+ partitions_dn = samdb.get_partitions_dn()
+ res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
+ attrs=["dnsRoot"],
+ expression='(&(objectClass=crossRef)(dnsRoot=*))')
+ new_realm = samdb.domain_dns_name()
+
+ # go through and add the new realm
+ for res_msg in res:
+ # dnsRoot can be multi-valued, so only look for the old realm
+ for dns_root in res_msg["dnsRoot"]:
+ dns_root = str(dns_root)
+ dn = res_msg.dn
+ if old_realm in dns_root:
+ new_dns_root = re.sub('%s$' % old_realm, new_realm,
+ dns_root)
+ logger.info("Adding %s dnsRoot to %s" % (new_dns_root, dn))
+
+ m = ldb.Message()
+ m.dn = dn
+ m["dnsRoot"] = ldb.MessageElement(new_dns_root,
+ ldb.FLAG_MOD_ADD,
+ "dnsRoot")
+ samdb.modify(m)
+
+ # optionally remove the dnsRoot for the old realm
+ if delete_old_dns:
+ logger.info("Removing %s dnsRoot from %s" % (dns_root,
+ dn))
+ m["dnsRoot"] = ldb.MessageElement(dns_root,
+ ldb.FLAG_MOD_DELETE,
+ "dnsRoot")
+ samdb.modify(m)
+
+ # Updates the CN=<domain>,CN=Partitions,CN=Configuration,... object to
+ # reflect the domain rename
+ def rename_domain_partition(self, logger, samdb, new_netbios_name):
+ """Renames the domain partition object and updates its nETBIOSName"""
+
+ # lookup the crossRef object that holds the nETBIOSName (nCName has
+ # already been updated by this point, but the netBIOS hasn't)
+ base_dn = samdb.get_default_basedn()
+ nc_name = ldb.binary_encode(str(base_dn))
+ partitions_dn = samdb.get_partitions_dn()
+ res = samdb.search(base=partitions_dn, scope=ldb.SCOPE_ONELEVEL,
+ attrs=["nETBIOSName"],
+ expression='ncName=%s' % nc_name)
+
+ logger.info("Changing backup domain's NetBIOS name to %s" %
+ new_netbios_name)
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["nETBIOSName"] = ldb.MessageElement(new_netbios_name,
+ ldb.FLAG_MOD_REPLACE,
+ "nETBIOSName")
+ samdb.modify(m)
+
+ # renames the object itself to reflect the change in domain
+ new_dn = "CN=%s,%s" % (new_netbios_name, partitions_dn)
+ logger.info("Renaming %s --> %s" % (res[0].dn, new_dn))
+ samdb.rename(res[0].dn, new_dn, controls=['relax:0'])
+
+ def delete_old_dns_zones(self, logger, samdb, old_realm):
+ # remove the top-level DNS entries for the old realm
+ basedn = samdb.get_default_basedn()
+ dn = "DC=%s,CN=MicrosoftDNS,DC=DomainDnsZones,%s" % (old_realm, basedn)
+ logger.info("Deleting old DNS zone %s" % dn)
+ samdb.delete(dn, ["tree_delete:1"])
+
+ forestdn = samdb.get_root_basedn().get_linearized()
+ dn = "DC=_msdcs.%s,CN=MicrosoftDNS,DC=ForestDnsZones,%s" % (old_realm,
+ forestdn)
+ logger.info("Deleting old DNS zone %s" % dn)
+ samdb.delete(dn, ["tree_delete:1"])
+
+ def fix_old_dn_attributes(self, samdb):
+ """Fixes attributes (i.e. objectCategory) that still use the old DN"""
+
+ samdb.transaction_start()
+ # Just fix any mismatches in DN detected (leave any other errors)
+ chk = dbcheck(samdb, quiet=True, fix=True, yes=False,
+ in_transaction=True)
+ # fix up incorrect objectCategory/etc attributes
+ setattr(chk, 'fix_all_old_dn_string_component_mismatch', 'ALL')
+ cross_ncs_ctrl = 'search_options:1:2'
+ controls = ['show_deleted:1', cross_ncs_ctrl]
+ chk.check_database(controls=controls)
+ samdb.transaction_commit()
+
+ def run(self, new_domain_name, new_dns_realm, sambaopts=None,
+ credopts=None, server=None, targetdir=None, keep_dns_realm=False,
+ no_secrets=False, backend_store=None):
+ logger = self.get_logger()
+ logger.setLevel(logging.INFO)
+
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+
+ # Make sure we have all the required args.
+ if server is None:
+ raise CommandError('Server required')
+
+ check_targetdir(logger, targetdir)
+
+ delete_old_dns = not keep_dns_realm
+
+ new_dns_realm = new_dns_realm.lower()
+ new_domain_name = new_domain_name.upper()
+
+ new_base_dn = samba.dn_from_dns_name(new_dns_realm)
+ logger.info("New realm for backed up domain: %s" % new_dns_realm)
+ logger.info("New base DN for backed up domain: %s" % new_base_dn)
+ logger.info("New domain NetBIOS name: %s" % new_domain_name)
+
+ tmpdir = tempfile.mkdtemp(dir=targetdir)
+
+ # setup a join-context for cloning the remote server
+ include_secrets = not no_secrets
+ ctx = DCCloneAndRenameContext(new_base_dn, new_domain_name,
+ new_dns_realm, logger=logger,
+ creds=creds, lp=lp,
+ include_secrets=include_secrets,
+ dns_backend='SAMBA_INTERNAL',
+ server=server, targetdir=tmpdir,
+ backend_store=backend_store)
+
+ # sanity-check we're not "renaming" the domain to the same values
+ old_domain = ctx.domain_name
+ if old_domain == new_domain_name:
+ shutil.rmtree(tmpdir)
+ raise CommandError("Cannot use the current domain NetBIOS name.")
+
+ old_realm = ctx.realm
+ if old_realm == new_dns_realm:
+ shutil.rmtree(tmpdir)
+ raise CommandError("Cannot use the current domain DNS realm.")
+
+ # do the clone/rename
+ ctx.do_join()
+
+ # get the paths used for the clone, then drop the old samdb connection
+ del ctx.local_samdb
+ paths = ctx.paths
+
+ # get a free RID to use as the new DC's SID (when it gets restored)
+ remote_sam = SamDB(url='ldap://' + server, credentials=creds,
+ session_info=system_session(), lp=lp)
+ new_sid = get_sid_for_restore(remote_sam, logger)
+
+ # Grab the remote DC's sysvol files and bundle them into a tar file.
+ # Note we end up with 2 sysvol dirs - the original domain's files (that
+ # use the old realm) backed here, as well as default files generated
+ # for the new realm as part of the clone/join.
+ sysvol_tar = os.path.join(tmpdir, 'sysvol.tar.gz')
+ smb_conn = smb_sysvol_conn(server, lp, creds)
+ backup_online(smb_conn, sysvol_tar, remote_sam.get_domain_sid())
+
+ # connect to the local DB (making sure we use the new/renamed config)
+ lp.load(paths.smbconf)
+ samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
+ flags=ldb.FLG_DONT_CREATE_DB)
+
+ # Edit the cloned sam.ldb to mark it as a backup
+ time_str = get_timestamp()
+ add_backup_marker(samdb, "backupDate", time_str)
+ add_backup_marker(samdb, "sidForRestore", new_sid)
+ add_backup_marker(samdb, "backupRename", old_realm)
+ add_backup_marker(samdb, "backupType", "rename")
+
+ # fix up the DNS objects that are using the old dnsRoot value
+ self.update_dns_root(logger, samdb, old_realm, delete_old_dns)
+
+ # update the netBIOS name and the Partition object for the domain
+ self.rename_domain_partition(logger, samdb, new_domain_name)
+
+ if delete_old_dns:
+ self.delete_old_dns_zones(logger, samdb, old_realm)
+
+ logger.info("Fixing DN attributes after rename...")
+ self.fix_old_dn_attributes(samdb)
+
+ # ensure the admin user always has a password set (same as provision)
+ if no_secrets:
+ set_admin_password(logger, samdb)
+
+ # Add everything in the tmpdir to the backup tar file
+ backup_file = backup_filepath(targetdir, new_dns_realm, time_str)
+ create_log_file(tmpdir, lp, "rename", server, include_secrets,
+ "Original domain %s (NetBIOS), %s (DNS realm)" %
+ (old_domain, old_realm))
+ create_backup_tar(logger, tmpdir, backup_file)
+
+ shutil.rmtree(tmpdir)
+
+
+class cmd_domain_backup_offline(samba.netcmd.Command):
+ """Backup the local domain directories safely into a tar file.
+
+ Takes a backup copy of the current domain from the local files on disk,
+ with proper locking of the DB to ensure consistency. If the domain were to
+ undergo a catastrophic failure, then the backup file can be used to recover
+ the domain.
+
+ An offline backup differs to an online backup in the following ways:
+ - a backup can be created even if the DC isn't currently running.
+ - includes non-replicated attributes that an online backup wouldn't store.
+ - takes a copy of the raw database files, which has the risk that any
+ hidden problems in the DB are preserved in the backup."""
+
+ synopsis = "%prog [options]"
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ }
+
+ takes_options = [
+ Option("--targetdir",
+ help="Output directory (required)",
+ type=str),
+ ]
+
+ backup_ext = '.bak-offline'
+
+ def offline_tdb_copy(self, path):
+ backup_path = path + self.backup_ext
+ try:
+ tdb_copy(path, backup_path, readonly=True)
+ except CalledProcessError as copy_err:
+ # If the copy didn't work, check if it was caused by an EINVAL
+ # error on opening the DB. If so, it's a mutex locked database,
+ # which we can safely ignore.
+ try:
+ tdb.open(path)
+ except Exception as e:
+ if hasattr(e, 'errno') and e.errno == errno.EINVAL:
+ return
+ raise e
+ raise copy_err
+
+ except FileNotFoundError as e:
+ # tdbbackup tool was not found.
+ raise CommandError(e.strerror, e)
+
+ if not os.path.exists(backup_path):
+ s = "tdbbackup said backup succeeded but {0} not found"
+ raise CommandError(s.format(backup_path))
+
+
+ def offline_mdb_copy(self, path):
+ mdb_copy(path, path + self.backup_ext)
+
+ # Secrets databases are a special case: a transaction must be started
+ # on the secrets.ldb file before backing up that file and secrets.tdb
+ def backup_secrets(self, private_dir, lp, logger):
+ secrets_path = os.path.join(private_dir, 'secrets')
+ secrets_obj = Ldb(secrets_path + '.ldb', lp=lp,
+ flags=ldb.FLG_DONT_CREATE_DB)
+ logger.info('Starting transaction on ' + secrets_path)
+ secrets_obj.transaction_start()
+ self.offline_tdb_copy(secrets_path + '.ldb')
+ self.offline_tdb_copy(secrets_path + '.tdb')
+ secrets_obj.transaction_cancel()
+
+ # sam.ldb must have a transaction started on it before backing up
+ # everything in sam.ldb.d with the appropriate backup function.
+ #
+ # Obtains the sidForRestore (SID for the new DC) and returns it
+ # from under the transaction
+ def backup_smb_dbs(self, private_dir, samdb, lp, logger):
+ sam_ldb_path = os.path.join(private_dir, 'sam.ldb')
+
+ # First, determine if DB backend is MDB. Assume not unless there is a
+ # 'backendStore' attribute on @PARTITION containing the text 'mdb'
+ store_label = "backendStore"
+ res = samdb.search(base="@PARTITION", scope=ldb.SCOPE_BASE,
+ attrs=[store_label])
+ mdb_backend = store_label in res[0] and str(res[0][store_label][0]) == 'mdb'
+
+ # This is needed to keep this variable in scope until the end
+ # of the transaction.
+ res_iterator = None
+
+ copy_function = None
+ if mdb_backend:
+ logger.info('MDB backend detected. Using mdb backup function.')
+ copy_function = self.offline_mdb_copy
+
+ # We can't backup with a write transaction open, so get a
+ # read lock with a search_iterator().
+ #
+ # We have tests in lib/ldb/tests/python/api.py that the
+ # search iterator takes a read lock effective against a
+ # transaction. This in turn will ensure there are no
+ # transactions on either the main or sub-database, even if
+ # the read locks were not enforced globally (they are).
+ res_iterator = samdb.search_iterator()
+ else:
+ logger.info('Starting transaction on ' + sam_ldb_path)
+ copy_function = self.offline_tdb_copy
+ samdb.transaction_start()
+
+ logger.info(' backing up ' + sam_ldb_path)
+ self.offline_tdb_copy(sam_ldb_path)
+ sam_ldb_d = sam_ldb_path + '.d'
+ for sam_file in os.listdir(sam_ldb_d):
+ sam_file = os.path.join(sam_ldb_d, sam_file)
+ if sam_file.endswith('.ldb'):
+ logger.info(' backing up locked/related file ' + sam_file)
+ copy_function(sam_file)
+ elif sam_file.endswith('.tdb'):
+ logger.info(' tdbbackup of locked/related file ' + sam_file)
+ self.offline_tdb_copy(sam_file)
+ else:
+ logger.info(' copying locked/related file ' + sam_file)
+ shutil.copyfile(sam_file, sam_file + self.backup_ext)
+
+ sid = get_sid_for_restore(samdb, logger)
+
+ if mdb_backend:
+ # Delete the iterator, release the read lock
+ del(res_iterator)
+ else:
+ samdb.transaction_cancel()
+
+ return sid
+
+ # Find where a path should go in the fixed backup archive structure.
+ def get_arc_path(self, path, conf_paths):
+ backup_dirs = {"private": conf_paths.private_dir,
+ "state": conf_paths.state_dir,
+ "etc": os.path.dirname(conf_paths.smbconf)}
+ matching_dirs = [(_, p) for (_, p) in backup_dirs.items() if
+ path.startswith(p)]
+ arc_path, fs_path = matching_dirs[0]
+
+ # If more than one directory is a parent of this path, then at least
+ # one configured path is a subdir of another. Use closest match.
+ if len(matching_dirs) > 1:
+ arc_path, fs_path = max(matching_dirs, key=lambda p: len(p[1]))
+ arc_path += path[len(fs_path):]
+
+ return arc_path
+
+ def run(self, sambaopts=None, targetdir=None):
+
+ logger = logging.getLogger()
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ # Get the absolute paths of all the directories we're going to backup
+ lp = sambaopts.get_loadparm()
+
+ paths = samba.provision.provision_paths_from_lp(lp, lp.get('realm'))
+ if not (paths.samdb and os.path.exists(paths.samdb)):
+ logger.error("No database found at {0}".format(paths.samdb))
+ raise CommandError('Please check you are root, and ' +
+ 'are running this command on an AD DC')
+
+ check_targetdir(logger, targetdir)
+
+ # Iterating over the directories in this specific order ensures that
+ # when the private directory contains hardlinks that are also contained
+ # in other directories to be backed up (such as in paths.binddns_dir),
+ # the hardlinks in the private directory take precedence.
+ backup_dirs = [paths.private_dir, paths.state_dir,
+ os.path.dirname(paths.smbconf)] # etc dir
+ logger.info('running backup on dirs: {0}'.format(' '.join(backup_dirs)))
+
+ # Recursively get all file paths in the backup directories
+ all_files = []
+ all_stats = set()
+ for backup_dir in backup_dirs:
+ for (working_dir, _, filenames) in os.walk(backup_dir):
+ if working_dir.startswith(paths.sysvol):
+ continue
+ if working_dir.endswith('.sock') or '.sock/' in working_dir:
+ continue
+ # The BIND DNS database can be regenerated, so it doesn't need
+ # to be backed up.
+ if working_dir.startswith(os.path.join(paths.binddns_dir, 'dns')):
+ continue
+
+ for filename in filenames:
+ full_path = os.path.join(working_dir, filename)
+
+ # Ignore files that have already been added. This prevents
+ # duplicates if one backup dir is a subdirectory of another,
+ # or if backup dirs contain hardlinks.
+ try:
+ s = os.stat(full_path, follow_symlinks=False)
+ except FileNotFoundError:
+ logger.warning(f"{full_path} does not exist!")
+ continue
+
+ if (s.st_ino, s.st_dev) in all_stats:
+ continue
+
+ # Assume existing backup files are from a previous backup.
+ # Delete and ignore.
+ if filename.endswith(self.backup_ext):
+ os.remove(full_path)
+ continue
+
+ # Sock files are autogenerated at runtime, ignore.
+ if filename.endswith('.sock'):
+ continue
+
+ all_files.append(full_path)
+ all_stats.add((s.st_ino, s.st_dev))
+
+ # We would prefer to open with FLG_RDONLY but then we can't
+ # start a transaction which is the strong isolation we want
+ # for the backup.
+ samdb = SamDB(url=paths.samdb, session_info=system_session(), lp=lp,
+ flags=ldb.FLG_DONT_CREATE_DB)
+
+ # Backup secrets, sam.ldb and their downstream files
+ self.backup_secrets(paths.private_dir, lp, logger)
+ sid = self.backup_smb_dbs(paths.private_dir, samdb, lp, logger)
+
+ # Get the domain SID so we can later place it in the backup
+ dom_sid_str = samdb.get_domain_sid()
+ dom_sid = security.dom_sid(dom_sid_str)
+
+ # Close the original samdb, to avoid any confusion, we will
+ # not use this any more as the data has all been copied under
+ # the transaction
+ samdb = None
+
+ # Open the new backed up samdb, flag it as backed up, and write
+ # the next SID so the restore tool can add objects. We use
+ # options=["modules:"] here to prevent any modules from loading.
+ # WARNING: Don't change this code unless you know what you're doing.
+ # Writing to a .bak file only works because the DN being
+ # written to happens to be top level.
+ samdb = Ldb(url=paths.samdb + self.backup_ext,
+ session_info=system_session(), lp=lp,
+ options=["modules:"], flags=ldb.FLG_DONT_CREATE_DB)
+ time_str = get_timestamp()
+ add_backup_marker(samdb, "backupDate", time_str)
+ add_backup_marker(samdb, "sidForRestore", sid)
+ add_backup_marker(samdb, "backupType", "offline")
+
+ # Close the backed up samdb
+ samdb = None
+
+ # Now handle all the LDB and TDB files that are not linked to
+ # anything else. Use transactions for LDBs.
+ for path in all_files:
+ if not os.path.exists(path + self.backup_ext):
+ if path.endswith('.ldb'):
+ logger.info('Starting transaction on solo db: ' + path)
+ ldb_obj = Ldb(path, lp=lp, flags=ldb.FLG_DONT_CREATE_DB)
+ ldb_obj.transaction_start()
+ logger.info(' running tdbbackup on the same file')
+ self.offline_tdb_copy(path)
+ ldb_obj.transaction_cancel()
+ elif path.endswith('.tdb'):
+ logger.info('running tdbbackup on lone tdb file ' + path)
+ self.offline_tdb_copy(path)
+
+ # Now make the backup tar file and add all
+ # backed up files and any other files to it.
+ temp_tar_dir = tempfile.mkdtemp(dir=targetdir,
+ prefix='INCOMPLETEsambabackupfile')
+ temp_tar_name = os.path.join(temp_tar_dir, "samba-backup.tar.bz2")
+ tar = tarfile.open(temp_tar_name, 'w:bz2')
+
+ logger.info('running offline ntacl backup of sysvol')
+ sysvol_tar_fn = 'sysvol.tar.gz'
+ sysvol_tar = os.path.join(temp_tar_dir, sysvol_tar_fn)
+ backup_offline(paths.sysvol, sysvol_tar, paths.smbconf, dom_sid)
+ tar.add(sysvol_tar, sysvol_tar_fn)
+ os.remove(sysvol_tar)
+
+ create_log_file(temp_tar_dir, lp, "offline", "localhost", True)
+ backup_fn = os.path.join(temp_tar_dir, "backup.txt")
+ tar.add(backup_fn, os.path.basename(backup_fn))
+ os.remove(backup_fn)
+
+ logger.info('building backup tar')
+ for path in all_files:
+ arc_path = self.get_arc_path(path, paths)
+
+ if os.path.exists(path + self.backup_ext):
+ logger.info(' adding backup ' + arc_path + self.backup_ext +
+ ' to tar and deleting file')
+ tar.add(path + self.backup_ext, arcname=arc_path)
+ os.remove(path + self.backup_ext)
+ elif path.endswith('.ldb') or path.endswith('.tdb'):
+ logger.info(' skipping ' + arc_path)
+ else:
+ logger.info(' adding misc file ' + arc_path)
+ tar.add(path, arcname=arc_path)
+
+ tar.close()
+ os.rename(temp_tar_name,
+ os.path.join(targetdir,
+ 'samba-backup-{0}.tar.bz2'.format(time_str)))
+ os.rmdir(temp_tar_dir)
+ logger.info('Backup succeeded.')
+
+
+class cmd_domain_backup(samba.netcmd.SuperCommand):
+ """Create or restore a backup of the domain."""
+ subcommands = {'offline': cmd_domain_backup_offline(),
+ 'online': cmd_domain_backup_online(),
+ 'rename': cmd_domain_backup_rename(),
+ 'restore': cmd_domain_backup_restore()}
diff --git a/python/samba/netcmd/domain/claim/__init__.py b/python/samba/netcmd/domain/claim/__init__.py
new file mode 100644
index 0000000..de7c4bb
--- /dev/null
+++ b/python/samba/netcmd/domain/claim/__init__.py
@@ -0,0 +1,35 @@
+# Unix SMB/CIFS implementation.
+#
+# claim management
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from samba.netcmd import SuperCommand
+
+from .claim_type import cmd_domain_claim_claim_type
+from .value_type import cmd_domain_claim_value_type
+
+
+class cmd_domain_claim(SuperCommand):
+ """Manage claims on the domain."""
+
+ subcommands = {
+ "claim-type": cmd_domain_claim_claim_type(),
+ "value-type": cmd_domain_claim_value_type(),
+ }
diff --git a/python/samba/netcmd/domain/claim/claim_type.py b/python/samba/netcmd/domain/claim/claim_type.py
new file mode 100644
index 0000000..c0825c6
--- /dev/null
+++ b/python/samba/netcmd/domain/claim/claim_type.py
@@ -0,0 +1,361 @@
+# Unix SMB/CIFS implementation.
+#
+# claim type management
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+import binascii
+import os
+
+import samba.getopt as options
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.netcmd.domain.models import AttributeSchema, ClassSchema,\
+ ClaimType, ValueType
+from samba.netcmd.domain.models.exceptions import ModelError
+
+
+class cmd_domain_claim_claim_type_create(Command):
+ """Create claim types on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--attribute", help="Attribute of claim type to create (required).",
+ dest="attribute_name", action="store", type=str, required=True),
+ Option("--class", help="Object classes to set claim type to.",
+ dest="class_names", action="append", type=str, required=True),
+ Option("--name", help="Optional display name or use attribute name.",
+ dest="name", action="store", type=str),
+ Option("--description",
+ help="Optional description or use from attribute.",
+ dest="description", action="store", type=str),
+ Option("--disable", help="Disable claim type.",
+ dest="disable", action="store_true"),
+ Option("--enable", help="Enable claim type.",
+ dest="enable", action="store_true"),
+ Option("--protect",
+ help="Protect claim type from accidental deletion.",
+ dest="protect", action="store_true"),
+ Option("--unprotect",
+ help="Unprotect claim type from accidental deletion.",
+ dest="unprotect", action="store_true")
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, name=None,
+ attribute_name=None, class_names=None, description=None,
+ disable=None, enable=None, protect=None, unprotect=None):
+
+ # mutually exclusive attributes
+ if enable and disable:
+ raise CommandError("--enable and --disable cannot be used together.")
+ if protect and unprotect:
+ raise CommandError("--protect and --unprotect cannot be used together.")
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ display_name = name or attribute_name
+ try:
+ claim_type = ClaimType.get(ldb, display_name=display_name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if a claim type with this display name already exists.
+ # Note: you can register the same claim type under another display name.
+ if claim_type:
+ raise CommandError(f"Claim type {display_name} already exists, "
+ "but you can use --name to use another name.")
+
+ # Lookup attribute and class names in schema.
+ try:
+ applies_to = [ClassSchema.lookup(ldb, name) for name in class_names]
+ attribute = AttributeSchema.lookup(ldb, attribute_name)
+ value_type = ValueType.lookup(ldb, attribute)
+ except (LookupError, ModelError, ValueError) as e:
+ raise CommandError(e)
+
+ # Generate the new Claim Type cn.
+ # Windows creates a random number here containing 16 hex digits.
+ # We can achieve something similar using urandom(8)
+ instance = binascii.hexlify(os.urandom(8)).decode()
+ cn = f"ad://ext/{display_name}:{instance}"
+
+ # adminDescription should be present but still have a fallback.
+ if description is None:
+ description = attribute.admin_description or display_name
+
+ # claim_is_value_space_restricted is always False because we don't
+ # yet support creating claims with a restricted possible values list.
+ claim_type = ClaimType(
+ cn=cn,
+ description=description,
+ display_name=display_name,
+ enabled=not disable,
+ claim_attribute_source=attribute.dn,
+ claim_is_single_valued=attribute.is_single_valued,
+ claim_is_value_space_restricted=False,
+ claim_source_type="AD",
+ claim_type_applies_to_class=[obj.dn for obj in applies_to],
+ claim_value_type=value_type.claim_value_type,
+ )
+
+ # Either --enable will be set or --disable but never both.
+ # The default if both are missing is enabled=True.
+ if enable is not None:
+ claim_type.enabled = enable
+ else:
+ claim_type.enabled = not disable
+
+ # Create claim type
+ try:
+ claim_type.save(ldb)
+
+ if protect:
+ claim_type.protect(ldb)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Claim type created successfully.
+ self.outf.write(f"Created claim type: {display_name}")
+ if attribute_name != display_name:
+ self.outf.write(f" ({attribute_name})\n")
+ else:
+ self.outf.write("\n")
+
+
+class cmd_domain_claim_claim_type_modify(Command):
+ """Modify claim types on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Display name of claim type to modify (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--class", help="Object classes to set claim type to.",
+ dest="class_names", action="append", type=str),
+ Option("--description", help="Set the claim type description.",
+ dest="description", action="store", type=str),
+ Option("--enable",
+ help="Enable claim type.",
+ dest="enable", action="store_true"),
+ Option("--disable",
+ help="Disable claim type.",
+ dest="disable", action="store_true"),
+ Option("--protect",
+ help="Protect claim type from accidental deletion.",
+ dest="protect", action="store_true"),
+ Option("--unprotect",
+ help="Unprotect claim type from accidental deletion.",
+ dest="unprotect", action="store_true")
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, name=None,
+ class_names=None, description=None, enable=None, disable=None,
+ protect=None, unprotect=None):
+
+ if enable and disable:
+ raise CommandError("--enable and --disable cannot be used together.")
+ if protect and unprotect:
+ raise CommandError("--protect and --unprotect cannot be used together.")
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ claim_type = ClaimType.get(ldb, display_name=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if claim type exists.
+ if not claim_type:
+ raise CommandError(f"Claim type {name} not found.")
+
+ # Either --enable will be set or --disable but never both.
+ if enable:
+ claim_type.enabled = True
+ elif disable:
+ claim_type.enabled = False
+
+ # Update the description.
+ if description is not None:
+ claim_type.description = description
+
+ # Change class names for claim type.
+ if class_names is not None:
+ try:
+ applies_to = [ClassSchema.lookup(ldb, name)
+ for name in class_names]
+ except (LookupError, ValueError) as e:
+ raise CommandError(e)
+
+ claim_type.claim_type_applies_to_class = [obj.dn for obj in applies_to]
+
+ # Update claim type.
+ try:
+ claim_type.save(ldb)
+
+ if protect:
+ claim_type.protect(ldb)
+ elif unprotect:
+ claim_type.unprotect(ldb)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Claim type updated successfully.
+ self.outf.write(f"Updated claim type: {name}\n")
+
+
+class cmd_domain_claim_claim_type_delete(Command):
+ """Delete claim types on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Display name of claim type to delete (required).",
+ dest="name", action="store", type=str, required=True),
+ Option("--force", help="Force claim type delete even if it is protected.",
+ dest="force", action="store_true")
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ name=None, force=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ claim_type = ClaimType.get(ldb, display_name=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if claim type exists first.
+ if claim_type is None:
+ raise CommandError(f"Claim type {name} not found.")
+
+ # Delete claim type.
+ try:
+ if force:
+ claim_type.unprotect(ldb)
+
+ claim_type.delete(ldb)
+ except ModelError as e:
+ if not force:
+ raise CommandError(
+ f"{e}\nTry --force to delete protected claim types.")
+ else:
+ raise CommandError(e)
+
+ # Claim type deleted successfully.
+ self.outf.write(f"Deleted claim type: {name}\n")
+
+
+class cmd_domain_claim_claim_type_list(Command):
+ """List claim types on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--json", help="Output results in JSON format.",
+ dest="output_format", action="store_const", const="json"),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ output_format=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ # Claim types grouped by displayName.
+ try:
+ claim_types = {claim_type.display_name: claim_type.as_dict()
+ for claim_type in ClaimType.query(ldb)}
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Using json output format gives more detail.
+ if output_format == "json":
+ self.print_json(claim_types)
+ else:
+ for claim_type in claim_types.keys():
+ self.outf.write(f"{claim_type}\n")
+
+
+class cmd_domain_claim_claim_type_view(Command):
+ """View a single claim type on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name", help="Display name of claim type to view (required).",
+ dest="name", action="store", type=str, required=True),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, name=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ claim_type = ClaimType.get(ldb, display_name=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if claim type exists first.
+ if claim_type is None:
+ raise CommandError(f"Claim type {name} not found.")
+
+ # Display claim type as JSON.
+ self.print_json(claim_type.as_dict())
+
+
+class cmd_domain_claim_claim_type(SuperCommand):
+ """Manage claim types on the domain."""
+
+ subcommands = {
+ "create": cmd_domain_claim_claim_type_create(),
+ "delete": cmd_domain_claim_claim_type_delete(),
+ "modify": cmd_domain_claim_claim_type_modify(),
+ "list": cmd_domain_claim_claim_type_list(),
+ "view": cmd_domain_claim_claim_type_view(),
+ }
diff --git a/python/samba/netcmd/domain/claim/value_type.py b/python/samba/netcmd/domain/claim/value_type.py
new file mode 100644
index 0000000..a261113
--- /dev/null
+++ b/python/samba/netcmd/domain/claim/value_type.py
@@ -0,0 +1,105 @@
+# Unix SMB/CIFS implementation.
+#
+# claim value type management
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+import samba.getopt as options
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.netcmd.domain.models import ValueType
+from samba.netcmd.domain.models.exceptions import ModelError
+
+
+class cmd_domain_claim_value_type_list(Command):
+ """List claim values types on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--json", help="Output results in JSON format.",
+ dest="output_format", action="store_const", const="json"),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None,
+ output_format=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ # Value types grouped by display name.
+ try:
+ value_types = {value_type.display_name: value_type.as_dict()
+ for value_type in ValueType.query(ldb)}
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Using json output format gives more detail.
+ if output_format == "json":
+ self.print_json(value_types)
+ else:
+ for value_type in value_types.keys():
+ self.outf.write(f"{value_type}\n")
+
+
+class cmd_domain_claim_value_type_view(Command):
+ """View a single claim value type on the domain."""
+
+ synopsis = "%prog -H <URL> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "hostopts": options.HostOptions,
+ }
+
+ takes_options = [
+ Option("--name",
+ help="Display name of claim value type to view (required).",
+ dest="name", action="store", type=str, required=True),
+ ]
+
+ def run(self, hostopts=None, sambaopts=None, credopts=None, name=None):
+
+ ldb = self.ldb_connect(hostopts, sambaopts, credopts)
+
+ try:
+ value_type = ValueType.get(ldb, display_name=name)
+ except ModelError as e:
+ raise CommandError(e)
+
+ # Check if value type exists first.
+ if value_type is None:
+ raise CommandError(f"Value type {name} not found.")
+
+ # Display vale type as JSON.
+ self.print_json(value_type.as_dict())
+
+
+class cmd_domain_claim_value_type(SuperCommand):
+ """Manage claim value types on the domain."""
+
+ subcommands = {
+ "list": cmd_domain_claim_value_type_list(),
+ "view": cmd_domain_claim_value_type_view(),
+ }
diff --git a/python/samba/netcmd/domain/classicupgrade.py b/python/samba/netcmd/domain/classicupgrade.py
new file mode 100644
index 0000000..5b6a8a8
--- /dev/null
+++ b/python/samba/netcmd/domain/classicupgrade.py
@@ -0,0 +1,189 @@
+# domain management - domain classicupgrade
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import os
+import tempfile
+import subprocess
+
+import samba
+import samba.getopt as options
+from samba.auth import system_session
+from samba.auth_util import system_session_unix
+from samba.common import get_string
+from samba.netcmd import Command, CommandError, Option
+from samba.samba3 import Samba3
+from samba.samba3 import param as s3param
+from samba.upgrade import upgrade_from_samba3
+
+from .common import common_ntvfs_options
+
+
+def get_testparm_var(testparm, smbconf, varname):
+ errfile = open(os.devnull, 'w')
+ p = subprocess.Popen([testparm, '-s', '-l',
+ '--parameter-name=%s' % varname, smbconf],
+ stdout=subprocess.PIPE, stderr=errfile)
+ (out, err) = p.communicate()
+ errfile.close()
+ lines = out.split(b'\n')
+ if lines:
+ return get_string(lines[0]).strip()
+ return ""
+
+
+class cmd_domain_classicupgrade(Command):
+ """Upgrade from Samba classic (NT4-like) database to Samba AD DC database.
+
+ Specify either a directory with all Samba classic DC databases and state files (with --dbdir) or
+ the testparm utility from your classic installation (with --testparm).
+ """
+
+ synopsis = "%prog [options] <classic_smb_conf>"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions
+ }
+
+ takes_options = [
+ Option("--dbdir", type="string", metavar="DIR",
+ help="Path to samba classic DC database directory"),
+ Option("--testparm", type="string", metavar="PATH",
+ help="Path to samba classic DC testparm utility from the previous installation. This allows the default paths of the previous installation to be followed"),
+ Option("--targetdir", type="string", metavar="DIR",
+ help="Path prefix where the new Samba 4.0 AD domain should be initialised"),
+ Option("-q", "--quiet", help="Be quiet", action="store_true"),
+ Option("-v", "--verbose", help="Be verbose", action="store_true"),
+ 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 (this DC will not be a DNS server)",
+ default="SAMBA_INTERNAL")
+ ]
+
+ ntvfs_options = [
+ Option("--use-xattrs", type="choice", choices=["yes", "no", "auto"],
+ 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 intelligent guess based on the user rights and system capabilities",
+ default="auto")
+ ]
+ if samba.is_ntvfs_fileserver_built():
+ takes_options.extend(common_ntvfs_options)
+ takes_options.extend(ntvfs_options)
+
+ takes_args = ["smbconf"]
+
+ def run(self, smbconf=None, targetdir=None, dbdir=None, testparm=None,
+ quiet=False, verbose=False, use_xattrs="auto", sambaopts=None, versionopts=None,
+ dns_backend=None, use_ntvfs=False):
+
+ if not os.path.exists(smbconf):
+ raise CommandError("File %s does not exist" % smbconf)
+
+ if testparm and not os.path.exists(testparm):
+ raise CommandError("Testparm utility %s does not exist" % testparm)
+
+ if dbdir and not os.path.exists(dbdir):
+ raise CommandError("Directory %s does not exist" % dbdir)
+
+ if not dbdir and not testparm:
+ raise CommandError("Please specify either dbdir or testparm")
+
+ logger = self.get_logger(verbose=verbose, quiet=quiet)
+
+ if dbdir and testparm:
+ logger.warning("both dbdir and testparm specified, ignoring dbdir.")
+ dbdir = None
+
+ lp = sambaopts.get_loadparm()
+
+ s3conf = s3param.get_context()
+
+ if sambaopts.realm:
+ s3conf.set("realm", sambaopts.realm)
+
+ if targetdir is not None:
+ if not os.path.isdir(targetdir):
+ os.mkdir(targetdir)
+
+ eadb = True
+ if use_xattrs == "yes":
+ eadb = False
+ elif use_xattrs == "auto" and not use_ntvfs:
+ eadb = False
+ elif not use_ntvfs:
+ raise CommandError("--use-xattrs=no requires --use-ntvfs (not supported for production use). "
+ "Please re-run with --use-xattrs omitted.")
+ elif use_xattrs == "auto" and not s3conf.get("posix:eadb"):
+ if targetdir:
+ tmpfile = tempfile.NamedTemporaryFile(dir=os.path.abspath(targetdir))
+ else:
+ tmpfile = tempfile.NamedTemporaryFile(dir=os.path.abspath(os.path.dirname(lp.get("private dir"))))
+ try:
+ try:
+ samba.ntacls.setntacl(lp, tmpfile.name,
+ "O:S-1-5-32G:S-1-5-32",
+ "S-1-5-32",
+ system_session_unix(),
+ "native")
+ eadb = False
+ except Exception:
+ # FIXME: Don't catch all exceptions here
+ logger.info("You are not root or your system does not support xattr, using tdb backend for attributes. "
+ "If you intend to use this provision in production, rerun the script as root on a system supporting xattrs.")
+ finally:
+ tmpfile.close()
+
+ # Set correct default values from dbdir or testparm
+ paths = {}
+ if dbdir:
+ paths["state directory"] = dbdir
+ paths["private dir"] = dbdir
+ paths["lock directory"] = dbdir
+ paths["smb passwd file"] = dbdir + "/smbpasswd"
+ else:
+ paths["state directory"] = get_testparm_var(testparm, smbconf, "state directory")
+ paths["private dir"] = get_testparm_var(testparm, smbconf, "private dir")
+ paths["smb passwd file"] = get_testparm_var(testparm, smbconf, "smb passwd file")
+ paths["lock directory"] = get_testparm_var(testparm, smbconf, "lock directory")
+ # "testparm" from Samba 3 < 3.4.x is not aware of the parameter
+ # "state directory", instead make use of "lock directory"
+ if len(paths["state directory"]) == 0:
+ paths["state directory"] = paths["lock directory"]
+
+ for p in paths:
+ s3conf.set(p, paths[p])
+
+ # load smb.conf parameters
+ logger.info("Reading smb.conf")
+ s3conf.load(smbconf)
+ samba3 = Samba3(smbconf, s3conf)
+
+ logger.info("Provisioning")
+ upgrade_from_samba3(samba3, logger, targetdir, session_info=system_session(),
+ useeadb=eadb, dns_backend=dns_backend, use_ntvfs=use_ntvfs)
diff --git a/python/samba/netcmd/domain/common.py b/python/samba/netcmd/domain/common.py
new file mode 100644
index 0000000..144d22b
--- /dev/null
+++ b/python/samba/netcmd/domain/common.py
@@ -0,0 +1,64 @@
+# domain management - common code
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+from samba.netcmd import Option
+from samba.samdb import get_default_backend_store
+
+common_ntvfs_options = [
+ Option("--use-ntvfs", help="Use NTVFS for the fileserver (default = no)",
+ action="store_true")
+]
+
+common_provision_join_options = [
+ Option("--machinepass", type="string", metavar="PASSWORD",
+ help="choose machine password (otherwise random)"),
+ Option("--plaintext-secrets", action="store_true",
+ help="Store secret/sensitive values as plain text on disk" +
+ "(default is to encrypt secret/sensitive values)"),
+ 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()),
+ Option("--backend-store-size", type="bytes", metavar="SIZE",
+ help="Specify the size of the backend database, currently only " +
+ "supported by lmdb backends (default is 8 Gb)."),
+ Option("--targetdir", metavar="DIR",
+ help="Set target directory (where to store provision)", type=str),
+ Option("-q", "--quiet", help="Be quiet", action="store_true"),
+]
+
+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"),
+ Option("-v", "--verbose", help="Be verbose", action="store_true")
+]
diff --git a/python/samba/netcmd/domain/dcpromo.py b/python/samba/netcmd/domain/dcpromo.py
new file mode 100644
index 0000000..bf78b74
--- /dev/null
+++ b/python/samba/netcmd/domain/dcpromo.py
@@ -0,0 +1,90 @@
+# domain management - domain dcpromo
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import samba
+import samba.getopt as options
+from samba.join import join_DC, join_RODC
+from samba.net import Net
+from samba.netcmd import Command, CommandError
+
+from .common import (common_join_options, common_ntvfs_options,
+ common_provision_join_options)
+
+
+class cmd_domain_dcpromo(Command):
+ """Promote an existing domain member or NT4 PDC to an AD DC."""
+
+ synopsis = "%prog <dnsdomain> [DC|RODC] [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = []
+ takes_options.extend(common_join_options)
+
+ takes_options.extend(common_provision_join_options)
+
+ if samba.is_ntvfs_fileserver_built():
+ takes_options.extend(common_ntvfs_options)
+
+ 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, machinepass=None,
+ use_ntvfs=False, dns_backend=None,
+ quiet=False, verbose=False, plaintext_secrets=False,
+ backend_store=None, backend_store_size=None):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+
+ logger = self.get_logger(verbose=verbose, quiet=quiet)
+
+ netbios_name = lp.get("netbios name")
+
+ if role is not None:
+ role = role.upper()
+
+ if role == "DC":
+ join_DC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
+ site=site, netbios_name=netbios_name, targetdir=targetdir,
+ domain_critical_only=domain_critical_only,
+ machinepass=machinepass, use_ntvfs=use_ntvfs,
+ dns_backend=dns_backend,
+ promote_existing=True, plaintext_secrets=plaintext_secrets,
+ backend_store=backend_store,
+ backend_store_size=backend_store_size)
+ elif role == "RODC":
+ join_RODC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
+ site=site, netbios_name=netbios_name, targetdir=targetdir,
+ domain_critical_only=domain_critical_only,
+ machinepass=machinepass, use_ntvfs=use_ntvfs, dns_backend=dns_backend,
+ promote_existing=True, plaintext_secrets=plaintext_secrets,
+ backend_store=backend_store,
+ backend_store_size=backend_store_size)
+ else:
+ raise CommandError("Invalid role '%s' (possible values: DC, RODC)" % role)
diff --git a/python/samba/netcmd/domain/demote.py b/python/samba/netcmd/domain/demote.py
new file mode 100644
index 0000000..ae4d11d
--- /dev/null
+++ b/python/samba/netcmd/domain/demote.py
@@ -0,0 +1,335 @@
+# domain management - domain demote
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import ldb
+import samba.getopt as options
+from samba import dsdb, remove_dc, werror
+from samba.auth import system_session
+from samba.dcerpc import drsuapi, misc
+from samba.drs_utils import drsuapi_connect
+from samba.dsdb import (
+ DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL,
+ DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL,
+ UF_PARTIAL_SECRETS_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT,
+ UF_TRUSTED_FOR_DELEGATION,
+ UF_WORKSTATION_TRUST_ACCOUNT
+)
+from samba.net import Net
+from samba.netcmd import Command, CommandError, Option
+from samba.samdb import SamDB
+
+
+class cmd_domain_demote(Command):
+ """Demote ourselves from the role of Domain Controller."""
+
+ synopsis = "%prog [options]"
+
+ takes_options = [
+ 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"),
+ Option("--remove-other-dead-server", help="Dead DC (name or NTDS GUID) "
+ "to remove ALL references to (rather than this DC)", type=str),
+ Option("-q", "--quiet", help="Be quiet", action="store_true"),
+ Option("-v", "--verbose", help="Be verbose", action="store_true"),
+ ]
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ def run(self, sambaopts=None, credopts=None,
+ versionopts=None, server=None,
+ remove_other_dead_server=None, H=None,
+ verbose=False, quiet=False):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+
+ logger = self.get_logger(verbose=verbose, quiet=quiet)
+
+ 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)
+ try:
+ remove_dc.remove_dc(samdb, logger, remove_other_dead_server)
+ except remove_dc.DemoteException as err:
+ raise CommandError("Demote failed: %s" % err)
+ return
+
+ netbios_name = lp.get("netbios name")
+ samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp)
+ 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):
+ raise CommandError("You are the last server in the domain")
+
+ server = None
+ for e in res:
+ if str(e["name"]).lower() != netbios_name.lower():
+ server = e["dnsHostName"]
+ break
+
+ ntds_guid = samdb.get_ntds_GUID()
+ msg = samdb.search(base=str(samdb.get_config_basedn()),
+ scope=ldb.SCOPE_SUBTREE, expression="(objectGUID=%s)" % ntds_guid,
+ attrs=['options'])
+ if len(msg) == 0 or "options" not in msg[0]:
+ raise CommandError("Failed to find options on %s" % ntds_guid)
+
+ ntds_dn = msg[0].dn
+ dsa_options = int(str(msg[0]['options']))
+
+ res = samdb.search(expression="(fSMORoleOwner=%s)" % str(ntds_dn),
+ controls=["search_options:1:2"])
+
+ if len(res) != 0:
+ raise CommandError("Current DC is still the owner of %d role(s), "
+ "use the role command to transfer roles to "
+ "another DC" %
+ len(res))
+
+ self.errf.write("Using %s as partner server for the demotion\n" %
+ server)
+ (drsuapiBind, drsuapi_handle, supportedExtensions) = drsuapi_connect(server, lp, creds)
+
+ self.errf.write("Deactivating inbound replication\n")
+
+ nmsg = ldb.Message()
+ nmsg.dn = msg[0].dn
+
+ if not (dsa_options & DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL) and not samdb.am_rodc():
+ dsa_options |= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
+ nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
+ samdb.modify(nmsg)
+
+ self.errf.write("Asking partner server %s to synchronize from us\n"
+ % server)
+ for part in (samdb.get_schema_basedn(),
+ samdb.get_config_basedn(),
+ samdb.get_root_basedn()):
+ nc = drsuapi.DsReplicaObjectIdentifier()
+ nc.dn = str(part)
+
+ req1 = drsuapi.DsReplicaSyncRequest1()
+ req1.naming_context = nc
+ req1.options = drsuapi.DRSUAPI_DRS_WRIT_REP
+ req1.source_dsa_guid = misc.GUID(ntds_guid)
+
+ try:
+ drsuapiBind.DsReplicaSync(drsuapi_handle, 1, req1)
+ except RuntimeError as e1:
+ (werr, string) = e1.args
+ if werr == werror.WERR_DS_DRA_NO_REPLICA:
+ pass
+ else:
+ self.errf.write(
+ "Error while replicating out last local changes from '%s' for demotion, "
+ "re-enabling inbound replication\n" % part)
+ dsa_options ^= DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL
+ nmsg["options"] = ldb.MessageElement(str(dsa_options), ldb.FLAG_MOD_REPLACE, "options")
+ samdb.modify(nmsg)
+ raise CommandError("Error while sending a DsReplicaSync for partition '%s'" % str(part), string)
+ try:
+ remote_samdb = SamDB(url="ldap://%s" % server,
+ session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ self.errf.write("Changing userControl and container\n")
+ res = remote_samdb.search(base=str(remote_samdb.domain_dn()),
+ expression="(&(objectClass=user)(sAMAccountName=%s$))" %
+ netbios_name.upper(),
+ attrs=["userAccountControl"])
+ dc_dn = res[0].dn
+ uac = int(str(res[0]["userAccountControl"]))
+
+ except Exception as e:
+ 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)
+ raise CommandError("Error while changing account control", e)
+
+ if (len(res) != 1):
+ 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)
+ raise CommandError("Unable to find object with samaccountName = %s$"
+ " in the remote dc" % netbios_name.upper())
+
+ uac &= ~(UF_SERVER_TRUST_ACCOUNT |
+ UF_TRUSTED_FOR_DELEGATION |
+ UF_PARTIAL_SECRETS_ACCOUNT)
+ uac |= UF_WORKSTATION_TRUST_ACCOUNT
+
+ msg = ldb.Message()
+ msg.dn = dc_dn
+
+ msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
+ ldb.FLAG_MOD_REPLACE,
+ "userAccountControl")
+ try:
+ remote_samdb.modify(msg)
+ except Exception as e:
+ 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)
+
+ raise CommandError("Error while changing account control", e)
+
+ dc_name = res[0].dn.get_rdn_value()
+ rdn = "CN=%s" % dc_name
+
+ # Let's move to the Computer container
+ i = 0
+ newrdn = str(rdn)
+
+ computer_dn = remote_samdb.get_wellknown_dn(
+ remote_samdb.get_default_basedn(),
+ dsdb.DS_GUID_COMPUTERS_CONTAINER)
+ 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),
+ scope=ldb.SCOPE_ONELEVEL)
+ while(len(res) != 0 and i < 100):
+ i = i + 1
+ res = remote_samdb.search(base=computer_dn, expression="%s-%d" % (rdn, i),
+ scope=ldb.SCOPE_ONELEVEL)
+
+ if i == 100:
+ 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)
+
+ msg = ldb.Message()
+ msg.dn = dc_dn
+
+ msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
+ ldb.FLAG_MOD_REPLACE,
+ "userAccountControl")
+
+ remote_samdb.modify(msg)
+
+ raise CommandError("Unable to find a slot for renaming %s,"
+ " all names from %s-1 to %s-%d seemed used" %
+ (str(dc_dn), rdn, rdn, i - 9))
+
+ newrdn = "%s-%d" % (rdn, i)
+
+ try:
+ newdn = ldb.Dn(remote_samdb, "%s,%s" % (newrdn, str(computer_dn)))
+ remote_samdb.rename(dc_dn, newdn)
+ except Exception as e:
+ 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)
+
+ msg = ldb.Message()
+ msg.dn = dc_dn
+
+ msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
+ ldb.FLAG_MOD_REPLACE,
+ "userAccountControl")
+
+ 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:
+ req1 = drsuapi.DsRemoveDSServerRequest1()
+ req1.server_dn = str(server_dsa_dn)
+ req1.domain_dn = str(domain)
+ req1.commit = 1
+
+ drsuapiBind.DsRemoveDSServer(drsuapi_handle, 1, req1)
+ except RuntimeError as e3:
+ (werr, string) = e3.args
+ 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)
+
+ msg = ldb.Message()
+ msg.dn = newdn
+
+ msg["userAccountControl"] = ldb.MessageElement("%d" % uac,
+ ldb.FLAG_MOD_REPLACE,
+ "userAccountControl")
+ remote_samdb.modify(msg)
+ remote_samdb.rename(newdn, dc_dn)
+ if werr == werror.WERR_DS_DRA_NO_REPLICA:
+ raise CommandError("The DC %s is not present on (already "
+ "removed from) the remote server: %s" %
+ (server_dsa_dn, e3))
+ else:
+ raise CommandError("Error while sending a removeDsServer "
+ "of %s: %s" %
+ (server_dsa_dn, e3))
+
+ remove_dc.remove_sysvol_references(remote_samdb, logger, dc_name)
+
+ # These are objects under the computer account that should be deleted
+ for s in ("CN=Enterprise,CN=NTFRS Subscriptions",
+ "CN=%s, CN=NTFRS Subscriptions" % lp.get("realm"),
+ "CN=Domain system Volumes (SYSVOL Share), CN=NTFRS Subscriptions",
+ "CN=NTFRS Subscriptions"):
+ try:
+ remote_samdb.delete(ldb.Dn(remote_samdb,
+ "%s,%s" % (s, str(newdn))))
+ except ldb.LdbError:
+ pass
+
+ # get dns host name for target server to demote, remove dns references
+ remove_dc.remove_dns_references(remote_samdb, logger, samdb.host_dns_name(),
+ ignore_no_name=True)
+
+ self.errf.write("Demote successful\n")
diff --git a/python/samba/netcmd/domain/functional_prep.py b/python/samba/netcmd/domain/functional_prep.py
new file mode 100644
index 0000000..3e1d4e1
--- /dev/null
+++ b/python/samba/netcmd/domain/functional_prep.py
@@ -0,0 +1,145 @@
+# domain management - domain functional_prep
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import ldb
+import samba.getopt as options
+from samba.auth import system_session
+from samba.dsdb import DS_DOMAIN_FUNCTION_2008, DS_DOMAIN_FUNCTION_2008_R2
+from samba.netcmd import Command, CommandError, Option
+from samba.netcmd.fsmo import get_fsmo_roleowner
+from samba.samdb import SamDB
+
+from samba import functional_level
+
+
+class cmd_domain_functional_prep(Command):
+ """Domain functional level preparation"""
+
+ synopsis = "%prog [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+ metavar="URL", dest="H"),
+ Option("-q", "--quiet", help="Be quiet", action="store_true"),
+ Option("-v", "--verbose", help="Be verbose", action="store_true"),
+ Option("--function-level", type="choice", metavar="FUNCTION_LEVEL",
+ choices=["2008_R2", "2012", "2012_R2", "2016"],
+ help="The functional level to prepare for. Default is (Windows) 2016.",
+ default="2016"),
+ Option("--forest-prep", action="store_true",
+ help="Run the forest prep (by default, both the domain and forest prep are run)."),
+ Option("--domain-prep", action="store_true",
+ help="Run the domain prep (by default, both the domain and forest prep are run).")
+ ]
+
+ def run(self, **kwargs):
+ updates_allowed_overridden = False
+ sambaopts = kwargs.get("sambaopts")
+ credopts = kwargs.get("credopts")
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+ H = kwargs.get("H")
+ function_level = kwargs.get("function_level")
+ try:
+ target_level = functional_level.string_to_level(function_level)
+ except KeyError:
+ raise CommandError(f"'{function_level}' is not known to Samba as an AD functional level")
+
+ forest_prep = kwargs.get("forest_prep")
+ domain_prep = kwargs.get("domain_prep")
+
+ samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp)
+
+ # we're not going to get far if the config doesn't allow schema updates
+ if lp.get("dsdb:schema update allowed") is None:
+ lp.set("dsdb:schema update allowed", "yes")
+ print("Temporarily overriding 'dsdb:schema update allowed' setting")
+ updates_allowed_overridden = True
+
+ if forest_prep is None and domain_prep is None:
+ forest_prep = True
+ domain_prep = True
+
+ own_dn = ldb.Dn(samdb, samdb.get_dsServiceName())
+ if forest_prep:
+ master = get_fsmo_roleowner(samdb, str(samdb.get_schema_basedn()),
+ 'schema')
+ if own_dn != master:
+ raise CommandError("This server is not the schema master.")
+
+ if domain_prep:
+ domain_dn = samdb.domain_dn()
+ infrastructure_dn = "CN=Infrastructure," + domain_dn
+ master = get_fsmo_roleowner(samdb, infrastructure_dn,
+ 'infrastructure')
+ if own_dn != master:
+ raise CommandError("This server is not the infrastructure master.")
+
+ exception_encountered = None
+
+ if forest_prep and exception_encountered is None:
+ samdb.transaction_start()
+ try:
+ from samba.forest_update import ForestUpdate
+ forest = ForestUpdate(samdb, fix=True)
+
+ forest.check_updates_iterator([11, 54, 79, 80, 81, 82, 83])
+ forest.check_updates_functional_level(target_level,
+ DS_DOMAIN_FUNCTION_2008_R2,
+ update_revision=True)
+
+ samdb.transaction_commit()
+ except Exception as e:
+ print("Exception: %s" % e)
+ samdb.transaction_cancel()
+ exception_encountered = e
+
+ if domain_prep and exception_encountered is None:
+ samdb.transaction_start()
+ try:
+ from samba.domain_update import DomainUpdate
+
+ domain = DomainUpdate(samdb, fix=True)
+ domain.check_updates_functional_level(target_level,
+ DS_DOMAIN_FUNCTION_2008,
+ update_revision=True)
+
+ samdb.transaction_commit()
+ except Exception as e:
+ print("Exception: %s" % e)
+ samdb.transaction_cancel()
+ exception_encountered = e
+
+ if updates_allowed_overridden:
+ lp.set("dsdb:schema update allowed", "no")
+
+ if exception_encountered is not None:
+ raise CommandError('Failed to perform functional prep: %r' %
+ exception_encountered)
diff --git a/python/samba/netcmd/domain/info.py b/python/samba/netcmd/domain/info.py
new file mode 100644
index 0000000..8454cb3
--- /dev/null
+++ b/python/samba/netcmd/domain/info.py
@@ -0,0 +1,58 @@
+# domain management - domain info
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import samba.getopt as options
+from samba.netcmd import Command, CommandError
+from samba.netcmd.common import netcmd_get_domain_infos_via_cldap
+
+
+class cmd_domain_info(Command):
+ """Print basic info about a domain and the DC passed as parameter."""
+
+ synopsis = "%prog <ip_address> [options]"
+
+ takes_options = [
+ ]
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ takes_args = ["address"]
+
+ def run(self, address, credopts=None, sambaopts=None, versionopts=None):
+ lp = sambaopts.get_loadparm()
+ try:
+ res = netcmd_get_domain_infos_via_cldap(lp, None, address)
+ except RuntimeError:
+ raise CommandError("Invalid IP address '" + address + "'!")
+ 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)
diff --git a/python/samba/netcmd/domain/join.py b/python/samba/netcmd/domain/join.py
new file mode 100644
index 0000000..936cfa8
--- /dev/null
+++ b/python/samba/netcmd/domain/join.py
@@ -0,0 +1,146 @@
+# domain management - domain join
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import os
+import tempfile
+
+import samba
+import samba.getopt as options
+from samba import is_ad_dc_built
+from samba.dcerpc import nbt
+from samba.join import join_DC, join_RODC
+from samba.net import LIBNET_JOIN_AUTOMATIC, Net
+from samba.net_s3 import Net as s3_Net
+from samba.netcmd import Command, CommandError, Option
+from samba.param import default_path
+from samba.samba3 import param as s3param
+
+from .common import common_join_options, common_provision_join_options
+
+
+class cmd_domain_join(Command):
+ """Join domain as either member or backup domain controller."""
+
+ synopsis = "%prog <dnsdomain> [DC|RODC|MEMBER] [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ ntvfs_options = [
+ Option(
+ "--use-ntvfs", help="Use NTVFS for the fileserver (default = no)",
+ action="store_true")
+ ]
+
+ selftest_options = [
+ Option("--experimental-s4-member", action="store_true",
+ help="Perform member joins using the s4 Net join_member. "
+ "Don't choose this unless you know what you're doing")
+ ]
+
+ takes_options = [
+ Option("--no-dns-updates", action="store_true",
+ help="Disable DNS updates")
+ ]
+ takes_options.extend(common_join_options)
+ takes_options.extend(common_provision_join_options)
+
+ if samba.is_ntvfs_fileserver_built():
+ takes_options.extend(ntvfs_options)
+
+ if samba.is_selftest_enabled():
+ takes_options.extend(selftest_options)
+
+ 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, machinepass=None,
+ use_ntvfs=False, experimental_s4_member=False, dns_backend=None,
+ quiet=False, verbose=False, no_dns_updates=False,
+ plaintext_secrets=False,
+ backend_store=None, backend_store_size=None):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+ net = Net(creds, lp, server=credopts.ipaddress)
+
+ logger = self.get_logger(verbose=verbose, quiet=quiet)
+
+ netbios_name = lp.get("netbios name")
+
+ if role is not None:
+ role = role.upper()
+
+ if role is None or role == "MEMBER":
+ if experimental_s4_member:
+ (join_password, sid, domain_name) = net.join_member(
+ domain, netbios_name, LIBNET_JOIN_AUTOMATIC,
+ machinepass=machinepass)
+ else:
+ lp.set('realm', domain)
+ if lp.get('workgroup') == 'WORKGROUP':
+ lp.set('workgroup', net.finddc(domain=domain,
+ flags=(nbt.NBT_SERVER_LDAP |
+ nbt.NBT_SERVER_DS)).domain_name)
+ lp.set('server role', 'member server')
+ smb_conf = lp.configfile if lp.configfile else default_path()
+ with tempfile.NamedTemporaryFile(delete=False,
+ dir=os.path.dirname(smb_conf)) as f:
+ lp.dump(False, f.name)
+ if os.path.exists(smb_conf):
+ mode = os.stat(smb_conf).st_mode
+ os.chmod(f.name, mode)
+ os.rename(f.name, smb_conf)
+ s3_lp = s3param.get_context()
+ s3_lp.load(smb_conf)
+ s3_net = s3_Net(creds, s3_lp, server=server)
+ (sid, domain_name) = s3_net.join_member(netbios_name,
+ machinepass=machinepass,
+ debug=verbose,
+ noDnsUpdates=no_dns_updates)
+
+ self.errf.write("Joined domain %s (%s)\n" % (domain_name, sid))
+ elif role == "DC" and is_ad_dc_built():
+ join_DC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
+ site=site, netbios_name=netbios_name, targetdir=targetdir,
+ domain_critical_only=domain_critical_only,
+ machinepass=machinepass, use_ntvfs=use_ntvfs,
+ dns_backend=dns_backend,
+ plaintext_secrets=plaintext_secrets,
+ backend_store=backend_store,
+ backend_store_size=backend_store_size)
+ elif role == "RODC" and is_ad_dc_built():
+ join_RODC(logger=logger, server=server, creds=creds, lp=lp, domain=domain,
+ site=site, netbios_name=netbios_name, targetdir=targetdir,
+ domain_critical_only=domain_critical_only,
+ machinepass=machinepass, use_ntvfs=use_ntvfs,
+ dns_backend=dns_backend,
+ plaintext_secrets=plaintext_secrets,
+ backend_store=backend_store,
+ backend_store_size=backend_store_size)
+ else:
+ raise CommandError("Invalid role '%s' (possible values: MEMBER, DC, RODC)" % role)
diff --git a/python/samba/netcmd/domain/keytab.py b/python/samba/netcmd/domain/keytab.py
new file mode 100644
index 0000000..b0955ca
--- /dev/null
+++ b/python/samba/netcmd/domain/keytab.py
@@ -0,0 +1,55 @@
+# domain management - domain keytab
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import samba.getopt as options
+from samba import enable_net_export_keytab
+from samba.net import Net
+from samba.netcmd import Command, Option
+
+try:
+ enable_net_export_keytab()
+except ImportError:
+ cmd_domain_export_keytab = None
+else:
+ class cmd_domain_export_keytab(Command):
+ """Dump Kerberos keys of the domain into a keytab."""
+
+ synopsis = "%prog <keytab> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ takes_options = [
+ Option("--principal", help="extract only this principal", type=str),
+ ]
+
+ takes_args = ["keytab"]
+
+ 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)
diff --git a/python/samba/netcmd/domain/leave.py b/python/samba/netcmd/domain/leave.py
new file mode 100644
index 0000000..0d58360
--- /dev/null
+++ b/python/samba/netcmd/domain/leave.py
@@ -0,0 +1,59 @@
+# domain management - domain leave
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import samba.getopt as options
+from samba.net_s3 import Net as s3_Net
+from samba.netcmd import Command, Option
+from samba.param import default_path
+from samba.samba3 import param as s3param
+
+
+class cmd_domain_leave(Command):
+ """Cause a domain member to leave the joined domain."""
+
+ synopsis = "%prog [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = [
+ Option("--keep-account", action="store_true",
+ help="Disable the machine account instead of deleting it.")
+ ]
+
+ takes_args = []
+
+ def run(self, sambaopts=None, credopts=None, versionopts=None,
+ keep_account=False):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+
+ s3_lp = s3param.get_context()
+ smb_conf = lp.configfile if lp.configfile else default_path()
+ s3_lp.load(smb_conf)
+ s3_net = s3_Net(creds, s3_lp)
+ s3_net.leave(keep_account)
diff --git a/python/samba/netcmd/domain/level.py b/python/samba/netcmd/domain/level.py
new file mode 100644
index 0000000..eefe360
--- /dev/null
+++ b/python/samba/netcmd/domain/level.py
@@ -0,0 +1,250 @@
+# domain management - domain level
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import ldb
+import samba.getopt as options
+from samba.auth import system_session
+from samba.dsdb import check_and_update_fl, DS_DOMAIN_FUNCTION_2000
+from samba.netcmd import Command, CommandError, Option
+from samba.samdb import SamDB
+
+from samba import functional_level
+
+
+class cmd_domain_level(Command):
+ """Raise domain and forest function levels."""
+
+ synopsis = "%prog (show|raise <options>) [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+ metavar="URL", dest="H"),
+ Option("-q", "--quiet", help="Be quiet", action="store_true"), # unused
+ Option("--forest-level", type="choice", choices=["2003", "2008", "2008_R2", "2012", "2012_R2", "2016"],
+ help="The forest function level (2003 | 2008 | 2008_R2 | 2012 | 2012_R2 | 2016)"),
+ Option("--domain-level", type="choice", choices=["2003", "2008", "2008_R2", "2012", "2012_R2", "2016"],
+ help="The domain function level (2003 | 2008 | 2008_R2 | 2012 | 2012_R2 | 2016)")
+ ]
+
+ takes_args = ["subcommand"]
+
+ def run(self, subcommand, H=None, forest_level=None, domain_level=None,
+ quiet=False, credopts=None, sambaopts=None, versionopts=None):
+ if subcommand not in ["show", "raise"]:
+ raise CommandError("invalid argument: '%s' (choose from 'show', 'raise')" % subcommand)
+
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp, fallback_machine=True)
+
+ samdb = SamDB(url=H, session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ domain_dn = samdb.domain_dn()
+
+ in_transaction = False
+ if subcommand == "raise" and (H is None or not H.startswith("ldap")):
+ samdb.transaction_start()
+ in_transaction = True
+ try:
+ check_and_update_fl(samdb, lp)
+ except Exception as e:
+ samdb.transaction_cancel()
+ raise e
+
+ try:
+ res_forest = samdb.search("CN=Partitions,%s" % samdb.get_config_basedn(),
+ scope=ldb.SCOPE_BASE, attrs=["msDS-Behavior-Version"])
+ assert len(res_forest) == 1
+
+ res_domain = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
+ attrs=["msDS-Behavior-Version", "nTMixedDomain"])
+ assert len(res_domain) == 1
+
+ res_domain_cross = samdb.search("CN=Partitions,%s" % samdb.get_config_basedn(),
+ scope=ldb.SCOPE_SUBTREE,
+ expression="(&(objectClass=crossRef)(nCName=%s))" % domain_dn,
+ attrs=["msDS-Behavior-Version"])
+ assert len(res_domain_cross) == 1
+
+ res_dc_s = samdb.search("CN=Sites,%s" % samdb.get_config_basedn(),
+ scope=ldb.SCOPE_SUBTREE, expression="(objectClass=nTDSDSA)",
+ attrs=["msDS-Behavior-Version"])
+ assert len(res_dc_s) >= 1
+
+ # default values, since "msDS-Behavior-Version" does not exist on Windows 2000 AD
+ level_forest = DS_DOMAIN_FUNCTION_2000
+ level_domain = DS_DOMAIN_FUNCTION_2000
+
+ if "msDS-Behavior-Version" in res_forest[0]:
+ level_forest = int(res_forest[0]["msDS-Behavior-Version"][0])
+ if "msDS-Behavior-Version" in res_domain[0]:
+ level_domain = int(res_domain[0]["msDS-Behavior-Version"][0])
+ level_domain_mixed = int(res_domain[0]["nTMixedDomain"][0])
+
+ min_level_dc = None
+ for msg in res_dc_s:
+ if "msDS-Behavior-Version" in msg:
+ if min_level_dc is None or int(msg["msDS-Behavior-Version"][0]) < min_level_dc:
+ min_level_dc = int(msg["msDS-Behavior-Version"][0])
+ else:
+ min_level_dc = DS_DOMAIN_FUNCTION_2000
+ # well, this is the least
+ break
+
+ if level_forest < DS_DOMAIN_FUNCTION_2000 or level_domain < DS_DOMAIN_FUNCTION_2000:
+ raise CommandError("Domain and/or forest function level(s) is/are invalid. Correct them or reprovision!")
+ if min_level_dc < DS_DOMAIN_FUNCTION_2000:
+ raise CommandError("Lowest function level of a DC is invalid. Correct this or reprovision!")
+ if level_forest > level_domain:
+ raise CommandError("Forest function level is higher than the domain level(s). Correct this or reprovision!")
+ if level_domain > min_level_dc:
+ raise CommandError("Domain function level is higher than the lowest function level of a DC. Correct this or reprovision!")
+ except Exception as e:
+ if in_transaction:
+ samdb.transaction_cancel()
+ raise e
+
+ def do_show():
+ self.message("Domain and forest function level for domain '%s'" % domain_dn)
+ if level_forest == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
+ self.message("\nATTENTION: You run SAMBA 4 on a forest function level lower than Windows 2000 (Native). This isn't supported! Please raise!")
+ if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
+ self.message("\nATTENTION: You run SAMBA 4 on a domain function level lower than Windows 2000 (Native). This isn't supported! Please raise!")
+ if min_level_dc == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed != 0:
+ self.message("\nATTENTION: You run SAMBA 4 on a lowest function level of a DC lower than Windows 2003. This isn't supported! Please step-up or upgrade the concerning DC(s)!")
+
+ self.message("")
+
+ outstr = functional_level.level_to_string(level_forest)
+ self.message("Forest function level: (Windows) " + outstr)
+
+ if level_domain == DS_DOMAIN_FUNCTION_2000 and level_domain_mixed:
+ outstr = "2000 mixed (NT4 DC support)"
+ else:
+ outstr = functional_level.level_to_string(level_domain)
+ self.message("Domain function level: (Windows) " + outstr)
+
+ outstr = functional_level.level_to_string(min_level_dc)
+ self.message("Lowest function level of a DC: (Windows) " + outstr)
+
+ def do_raise():
+ msgs = []
+
+ current_level_domain = level_domain
+
+ if domain_level is not None:
+ try:
+ new_level_domain = functional_level.string_to_level(domain_level)
+ except KeyError:
+ raise CommandError(f"New functional level '{domain_level}' is not known to Samba as an AD functional level")
+
+ if new_level_domain <= level_domain and level_domain_mixed == 0:
+ raise CommandError("Domain function level can't be smaller than or equal to the actual one!")
+ if new_level_domain > min_level_dc:
+ raise CommandError("Domain function level can't be higher than the lowest function level of a DC!")
+
+ # Deactivate mixed/interim domain support
+ if level_domain_mixed != 0:
+ # Directly on the base DN
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, domain_dn)
+ m["nTMixedDomain"] = ldb.MessageElement("0",
+ ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
+ samdb.modify(m)
+ # Under partitions
+ m = ldb.Message()
+ m.dn = res_domain_cross[0].dn
+ m["nTMixedDomain"] = ldb.MessageElement("0",
+ ldb.FLAG_MOD_REPLACE, "nTMixedDomain")
+ try:
+ samdb.modify(m)
+ except ldb.LdbError as e:
+ (enum, emsg) = e.args
+ if enum != ldb.ERR_UNWILLING_TO_PERFORM:
+ raise
+
+ # Directly on the base DN
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, domain_dn)
+ m["msDS-Behavior-Version"] = ldb.MessageElement(
+ str(new_level_domain), ldb.FLAG_MOD_REPLACE,
+ "msDS-Behavior-Version")
+ samdb.modify(m)
+ # Under partitions
+ m = ldb.Message()
+ m.dn = res_domain_cross[0].dn
+ m["msDS-Behavior-Version"] = ldb.MessageElement(
+ str(new_level_domain), ldb.FLAG_MOD_REPLACE,
+ "msDS-Behavior-Version")
+ try:
+ samdb.modify(m)
+ except ldb.LdbError as e2:
+ (enum, emsg) = e2.args
+ if enum != ldb.ERR_UNWILLING_TO_PERFORM:
+ raise
+
+ current_level_domain = new_level_domain
+ msgs.append("Domain function level changed!")
+
+ if forest_level is not None:
+ new_level_forest = functional_level.string_to_level(forest_level)
+
+ if new_level_forest <= level_forest:
+ raise CommandError("Forest function level can't be smaller than or equal to the actual one!")
+ if new_level_forest > current_level_domain:
+ raise CommandError("Forest function level can't be higher than the domain function level(s). Please raise it/them first!")
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, "CN=Partitions,%s" % samdb.get_config_basedn())
+ m["msDS-Behavior-Version"] = ldb.MessageElement(
+ str(new_level_forest), ldb.FLAG_MOD_REPLACE,
+ "msDS-Behavior-Version")
+ samdb.modify(m)
+ msgs.append("Forest function level changed!")
+ msgs.append("All changes applied successfully!")
+ self.message("\n".join(msgs))
+ return
+
+ if subcommand == "show":
+ assert not in_transaction
+ do_show()
+ return
+ elif subcommand == "raise":
+ try:
+ do_raise()
+ except Exception as e:
+ if in_transaction:
+ samdb.transaction_cancel()
+ raise e
+ if in_transaction:
+ samdb.transaction_commit()
+ return
+
+ raise AssertionError("Internal Error subcommand[%s] not handled" % subcommand)
diff --git a/python/samba/netcmd/domain/models/__init__.py b/python/samba/netcmd/domain/models/__init__.py
new file mode 100644
index 0000000..8a6b254
--- /dev/null
+++ b/python/samba/netcmd/domain/models/__init__.py
@@ -0,0 +1,32 @@
+# Unix SMB/CIFS implementation.
+#
+# Samba domain models.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from .auth_policy import AuthenticationPolicy
+from .auth_silo import AuthenticationSilo
+from .claim_type import ClaimType
+from .group import Group
+from .model import MODELS
+from .schema import AttributeSchema, ClassSchema
+from .site import Site
+from .subnet import Subnet
+from .user import User
+from .value_type import ValueType
diff --git a/python/samba/netcmd/domain/models/auth_policy.py b/python/samba/netcmd/domain/models/auth_policy.py
new file mode 100644
index 0000000..c56966c
--- /dev/null
+++ b/python/samba/netcmd/domain/models/auth_policy.py
@@ -0,0 +1,109 @@
+# Unix SMB/CIFS implementation.
+#
+# Authentication policy model.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from enum import IntEnum
+from ldb import Dn
+
+from .fields import (BooleanField, EnumField, IntegerField, SDDLField,
+ StringField)
+from .model import Model
+
+# Ticket-Granting-Ticket lifetimes.
+MIN_TGT_LIFETIME = 45
+MAX_TGT_LIFETIME = 2147483647
+
+
+class StrongNTLMPolicy(IntEnum):
+ DISABLED = 0
+ OPTIONAL = 1
+ REQUIRED = 2
+
+ @classmethod
+ def get_choices(cls):
+ return sorted([choice.capitalize() for choice in cls._member_names_])
+
+ @classmethod
+ def choices_str(cls):
+ return ", ".join(cls.get_choices())
+
+
+class AuthenticationPolicy(Model):
+ description = StringField("description")
+ enforced = BooleanField("msDS-AuthNPolicyEnforced")
+ strong_ntlm_policy = EnumField("msDS-StrongNTLMPolicy", StrongNTLMPolicy)
+ user_allow_ntlm_network_auth = BooleanField(
+ "msDS-UserAllowedNTLMNetworkAuthentication")
+ user_tgt_lifetime = IntegerField("msDS-UserTGTLifetime")
+ service_allow_ntlm_network_auth = BooleanField(
+ "msDS-ServiceAllowedNTLMNetworkAuthentication")
+ service_tgt_lifetime = IntegerField("msDS-ServiceTGTLifetime")
+ computer_tgt_lifetime = IntegerField("msDS-ComputerTGTLifetime")
+ user_allowed_to_authenticate_from = SDDLField(
+ "msDS-UserAllowedToAuthenticateFrom", allow_device_in_sddl=False)
+ user_allowed_to_authenticate_to = SDDLField(
+ "msDS-UserAllowedToAuthenticateTo")
+ service_allowed_to_authenticate_from = SDDLField(
+ "msDS-ServiceAllowedToAuthenticateFrom", allow_device_in_sddl=False)
+ service_allowed_to_authenticate_to = SDDLField(
+ "msDS-ServiceAllowedToAuthenticateTo")
+ computer_allowed_to_authenticate_to = SDDLField(
+ "msDS-ComputerAllowedToAuthenticateTo")
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the AuthenticationPolicy model.
+
+ :param ldb: Ldb connection
+ :return: Dn object of container
+ """
+ base_dn = ldb.get_config_basedn()
+ base_dn.add_child(
+ "CN=AuthN Policies,CN=AuthN Policy Configuration,CN=Services")
+ return base_dn
+
+ @staticmethod
+ def get_object_class():
+ return "msDS-AuthNPolicy"
+
+ @staticmethod
+ def lookup(ldb, name):
+ """Helper function to return auth policy or raise LookupError.
+
+ :param ldb: Ldb connection
+ :param name: Either DN or name of Authentication Policy
+ :raises: LookupError if not found
+ :raises: ValueError if name is not set
+ """
+ if not name:
+ raise ValueError("Attribute 'name' is required.")
+
+ try:
+ # It's possible name is already a Dn.
+ dn = name if isinstance(name, Dn) else Dn(ldb, name)
+ policy = AuthenticationPolicy.get(ldb, dn=dn)
+ except ValueError:
+ policy = AuthenticationPolicy.get(ldb, cn=name)
+
+ if policy is None:
+ raise LookupError(f"Authentication policy {name} not found.")
+
+ return policy
diff --git a/python/samba/netcmd/domain/models/auth_silo.py b/python/samba/netcmd/domain/models/auth_silo.py
new file mode 100644
index 0000000..9747671
--- /dev/null
+++ b/python/samba/netcmd/domain/models/auth_silo.py
@@ -0,0 +1,104 @@
+# Unix SMB/CIFS implementation.
+#
+# Authentication silo model.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from ldb import FLAG_MOD_ADD, FLAG_MOD_DELETE, LdbError, Message, MessageElement
+
+from samba.sd_utils import escaped_claim_id
+
+from .exceptions import GrantMemberError, RevokeMemberError
+from .fields import DnField, BooleanField, StringField
+from .model import Model
+
+
+class AuthenticationSilo(Model):
+ description = StringField("description")
+ enforced = BooleanField("msDS-AuthNPolicySiloEnforced")
+ user_authentication_policy = DnField("msDS-UserAuthNPolicy")
+ service_authentication_policy = DnField("msDS-ServiceAuthNPolicy")
+ computer_authentication_policy = DnField("msDS-ComputerAuthNPolicy")
+ members = DnField("msDS-AuthNPolicySiloMembers", many=True)
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the AuthenticationSilo model.
+
+ :param ldb: Ldb connection
+ :return: Dn object of container
+ """
+ base_dn = ldb.get_config_basedn()
+ base_dn.add_child(
+ "CN=AuthN Silos,CN=AuthN Policy Configuration,CN=Services")
+ return base_dn
+
+ @staticmethod
+ def get_object_class():
+ return "msDS-AuthNPolicySilo"
+
+ def grant(self, ldb, member):
+ """Grant a member access to the Authentication Silo.
+
+ Rather than saving the silo object and writing the entire member
+ list out again, just add one member only.
+
+ :param ldb: Ldb connection
+ :param member: Member to grant access to silo
+ """
+ # Create a message with only an add member operation.
+ message = Message(dn=self.dn)
+ message.add(MessageElement(str(member.dn), FLAG_MOD_ADD,
+ "msDS-AuthNPolicySiloMembers"))
+
+ # Update authentication silo.
+ try:
+ ldb.modify(message)
+ except LdbError as e:
+ raise GrantMemberError(f"Failed to grant access to silo member: {e}")
+
+ # If the modify operation was successful refresh members field.
+ self.refresh(ldb, fields=["members"])
+
+ def revoke(self, ldb, member):
+ """Revoke a member from the Authentication Silo.
+
+ Rather than saving the silo object and writing the entire member
+ list out again, just remove one member only.
+
+ :param ldb: Ldb connection
+ :param member: Member to revoke from silo
+ """
+ # Create a message with only a remove member operation.
+ message = Message(dn=self.dn)
+ message.add(MessageElement(str(member.dn), FLAG_MOD_DELETE,
+ "msDS-AuthNPolicySiloMembers"))
+
+ # Update authentication silo.
+ try:
+ ldb.modify(message)
+ except LdbError as e:
+ raise RevokeMemberError(f"Failed to revoke silo member: {e}")
+
+ # If the modify operation was successful refresh members field.
+ self.refresh(ldb, fields=["members"])
+
+ def get_authentication_sddl(self):
+ return ('O:SYG:SYD:(XA;OICI;CR;;;WD;(@USER.ad://ext/'
+ f'AuthenticationSilo == "{escaped_claim_id(self.name)}"))')
diff --git a/python/samba/netcmd/domain/models/claim_type.py b/python/samba/netcmd/domain/models/claim_type.py
new file mode 100644
index 0000000..7e1c816
--- /dev/null
+++ b/python/samba/netcmd/domain/models/claim_type.py
@@ -0,0 +1,58 @@
+# Unix SMB/CIFS implementation.
+#
+# Claim type model.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from .fields import BooleanField, DnField, IntegerField,\
+ PossibleClaimValuesField, StringField
+from .model import Model
+
+
+class ClaimType(Model):
+ enabled = BooleanField("Enabled")
+ description = StringField("description")
+ display_name = StringField("displayName")
+ claim_attribute_source = DnField("msDS-ClaimAttributeSource")
+ claim_is_single_valued = BooleanField("msDS-ClaimIsSingleValued")
+ claim_is_value_space_restricted = BooleanField(
+ "msDS-ClaimIsValueSpaceRestricted")
+ claim_possible_values = PossibleClaimValuesField("msDS-ClaimPossibleValues")
+ claim_source_type = StringField("msDS-ClaimSourceType")
+ claim_type_applies_to_class = DnField(
+ "msDS-ClaimTypeAppliesToClass", many=True)
+ claim_value_type = IntegerField("msDS-ClaimValueType")
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the ClaimType model.
+
+ :param ldb: Ldb connection
+ :return: Dn object of container
+ """
+ base_dn = ldb.get_config_basedn()
+ base_dn.add_child("CN=Claim Types,CN=Claims Configuration,CN=Services")
+ return base_dn
+
+ @staticmethod
+ def get_object_class():
+ return "msDS-ClaimType"
+
+ def __str__(self):
+ return str(self.display_name)
diff --git a/python/samba/netcmd/domain/models/exceptions.py b/python/samba/netcmd/domain/models/exceptions.py
new file mode 100644
index 0000000..14ebd77
--- /dev/null
+++ b/python/samba/netcmd/domain/models/exceptions.py
@@ -0,0 +1,64 @@
+# Unix SMB/CIFS implementation.
+#
+# Model and ORM exceptions.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+class ModelError(Exception):
+ pass
+
+
+class FieldError(ModelError):
+ """A ModelError on a specific field."""
+
+ def __init__(self, *args, field=None):
+ self.field = field
+ super().__init__(*args)
+
+ def __str__(self):
+ message = super().__str__()
+ return f"{self.field.name}: {message}"
+
+
+class MultipleObjectsReturned(ModelError):
+ pass
+
+
+class DoesNotExist(ModelError):
+ pass
+
+
+class GrantMemberError(ModelError):
+ pass
+
+
+class RevokeMemberError(ModelError):
+ pass
+
+
+class ProtectError(ModelError):
+ pass
+
+
+class UnprotectError(ModelError):
+ pass
+
+
+class DeleteError(ModelError):
+ pass
diff --git a/python/samba/netcmd/domain/models/fields.py b/python/samba/netcmd/domain/models/fields.py
new file mode 100644
index 0000000..0b7e1eb
--- /dev/null
+++ b/python/samba/netcmd/domain/models/fields.py
@@ -0,0 +1,507 @@
+# Unix SMB/CIFS implementation.
+#
+# Model fields.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from enum import IntEnum
+
+import io
+from abc import ABCMeta, abstractmethod
+from datetime import datetime
+from xml.etree import ElementTree
+
+from ldb import Dn, MessageElement, string_to_time, timestring
+from samba.dcerpc import security
+from samba.dcerpc.misc import GUID
+from samba.ndr import ndr_pack, ndr_unpack
+
+
+class Field(metaclass=ABCMeta):
+ """Base class for all fields.
+
+ Each field will need to implement from_db_value and to_db_value.
+
+ A field must correctly support converting both single valued fields,
+ and list type fields.
+
+ The only thing many=True does is say the field "prefers" to be a list,
+ but really any field can be a list or single value.
+ """
+
+ def __init__(self, name, many=False, default=None, hidden=False,
+ readonly=False):
+ """Creates a new field, should be subclassed.
+
+ :param name: Ldb field name.
+ :param many: If true always convert field to a list when loaded.
+ :param default: Default value or callback method (obj is first argument)
+ :param hidden: If this is True, exclude the field when calling as_dict()
+ :param readonly: If true don't write this value when calling save.
+ """
+ self.name = name
+ self.many = many
+ self.hidden = hidden
+ self.readonly = readonly
+
+ # This ensures that fields with many=True are always lists.
+ # If this is inconsistent anywhere, it isn't so great to use.
+ if self.many and default is None:
+ self.default = []
+ else:
+ self.default = default
+
+ @abstractmethod
+ def from_db_value(self, ldb, value):
+ """Converts value read from the database to Python value.
+
+ :param ldb: Ldb connection
+ :param value: MessageElement value from the database
+ :returns: Parsed value as Python type
+ """
+ pass
+
+ @abstractmethod
+ def to_db_value(self, ldb, value, flags):
+ """Converts value to database value.
+
+ This should return a MessageElement or None, where None means
+ the field will be unset on the next save.
+
+ :param ldb: Ldb connection
+ :param value: Input value from Python field
+ :param flags: MessageElement flags
+ :returns: MessageElement or None
+ """
+ pass
+
+
+class IntegerField(Field):
+ """A simple integer field, can be an int or list of int."""
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement to int or list of int."""
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [int(item) for item in value]
+ else:
+ return int(value[0])
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert int or list of int to MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [str(item) for item in value], flags, self.name)
+ else:
+ return MessageElement(str(value), flags, self.name)
+
+
+class BinaryField(Field):
+ """Similar to StringField but using bytes instead of str.
+
+ This tends to be quite easy because a MessageElement already uses bytes.
+ """
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement to bytes or list of bytes.
+
+ The values on the MessageElement should already be bytes so the
+ cast to bytes() is likely not needed in from_db_value.
+ """
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [bytes(item) for item in value]
+ else:
+ return bytes(value[0])
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert bytes or list of bytes to MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [bytes(item) for item in value], flags, self.name)
+ else:
+ return MessageElement(bytes(value), flags, self.name)
+
+
+class StringField(Field):
+ """A simple string field, may contain str or list of str."""
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement to str or list of str."""
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [str(item) for item in value]
+ else:
+ return str(value)
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert str or list of str to MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [str(item) for item in value], flags, self.name)
+ else:
+ return MessageElement(str(value), flags, self.name)
+
+
+class EnumField(Field):
+ """A field based around Python's Enum type."""
+
+ def __init__(self, name, enum, many=False, default=None):
+ """Create a new EnumField for the given enum class."""
+ self.enum = enum
+ super().__init__(name, many, default)
+
+ def enum_from_value(self, value):
+ """Return Enum instance from value.
+
+ Has a special case for IntEnum as the constructor only accepts int.
+ """
+ if issubclass(self.enum, IntEnum):
+ return self.enum(int(str(value)))
+ else:
+ return self.enum(str(value))
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement to enum or list of enum."""
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [self.enum_from_value(item) for item in value]
+ else:
+ return self.enum_from_value(value)
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert enum or list of enum to MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [str(item.value) for item in value], flags, self.name)
+ else:
+ return MessageElement(str(value.value), flags, self.name)
+
+
+class DateTimeField(Field):
+ """A field for parsing ldb timestamps into Python datetime."""
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement to datetime or list of datetime."""
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [datetime.fromtimestamp(string_to_time(str(item)))
+ for item in value]
+ else:
+ return datetime.fromtimestamp(string_to_time(str(value)))
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert datetime or list of datetime to MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [timestring(int(datetime.timestamp(item))) for item in value],
+ flags, self.name)
+ else:
+ return MessageElement(timestring(int(datetime.timestamp(value))),
+ flags, self.name)
+
+
+class RelatedField(Field):
+ """A field that automatically fetches the related objects.
+
+ Use sparingly, can be a little slow. If in doubt just use DnField instead.
+ """
+
+ def __init__(self, name, model, many=False, default=None):
+ """Create a new RelatedField for the given model."""
+ self.model = model
+ super().__init__(name, many, default)
+
+ def from_db_value(self, ldb, value):
+ """Convert Message element to related object or list of objects.
+
+ Note that fetching related items is not using any sort of lazy
+ loading so use this field sparingly.
+ """
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [self.model.get(ldb, dn=Dn(ldb, str(item))) for item in value]
+ else:
+ return self.model.get(ldb, dn=Dn(ldb, str(value)))
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert related object or list of objects to MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [str(item.dn) for item in value], flags, self.name)
+ else:
+ return MessageElement(str(value.dn), flags, self.name)
+
+
+class DnField(Field):
+ """A Dn field parses the current field into a Dn object."""
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement to a Dn object or list of Dn objects."""
+ if value is None:
+ return
+ elif isinstance(value, Dn):
+ return value
+ elif len(value) > 1 or self.many:
+ return [Dn(ldb, str(item)) for item in value]
+ else:
+ return Dn(ldb, str(value))
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert Dn object or list of Dn objects into a MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [str(item) for item in value], flags, self.name)
+ else:
+ return MessageElement(str(value), flags, self.name)
+
+
+class GUIDField(Field):
+ """A GUID field decodes fields containing binary GUIDs."""
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement with a GUID into a str or list of str."""
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [str(ndr_unpack(GUID, item)) for item in value]
+ else:
+ return str(ndr_unpack(GUID, value[0]))
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert str with GUID into MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [ndr_pack(GUID(item)) for item in value], flags, self.name)
+ else:
+ return MessageElement(ndr_pack(GUID(value)), flags, self.name)
+
+
+class SIDField(Field):
+ """A SID field encodes and decodes SID data."""
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement with a GUID into a str or list of str."""
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [str(ndr_unpack(security.dom_sid, item)) for item in value]
+ else:
+ return str(ndr_unpack(security.dom_sid, value[0]))
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert str with GUID into MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [ndr_pack(security.dom_sid(item)) for item in value],
+ flags, self.name)
+ else:
+ return MessageElement(ndr_pack(security.dom_sid(value)),
+ flags, self.name)
+
+
+class SDDLField(Field):
+ """A SDDL field encodes and decodes SDDL data."""
+
+ def __init__(self,
+ name,
+ *,
+ many=False,
+ default=None,
+ hidden=False,
+ allow_device_in_sddl=True):
+ """Create a new SDDLField."""
+ self.allow_device_in_sddl = allow_device_in_sddl
+ super().__init__(name, many=many, default=default, hidden=hidden)
+
+ def from_db_value(self, ldb, value):
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [ndr_unpack(security.descriptor, item).as_sddl()
+ for item in value]
+ else:
+ return ndr_unpack(security.descriptor, value[0]).as_sddl()
+
+ def to_db_value(self, ldb, value, flags):
+ domain_sid = security.dom_sid(ldb.get_domain_sid())
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement([ndr_pack(security.descriptor.from_sddl(
+ item,
+ domain_sid,
+ allow_device_in_sddl=self.allow_device_in_sddl))
+ for item in value],
+ flags,
+ self.name)
+ else:
+ return MessageElement(
+ ndr_pack(security.descriptor.from_sddl(
+ value,
+ domain_sid,
+ allow_device_in_sddl=self.allow_device_in_sddl)),
+ flags,
+ self.name
+ )
+
+
+class BooleanField(Field):
+ """A simple boolean field, can be a bool or list of bool."""
+
+ def from_db_value(self, ldb, value):
+ """Convert MessageElement into a bool or list of bool."""
+ if value is None:
+ return
+ elif len(value) > 1 or self.many:
+ return [str(item) == "TRUE" for item in value]
+ else:
+ return str(value) == "TRUE"
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert bool or list of bool into a MessageElement."""
+ if value is None:
+ return
+ elif isinstance(value, list):
+ return MessageElement(
+ [str(bool(item)).upper() for item in value], flags, self.name)
+ else:
+ return MessageElement(str(bool(value)).upper(), flags, self.name)
+
+
+class PossibleClaimValuesField(Field):
+ """Field for parsing possible values XML for claim types.
+
+ This field will be represented by a list of dicts as follows:
+
+ [
+ {"ValueGUID": <GUID>},
+ {"ValueDisplayName: "Display name"},
+ {"ValueDescription: "Optional description or None for no description"},
+ {"Value": <Value>},
+ ]
+
+ Note that the GUID needs to be created client-side when adding entries,
+ leaving it as None then saving it doesn't generate the GUID.
+
+ The field itself just converts the XML to list and vice versa, it doesn't
+ automatically generate GUIDs for entries, this is entirely up to the caller.
+ """
+
+ # Namespaces for PossibleValues xml parsing.
+ NAMESPACE = {
+ "xsd": "http://www.w3.org/2001/XMLSchema",
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+ "": "http://schemas.microsoft.com/2010/08/ActiveDirectory/PossibleValues"
+ }
+
+ def from_db_value(self, ldb, value):
+ """Parse MessageElement with XML to list of dicts."""
+ if value is not None:
+ root = ElementTree.fromstring(str(value))
+ string_list = root.find("StringList", self.NAMESPACE)
+
+ values = []
+ for item in string_list.findall("Item", self.NAMESPACE):
+ values.append({
+ "ValueGUID": item.find("ValueGUID", self.NAMESPACE).text,
+ "ValueDisplayName": item.find("ValueDisplayName",
+ self.NAMESPACE).text,
+ "ValueDescription": item.find("ValueDescription",
+ self.NAMESPACE).text,
+ "Value": item.find("Value", self.NAMESPACE).text,
+ })
+
+ return values
+
+ def to_db_value(self, ldb, value, flags):
+ """Convert list of dicts back to XML as a MessageElement."""
+ if value is None:
+ return
+
+ # Possible values should always be a list of dict, but for consistency
+ # with other fields just wrap a single value into a list and continue.
+ if isinstance(value, list):
+ possible_values = value
+ else:
+ possible_values = [value]
+
+ # No point storing XML of an empty list.
+ # Return None, the field will be unset on the next save.
+ if len(possible_values) == 0:
+ return
+
+ # root node
+ root = ElementTree.Element("PossibleClaimValues")
+ for name, url in self.NAMESPACE.items():
+ if name == "":
+ root.set("xmlns", url)
+ else:
+ root.set(f"xmlns:{name}", url)
+
+ # StringList node
+ string_list = ElementTree.SubElement(root, "StringList")
+
+ # List of values
+ for item_dict in possible_values:
+ item = ElementTree.SubElement(string_list, "Item")
+ item_guid = ElementTree.SubElement(item, "ValueGUID")
+ item_guid.text = item_dict["ValueGUID"]
+ item_name = ElementTree.SubElement(item, "ValueDisplayName")
+ item_name.text = item_dict["ValueDisplayName"]
+ item_desc = ElementTree.SubElement(item, "ValueDescription")
+ item_desc.text = item_dict["ValueDescription"]
+ item_value = ElementTree.SubElement(item, "Value")
+ item_value.text = item_dict["Value"]
+
+ # NOTE: indent was only added in Python 3.9 so can't be used yet.
+ # ElementTree.indent(root, space="\t", level=0)
+
+ out = io.BytesIO()
+ ElementTree.ElementTree(root).write(out,
+ encoding="utf-16",
+ xml_declaration=True,
+ short_empty_elements=False)
+
+ # Back to str as that is what MessageElement needs.
+ return MessageElement(out.getvalue().decode("utf-16"), flags, self.name)
diff --git a/python/samba/netcmd/domain/models/group.py b/python/samba/netcmd/domain/models/group.py
new file mode 100644
index 0000000..9473127
--- /dev/null
+++ b/python/samba/netcmd/domain/models/group.py
@@ -0,0 +1,42 @@
+# Unix SMB/CIFS implementation.
+#
+# Group model.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from .fields import BooleanField, DnField, IntegerField, SIDField, StringField
+from .model import Model
+
+
+class Group(Model):
+ admin_count = IntegerField("adminCount")
+ description = StringField("description")
+ is_critical_system_object = BooleanField("isCriticalSystemObject",
+ default=False, readonly=True)
+ member = DnField("member", many=True)
+ object_sid = SIDField("objectSid")
+ system_flags = IntegerField("systemFlags")
+
+ @staticmethod
+ def get_object_class():
+ return "group"
+
+ def get_authentication_sddl(self):
+ return "O:SYG:SYD:(XA;OICI;CR;;;WD;(Member_of_any {SID(%s)}))" % (
+ self.object_sid)
diff --git a/python/samba/netcmd/domain/models/model.py b/python/samba/netcmd/domain/models/model.py
new file mode 100644
index 0000000..602c6ca
--- /dev/null
+++ b/python/samba/netcmd/domain/models/model.py
@@ -0,0 +1,426 @@
+# Unix SMB/CIFS implementation.
+#
+# Model and basic ORM for the Ldb database.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+import inspect
+from abc import ABCMeta, abstractmethod
+
+from ldb import ERR_NO_SUCH_OBJECT, FLAG_MOD_ADD, FLAG_MOD_REPLACE, LdbError,\
+ Message, MessageElement, SCOPE_BASE, SCOPE_SUBTREE, binary_encode
+from samba.sd_utils import SDUtils
+
+from .exceptions import DeleteError, DoesNotExist, FieldError,\
+ ProtectError, UnprotectError
+from .fields import DateTimeField, DnField, Field, GUIDField, IntegerField,\
+ StringField
+from .query import Query
+
+# Keeps track of registered models.
+# This gets populated by the ModelMeta class.
+MODELS = {}
+
+
+class ModelMeta(ABCMeta):
+
+ def __new__(mcls, name, bases, namespace, **kwargs):
+ cls = super().__new__(mcls, name, bases, namespace, **kwargs)
+
+ if cls.__name__ != "Model":
+ cls.fields = dict(inspect.getmembers(cls, lambda f: isinstance(f, Field)))
+ cls.meta = mcls
+ MODELS[name] = cls
+
+ return cls
+
+
+class Model(metaclass=ModelMeta):
+ cn = StringField("cn")
+ distinguished_name = DnField("distinguishedName")
+ dn = DnField("dn")
+ ds_core_propagation_data = DateTimeField("dsCorePropagationData",
+ hidden=True)
+ instance_type = IntegerField("instanceType")
+ name = StringField("name")
+ object_category = DnField("objectCategory")
+ object_class = StringField("objectClass",
+ default=lambda obj: obj.get_object_class())
+ object_guid = GUIDField("objectGUID")
+ usn_changed = IntegerField("uSNChanged", hidden=True)
+ usn_created = IntegerField("uSNCreated", hidden=True)
+ when_changed = DateTimeField("whenChanged", hidden=True)
+ when_created = DateTimeField("whenCreated", hidden=True)
+
+ def __init__(self, **kwargs):
+ """Create a new model instance and optionally populate fields.
+
+ Does not save the object to the database, call .save() for that.
+
+ :param kwargs: Optional input fields to populate object with
+ """
+ # Used by the _apply method, holds the original ldb Message,
+ # which is used by save() to determine what fields changed.
+ self._message = None
+
+ for field_name, field in self.fields.items():
+ if field_name in kwargs:
+ default = kwargs[field_name]
+ elif callable(field.default):
+ default = field.default(self)
+ else:
+ default = field.default
+
+ setattr(self, field_name, default)
+
+ def __repr__(self):
+ """Return object representation for this model."""
+ return f"<{self.__class__.__name__}: {self}>"
+
+ def __str__(self):
+ """Stringify model instance to implement in each model."""
+ return str(self.cn)
+
+ def __eq__(self, other):
+ """Basic object equality check only really checks if the dn matches.
+
+ :param other: The other object to compare with
+ """
+ if other is None:
+ return False
+ else:
+ return self.dn == other.dn
+
+ def __json__(self):
+ """Automatically called by custom JSONEncoder class.
+
+ When turning an object into json any fields of type RelatedField
+ will also end up calling this method.
+ """
+ if self.dn is not None:
+ return str(self.dn)
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the container of this model.
+
+ :param ldb: Ldb connection
+ :return: Dn to use for new objects
+ """
+ return ldb.get_default_basedn()
+
+ @classmethod
+ def get_search_dn(cls, ldb):
+ """Return the DN used for querying.
+
+ By default, this just calls get_base_dn, but it is possible to
+ return a different Dn for querying.
+
+ :param ldb: Ldb connection
+ :return: Dn to use for searching
+ """
+ return cls.get_base_dn(ldb)
+
+ @staticmethod
+ @abstractmethod
+ def get_object_class():
+ """Returns the objectClass for this model."""
+ pass
+
+ @classmethod
+ def from_message(cls, ldb, message):
+ """Create a new model instance from the Ldb Message object.
+
+ :param ldb: Ldb connection
+ :param message: Ldb Message object to create instance from
+ """
+ obj = cls()
+ obj._apply(ldb, message)
+ return obj
+
+ def _apply(self, ldb, message):
+ """Internal method to apply Ldb Message to current object.
+
+ :param ldb: Ldb connection
+ :param message: Ldb Message object to apply
+ """
+ # Store the ldb Message so that in save we can see what changed.
+ self._message = message
+
+ for attr, field in self.fields.items():
+ if field.name in message:
+ setattr(self, attr, field.from_db_value(ldb, message[field.name]))
+
+ def refresh(self, ldb, fields=None):
+ """Refresh object from database.
+
+ :param ldb: Ldb connection
+ :param fields: Optional list of field names to refresh
+ """
+ attrs = [self.fields[f].name for f in fields] if fields else None
+
+ # This shouldn't normally happen but in case the object refresh fails.
+ try:
+ res = ldb.search(self.dn, scope=SCOPE_BASE, attrs=attrs)
+ except LdbError as e:
+ if e.args[0] == ERR_NO_SUCH_OBJECT:
+ raise DoesNotExist(f"Refresh failed, object gone: {self.dn}")
+ raise
+
+ self._apply(ldb, res[0])
+
+ def as_dict(self, include_hidden=False):
+ """Returns a dict representation of the model.
+
+ :param include_hidden: Include fields with hidden=True when set
+ :returns: dict representation of model using Ldb field names as keys
+ """
+ obj_dict = {}
+
+ for attr, field in self.fields.items():
+ if not field.hidden or include_hidden:
+ value = getattr(self, attr)
+ if value is not None:
+ obj_dict[field.name] = value
+
+ return obj_dict
+
+ @classmethod
+ def build_expression(cls, **kwargs):
+ """Build LDAP search expression from kwargs.
+
+ :kwargs: fields to use for expression using model field names
+ """
+ # Take a copy, never modify the original if it can be avoided.
+ # Then always add the object_class to the search criteria.
+ criteria = dict(kwargs)
+ criteria["object_class"] = cls.get_object_class()
+
+ # Build search expression.
+ num_fields = len(criteria)
+ expression = "" if num_fields == 1 else "(&"
+
+ for field_name, value in criteria.items():
+ field = cls.fields.get(field_name)
+ if not field:
+ raise ValueError(f"Unknown field '{field_name}'")
+ expression += f"({field.name}={binary_encode(value)})"
+
+ if num_fields > 1:
+ expression += ")"
+
+ return expression
+
+ @classmethod
+ def query(cls, ldb, **kwargs):
+ """Returns a search query for this model.
+
+ :param ldb: Ldb connection
+ :param kwargs: Search criteria as keyword args
+ """
+ base_dn = cls.get_search_dn(ldb)
+
+ # If the container does not exist produce a friendly error message.
+ try:
+ result = ldb.search(base_dn,
+ scope=SCOPE_SUBTREE,
+ expression=cls.build_expression(**kwargs))
+ except LdbError as e:
+ if e.args[0] == ERR_NO_SUCH_OBJECT:
+ raise DoesNotExist(f"Container does not exist: {base_dn}")
+ raise
+
+ return Query(cls, ldb, result)
+
+ @classmethod
+ def get(cls, ldb, **kwargs):
+ """Get one object, must always return one item.
+
+ Either find object by dn=, or any combination of attributes via kwargs.
+ If there are more than one result, MultipleObjectsReturned is raised.
+
+ :param ldb: Ldb connection
+ :param kwargs: Search criteria as keyword args
+ :returns: Model instance or None if not found
+ :raises: MultipleObjects returned if there are more than one results
+ """
+ # If a DN is provided use that to get the object directly.
+ # Otherwise, build a search expression using kwargs provided.
+ dn = kwargs.get("dn")
+
+ if dn:
+ # Handle LDAP error 32 LDAP_NO_SUCH_OBJECT, but raise for the rest.
+ # Return None if the User does not exist.
+ try:
+ res = ldb.search(dn, scope=SCOPE_BASE)
+ except LdbError as e:
+ if e.args[0] == ERR_NO_SUCH_OBJECT:
+ return None
+ else:
+ raise
+
+ return cls.from_message(ldb, res[0])
+ else:
+ return cls.query(ldb, **kwargs).get()
+
+ @classmethod
+ def create(cls, ldb, **kwargs):
+ """Create object constructs object and calls save straight after.
+
+ :param ldb: Ldb connection
+ :param kwargs: Fields to populate object from
+ :returns: object
+ """
+ obj = cls(**kwargs)
+ obj.save(ldb)
+ return obj
+
+ @classmethod
+ def get_or_create(cls, ldb, defaults=None, **kwargs):
+ """Retrieve object and if it doesn't exist create a new instance.
+
+ :param ldb: Ldb connection
+ :param defaults: Attributes only used for create but not search
+ :param kwargs: Attributes used for searching existing object
+ :returns: (object, bool created)
+ """
+ obj = cls.get(ldb, **kwargs)
+ if obj is None:
+ attrs = dict(kwargs)
+ if defaults is not None:
+ attrs.update(defaults)
+ return cls.create(ldb, **attrs), True
+ else:
+ return obj, False
+
+ def save(self, ldb):
+ """Save model to Ldb database.
+
+ The save operation will save all fields excluding fields that
+ return None when calling their `to_db_value` methods.
+
+ The `to_db_value` method can either return a ldb Message object,
+ or None if the field is to be excluded.
+
+ For updates, the existing object is fetched and only fields
+ that are changed are included in the update ldb Message.
+
+ Also for updates, any fields that currently have a value,
+ but are to be set to None will be seen as a delete operation.
+
+ After the save operation the object is refreshed from the server,
+ as often the server will populate some fields.
+
+ :param ldb: Ldb connection
+ """
+ if self.dn is None:
+ dn = self.get_base_dn(ldb)
+ dn.add_child(f"CN={self.cn or self.name}")
+ self.dn = dn
+
+ message = Message(dn=self.dn)
+ for attr, field in self.fields.items():
+ if attr != "dn" and not field.readonly:
+ value = getattr(self, attr)
+ try:
+ db_value = field.to_db_value(ldb, value, FLAG_MOD_ADD)
+ except ValueError as e:
+ raise FieldError(e, field=field)
+
+ # Don't add empty fields.
+ if db_value is not None and len(db_value):
+ message.add(db_value)
+
+ # Create object
+ ldb.add(message)
+
+ # Fetching object refreshes any automatically populated fields.
+ res = ldb.search(dn, scope=SCOPE_BASE)
+ self._apply(ldb, res[0])
+ else:
+ # Existing Message was stored to work out what fields changed.
+ existing_obj = self.from_message(ldb, self._message)
+
+ # Only modify replace or modify fields that have changed.
+ # Any fields that are set to None or an empty list get unset.
+ message = Message(dn=self.dn)
+ for attr, field in self.fields.items():
+ if attr != "dn" and not field.readonly:
+ value = getattr(self, attr)
+ old_value = getattr(existing_obj, attr)
+
+ if value != old_value:
+ try:
+ db_value = field.to_db_value(ldb, value,
+ FLAG_MOD_REPLACE)
+ except ValueError as e:
+ raise FieldError(e, field=field)
+
+ # When a field returns None or empty list, delete attr.
+ if db_value in (None, []):
+ db_value = MessageElement([],
+ FLAG_MOD_REPLACE,
+ field.name)
+ message.add(db_value)
+
+ # Saving nothing only triggers an error.
+ if len(message):
+ ldb.modify(message)
+
+ # Fetching object refreshes any automatically populated fields.
+ self.refresh(ldb)
+
+ def delete(self, ldb):
+ """Delete item from Ldb database.
+
+ If self.dn is None then the object has not yet been saved.
+
+ :param ldb: Ldb connection
+ """
+ if self.dn is None:
+ raise DeleteError("Cannot delete object that doesn't have a dn.")
+
+ try:
+ ldb.delete(self.dn)
+ except LdbError as e:
+ raise DeleteError(f"Delete failed: {e}")
+
+ def protect(self, ldb):
+ """Protect object from accidental deletion.
+
+ :param ldb: Ldb connection
+ """
+ utils = SDUtils(ldb)
+
+ try:
+ utils.dacl_add_ace(self.dn, "(D;;DTSD;;;WD)")
+ except LdbError as e:
+ raise ProtectError(f"Failed to protect object: {e}")
+
+ def unprotect(self, ldb):
+ """Unprotect object from accidental deletion.
+
+ :param ldb: Ldb connection
+ """
+ utils = SDUtils(ldb)
+
+ try:
+ utils.dacl_delete_aces(self.dn, "(D;;DTSD;;;WD)")
+ except LdbError as e:
+ raise UnprotectError(f"Failed to unprotect object: {e}")
diff --git a/python/samba/netcmd/domain/models/query.py b/python/samba/netcmd/domain/models/query.py
new file mode 100644
index 0000000..9cdb650
--- /dev/null
+++ b/python/samba/netcmd/domain/models/query.py
@@ -0,0 +1,81 @@
+# Unix SMB/CIFS implementation.
+#
+# Query class for the ORM to the Ldb database.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+import re
+
+from .exceptions import DoesNotExist, MultipleObjectsReturned
+
+RE_SPLIT_CAMELCASE = re.compile(r"[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))")
+
+
+class Query:
+ """Simple Query class used by the `Model.query` method."""
+
+ def __init__(self, model, ldb, result):
+ self.model = model
+ self.ldb = ldb
+ self.result = result
+ self.count = result.count
+ self.name = " ".join(RE_SPLIT_CAMELCASE.findall(model.__name__)).lower()
+
+ def __iter__(self):
+ """Loop over Query class yields Model instances."""
+ for message in self.result:
+ yield self.model.from_message(self.ldb, message)
+
+ def first(self):
+ """Returns the first item in the Query or None for no results."""
+ if self.result.count:
+ return self.model.from_message(self.ldb, self.result[0])
+
+ def last(self):
+ """Returns the last item in the Query or None for no results."""
+ if self.result.count:
+ return self.model.from_message(self.ldb, self.result[-1])
+
+ def get(self):
+ """Returns one item or None if no results were found.
+
+ :returns: Model instance or None if not found.
+ :raises MultipleObjectsReturned: if more than one results were returned
+ """
+ if self.count > 1:
+ raise MultipleObjectsReturned(
+ f"More than one {self.name} objects returned (got {self.count}).")
+ elif self.count:
+ return self.model.from_message(self.ldb, self.result[0])
+
+ def one(self):
+ """Must return EXACTLY one item or raise an exception.
+
+ :returns: Model instance
+ :raises DoesNotExist: if no results were returned
+ :raises MultipleObjectsReturned: if more than one results were returned
+ """
+ if self.count < 1:
+ raise DoesNotExist(
+ f"{self.name.capitalize()} matching query not found")
+ elif self.count > 1:
+ raise MultipleObjectsReturned(
+ f"More than one {self.name} objects returned (got {self.count}).")
+ else:
+ return self.model.from_message(self.ldb, self.result[0])
diff --git a/python/samba/netcmd/domain/models/schema.py b/python/samba/netcmd/domain/models/schema.py
new file mode 100644
index 0000000..59ece05
--- /dev/null
+++ b/python/samba/netcmd/domain/models/schema.py
@@ -0,0 +1,124 @@
+# Unix SMB/CIFS implementation.
+#
+# Class and attribute schema models.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from .fields import BinaryField, BooleanField, DnField, GUIDField,\
+ IntegerField, StringField
+from .model import Model
+
+
+class ClassSchema(Model):
+ default_object_category = DnField("defaultObjectCategory")
+ governs_id = StringField("governsID")
+ schema_id_guid = GUIDField("schemaIDGUID")
+ subclass_of = StringField("subclassOf")
+ admin_description = StringField("adminDescription")
+ admin_display_name = StringField("adminDisplayName")
+ default_hiding_value = BooleanField("defaultHidingValue")
+ default_security_descriptor = BinaryField("defaultSecurityDescriptor")
+ ldap_display_name = StringField("lDAPDisplayName")
+ may_contain = StringField("mayContain", many=True)
+ poss_superiors = StringField("possSuperiors", many=True)
+ rdn_att_id = StringField("rDNAttID")
+ show_in_advanced_view_only = BooleanField("showInAdvancedViewOnly")
+ system_only = BooleanField("systemOnly", readonly=True)
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the ClassSchema model.
+
+ This is the same as AttributeSchema, but the objectClass is different.
+
+ :param ldb: Ldb connection
+ :return: Dn object of container
+ """
+ return ldb.get_schema_basedn()
+
+ @staticmethod
+ def get_object_class():
+ return "classSchema"
+
+ @classmethod
+ def lookup(cls, ldb, name):
+ """Helper function to lookup class or raise LookupError.
+
+ :param ldb: Ldb connection
+ :param name: Class name
+ :raises: LookupError if not found
+ :raises: ValueError if name is not provided
+ """
+ if not name:
+ raise ValueError("Class name is required.")
+
+ attr = cls.get(ldb, ldap_display_name=name)
+ if attr is None:
+ raise LookupError(f"Could not locate {name} in class schema.")
+
+ return attr
+
+
+class AttributeSchema(Model):
+ attribute_id = StringField("attributeID")
+ attribute_syntax = StringField("attributeSyntax")
+ is_single_valued = BooleanField("isSingleValued")
+ ldap_display_name = StringField("lDAPDisplayName")
+ om_syntax = IntegerField("oMSyntax")
+ admin_description = StringField("adminDescription")
+ admin_display_name = StringField("adminDisplayName")
+ attribute_security_guid = GUIDField("attributeSecurityGUID")
+ schema_flags_ex = IntegerField("schemaFlagsEx")
+ search_flags = IntegerField("searchFlags")
+ show_in_advanced_view_only = BooleanField("showInAdvancedViewOnly")
+ system_flags = IntegerField("systemFlags", readonly=True)
+ system_only = BooleanField("systemOnly", readonly=True)
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the AttributeSchema model.
+
+ This is the same as ClassSchema, but the objectClass is different.
+
+ :param ldb: Ldb connection
+ :return: Dn object of container
+ """
+ return ldb.get_schema_basedn()
+
+ @staticmethod
+ def get_object_class():
+ return "attributeSchema"
+
+ @classmethod
+ def lookup(cls, ldb, name):
+ """Helper function to lookup attribute or raise LookupError.
+
+ :param ldb: Ldb connection
+ :param name: Attribute name
+ :raises: LookupError if not found
+ :raises: ValueError if name is not provided
+ """
+ if not name:
+ raise ValueError("Attribute name is required.")
+
+ attr = cls.get(ldb, ldap_display_name=name)
+ if attr is None:
+ raise LookupError(f"Could not locate {name} in attribute schema.")
+
+ return attr
diff --git a/python/samba/netcmd/domain/models/site.py b/python/samba/netcmd/domain/models/site.py
new file mode 100644
index 0000000..44643f3
--- /dev/null
+++ b/python/samba/netcmd/domain/models/site.py
@@ -0,0 +1,47 @@
+# Unix SMB/CIFS implementation.
+#
+# Site model.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from .fields import BooleanField, DnField, IntegerField
+from .model import Model
+
+
+class Site(Model):
+ show_in_advanced_view_only = BooleanField("showInAdvancedViewOnly")
+ system_flags = IntegerField("systemFlags", readonly=True)
+
+ # Backlinks
+ site_object_bl = DnField("siteObjectBL", readonly=True)
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the Site model.
+
+ :param ldb: Ldb connection
+ :return: Dn to use for new objects
+ """
+ base_dn = ldb.get_config_basedn()
+ base_dn.add_child("CN=Sites")
+ return base_dn
+
+ @staticmethod
+ def get_object_class():
+ return "site"
diff --git a/python/samba/netcmd/domain/models/subnet.py b/python/samba/netcmd/domain/models/subnet.py
new file mode 100644
index 0000000..bb249d4
--- /dev/null
+++ b/python/samba/netcmd/domain/models/subnet.py
@@ -0,0 +1,45 @@
+# Unix SMB/CIFS implementation.
+#
+# Subnet model.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from .fields import BooleanField, DnField, IntegerField
+from .model import Model
+
+
+class Subnet(Model):
+ show_in_advanced_view_only = BooleanField("showInAdvancedViewOnly")
+ site_object = DnField("siteObject")
+ system_flags = IntegerField("systemFlags", readonly=True)
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the Subnet model.
+
+ :param ldb: Ldb connection
+ :return: Dn to use for new objects
+ """
+ base_dn = ldb.get_config_basedn()
+ base_dn.add_child("CN=Subnets,CN=Sites")
+ return base_dn
+
+ @staticmethod
+ def get_object_class():
+ return "subnet"
diff --git a/python/samba/netcmd/domain/models/user.py b/python/samba/netcmd/domain/models/user.py
new file mode 100644
index 0000000..7b0785a
--- /dev/null
+++ b/python/samba/netcmd/domain/models/user.py
@@ -0,0 +1,75 @@
+# Unix SMB/CIFS implementation.
+#
+# User model.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from ldb import Dn
+
+from samba.dsdb import DS_GUID_USERS_CONTAINER
+
+from .fields import DnField, SIDField, StringField
+from .model import Model
+
+
+class User(Model):
+ username = StringField("sAMAccountName")
+ assigned_policy = DnField("msDS-AssignedAuthNPolicy")
+ assigned_silo = DnField("msDS-AssignedAuthNPolicySilo")
+ object_sid = SIDField("objectSid")
+
+ def __str__(self):
+ """Return username rather than cn for User model."""
+ return self.username
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the User model.
+
+ :param ldb: Ldb connection
+ :return: Dn to use for new objects
+ """
+ return ldb.get_wellknown_dn(ldb.get_default_basedn(),
+ DS_GUID_USERS_CONTAINER)
+
+ @classmethod
+ def get_search_dn(cls, ldb):
+ """Return Dn used for searching so Computers will also be found.
+
+ :param ldb: Ldb connection
+ :return: Dn to use for searching
+ """
+ return ldb.get_root_basedn()
+
+ @staticmethod
+ def get_object_class():
+ return "user"
+
+ @classmethod
+ def find(cls, ldb, name):
+ """Helper function to find a user first by Dn then username.
+
+ If the Dn can't be parsed, use sAMAccountName instead.
+ """
+ try:
+ query = {"dn": Dn(ldb, name)}
+ except ValueError:
+ query = {"username": name}
+
+ return cls.get(ldb, **query)
diff --git a/python/samba/netcmd/domain/models/value_type.py b/python/samba/netcmd/domain/models/value_type.py
new file mode 100644
index 0000000..00a4e07
--- /dev/null
+++ b/python/samba/netcmd/domain/models/value_type.py
@@ -0,0 +1,96 @@
+# Unix SMB/CIFS implementation.
+#
+# Claim value type model.
+#
+# Copyright (C) Catalyst.Net Ltd. 2023
+#
+# Written by Rob van der Linde <rob@catalyst.net.nz>
+#
+# 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/>.
+#
+
+from .fields import BooleanField, DnField, IntegerField, StringField
+from .model import Model
+
+# LDAP Syntax to Value Type CN lookup table.
+# These are the lookups used by known AD attributes, add new ones as required.
+SYNTAX_TO_VALUE_TYPE_CN = {
+ "2.5.5.1": "MS-DS-Text", # Object(DS-DN)
+ "2.5.5.2": "MS-DS-Text", # String(Object-Identifier)
+ "2.5.5.8": "MS-DS-YesNo", # Boolean
+ "2.5.5.9": "MS-DS-Number", # Integer
+ "2.5.5.12": "MS-DS-Text", # String(Unicode)
+ "2.5.5.15": "MS-DS-Text", # String(NT-Sec-Desc)
+ "2.5.5.16": "MS-DS-Number", # LargeInteger
+}
+
+
+class ValueType(Model):
+ description = StringField("description")
+ display_name = StringField("displayName")
+ claim_is_single_valued = BooleanField("msDS-ClaimIsSingleValued")
+ claim_is_value_space_restricted = BooleanField(
+ "msDS-ClaimIsValueSpaceRestricted")
+ claim_value_type = IntegerField("msDS-ClaimValueType")
+ is_possible_values_present = BooleanField("msDS-IsPossibleValuesPresent")
+ show_in_advanced_view_only = BooleanField("showInAdvancedViewOnly")
+
+ # Backlinks
+ value_type_reference_bl = DnField(
+ "msDS-ValueTypeReferenceBL", readonly=True)
+
+ @staticmethod
+ def get_base_dn(ldb):
+ """Return the base DN for the ValueType model.
+
+ :param ldb: Ldb connection
+ :return: Dn object of container
+ """
+ base_dn = ldb.get_config_basedn()
+ base_dn.add_child("CN=Value Types,CN=Claims Configuration,CN=Services")
+ return base_dn
+
+ @staticmethod
+ def get_object_class():
+ return "msDS-ValueType"
+
+ @classmethod
+ def lookup(cls, ldb, attribute):
+ """Helper function to get ValueType by attribute or raise LookupError.
+
+ :param ldb: Ldb connection
+ :param attribute: AttributeSchema object
+ :raises: LookupError if not found
+ :raises: ValueError for unknown attribute syntax
+ """
+ # If attribute is None.
+ if not attribute:
+ raise ValueError("Attribute is required for value type lookup.")
+
+ # Unknown attribute syntax as it isn't in the lookup table.
+ syntax = attribute.attribute_syntax
+ cn = SYNTAX_TO_VALUE_TYPE_CN.get(syntax)
+ if not cn:
+ raise ValueError(f"Unable to process attribute syntax {syntax}")
+
+ # This should always return something but should still be handled.
+ value_type = cls.get(ldb, cn=cn)
+ if value_type is None:
+ raise LookupError(
+ f"Could not find claim value type for {attribute}.")
+
+ return value_type
+
+ def __str__(self):
+ return str(self.display_name)
diff --git a/python/samba/netcmd/domain/passwordsettings.py b/python/samba/netcmd/domain/passwordsettings.py
new file mode 100644
index 0000000..d0cf47b
--- /dev/null
+++ b/python/samba/netcmd/domain/passwordsettings.py
@@ -0,0 +1,316 @@
+# domain management - domain passwordsettings
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import ldb
+import samba.getopt as options
+from samba.auth import system_session
+from samba.dcerpc.samr import (DOMAIN_PASSWORD_COMPLEX,
+ DOMAIN_PASSWORD_STORE_CLEARTEXT)
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.netcmd.common import (NEVER_TIMESTAMP, timestamp_to_days,
+ timestamp_to_mins)
+from samba.netcmd.pso import cmd_domain_passwordsettings_pso
+from samba.samdb import SamDB
+
+
+class cmd_domain_passwordsettings_show(Command):
+ """Display current password settings for the domain."""
+
+ synopsis = "%prog [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+ metavar="URL", dest="H"),
+ ]
+
+ def run(self, H=None, credopts=None, sambaopts=None, versionopts=None):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+
+ samdb = SamDB(url=H, session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ domain_dn = samdb.domain_dn()
+ res = samdb.search(domain_dn, scope=ldb.SCOPE_BASE,
+ attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength",
+ "minPwdAge", "maxPwdAge", "lockoutDuration", "lockoutThreshold",
+ "lockOutObservationWindow"])
+ assert(len(res) == 1)
+ try:
+ pwd_props = int(res[0]["pwdProperties"][0])
+ pwd_hist_len = int(res[0]["pwdHistoryLength"][0])
+ cur_min_pwd_len = int(res[0]["minPwdLength"][0])
+ # ticks -> days
+ cur_min_pwd_age = timestamp_to_days(res[0]["minPwdAge"][0])
+ cur_max_pwd_age = timestamp_to_days(res[0]["maxPwdAge"][0])
+
+ cur_account_lockout_threshold = int(res[0]["lockoutThreshold"][0])
+
+ # ticks -> mins
+ cur_account_lockout_duration = timestamp_to_mins(res[0]["lockoutDuration"][0])
+ cur_reset_account_lockout_after = timestamp_to_mins(res[0]["lockOutObservationWindow"][0])
+ except Exception as e:
+ raise CommandError("Could not retrieve password properties!", e)
+
+ self.message("Password information for domain '%s'" % domain_dn)
+ self.message("")
+ if pwd_props & DOMAIN_PASSWORD_COMPLEX != 0:
+ self.message("Password complexity: on")
+ else:
+ self.message("Password complexity: off")
+ if pwd_props & DOMAIN_PASSWORD_STORE_CLEARTEXT != 0:
+ self.message("Store plaintext passwords: on")
+ else:
+ self.message("Store plaintext passwords: off")
+ self.message("Password history length: %d" % pwd_hist_len)
+ self.message("Minimum password length: %d" % cur_min_pwd_len)
+ self.message("Minimum password age (days): %d" % cur_min_pwd_age)
+ self.message("Maximum password age (days): %d" % cur_max_pwd_age)
+ self.message("Account lockout duration (mins): %d" % cur_account_lockout_duration)
+ self.message("Account lockout threshold (attempts): %d" % cur_account_lockout_threshold)
+ self.message("Reset account lockout after (mins): %d" % cur_reset_account_lockout_after)
+
+
+class cmd_domain_passwordsettings_set(Command):
+ """Set password settings.
+
+ Password complexity, password lockout policy, history length,
+ minimum password length, the minimum and maximum password age) on
+ a Samba AD DC server.
+
+ Use against a Windows DC is possible, but group policy will override it.
+ """
+
+ synopsis = "%prog <options> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+ metavar="URL", dest="H"),
+ Option("-q", "--quiet", help="Be quiet", action="store_true"), # unused
+ Option("--complexity", type="choice", choices=["on", "off", "default"],
+ help="The password complexity (on | off | default). Default is 'on'"),
+ Option("--store-plaintext", type="choice", choices=["on", "off", "default"],
+ help="Store plaintext passwords where account have 'store passwords with reversible encryption' set (on | off | default). Default is 'off'"),
+ Option("--history-length",
+ help="The password history length (<integer> | default). Default is 24.", type=str),
+ Option("--min-pwd-length",
+ help="The minimum password length (<integer> | default). Default is 7.", type=str),
+ Option("--min-pwd-age",
+ help="The minimum password age (<integer in days> | default). Default is 1.", type=str),
+ Option("--max-pwd-age",
+ help="The maximum password age (<integer in days> | default). Default is 43.", type=str),
+ Option("--account-lockout-duration",
+ help="The length of time an account is locked out after exceeding the limit on bad password attempts (<integer in mins> | default). Default is 30 mins.", type=str),
+ Option("--account-lockout-threshold",
+ help="The number of bad password attempts allowed before locking out the account (<integer> | default). Default is 0 (never lock out).", type=str),
+ Option("--reset-account-lockout-after",
+ help="After this time is elapsed, the recorded number of attempts restarts from zero (<integer> | default). Default is 30.", type=str),
+ ]
+
+ def run(self, H=None, min_pwd_age=None, max_pwd_age=None,
+ quiet=False, complexity=None, store_plaintext=None, history_length=None,
+ min_pwd_length=None, account_lockout_duration=None, account_lockout_threshold=None,
+ reset_account_lockout_after=None, credopts=None, sambaopts=None,
+ versionopts=None):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+
+ samdb = SamDB(url=H, session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ domain_dn = samdb.domain_dn()
+ msgs = []
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, domain_dn)
+ pwd_props = int(samdb.get_pwdProperties())
+
+ # get the current password age settings
+ max_pwd_age_ticks = samdb.get_maxPwdAge()
+ min_pwd_age_ticks = samdb.get_minPwdAge()
+
+ if complexity is not None:
+ if complexity == "on" or complexity == "default":
+ pwd_props = pwd_props | DOMAIN_PASSWORD_COMPLEX
+ msgs.append("Password complexity activated!")
+ elif complexity == "off":
+ pwd_props = pwd_props & (~DOMAIN_PASSWORD_COMPLEX)
+ msgs.append("Password complexity deactivated!")
+
+ if store_plaintext is not None:
+ if store_plaintext == "on" or store_plaintext == "default":
+ pwd_props = pwd_props | DOMAIN_PASSWORD_STORE_CLEARTEXT
+ msgs.append("Plaintext password storage for changed passwords activated!")
+ elif store_plaintext == "off":
+ pwd_props = pwd_props & (~DOMAIN_PASSWORD_STORE_CLEARTEXT)
+ msgs.append("Plaintext password storage for changed passwords deactivated!")
+
+ if complexity is not None or store_plaintext is not None:
+ m["pwdProperties"] = ldb.MessageElement(str(pwd_props),
+ ldb.FLAG_MOD_REPLACE, "pwdProperties")
+
+ if history_length is not None:
+ if history_length == "default":
+ pwd_hist_len = 24
+ else:
+ pwd_hist_len = int(history_length)
+
+ if pwd_hist_len < 0 or pwd_hist_len > 24:
+ raise CommandError("Password history length must be in the range of 0 to 24!")
+
+ m["pwdHistoryLength"] = ldb.MessageElement(str(pwd_hist_len),
+ ldb.FLAG_MOD_REPLACE, "pwdHistoryLength")
+ msgs.append("Password history length changed!")
+
+ if min_pwd_length is not None:
+ if min_pwd_length == "default":
+ min_pwd_len = 7
+ else:
+ min_pwd_len = int(min_pwd_length)
+
+ if min_pwd_len < 0 or min_pwd_len > 14:
+ raise CommandError("Minimum password length must be in the range of 0 to 14!")
+
+ m["minPwdLength"] = ldb.MessageElement(str(min_pwd_len),
+ ldb.FLAG_MOD_REPLACE, "minPwdLength")
+ msgs.append("Minimum password length changed!")
+
+ if min_pwd_age is not None:
+ if min_pwd_age == "default":
+ min_pwd_age = 1
+ else:
+ min_pwd_age = int(min_pwd_age)
+
+ if min_pwd_age < 0 or min_pwd_age > 998:
+ raise CommandError("Minimum password age must be in the range of 0 to 998!")
+
+ # days -> ticks
+ min_pwd_age_ticks = -int(min_pwd_age * (24 * 60 * 60 * 1e7))
+
+ m["minPwdAge"] = ldb.MessageElement(str(min_pwd_age_ticks),
+ ldb.FLAG_MOD_REPLACE, "minPwdAge")
+ msgs.append("Minimum password age changed!")
+
+ if max_pwd_age is not None:
+ if max_pwd_age == "default":
+ max_pwd_age = 43
+ else:
+ max_pwd_age = int(max_pwd_age)
+
+ if max_pwd_age < 0 or max_pwd_age > 999:
+ raise CommandError("Maximum password age must be in the range of 0 to 999!")
+
+ # days -> ticks
+ if max_pwd_age == 0:
+ max_pwd_age_ticks = NEVER_TIMESTAMP
+ else:
+ max_pwd_age_ticks = -int(max_pwd_age * (24 * 60 * 60 * 1e7))
+
+ m["maxPwdAge"] = ldb.MessageElement(str(max_pwd_age_ticks),
+ ldb.FLAG_MOD_REPLACE, "maxPwdAge")
+ msgs.append("Maximum password age changed!")
+
+ if account_lockout_duration is not None:
+ if account_lockout_duration == "default":
+ account_lockout_duration = 30
+ else:
+ account_lockout_duration = int(account_lockout_duration)
+
+ if account_lockout_duration < 0 or account_lockout_duration > 99999:
+ raise CommandError("Account lockout duration "
+ "must be in the range of 0 to 99999!")
+
+ # minutes -> ticks
+ if account_lockout_duration == 0:
+ account_lockout_duration_ticks = NEVER_TIMESTAMP
+ else:
+ account_lockout_duration_ticks = -int(account_lockout_duration * (60 * 1e7))
+
+ m["lockoutDuration"] = ldb.MessageElement(str(account_lockout_duration_ticks),
+ ldb.FLAG_MOD_REPLACE, "lockoutDuration")
+ msgs.append("Account lockout duration changed!")
+
+ if account_lockout_threshold is not None:
+ if account_lockout_threshold == "default":
+ account_lockout_threshold = 0
+ else:
+ account_lockout_threshold = int(account_lockout_threshold)
+
+ m["lockoutThreshold"] = ldb.MessageElement(str(account_lockout_threshold),
+ ldb.FLAG_MOD_REPLACE, "lockoutThreshold")
+ msgs.append("Account lockout threshold changed!")
+
+ if reset_account_lockout_after is not None:
+ if reset_account_lockout_after == "default":
+ reset_account_lockout_after = 30
+ else:
+ reset_account_lockout_after = int(reset_account_lockout_after)
+
+ if reset_account_lockout_after < 0 or reset_account_lockout_after > 99999:
+ raise CommandError("Maximum password age must be in the range of 0 to 99999!")
+
+ # minutes -> ticks
+ if reset_account_lockout_after == 0:
+ reset_account_lockout_after_ticks = NEVER_TIMESTAMP
+ else:
+ reset_account_lockout_after_ticks = -int(reset_account_lockout_after * (60 * 1e7))
+
+ m["lockOutObservationWindow"] = ldb.MessageElement(str(reset_account_lockout_after_ticks),
+ ldb.FLAG_MOD_REPLACE, "lockOutObservationWindow")
+ msgs.append("Duration to reset account lockout after changed!")
+
+ if max_pwd_age or min_pwd_age:
+ # If we're setting either min or max password, make sure the max is
+ # still greater overall. As either setting could be None, we use the
+ # ticks here (which are always set) and work backwards.
+ max_pwd_age = timestamp_to_days(max_pwd_age_ticks)
+ min_pwd_age = timestamp_to_days(min_pwd_age_ticks)
+ if max_pwd_age != 0 and min_pwd_age >= max_pwd_age:
+ raise CommandError("Maximum password age (%d) must be greater than minimum password age (%d)!" % (max_pwd_age, min_pwd_age))
+
+ if len(m) == 0:
+ raise CommandError("You must specify at least one option to set. Try --help")
+ samdb.modify(m)
+ msgs.append("All changes applied successfully!")
+ self.message("\n".join(msgs))
+
+
+class cmd_domain_passwordsettings(SuperCommand):
+ """Manage password policy settings."""
+
+ subcommands = {}
+ subcommands["pso"] = cmd_domain_passwordsettings_pso()
+ subcommands["show"] = cmd_domain_passwordsettings_show()
+ subcommands["set"] = cmd_domain_passwordsettings_set()
diff --git a/python/samba/netcmd/domain/provision.py b/python/samba/netcmd/domain/provision.py
new file mode 100644
index 0000000..8f13e54
--- /dev/null
+++ b/python/samba/netcmd/domain/provision.py
@@ -0,0 +1,405 @@
+# domain management - domain provision
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import os
+import sys
+import tempfile
+
+import samba
+import samba.getopt as options
+from samba.auth import system_session
+from samba.auth_util import system_session_unix
+from samba.dcerpc import security
+from samba.dsdb import (
+ DS_DOMAIN_FUNCTION_2000,
+ DS_DOMAIN_FUNCTION_2003,
+ DS_DOMAIN_FUNCTION_2008,
+ DS_DOMAIN_FUNCTION_2008_R2,
+ DS_DOMAIN_FUNCTION_2012,
+ DS_DOMAIN_FUNCTION_2012_R2,
+ DS_DOMAIN_FUNCTION_2016
+)
+from samba.netcmd import Command, CommandError, Option
+from samba.provision import DEFAULT_MIN_PWD_LENGTH, ProvisioningError, provision
+from samba.provision.common import FILL_DRS, FILL_FULL, FILL_NT4SYNC
+from samba.samdb import get_default_backend_store
+from samba import functional_level
+
+from .common import common_ntvfs_options, common_provision_join_options
+
+
+class cmd_domain_provision(Command):
+ """Provision a domain."""
+
+ synopsis = "%prog [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ takes_options = [
+ 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", "2016"],
+ help="The domain and forest function level (2000 | 2003 | 2008 | 2008_R2 - always native | 2016). 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", "2016", "2019"],
+ help="The base schema files to use. Default is (Windows) 2019.",
+ default="2019"),
+ Option("--adprep-level", type="choice", metavar="FUNCTION_LEVEL",
+ choices=["SKIP", "2008_R2", "2012", "2012_R2", "2016"],
+ help="The highest functional level to prepare for. Default is based on --base-schema",
+ default=None),
+ 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)"),
+ ]
+
+ ntvfs_options = [
+ Option("--use-xattrs", type="choice", choices=["yes", "no", "auto"],
+ 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 intelligent guess based on the user rights and system capabilities",
+ default="auto")
+ ]
+
+ takes_options.extend(common_provision_join_options)
+
+ if samba.is_ntvfs_fileserver_built():
+ takes_options.extend(common_ntvfs_options)
+ takes_options.extend(ntvfs_options)
+
+ takes_args = []
+
+ def run(self, sambaopts=None, versionopts=None,
+ 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,
+ site=None,
+ krbtgtpass=None,
+ machinepass=None,
+ dns_backend=None,
+ dns_forwarder=None,
+ dnspass=None,
+ ldapadminpass=None,
+ root=None,
+ nobody=None,
+ users=None,
+ quiet=None,
+ blank=None,
+ server_role=None,
+ function_level=None,
+ adprep_level=None,
+ next_rid=None,
+ partitions_only=None,
+ targetdir=None,
+ use_xattrs="auto",
+ use_ntvfs=False,
+ use_rfc2307=None,
+ base_schema=None,
+ plaintext_secrets=False,
+ backend_store=None,
+ backend_store_size=None):
+
+ self.logger = self.get_logger(name="provision", quiet=quiet)
+
+ lp = sambaopts.get_loadparm()
+ smbconf = lp.configfile
+
+ if dns_forwarder is not None:
+ suggested_forwarder = dns_forwarder
+ else:
+ suggested_forwarder = self._get_nameserver_ip()
+ if suggested_forwarder is None:
+ suggested_forwarder = "none"
+
+ if not self.raw_argv:
+ interactive = True
+
+ if interactive:
+ from getpass import getpass
+ import socket
+
+ def ask(prompt, default=None):
+ if default is not None:
+ print("%s [%s]: " % (prompt, default), end=' ')
+ else:
+ print("%s: " % (prompt,), end=' ')
+ sys.stdout.flush()
+ 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")
+
+ dns_backend = ask("DNS backend (SAMBA_INTERNAL, BIND9_FLATFILE, BIND9_DLZ, NONE)", "SAMBA_INTERNAL")
+ if dns_backend in (None, ''):
+ raise CommandError("No DNS backend set!")
+
+ 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
+
+ while True:
+ adminpassplain = getpass("Administrator password: ")
+ issue = self._adminpass_issue(adminpassplain)
+ if issue:
+ self.errf.write("%s.\n" % issue)
+ else:
+ adminpassverify = getpass("Retype password: ")
+ if not adminpassplain == adminpassverify:
+ self.errf.write("Sorry, passwords do not match.\n")
+ 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!")
+
+ if adminpass:
+ issue = self._adminpass_issue(adminpass)
+ if issue:
+ raise CommandError(issue)
+ else:
+ self.logger.info("Administrator password will be set randomly!")
+
+ try:
+ dom_for_fun_level = functional_level.string_to_level(function_level)
+ except KeyError:
+ raise CommandError(f"'{function_level}' is not a valid domain level")
+
+ if adprep_level is None:
+ # Select the adprep_level default based
+ # on what the base schema permits
+ if base_schema in ["2008_R2", "2008_R2_old"]:
+ # without explicit --adprep-level=2008_R2
+ # we will skip the adprep step on
+ # provision
+ adprep_level = "SKIP"
+ elif base_schema in ["2012"]:
+ adprep_level = "2012"
+ elif base_schema in ["2012_R2"]:
+ adprep_level = "2012_R2"
+ else:
+ adprep_level = "2016"
+
+ if adprep_level == "SKIP":
+ provision_adprep_level = None
+ elif adprep_level == "2008R2":
+ provision_adprep_level = DS_DOMAIN_FUNCTION_2008_R2
+ elif adprep_level == "2012":
+ provision_adprep_level = DS_DOMAIN_FUNCTION_2012
+ elif adprep_level == "2012_R2":
+ provision_adprep_level = DS_DOMAIN_FUNCTION_2012_R2
+ elif adprep_level == "2016":
+ provision_adprep_level = DS_DOMAIN_FUNCTION_2016
+
+ if dns_backend == "SAMBA_INTERNAL" and dns_forwarder is None:
+ dns_forwarder = suggested_forwarder
+
+ 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.makedirs(targetdir)
+
+ eadb = True
+
+ if use_xattrs == "yes":
+ eadb = False
+ elif use_xattrs == "auto" and not use_ntvfs:
+ eadb = False
+ elif not use_ntvfs:
+ raise CommandError("--use-xattrs=no requires --use-ntvfs (not supported for production use). "
+ "Please re-run with --use-xattrs omitted.")
+ 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",
+ system_session_unix(),
+ "native")
+ eadb = False
+ except Exception:
+ self.logger.info("You are not root or your system does not support xattr, using tdb backend for attributes. ")
+ finally:
+ file.close()
+
+ if eadb:
+ 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.")
+
+ if domain_sid is not None:
+ domain_sid = security.dom_sid(domain_sid)
+
+ session = system_session()
+ if backend_store is None:
+ backend_store = get_default_backend_store()
+ try:
+ result = provision(self.logger,
+ 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,
+ useeadb=eadb, next_rid=next_rid, lp=lp, use_ntvfs=use_ntvfs,
+ use_rfc2307=use_rfc2307, skip_sysvolacl=False,
+ base_schema=base_schema,
+ adprep_level=provision_adprep_level,
+ plaintext_secrets=plaintext_secrets,
+ backend_store=backend_store,
+ backend_store_size=backend_store_size)
+
+ except ProvisioningError as e:
+ raise CommandError("Provision failed", e)
+
+ result.report_logger(self.logger)
+
+ def _get_nameserver_ip(self):
+ """Grab the nameserver IP address from /etc/resolv.conf."""
+ from os import path
+ RESOLV_CONF = "/etc/resolv.conf"
+
+ 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)
+
+ def _adminpass_issue(self, adminpass):
+ """Returns error string for a bad administrator password,
+ or None if acceptable"""
+ if isinstance(adminpass, bytes):
+ adminpass = adminpass.decode('utf8')
+ if len(adminpass) < DEFAULT_MIN_PWD_LENGTH:
+ 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
diff --git a/python/samba/netcmd/domain/samba3upgrade.py b/python/samba/netcmd/domain/samba3upgrade.py
new file mode 100644
index 0000000..67f4b42
--- /dev/null
+++ b/python/samba/netcmd/domain/samba3upgrade.py
@@ -0,0 +1,34 @@
+# domain management - domain samba3upgrade
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+from .classicupgrade import cmd_domain_classicupgrade
+
+
+class cmd_domain_samba3upgrade(cmd_domain_classicupgrade):
+ __doc__ = cmd_domain_classicupgrade.__doc__
+
+ # This command is present for backwards compatibility only,
+ # and should not be shown.
+
+ hidden = True
diff --git a/python/samba/netcmd/domain/schemaupgrade.py b/python/samba/netcmd/domain/schemaupgrade.py
new file mode 100644
index 0000000..ff00a77
--- /dev/null
+++ b/python/samba/netcmd/domain/schemaupgrade.py
@@ -0,0 +1,350 @@
+# domain management - domain schemaupgrade
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import os
+import shutil
+import subprocess
+import tempfile
+
+import ldb
+import samba.getopt as options
+from samba.auth import system_session
+from samba.netcmd import Command, CommandError, Option
+from samba.netcmd.fsmo import get_fsmo_roleowner
+from samba.provision import setup_path
+from samba.samdb import SamDB
+
+
+class ldif_schema_update:
+ """Helper class for applying LDIF schema updates"""
+
+ def __init__(self):
+ self.is_defunct = False
+ self.unknown_oid = None
+ self.dn = None
+ self.ldif = ""
+
+ def can_ignore_failure(self, error):
+ """Checks if we can safely ignore failure to apply an LDIF update"""
+ (num, errstr) = error.args
+
+ # Microsoft has marked objects as defunct that Samba doesn't know about
+ if num == ldb.ERR_NO_SUCH_OBJECT and self.is_defunct:
+ print("Defunct object %s doesn't exist, skipping" % self.dn)
+ return True
+ elif self.unknown_oid is not None:
+ print("Skipping unknown OID %s for object %s" % (self.unknown_oid, self.dn))
+ return True
+
+ return False
+
+ def apply(self, samdb):
+ """Applies a single LDIF update to the schema"""
+
+ try:
+ try:
+ samdb.modify_ldif(self.ldif, controls=['relax:0'])
+ except ldb.LdbError as e:
+ if e.args[0] == ldb.ERR_INVALID_ATTRIBUTE_SYNTAX:
+
+ # REFRESH after a failed change
+
+ # Otherwise the OID-to-attribute mapping in
+ # _apply_updates_in_file() won't work, because it
+ # can't lookup the new OID in the schema
+ samdb.set_schema_update_now()
+
+ samdb.modify_ldif(self.ldif, controls=['relax:0'])
+ else:
+ raise
+ except ldb.LdbError as e:
+ if self.can_ignore_failure(e):
+ return 0
+ else:
+ print("Exception: %s" % e)
+ print("Encountered while trying to apply the following LDIF")
+ print("----------------------------------------------------")
+ print("%s" % self.ldif)
+
+ raise
+
+ return 1
+
+
+class cmd_domain_schema_upgrade(Command):
+ """Domain schema upgrading"""
+
+ synopsis = "%prog [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+ metavar="URL", dest="H"),
+ Option("-q", "--quiet", help="Be quiet", action="store_true"), # unused
+ Option("-v", "--verbose", help="Be verbose", action="store_true"),
+ Option("--schema", type="choice", metavar="SCHEMA",
+ choices=["2012", "2012_R2", "2016", "2019"],
+ help="The schema file to upgrade to. Default is (Windows) 2019.",
+ default="2019"),
+ Option("--ldf-file", type=str, default=None,
+ help="Just apply the schema updates in the adprep/.LDF file(s) specified"),
+ Option("--base-dir", type=str, default=None,
+ help="Location of ldf files Default is ${SETUPDIR}/adprep.")
+ ]
+
+ def _apply_updates_in_file(self, samdb, ldif_file):
+ """
+ Applies a series of updates specified in an .LDIF file. The .LDIF file
+ is based on the adprep Schema updates provided by Microsoft.
+ """
+ count = 0
+ ldif_op = ldif_schema_update()
+
+ # parse the file line by line and work out each update operation to apply
+ for line in ldif_file:
+
+ line = line.rstrip()
+
+ # the operations in the .LDIF file are separated by blank lines. If
+ # we hit a blank line, try to apply the update we've parsed so far
+ if line == '':
+
+ # keep going if we haven't parsed anything yet
+ if ldif_op.ldif == '':
+ continue
+
+ # Apply the individual change
+ count += ldif_op.apply(samdb)
+
+ # start storing the next operation from scratch again
+ ldif_op = ldif_schema_update()
+ continue
+
+ # replace the placeholder domain name in the .ldif file with the real domain
+ if line.upper().endswith('DC=X'):
+ line = line[:-len('DC=X')] + str(samdb.get_default_basedn())
+ elif line.upper().endswith('CN=X'):
+ line = line[:-len('CN=X')] + str(samdb.get_default_basedn())
+
+ values = line.split(':')
+
+ if values[0].lower() == 'dn':
+ ldif_op.dn = values[1].strip()
+
+ # replace the Windows-specific operation with the Samba one
+ if values[0].lower() == 'changetype':
+ line = line.lower().replace(': ntdsschemaadd',
+ ': add')
+ line = line.lower().replace(': ntdsschemamodify',
+ ': modify')
+ line = line.lower().replace(': ntdsschemamodrdn',
+ ': modrdn')
+ line = line.lower().replace(': ntdsschemadelete',
+ ': delete')
+
+ if values[0].lower() in ['rdnattid', 'subclassof',
+ 'systemposssuperiors',
+ 'systemmaycontain',
+ 'systemauxiliaryclass']:
+ _, value = values
+
+ # The Microsoft updates contain some OIDs we don't recognize.
+ # Query the DB to see if we can work out the OID this update is
+ # referring to. If we find a match, then replace the OID with
+ # the ldapDisplayname
+ if '.' in value:
+ res = samdb.search(base=samdb.get_schema_basedn(),
+ expression="(|(attributeId=%s)(governsId=%s))" %
+ (value, value),
+ attrs=['ldapDisplayName'])
+
+ if len(res) != 1:
+ ldif_op.unknown_oid = value
+ else:
+ display_name = str(res[0]['ldapDisplayName'][0])
+ line = line.replace(value, ' ' + display_name)
+
+ # Microsoft has marked objects as defunct that Samba doesn't know about
+ if values[0].lower() == 'isdefunct' and values[1].strip().lower() == 'true':
+ ldif_op.is_defunct = True
+
+ # Samba has added the showInAdvancedViewOnly attribute to all objects,
+ # so rather than doing an add, we need to do a replace
+ if values[0].lower() == 'add' and values[1].strip().lower() == 'showinadvancedviewonly':
+ line = 'replace: showInAdvancedViewOnly'
+
+ # Add the line to the current LDIF operation (including the newline
+ # we stripped off at the start of the loop)
+ ldif_op.ldif += line + '\n'
+
+ return count
+
+ def _apply_update(self, samdb, update_file, base_dir):
+ """Wrapper function for parsing an LDIF file and applying the updates"""
+
+ print("Applying %s updates..." % update_file)
+
+ ldif_file = None
+ try:
+ ldif_file = open(os.path.join(base_dir, update_file))
+
+ count = self._apply_updates_in_file(samdb, ldif_file)
+
+ finally:
+ if ldif_file:
+ ldif_file.close()
+
+ print("%u changes applied" % count)
+
+ return count
+
+ def run(self, **kwargs):
+ try:
+ from samba.ms_schema_markdown import read_ms_markdown
+ except ImportError as e:
+ self.outf.write("Exception in importing markdown: %s\n" % e)
+ raise CommandError('Failed to import module markdown')
+ from samba.schema import Schema
+
+ updates_allowed_overridden = False
+ sambaopts = kwargs.get("sambaopts")
+ credopts = kwargs.get("credopts")
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+ H = kwargs.get("H")
+ target_schema = kwargs.get("schema")
+ ldf_files = kwargs.get("ldf_file")
+ base_dir = kwargs.get("base_dir")
+
+ temp_folder = None
+
+ samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp)
+
+ # we're not going to get far if the config doesn't allow schema updates
+ if lp.get("dsdb:schema update allowed") is None:
+ lp.set("dsdb:schema update allowed", "yes")
+ print("Temporarily overriding 'dsdb:schema update allowed' setting")
+ updates_allowed_overridden = True
+
+ own_dn = ldb.Dn(samdb, samdb.get_dsServiceName())
+ master = get_fsmo_roleowner(samdb, str(samdb.get_schema_basedn()),
+ 'schema')
+ if own_dn != master:
+ raise CommandError("This server is not the schema master.")
+
+ # if specific LDIF files were specified, just apply them
+ if ldf_files:
+ schema_updates = ldf_files.split(",")
+ else:
+ schema_updates = []
+
+ # work out the version of the target schema we're upgrading to
+ end = Schema.get_version(target_schema)
+
+ # work out the version of the schema we're currently using
+ res = samdb.search(base=samdb.get_schema_basedn(),
+ scope=ldb.SCOPE_BASE, attrs=['objectVersion'])
+
+ if len(res) != 1:
+ raise CommandError('Could not determine current schema version')
+ start = int(res[0]['objectVersion'][0]) + 1
+
+ diff_dir = setup_path("adprep/WindowsServerDocs")
+ if base_dir is None:
+ # Read from the Schema-Updates.md file
+ temp_folder = tempfile.mkdtemp()
+
+ update_file = setup_path("adprep/WindowsServerDocs/Schema-Updates.md")
+
+ try:
+ read_ms_markdown(update_file, temp_folder)
+ except Exception as e:
+ print("Exception in markdown parsing: %s" % e)
+ shutil.rmtree(temp_folder)
+ raise CommandError('Failed to upgrade schema')
+
+ base_dir = temp_folder
+
+ for version in range(start, end + 1):
+ update = 'Sch%d.ldf' % version
+ schema_updates.append(update)
+
+ # Apply patches if we parsed the Schema-Updates.md file
+ diff = os.path.abspath(os.path.join(diff_dir, update + '.diff'))
+ if temp_folder and os.path.exists(diff):
+ try:
+ p = subprocess.Popen(['patch', update, '-i', diff],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, cwd=temp_folder)
+ except (OSError, IOError):
+ shutil.rmtree(temp_folder)
+ raise CommandError("Failed to upgrade schema. "
+ "Is '/usr/bin/patch' missing?")
+
+ stdout, stderr = p.communicate()
+
+ if p.returncode:
+ print("Exception in patch: %s\n%s" % (stdout, stderr))
+ shutil.rmtree(temp_folder)
+ raise CommandError('Failed to upgrade schema')
+
+ print("Patched %s using %s" % (update, diff))
+
+ if base_dir is None:
+ base_dir = setup_path("adprep")
+
+ samdb.transaction_start()
+ count = 0
+ error_encountered = False
+
+ try:
+ # Apply the schema updates needed to move to the new schema version
+ for ldif_file in schema_updates:
+ count += self._apply_update(samdb, ldif_file, base_dir)
+
+ if count > 0:
+ samdb.transaction_commit()
+ print("Schema successfully updated")
+ else:
+ print("No changes applied to schema")
+ samdb.transaction_cancel()
+ except Exception as e:
+ print("Exception: %s" % e)
+ print("Error encountered, aborting schema upgrade")
+ samdb.transaction_cancel()
+ error_encountered = True
+
+ if updates_allowed_overridden:
+ lp.set("dsdb:schema update allowed", "no")
+
+ if temp_folder:
+ shutil.rmtree(temp_folder)
+
+ if error_encountered:
+ raise CommandError('Failed to upgrade schema')
diff --git a/python/samba/netcmd/domain/tombstones.py b/python/samba/netcmd/domain/tombstones.py
new file mode 100644
index 0000000..673bb9a
--- /dev/null
+++ b/python/samba/netcmd/domain/tombstones.py
@@ -0,0 +1,116 @@
+# domain management - domain tombstones
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import time
+
+import ldb
+import samba.getopt as options
+from samba.auth import system_session
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.samdb import SamDB
+
+
+class cmd_domain_tombstones_expunge(Command):
+ """Expunge tombstones from the database.
+
+This command expunges tombstones from the database."""
+ synopsis = "%prog NC [NC [...]] [options]"
+
+ takes_options = [
+ Option("-H", "--URL", help="LDB URL for database or target server", type=str,
+ metavar="URL", dest="H"),
+ Option("--current-time",
+ help="The current time to evaluate the tombstone lifetime from, expressed as YYYY-MM-DD",
+ type=str),
+ Option("--tombstone-lifetime", help="Number of days a tombstone should be preserved for", type=int),
+ ]
+
+ takes_args = ["nc*"]
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "credopts": options.CredentialsOptions,
+ "versionopts": options.VersionOptions,
+ }
+
+ def run(self, *ncs, **kwargs):
+ sambaopts = kwargs.get("sambaopts")
+ credopts = kwargs.get("credopts")
+ H = kwargs.get("H")
+ current_time_string = kwargs.get("current_time")
+ tombstone_lifetime = kwargs.get("tombstone_lifetime")
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+ samdb = SamDB(url=H, session_info=system_session(),
+ credentials=creds, lp=lp)
+
+ if current_time_string is None and tombstone_lifetime is None:
+ print("Note: without --current-time or --tombstone-lifetime "
+ "only tombstones already scheduled for deletion will "
+ "be deleted.", file=self.outf)
+ print("To remove all tombstones, use --tombstone-lifetime=0.",
+ file=self.outf)
+
+ if current_time_string is not None:
+ current_time_obj = time.strptime(current_time_string, "%Y-%m-%d")
+ current_time = int(time.mktime(current_time_obj))
+
+ else:
+ current_time = int(time.time())
+
+ if len(ncs) == 0:
+ res = samdb.search(expression="", base="", scope=ldb.SCOPE_BASE,
+ attrs=["namingContexts"])
+
+ ncs = []
+ for nc in res[0]["namingContexts"]:
+ ncs.append(str(nc))
+ else:
+ ncs = list(ncs)
+
+ started_transaction = False
+ try:
+ samdb.transaction_start()
+ started_transaction = True
+ (removed_objects,
+ removed_links) = samdb.garbage_collect_tombstones(ncs,
+ current_time=current_time,
+ tombstone_lifetime=tombstone_lifetime)
+
+ except Exception as err:
+ if started_transaction:
+ samdb.transaction_cancel()
+ raise CommandError("Failed to expunge / garbage collect tombstones", err)
+
+ samdb.transaction_commit()
+
+ self.outf.write("Removed %d objects and %d links successfully\n"
+ % (removed_objects, removed_links))
+
+
+class cmd_domain_tombstones(SuperCommand):
+ """Domain tombstone and recycled object management."""
+
+ subcommands = {}
+ subcommands["expunge"] = cmd_domain_tombstones_expunge()
diff --git a/python/samba/netcmd/domain/trust.py b/python/samba/netcmd/domain/trust.py
new file mode 100644
index 0000000..e930f00
--- /dev/null
+++ b/python/samba/netcmd/domain/trust.py
@@ -0,0 +1,2338 @@
+# domain management - domain trust
+#
+# Copyright Matthias Dieter Wallnoefer 2009
+# Copyright Andrew Kroeger 2009
+# Copyright Jelmer Vernooij 2007-2012
+# Copyright Giampaolo Lauria 2011
+# Copyright Matthieu Patou <mat@matws.net> 2011
+# Copyright Andrew Bartlett 2008-2015
+# Copyright Stefan Metzmacher 2012
+#
+# 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/>.
+#
+
+import ctypes
+from getpass import getpass
+
+import ldb
+import samba.getopt as options
+import samba.ntacls
+from samba import NTSTATUSError, ntstatus, string_to_byte_array, werror
+from samba.auth import system_session
+from samba.dcerpc import drsblobs, lsa, nbt, netlogon, security
+from samba.net import Net
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.samdb import SamDB
+from samba.trust_utils import CreateTrustedDomainRelax
+
+
+class LocalDCCredentialsOptions(options.CredentialsOptions):
+ def __init__(self, parser):
+ options.CredentialsOptions.__init__(self, parser, special_name="local-dc")
+
+
+class DomainTrustCommand(Command):
+ """List domain trusts."""
+
+ def __init__(self):
+ Command.__init__(self)
+ self.local_lp = None
+
+ self.local_server = None
+ self.local_binding_string = None
+ self.local_creds = None
+
+ self.remote_server = None
+ self.remote_binding_string = None
+ self.remote_creds = None
+
+ def _uint32(self, v):
+ return ctypes.c_uint32(v).value
+
+ def check_runtime_error(self, runtime, val):
+ if runtime is None:
+ return False
+
+ err32 = self._uint32(runtime.args[0])
+ if err32 == val:
+ return True
+
+ return False
+
+ class LocalRuntimeError(CommandError):
+ def __init__(exception_self, self, runtime, message):
+ err32 = self._uint32(runtime.args[0])
+ errstr = runtime.args[1]
+ msg = "LOCAL_DC[%s]: %s - ERROR(0x%08X) - %s" % (
+ self.local_server, message, err32, errstr)
+ CommandError.__init__(exception_self, msg)
+
+ class RemoteRuntimeError(CommandError):
+ def __init__(exception_self, self, runtime, message):
+ err32 = self._uint32(runtime.args[0])
+ errstr = runtime.args[1]
+ msg = "REMOTE_DC[%s]: %s - ERROR(0x%08X) - %s" % (
+ self.remote_server, message, err32, errstr)
+ CommandError.__init__(exception_self, msg)
+
+ class LocalLdbError(CommandError):
+ def __init__(exception_self, self, ldb_error, message):
+ errval = ldb_error.args[0]
+ errstr = ldb_error.args[1]
+ msg = "LOCAL_DC[%s]: %s - ERROR(%d) - %s" % (
+ self.local_server, message, errval, errstr)
+ CommandError.__init__(exception_self, msg)
+
+ def setup_local_server(self, sambaopts, localdcopts):
+ if self.local_server is not None:
+ return self.local_server
+
+ lp = sambaopts.get_loadparm()
+
+ local_server = localdcopts.ipaddress
+ if local_server is None:
+ server_role = lp.server_role()
+ if server_role != "ROLE_ACTIVE_DIRECTORY_DC":
+ raise CommandError("Invalid server_role %s" % (server_role))
+ local_server = lp.get('netbios name')
+ local_transport = "ncalrpc"
+ local_binding_options = ""
+ local_binding_options += ",auth_type=ncalrpc_as_system"
+ local_ldap_url = None
+ local_creds = None
+ else:
+ local_transport = "ncacn_np"
+ local_binding_options = ""
+ local_ldap_url = "ldap://%s" % local_server
+ local_creds = localdcopts.get_credentials(lp)
+
+ self.local_lp = lp
+
+ self.local_server = local_server
+ self.local_binding_string = "%s:%s[%s]" % (local_transport, local_server, local_binding_options)
+ self.local_ldap_url = local_ldap_url
+ self.local_creds = local_creds
+ return self.local_server
+
+ def new_local_lsa_connection(self):
+ return lsa.lsarpc(self.local_binding_string, self.local_lp, self.local_creds)
+
+ def new_local_netlogon_connection(self):
+ return netlogon.netlogon(self.local_binding_string, self.local_lp, self.local_creds)
+
+ def new_local_ldap_connection(self):
+ return SamDB(url=self.local_ldap_url,
+ session_info=system_session(),
+ credentials=self.local_creds,
+ lp=self.local_lp)
+
+ def setup_remote_server(self, credopts, domain,
+ require_pdc=True,
+ require_writable=True):
+
+ if require_pdc:
+ assert require_writable
+
+ if self.remote_server is not None:
+ return self.remote_server
+
+ self.remote_server = "__unknown__remote_server__.%s" % domain
+ assert self.local_server is not None
+
+ remote_creds = credopts.get_credentials(self.local_lp)
+ remote_server = credopts.ipaddress
+ remote_binding_options = ""
+
+ # TODO: we should also support NT4 domains
+ # we could use local_netlogon.netr_DsRGetDCNameEx2() with the remote domain name
+ # and delegate NBT or CLDAP to the local netlogon server
+ try:
+ remote_net = Net(remote_creds, self.local_lp, server=remote_server)
+ remote_flags = nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS
+ if require_writable:
+ remote_flags |= nbt.NBT_SERVER_WRITABLE
+ if require_pdc:
+ remote_flags |= nbt.NBT_SERVER_PDC
+ remote_info = remote_net.finddc(flags=remote_flags, domain=domain, address=remote_server)
+ except NTSTATUSError as error:
+ raise CommandError("Failed to find a writeable DC for domain '%s': %s" %
+ (domain, error.args[1]))
+ except Exception:
+ raise CommandError("Failed to find a writeable DC for domain '%s'" % domain)
+ flag_map = {
+ nbt.NBT_SERVER_PDC: "PDC",
+ nbt.NBT_SERVER_GC: "GC",
+ nbt.NBT_SERVER_LDAP: "LDAP",
+ nbt.NBT_SERVER_DS: "DS",
+ nbt.NBT_SERVER_KDC: "KDC",
+ nbt.NBT_SERVER_TIMESERV: "TIMESERV",
+ nbt.NBT_SERVER_CLOSEST: "CLOSEST",
+ nbt.NBT_SERVER_WRITABLE: "WRITABLE",
+ nbt.NBT_SERVER_GOOD_TIMESERV: "GOOD_TIMESERV",
+ nbt.NBT_SERVER_NDNC: "NDNC",
+ nbt.NBT_SERVER_SELECT_SECRET_DOMAIN_6: "SELECT_SECRET_DOMAIN_6",
+ nbt.NBT_SERVER_FULL_SECRET_DOMAIN_6: "FULL_SECRET_DOMAIN_6",
+ nbt.NBT_SERVER_ADS_WEB_SERVICE: "ADS_WEB_SERVICE",
+ nbt.NBT_SERVER_DS_8: "DS_8",
+ nbt.NBT_SERVER_DS_9: "DS_9",
+ nbt.NBT_SERVER_DS_10: "DS_10",
+ nbt.NBT_SERVER_HAS_DNS_NAME: "HAS_DNS_NAME",
+ nbt.NBT_SERVER_IS_DEFAULT_NC: "IS_DEFAULT_NC",
+ nbt.NBT_SERVER_FOREST_ROOT: "FOREST_ROOT",
+ }
+ server_type_string = self.generic_bitmap_to_string(flag_map,
+ remote_info.server_type, names_only=True)
+ self.outf.write("RemoteDC Netbios[%s] DNS[%s] ServerType[%s]\n" % (
+ remote_info.pdc_name,
+ remote_info.pdc_dns_name,
+ server_type_string))
+
+ self.remote_server = remote_info.pdc_dns_name
+ self.remote_binding_string = "ncacn_np:%s[%s]" % (self.remote_server, remote_binding_options)
+ self.remote_creds = remote_creds
+ return self.remote_server
+
+ def new_remote_lsa_connection(self):
+ return lsa.lsarpc(self.remote_binding_string, self.local_lp, self.remote_creds)
+
+ def new_remote_netlogon_connection(self):
+ return netlogon.netlogon(self.remote_binding_string, self.local_lp, self.remote_creds)
+
+ def get_lsa_info(self, conn, policy_access):
+ objectAttr = lsa.ObjectAttribute()
+ objectAttr.sec_qos = lsa.QosInfo()
+
+ policy = conn.OpenPolicy2(b''.decode('utf-8'),
+ objectAttr, policy_access)
+
+ info = conn.QueryInfoPolicy2(policy, lsa.LSA_POLICY_INFO_DNS)
+
+ return (policy, info)
+
+ def get_netlogon_dc_unc(self, conn, server, domain):
+ try:
+ info = conn.netr_DsRGetDCNameEx2(server,
+ None, 0, None, None, None,
+ netlogon.DS_RETURN_DNS_NAME)
+ return info.dc_unc
+ except RuntimeError:
+ return conn.netr_GetDcName(server, domain)
+
+ def get_netlogon_dc_info(self, conn, server):
+ info = conn.netr_DsRGetDCNameEx2(server,
+ None, 0, None, None, None,
+ netlogon.DS_RETURN_DNS_NAME)
+ return info
+
+ def netr_DomainTrust_to_name(self, t):
+ if t.trust_type == lsa.LSA_TRUST_TYPE_DOWNLEVEL:
+ return t.netbios_name
+
+ return t.dns_name
+
+ def netr_DomainTrust_to_type(self, a, t):
+ primary = None
+ primary_parent = None
+ for _t in a:
+ if _t.trust_flags & netlogon.NETR_TRUST_FLAG_PRIMARY:
+ primary = _t
+ if not _t.trust_flags & netlogon.NETR_TRUST_FLAG_TREEROOT:
+ primary_parent = a[_t.parent_index]
+ break
+
+ if t.trust_flags & netlogon.NETR_TRUST_FLAG_IN_FOREST:
+ if t is primary_parent:
+ return "Parent"
+
+ if t.trust_flags & netlogon.NETR_TRUST_FLAG_TREEROOT:
+ return "TreeRoot"
+
+ parent = a[t.parent_index]
+ if parent is primary:
+ return "Child"
+
+ return "Shortcut"
+
+ if t.trust_attributes & lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE:
+ return "Forest"
+
+ return "External"
+
+ def netr_DomainTrust_to_transitive(self, t):
+ if t.trust_flags & netlogon.NETR_TRUST_FLAG_IN_FOREST:
+ return "Yes"
+
+ if t.trust_attributes & lsa.LSA_TRUST_ATTRIBUTE_NON_TRANSITIVE:
+ return "No"
+
+ if t.trust_attributes & lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE:
+ return "Yes"
+
+ return "No"
+
+ def netr_DomainTrust_to_direction(self, t):
+ if t.trust_flags & netlogon.NETR_TRUST_FLAG_INBOUND and \
+ t.trust_flags & netlogon.NETR_TRUST_FLAG_OUTBOUND:
+ return "BOTH"
+
+ if t.trust_flags & netlogon.NETR_TRUST_FLAG_INBOUND:
+ return "INCOMING"
+
+ if t.trust_flags & netlogon.NETR_TRUST_FLAG_OUTBOUND:
+ return "OUTGOING"
+
+ return "INVALID"
+
+ def generic_enum_to_string(self, e_dict, v, names_only=False):
+ try:
+ w = e_dict[v]
+ except KeyError:
+ v32 = self._uint32(v)
+ w = "__unknown__%08X__" % v32
+
+ r = "0x%x (%s)" % (v, w)
+ return r
+
+ def generic_bitmap_to_string(self, b_dict, v, names_only=False):
+
+ s = []
+
+ c = v
+ for b in sorted(b_dict.keys()):
+ if not (c & b):
+ continue
+ c &= ~b
+ s += [b_dict[b]]
+
+ if c != 0:
+ c32 = self._uint32(c)
+ s += ["__unknown_%08X__" % c32]
+
+ w = ",".join(s)
+ if names_only:
+ return w
+ r = "0x%x (%s)" % (v, w)
+ return r
+
+ def trustType_string(self, v):
+ types = {
+ lsa.LSA_TRUST_TYPE_DOWNLEVEL: "DOWNLEVEL",
+ lsa.LSA_TRUST_TYPE_UPLEVEL: "UPLEVEL",
+ lsa.LSA_TRUST_TYPE_MIT: "MIT",
+ lsa.LSA_TRUST_TYPE_DCE: "DCE",
+ }
+ return self.generic_enum_to_string(types, v)
+
+ def trustDirection_string(self, v):
+ directions = {
+ lsa.LSA_TRUST_DIRECTION_INBOUND |
+ lsa.LSA_TRUST_DIRECTION_OUTBOUND: "BOTH",
+ lsa.LSA_TRUST_DIRECTION_INBOUND: "INBOUND",
+ lsa.LSA_TRUST_DIRECTION_OUTBOUND: "OUTBOUND",
+ }
+ return self.generic_enum_to_string(directions, v)
+
+ def trustAttributes_string(self, v):
+ attributes = {
+ lsa.LSA_TRUST_ATTRIBUTE_NON_TRANSITIVE: "NON_TRANSITIVE",
+ lsa.LSA_TRUST_ATTRIBUTE_UPLEVEL_ONLY: "UPLEVEL_ONLY",
+ lsa.LSA_TRUST_ATTRIBUTE_QUARANTINED_DOMAIN: "QUARANTINED_DOMAIN",
+ lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE: "FOREST_TRANSITIVE",
+ lsa.LSA_TRUST_ATTRIBUTE_CROSS_ORGANIZATION: "CROSS_ORGANIZATION",
+ lsa.LSA_TRUST_ATTRIBUTE_WITHIN_FOREST: "WITHIN_FOREST",
+ lsa.LSA_TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL: "TREAT_AS_EXTERNAL",
+ lsa.LSA_TRUST_ATTRIBUTE_USES_RC4_ENCRYPTION: "USES_RC4_ENCRYPTION",
+ }
+ return self.generic_bitmap_to_string(attributes, v)
+
+ def kerb_EncTypes_string(self, v):
+ enctypes = {
+ security.KERB_ENCTYPE_DES_CBC_CRC: "DES_CBC_CRC",
+ security.KERB_ENCTYPE_DES_CBC_MD5: "DES_CBC_MD5",
+ security.KERB_ENCTYPE_RC4_HMAC_MD5: "RC4_HMAC_MD5",
+ security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96: "AES128_CTS_HMAC_SHA1_96",
+ security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96: "AES256_CTS_HMAC_SHA1_96",
+ security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96_SK: "AES256_CTS_HMAC_SHA1_96-SK",
+ security.KERB_ENCTYPE_FAST_SUPPORTED: "FAST_SUPPORTED",
+ security.KERB_ENCTYPE_COMPOUND_IDENTITY_SUPPORTED: "COMPOUND_IDENTITY_SUPPORTED",
+ security.KERB_ENCTYPE_CLAIMS_SUPPORTED: "CLAIMS_SUPPORTED",
+ security.KERB_ENCTYPE_RESOURCE_SID_COMPRESSION_DISABLED: "RESOURCE_SID_COMPRESSION_DISABLED",
+ }
+ return self.generic_bitmap_to_string(enctypes, v)
+
+ def entry_tln_status(self, e_flags, ):
+ if e_flags == 0:
+ return "Status[Enabled]"
+
+ flags = {
+ lsa.LSA_TLN_DISABLED_NEW: "Disabled-New",
+ lsa.LSA_TLN_DISABLED_ADMIN: "Disabled",
+ lsa.LSA_TLN_DISABLED_CONFLICT: "Disabled-Conflicting",
+ }
+ return "Status[%s]" % self.generic_bitmap_to_string(flags, e_flags, names_only=True)
+
+ def entry_dom_status(self, e_flags):
+ if e_flags == 0:
+ return "Status[Enabled]"
+
+ flags = {
+ lsa.LSA_SID_DISABLED_ADMIN: "Disabled-SID",
+ lsa.LSA_SID_DISABLED_CONFLICT: "Disabled-SID-Conflicting",
+ lsa.LSA_NB_DISABLED_ADMIN: "Disabled-NB",
+ lsa.LSA_NB_DISABLED_CONFLICT: "Disabled-NB-Conflicting",
+ }
+ return "Status[%s]" % self.generic_bitmap_to_string(flags, e_flags, names_only=True)
+
+ def write_forest_trust_info(self, fti, tln=None, collisions=None):
+ if tln is not None:
+ tln_string = " TDO[%s]" % tln
+ else:
+ tln_string = ""
+
+ self.outf.write("Namespaces[%d]%s:\n" % (
+ len(fti.entries), tln_string))
+
+ for i, e in enumerate(fti.entries):
+
+ flags = e.flags
+ collision_string = ""
+
+ if collisions is not None:
+ for c in collisions.entries:
+ if c.index != i:
+ continue
+ flags = c.flags
+ collision_string = " Collision[%s]" % (c.name.string)
+
+ d = e.forest_trust_data
+ if e.type == lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ self.outf.write("TLN: %-32s DNS[*.%s]%s\n" % (
+ self.entry_tln_status(flags),
+ d.string, collision_string))
+ elif e.type == lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ self.outf.write("TLN_EX: %-29s DNS[*.%s]\n" % (
+ "", d.string))
+ elif e.type == lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
+ self.outf.write("DOM: %-32s DNS[%s] Netbios[%s] SID[%s]%s\n" % (
+ self.entry_dom_status(flags),
+ d.dns_domain_name.string,
+ d.netbios_domain_name.string,
+ d.domain_sid, collision_string))
+ return
+
+
+class cmd_domain_trust_list(DomainTrustCommand):
+ """List domain trusts."""
+
+ synopsis = "%prog [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "localdcopts": LocalDCCredentialsOptions,
+ }
+
+ takes_options = [
+ ]
+
+ def run(self, sambaopts=None, versionopts=None, localdcopts=None):
+
+ local_server = self.setup_local_server(sambaopts, localdcopts)
+ try:
+ local_netlogon = self.new_local_netlogon_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect netlogon server")
+
+ try:
+ local_netlogon_trusts = \
+ local_netlogon.netr_DsrEnumerateDomainTrusts(local_server,
+ netlogon.NETR_TRUST_FLAG_IN_FOREST |
+ netlogon.NETR_TRUST_FLAG_OUTBOUND |
+ netlogon.NETR_TRUST_FLAG_INBOUND)
+ except RuntimeError as error:
+ if self.check_runtime_error(error, werror.WERR_RPC_S_PROCNUM_OUT_OF_RANGE):
+ # TODO: we could implement a fallback to lsa.EnumTrustDom()
+ raise CommandError("LOCAL_DC[%s]: netr_DsrEnumerateDomainTrusts not supported." % (
+ local_server))
+ raise self.LocalRuntimeError(self, error, "netr_DsrEnumerateDomainTrusts failed")
+
+ a = local_netlogon_trusts.array
+ for t in a:
+ if t.trust_flags & netlogon.NETR_TRUST_FLAG_PRIMARY:
+ continue
+ self.outf.write("%-14s %-15s %-19s %s\n" % (
+ "Type[%s]" % self.netr_DomainTrust_to_type(a, t),
+ "Transitive[%s]" % self.netr_DomainTrust_to_transitive(t),
+ "Direction[%s]" % self.netr_DomainTrust_to_direction(t),
+ "Name[%s]" % self.netr_DomainTrust_to_name(t)))
+ return
+
+
+class cmd_domain_trust_show(DomainTrustCommand):
+ """Show trusted domain details."""
+
+ synopsis = "%prog NAME [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "localdcopts": LocalDCCredentialsOptions,
+ }
+
+ takes_options = [
+ ]
+
+ takes_args = ["domain"]
+
+ def run(self, domain, sambaopts=None, versionopts=None, localdcopts=None):
+
+ self.setup_local_server(sambaopts, localdcopts)
+ try:
+ local_lsa = self.new_local_lsa_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect lsa server")
+
+ try:
+ local_policy_access = lsa.LSA_POLICY_VIEW_LOCAL_INFORMATION
+ (local_policy, local_lsa_info) = self.get_lsa_info(local_lsa, local_policy_access)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to query LSA_POLICY_INFO_DNS")
+
+ self.outf.write("LocalDomain Netbios[%s] DNS[%s] SID[%s]\n" % (
+ local_lsa_info.name.string,
+ local_lsa_info.dns_domain.string,
+ local_lsa_info.sid))
+
+ lsaString = lsa.String()
+ lsaString.string = domain
+ try:
+ local_tdo_full = \
+ local_lsa.QueryTrustedDomainInfoByName(local_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO)
+ local_tdo_info = local_tdo_full.info_ex
+ local_tdo_posix = local_tdo_full.posix_offset
+ except NTSTATUSError as error:
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise CommandError("trusted domain object does not exist for domain [%s]" % domain)
+
+ raise self.LocalRuntimeError(self, error, "QueryTrustedDomainInfoByName(FULL_INFO) failed")
+
+ try:
+ local_tdo_enctypes = \
+ local_lsa.QueryTrustedDomainInfoByName(local_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_SUPPORTED_ENCRYPTION_TYPES)
+ except NTSTATUSError as error:
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_INVALID_PARAMETER):
+ error = None
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_INVALID_INFO_CLASS):
+ error = None
+
+ if error is not None:
+ raise self.LocalRuntimeError(self, error,
+ "QueryTrustedDomainInfoByName(SUPPORTED_ENCRYPTION_TYPES) failed")
+
+ local_tdo_enctypes = lsa.TrustDomainInfoSupportedEncTypes()
+ local_tdo_enctypes.enc_types = 0
+
+ try:
+ local_tdo_forest = None
+ if local_tdo_info.trust_attributes & lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE:
+ local_tdo_forest = \
+ local_lsa.lsaRQueryForestTrustInformation(local_policy,
+ lsaString,
+ lsa.LSA_FOREST_TRUST_DOMAIN_INFO)
+ except RuntimeError as error:
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_RPC_PROCNUM_OUT_OF_RANGE):
+ error = None
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_NOT_FOUND):
+ error = None
+ if error is not None:
+ raise self.LocalRuntimeError(self, error, "lsaRQueryForestTrustInformation failed")
+
+ local_tdo_forest = lsa.ForestTrustInformation()
+ local_tdo_forest.count = 0
+ local_tdo_forest.entries = []
+
+ self.outf.write("TrustedDomain:\n\n")
+ self.outf.write("NetbiosName: %s\n" % local_tdo_info.netbios_name.string)
+ if local_tdo_info.netbios_name.string != local_tdo_info.domain_name.string:
+ self.outf.write("DnsName: %s\n" % local_tdo_info.domain_name.string)
+ self.outf.write("SID: %s\n" % local_tdo_info.sid)
+ self.outf.write("Type: %s\n" % self.trustType_string(local_tdo_info.trust_type))
+ self.outf.write("Direction: %s\n" % self.trustDirection_string(local_tdo_info.trust_direction))
+ self.outf.write("Attributes: %s\n" % self.trustAttributes_string(local_tdo_info.trust_attributes))
+ posix_offset_u32 = ctypes.c_uint32(local_tdo_posix.posix_offset).value
+ posix_offset_i32 = ctypes.c_int32(local_tdo_posix.posix_offset).value
+ self.outf.write("PosixOffset: 0x%08X (%d)\n" % (posix_offset_u32, posix_offset_i32))
+ self.outf.write("kerb_EncTypes: %s\n" % self.kerb_EncTypes_string(local_tdo_enctypes.enc_types))
+
+ if local_tdo_info.trust_attributes & lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE:
+ self.write_forest_trust_info(local_tdo_forest,
+ tln=local_tdo_info.domain_name.string)
+
+ return
+
+class cmd_domain_trust_modify(DomainTrustCommand):
+ """Show trusted domain details."""
+
+ synopsis = "%prog NAME [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "localdcopts": LocalDCCredentialsOptions,
+ }
+
+ takes_options = [
+ Option("--use-aes-keys", action="store_true",
+ help="The trust uses AES kerberos keys.",
+ dest='use_aes_keys',
+ default=None),
+ Option("--no-aes-keys", action="store_true",
+ help="The trust does not have any support for AES kerberos keys.",
+ dest='disable_aes_keys',
+ default=None),
+ Option("--raw-kerb-enctypes", action="store",
+ help="The raw kerberos enctype bits",
+ dest='kerb_enctypes',
+ default=None),
+ ]
+
+ takes_args = ["domain"]
+
+ def run(self, domain, sambaopts=None, versionopts=None, localdcopts=None,
+ disable_aes_keys=None, use_aes_keys=None, kerb_enctypes=None):
+
+ num_modifications = 0
+
+ enctype_args = 0
+ if kerb_enctypes is not None:
+ enctype_args += 1
+ if use_aes_keys is not None:
+ enctype_args += 1
+ if disable_aes_keys is not None:
+ enctype_args += 1
+ if enctype_args > 1:
+ raise CommandError("--no-aes-keys, --use-aes-keys and --raw-kerb-enctypes are mutually exclusive")
+ if enctype_args == 1:
+ num_modifications += 1
+
+ if num_modifications == 0:
+ raise CommandError("modification arguments are required, try --help")
+
+ self.setup_local_server(sambaopts, localdcopts)
+ try:
+ local_lsa = self.new_local_lsa_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect to lsa server")
+
+ try:
+ local_policy_access = lsa.LSA_POLICY_VIEW_LOCAL_INFORMATION
+ local_policy_access |= lsa.LSA_POLICY_TRUST_ADMIN
+ (local_policy, local_lsa_info) = self.get_lsa_info(local_lsa, local_policy_access)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to query LSA_POLICY_INFO_DNS")
+
+ self.outf.write("LocalDomain Netbios[%s] DNS[%s] SID[%s]\n" % (
+ local_lsa_info.name.string,
+ local_lsa_info.dns_domain.string,
+ local_lsa_info.sid))
+
+ if enctype_args == 1:
+ lsaString = lsa.String()
+ lsaString.string = domain
+
+ try:
+ local_tdo_enctypes = \
+ local_lsa.QueryTrustedDomainInfoByName(local_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_SUPPORTED_ENCRYPTION_TYPES)
+ except NTSTATUSError as error:
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_INVALID_PARAMETER):
+ error = None
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_INVALID_INFO_CLASS):
+ error = None
+
+ if error is not None:
+ raise self.LocalRuntimeError(self, error,
+ "QueryTrustedDomainInfoByName(SUPPORTED_ENCRYPTION_TYPES) failed")
+
+ local_tdo_enctypes = lsa.TrustDomainInfoSupportedEncTypes()
+ local_tdo_enctypes.enc_types = 0
+
+ self.outf.write("Old kerb_EncTypes: %s\n" % self.kerb_EncTypes_string(local_tdo_enctypes.enc_types))
+
+ enc_types = lsa.TrustDomainInfoSupportedEncTypes()
+ if kerb_enctypes is not None:
+ enc_types.enc_types = int(kerb_enctypes, base=0)
+ elif use_aes_keys is not None:
+ enc_types.enc_types = security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96
+ enc_types.enc_types |= security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96
+ elif disable_aes_keys is not None:
+ # CVE-2022-37966: Trust objects are no longer assumed to support
+ # RC4, so we must indicate support explicitly.
+ enc_types.enc_types = security.KERB_ENCTYPE_RC4_HMAC_MD5
+ else:
+ raise CommandError("Internal error should be checked above")
+
+ if enc_types.enc_types != local_tdo_enctypes.enc_types:
+ try:
+ local_tdo_enctypes = \
+ local_lsa.SetTrustedDomainInfoByName(local_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_SUPPORTED_ENCRYPTION_TYPES,
+ enc_types)
+ self.outf.write("New kerb_EncTypes: %s\n" % self.kerb_EncTypes_string(enc_types.enc_types))
+ except NTSTATUSError as error:
+ if error is not None:
+ raise self.LocalRuntimeError(self, error,
+ "SetTrustedDomainInfoByName(SUPPORTED_ENCRYPTION_TYPES) failed")
+ else:
+ self.outf.write("No kerb_EncTypes update needed\n")
+
+ return
+
+class cmd_domain_trust_create(DomainTrustCommand):
+ """Create a domain or forest trust."""
+
+ synopsis = "%prog DOMAIN [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ "localdcopts": LocalDCCredentialsOptions,
+ }
+
+ takes_options = [
+ Option("--type", type="choice", metavar="TYPE",
+ choices=["external", "forest"],
+ help="The type of the trust: 'external' or 'forest'.",
+ dest='trust_type',
+ default="external"),
+ Option("--direction", type="choice", metavar="DIRECTION",
+ choices=["incoming", "outgoing", "both"],
+ help="The trust direction: 'incoming', 'outgoing' or 'both'.",
+ dest='trust_direction',
+ default="both"),
+ Option("--create-location", type="choice", metavar="LOCATION",
+ choices=["local", "both"],
+ help="Where to create the trusted domain object: 'local' or 'both'.",
+ dest='create_location',
+ default="both"),
+ Option("--cross-organisation", action="store_true",
+ help="The related domains does not belong to the same organisation.",
+ dest='cross_organisation',
+ default=False),
+ Option("--quarantined", type="choice", metavar="yes|no",
+ choices=["yes", "no", None],
+ help="Special SID filtering rules are applied to the trust. "
+ "With --type=external the default is yes. "
+ "With --type=forest the default is no.",
+ dest='quarantined_arg',
+ default=None),
+ Option("--not-transitive", action="store_true",
+ help="The forest trust is not transitive.",
+ dest='not_transitive',
+ default=False),
+ Option("--treat-as-external", action="store_true",
+ help="The treat the forest trust as external.",
+ dest='treat_as_external',
+ default=False),
+ Option("--no-aes-keys", action="store_false",
+ help="The trust does not use AES kerberos keys.",
+ dest='use_aes_keys',
+ default=True),
+ Option("--skip-validation", action="store_false",
+ help="Skip validation of the trust.",
+ dest='validate',
+ default=True),
+ ]
+
+ takes_args = ["domain"]
+
+ def run(self, domain, sambaopts=None, localdcopts=None, credopts=None, versionopts=None,
+ trust_type=None, trust_direction=None, create_location=None,
+ cross_organisation=False, quarantined_arg=None,
+ not_transitive=False, treat_as_external=False,
+ use_aes_keys=False, validate=True):
+
+ lsaString = lsa.String()
+
+ quarantined = False
+ if quarantined_arg is None:
+ if trust_type == 'external':
+ quarantined = True
+ elif quarantined_arg == 'yes':
+ quarantined = True
+
+ if trust_type != 'forest':
+ if not_transitive:
+ raise CommandError("--not-transitive requires --type=forest")
+ if treat_as_external:
+ raise CommandError("--treat-as-external requires --type=forest")
+
+ enc_types = lsa.TrustDomainInfoSupportedEncTypes()
+ if use_aes_keys:
+ enc_types.enc_types = security.KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96
+ enc_types.enc_types |= security.KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96
+ else:
+ # CVE-2022-37966: Trust objects are no longer assumed to support
+ # RC4, so we must indicate support explicitly.
+ enc_types.enc_types = security.KERB_ENCTYPE_RC4_HMAC_MD5
+
+ local_policy_access = lsa.LSA_POLICY_VIEW_LOCAL_INFORMATION
+ local_policy_access |= lsa.LSA_POLICY_TRUST_ADMIN
+ local_policy_access |= lsa.LSA_POLICY_CREATE_SECRET
+
+ local_trust_info = lsa.TrustDomainInfoInfoEx()
+ local_trust_info.trust_type = lsa.LSA_TRUST_TYPE_UPLEVEL
+ local_trust_info.trust_direction = 0
+ if trust_direction == "both":
+ local_trust_info.trust_direction |= lsa.LSA_TRUST_DIRECTION_INBOUND
+ local_trust_info.trust_direction |= lsa.LSA_TRUST_DIRECTION_OUTBOUND
+ elif trust_direction == "incoming":
+ local_trust_info.trust_direction |= lsa.LSA_TRUST_DIRECTION_INBOUND
+ elif trust_direction == "outgoing":
+ local_trust_info.trust_direction |= lsa.LSA_TRUST_DIRECTION_OUTBOUND
+ local_trust_info.trust_attributes = 0
+ if cross_organisation:
+ local_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_CROSS_ORGANIZATION
+ if quarantined:
+ local_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_QUARANTINED_DOMAIN
+ if trust_type == "forest":
+ local_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE
+ if not_transitive:
+ local_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_NON_TRANSITIVE
+ if treat_as_external:
+ local_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL
+
+ def get_password(name):
+ password = None
+ while True:
+ if password is not None and password != '':
+ return password
+ password = getpass("New %s Password: " % name)
+ passwordverify = getpass("Retype %s Password: " % name)
+ if not password == passwordverify:
+ password = None
+ self.outf.write("Sorry, passwords do not match.\n")
+
+ incoming_secret = None
+ outgoing_secret = None
+ remote_policy_access = lsa.LSA_POLICY_VIEW_LOCAL_INFORMATION
+ if create_location == "local":
+ if local_trust_info.trust_direction & lsa.LSA_TRUST_DIRECTION_INBOUND:
+ incoming_password = get_password("Incoming Trust")
+ incoming_secret = string_to_byte_array(incoming_password.encode('utf-16-le'))
+ if local_trust_info.trust_direction & lsa.LSA_TRUST_DIRECTION_OUTBOUND:
+ outgoing_password = get_password("Outgoing Trust")
+ outgoing_secret = string_to_byte_array(outgoing_password.encode('utf-16-le'))
+
+ remote_trust_info = None
+ else:
+ # We use 240 random bytes.
+ # Windows uses 28 or 240 random bytes. I guess it's
+ # based on the trust type external vs. forest.
+ #
+ # The initial trust password can be up to 512 bytes
+ # while the versioned passwords used for periodic updates
+ # can only be up to 498 bytes, as netr_ServerPasswordSet2()
+ # needs to pass the NL_PASSWORD_VERSION structure within the
+ # 512 bytes and a 2 bytes confounder is required.
+ #
+ def random_trust_secret(length):
+ pw = samba.generate_random_machine_password(length // 2, length // 2)
+ return string_to_byte_array(pw.encode('utf-16-le'))
+
+ if local_trust_info.trust_direction & lsa.LSA_TRUST_DIRECTION_INBOUND:
+ incoming_secret = random_trust_secret(240)
+ if local_trust_info.trust_direction & lsa.LSA_TRUST_DIRECTION_OUTBOUND:
+ outgoing_secret = random_trust_secret(240)
+
+ remote_policy_access |= lsa.LSA_POLICY_TRUST_ADMIN
+ remote_policy_access |= lsa.LSA_POLICY_CREATE_SECRET
+
+ remote_trust_info = lsa.TrustDomainInfoInfoEx()
+ remote_trust_info.trust_type = lsa.LSA_TRUST_TYPE_UPLEVEL
+ remote_trust_info.trust_direction = 0
+ if trust_direction == "both":
+ remote_trust_info.trust_direction |= lsa.LSA_TRUST_DIRECTION_INBOUND
+ remote_trust_info.trust_direction |= lsa.LSA_TRUST_DIRECTION_OUTBOUND
+ elif trust_direction == "incoming":
+ remote_trust_info.trust_direction |= lsa.LSA_TRUST_DIRECTION_OUTBOUND
+ elif trust_direction == "outgoing":
+ remote_trust_info.trust_direction |= lsa.LSA_TRUST_DIRECTION_INBOUND
+ remote_trust_info.trust_attributes = 0
+ if cross_organisation:
+ remote_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_CROSS_ORGANIZATION
+ if quarantined:
+ remote_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_QUARANTINED_DOMAIN
+ if trust_type == "forest":
+ remote_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE
+ if not_transitive:
+ remote_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_NON_TRANSITIVE
+ if treat_as_external:
+ remote_trust_info.trust_attributes |= lsa.LSA_TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL
+
+ local_server = self.setup_local_server(sambaopts, localdcopts)
+ try:
+ local_lsa = self.new_local_lsa_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect lsa server")
+
+ try:
+ (local_policy, local_lsa_info) = self.get_lsa_info(local_lsa, local_policy_access)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to query LSA_POLICY_INFO_DNS")
+
+ self.outf.write("LocalDomain Netbios[%s] DNS[%s] SID[%s]\n" % (
+ local_lsa_info.name.string,
+ local_lsa_info.dns_domain.string,
+ local_lsa_info.sid))
+
+ try:
+ remote_server = self.setup_remote_server(credopts, domain)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to locate remote server")
+
+ try:
+ remote_lsa = self.new_remote_lsa_connection()
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to connect lsa server")
+
+ try:
+ (remote_policy, remote_lsa_info) = self.get_lsa_info(remote_lsa, remote_policy_access)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to query LSA_POLICY_INFO_DNS")
+
+ self.outf.write("RemoteDomain Netbios[%s] DNS[%s] SID[%s]\n" % (
+ remote_lsa_info.name.string,
+ remote_lsa_info.dns_domain.string,
+ remote_lsa_info.sid))
+
+ local_trust_info.domain_name.string = remote_lsa_info.dns_domain.string
+ local_trust_info.netbios_name.string = remote_lsa_info.name.string
+ local_trust_info.sid = remote_lsa_info.sid
+
+ if remote_trust_info:
+ remote_trust_info.domain_name.string = local_lsa_info.dns_domain.string
+ remote_trust_info.netbios_name.string = local_lsa_info.name.string
+ remote_trust_info.sid = local_lsa_info.sid
+
+ try:
+ lsaString.string = local_trust_info.domain_name.string
+ local_lsa.QueryTrustedDomainInfoByName(local_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO)
+ raise CommandError("TrustedDomain %s already exist'" % lsaString.string)
+ except NTSTATUSError as error:
+ if not self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise self.LocalRuntimeError(self, error,
+ "QueryTrustedDomainInfoByName(%s, FULL_INFO) failed" % (
+ lsaString.string))
+
+ try:
+ lsaString.string = local_trust_info.netbios_name.string
+ local_lsa.QueryTrustedDomainInfoByName(local_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO)
+ raise CommandError("TrustedDomain %s already exist'" % lsaString.string)
+ except NTSTATUSError as error:
+ if not self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise self.LocalRuntimeError(self, error,
+ "QueryTrustedDomainInfoByName(%s, FULL_INFO) failed" % (
+ lsaString.string))
+
+ if remote_trust_info:
+ try:
+ lsaString.string = remote_trust_info.domain_name.string
+ remote_lsa.QueryTrustedDomainInfoByName(remote_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO)
+ raise CommandError("TrustedDomain %s already exist'" % lsaString.string)
+ except NTSTATUSError as error:
+ if not self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise self.RemoteRuntimeError(self, error,
+ "QueryTrustedDomainInfoByName(%s, FULL_INFO) failed" % (
+ lsaString.string))
+
+ try:
+ lsaString.string = remote_trust_info.netbios_name.string
+ remote_lsa.QueryTrustedDomainInfoByName(remote_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_INFO_FULL_INFO)
+ raise CommandError("TrustedDomain %s already exist'" % lsaString.string)
+ except NTSTATUSError as error:
+ if not self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise self.RemoteRuntimeError(self, error,
+ "QueryTrustedDomainInfoByName(%s, FULL_INFO) failed" % (
+ lsaString.string))
+
+ try:
+ local_netlogon = self.new_local_netlogon_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect netlogon server")
+
+ try:
+ local_netlogon_info = self.get_netlogon_dc_info(local_netlogon, local_server)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to get netlogon dc info")
+
+ if remote_trust_info:
+ try:
+ remote_netlogon = self.new_remote_netlogon_connection()
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to connect netlogon server")
+
+ try:
+ remote_netlogon_dc_unc = self.get_netlogon_dc_unc(remote_netlogon,
+ remote_server, domain)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to get netlogon dc info")
+
+ def generate_AuthInOutBlob(secret, update_time):
+ if secret is None:
+ blob = drsblobs.trustAuthInOutBlob()
+ blob.count = 0
+
+ return blob
+
+ clear = drsblobs.AuthInfoClear()
+ clear.size = len(secret)
+ clear.password = secret
+
+ info = drsblobs.AuthenticationInformation()
+ info.LastUpdateTime = samba.unix2nttime(update_time)
+ info.AuthType = lsa.TRUST_AUTH_TYPE_CLEAR
+ info.AuthInfo = clear
+
+ array = drsblobs.AuthenticationInformationArray()
+ array.count = 1
+ array.array = [info]
+
+ blob = drsblobs.trustAuthInOutBlob()
+ blob.count = 1
+ blob.current = array
+
+ return blob
+
+ update_time = samba.current_unix_time()
+ incoming_blob = generate_AuthInOutBlob(incoming_secret, update_time)
+ outgoing_blob = generate_AuthInOutBlob(outgoing_secret, update_time)
+
+ local_tdo_handle = None
+ remote_tdo_handle = None
+
+ try:
+ if remote_trust_info:
+ self.outf.write("Creating remote TDO.\n")
+ current_request = {"location": "remote", "name": "CreateTrustedDomainEx2"}
+ remote_tdo_handle = CreateTrustedDomainRelax(remote_lsa,
+ remote_policy,
+ remote_trust_info,
+ lsa.LSA_TRUSTED_DOMAIN_ALL_ACCESS,
+ outgoing_blob,
+ incoming_blob)
+ self.outf.write("Remote TDO created.\n")
+ if enc_types:
+ self.outf.write("Setting supported encryption types on remote TDO.\n")
+ current_request = {"location": "remote", "name": "SetInformationTrustedDomain"}
+ remote_lsa.SetInformationTrustedDomain(remote_tdo_handle,
+ lsa.LSA_TRUSTED_DOMAIN_SUPPORTED_ENCRYPTION_TYPES,
+ enc_types)
+
+ self.outf.write("Creating local TDO.\n")
+ current_request = {"location": "local", "name": "CreateTrustedDomainEx2"}
+ local_tdo_handle = CreateTrustedDomainRelax(local_lsa,
+ local_policy,
+ local_trust_info,
+ lsa.LSA_TRUSTED_DOMAIN_ALL_ACCESS,
+ incoming_blob,
+ outgoing_blob)
+ self.outf.write("Local TDO created\n")
+ if enc_types:
+ self.outf.write("Setting supported encryption types on local TDO.\n")
+ current_request = {"location": "local", "name": "SetInformationTrustedDomain"}
+ local_lsa.SetInformationTrustedDomain(local_tdo_handle,
+ lsa.LSA_TRUSTED_DOMAIN_SUPPORTED_ENCRYPTION_TYPES,
+ enc_types)
+ except RuntimeError as error:
+ self.outf.write("Error: %s failed %sly - cleaning up\n" % (
+ current_request['name'], current_request['location']))
+ if remote_tdo_handle:
+ self.outf.write("Deleting remote TDO.\n")
+ remote_lsa.DeleteObject(remote_tdo_handle)
+ remote_tdo_handle = None
+ if local_tdo_handle:
+ self.outf.write("Deleting local TDO.\n")
+ local_lsa.DeleteObject(local_tdo_handle)
+ local_tdo_handle = None
+ if current_request['location'] == "remote":
+ raise self.RemoteRuntimeError(self, error, "%s" % (
+ current_request['name']))
+ raise self.LocalRuntimeError(self, error, "%s" % (
+ current_request['name']))
+
+ if validate:
+ if local_trust_info.trust_attributes & lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE:
+ self.outf.write("Setup local forest trust information...\n")
+ try:
+ # get all information about the remote trust
+ # this triggers netr_GetForestTrustInformation to the remote domain
+ # and lsaRSetForestTrustInformation() locally, but new top level
+ # names are disabled by default.
+ local_forest_info = \
+ local_netlogon.netr_DsRGetForestTrustInformation(local_netlogon_info.dc_unc,
+ remote_lsa_info.dns_domain.string,
+ netlogon.DS_GFTI_UPDATE_TDO)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "netr_DsRGetForestTrustInformation() failed")
+
+ try:
+ # here we try to enable all top level names
+ local_forest_collision = \
+ local_lsa.lsaRSetForestTrustInformation(local_policy,
+ remote_lsa_info.dns_domain,
+ lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
+ local_forest_info,
+ 0)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "lsaRSetForestTrustInformation() failed")
+
+ self.write_forest_trust_info(local_forest_info,
+ tln=remote_lsa_info.dns_domain.string,
+ collisions=local_forest_collision)
+
+ if remote_trust_info:
+ self.outf.write("Setup remote forest trust information...\n")
+ try:
+ # get all information about the local trust (from the perspective of the remote domain)
+ # this triggers netr_GetForestTrustInformation to our domain.
+ # and lsaRSetForestTrustInformation() remotely, but new top level
+ # names are disabled by default.
+ remote_forest_info = \
+ remote_netlogon.netr_DsRGetForestTrustInformation(remote_netlogon_dc_unc,
+ local_lsa_info.dns_domain.string,
+ netlogon.DS_GFTI_UPDATE_TDO)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "netr_DsRGetForestTrustInformation() failed")
+
+ try:
+ # here we try to enable all top level names
+ remote_forest_collision = \
+ remote_lsa.lsaRSetForestTrustInformation(remote_policy,
+ local_lsa_info.dns_domain,
+ lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
+ remote_forest_info,
+ 0)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "lsaRSetForestTrustInformation() failed")
+
+ self.write_forest_trust_info(remote_forest_info,
+ tln=local_lsa_info.dns_domain.string,
+ collisions=remote_forest_collision)
+
+ if local_trust_info.trust_direction & lsa.LSA_TRUST_DIRECTION_OUTBOUND:
+ self.outf.write("Validating outgoing trust...\n")
+ try:
+ local_trust_verify = local_netlogon.netr_LogonControl2Ex(local_netlogon_info.dc_unc,
+ netlogon.NETLOGON_CONTROL_TC_VERIFY,
+ 2,
+ remote_lsa_info.dns_domain.string)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "NETLOGON_CONTROL_TC_VERIFY failed")
+
+ local_trust_status = self._uint32(local_trust_verify.pdc_connection_status[0])
+ local_conn_status = self._uint32(local_trust_verify.tc_connection_status[0])
+
+ if local_trust_verify.flags & netlogon.NETLOGON_VERIFY_STATUS_RETURNED:
+ local_validation = "LocalValidation: DC[%s] CONNECTION[%s] TRUST[%s] VERIFY_STATUS_RETURNED" % (
+ local_trust_verify.trusted_dc_name,
+ local_trust_verify.tc_connection_status[1],
+ local_trust_verify.pdc_connection_status[1])
+ else:
+ local_validation = "LocalValidation: DC[%s] CONNECTION[%s] TRUST[%s]" % (
+ local_trust_verify.trusted_dc_name,
+ local_trust_verify.tc_connection_status[1],
+ local_trust_verify.pdc_connection_status[1])
+
+ if local_trust_status != werror.WERR_SUCCESS or local_conn_status != werror.WERR_SUCCESS:
+ raise CommandError(local_validation)
+ else:
+ self.outf.write("OK: %s\n" % local_validation)
+
+ if remote_trust_info:
+ if remote_trust_info.trust_direction & lsa.LSA_TRUST_DIRECTION_OUTBOUND:
+ self.outf.write("Validating incoming trust...\n")
+ try:
+ remote_trust_verify = \
+ remote_netlogon.netr_LogonControl2Ex(remote_netlogon_dc_unc,
+ netlogon.NETLOGON_CONTROL_TC_VERIFY,
+ 2,
+ local_lsa_info.dns_domain.string)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "NETLOGON_CONTROL_TC_VERIFY failed")
+
+ remote_trust_status = self._uint32(remote_trust_verify.pdc_connection_status[0])
+ remote_conn_status = self._uint32(remote_trust_verify.tc_connection_status[0])
+
+ if remote_trust_verify.flags & netlogon.NETLOGON_VERIFY_STATUS_RETURNED:
+ remote_validation = "RemoteValidation: DC[%s] CONNECTION[%s] TRUST[%s] VERIFY_STATUS_RETURNED" % (
+ remote_trust_verify.trusted_dc_name,
+ remote_trust_verify.tc_connection_status[1],
+ remote_trust_verify.pdc_connection_status[1])
+ else:
+ remote_validation = "RemoteValidation: DC[%s] CONNECTION[%s] TRUST[%s]" % (
+ remote_trust_verify.trusted_dc_name,
+ remote_trust_verify.tc_connection_status[1],
+ remote_trust_verify.pdc_connection_status[1])
+
+ if remote_trust_status != werror.WERR_SUCCESS or remote_conn_status != werror.WERR_SUCCESS:
+ raise CommandError(remote_validation)
+ else:
+ self.outf.write("OK: %s\n" % remote_validation)
+
+ if remote_tdo_handle is not None:
+ try:
+ remote_lsa.Close(remote_tdo_handle)
+ except RuntimeError:
+ pass
+ remote_tdo_handle = None
+ if local_tdo_handle is not None:
+ try:
+ local_lsa.Close(local_tdo_handle)
+ except RuntimeError:
+ pass
+ local_tdo_handle = None
+
+ self.outf.write("Success.\n")
+ return
+
+
+class cmd_domain_trust_delete(DomainTrustCommand):
+ """Delete a domain trust."""
+
+ synopsis = "%prog DOMAIN [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ "localdcopts": LocalDCCredentialsOptions,
+ }
+
+ takes_options = [
+ Option("--delete-location", type="choice", metavar="LOCATION",
+ choices=["local", "both"],
+ help="Where to delete the trusted domain object: 'local' or 'both'.",
+ dest='delete_location',
+ default="both"),
+ ]
+
+ takes_args = ["domain"]
+
+ def run(self, domain, sambaopts=None, localdcopts=None, credopts=None, versionopts=None,
+ delete_location=None):
+
+ local_policy_access = lsa.LSA_POLICY_VIEW_LOCAL_INFORMATION
+ local_policy_access |= lsa.LSA_POLICY_TRUST_ADMIN
+ local_policy_access |= lsa.LSA_POLICY_CREATE_SECRET
+
+ if delete_location == "local":
+ remote_policy_access = None
+ else:
+ remote_policy_access = lsa.LSA_POLICY_VIEW_LOCAL_INFORMATION
+ remote_policy_access |= lsa.LSA_POLICY_TRUST_ADMIN
+ remote_policy_access |= lsa.LSA_POLICY_CREATE_SECRET
+
+ self.setup_local_server(sambaopts, localdcopts)
+ try:
+ local_lsa = self.new_local_lsa_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect lsa server")
+
+ try:
+ (local_policy, local_lsa_info) = self.get_lsa_info(local_lsa, local_policy_access)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to query LSA_POLICY_INFO_DNS")
+
+ self.outf.write("LocalDomain Netbios[%s] DNS[%s] SID[%s]\n" % (
+ local_lsa_info.name.string,
+ local_lsa_info.dns_domain.string,
+ local_lsa_info.sid))
+
+ local_tdo_info = None
+ local_tdo_handle = None
+ remote_tdo_info = None
+ remote_tdo_handle = None
+
+ lsaString = lsa.String()
+ try:
+ lsaString.string = domain
+ local_tdo_info = local_lsa.QueryTrustedDomainInfoByName(local_policy,
+ lsaString, lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX)
+ except NTSTATUSError as error:
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise CommandError("Failed to find trust for domain '%s'" % domain)
+ raise self.RemoteRuntimeError(self, error, "failed to locate remote server")
+
+ if remote_policy_access is not None:
+ try:
+ self.setup_remote_server(credopts, domain)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to locate remote server")
+
+ try:
+ remote_lsa = self.new_remote_lsa_connection()
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to connect lsa server")
+
+ try:
+ (remote_policy, remote_lsa_info) = self.get_lsa_info(remote_lsa, remote_policy_access)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to query LSA_POLICY_INFO_DNS")
+
+ self.outf.write("RemoteDomain Netbios[%s] DNS[%s] SID[%s]\n" % (
+ remote_lsa_info.name.string,
+ remote_lsa_info.dns_domain.string,
+ remote_lsa_info.sid))
+
+ if remote_lsa_info.sid != local_tdo_info.sid or \
+ remote_lsa_info.name.string != local_tdo_info.netbios_name.string or \
+ remote_lsa_info.dns_domain.string != local_tdo_info.domain_name.string:
+ raise CommandError("LocalTDO inconsistent: Netbios[%s] DNS[%s] SID[%s]" % (
+ local_tdo_info.netbios_name.string,
+ local_tdo_info.domain_name.string,
+ local_tdo_info.sid))
+
+ try:
+ lsaString.string = local_lsa_info.dns_domain.string
+ remote_tdo_info = \
+ remote_lsa.QueryTrustedDomainInfoByName(remote_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX)
+ except NTSTATUSError as error:
+ if not self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise self.RemoteRuntimeError(self, error, "QueryTrustedDomainInfoByName(%s)" % (
+ lsaString.string))
+
+ if remote_tdo_info is not None:
+ if local_lsa_info.sid != remote_tdo_info.sid or \
+ local_lsa_info.name.string != remote_tdo_info.netbios_name.string or \
+ local_lsa_info.dns_domain.string != remote_tdo_info.domain_name.string:
+ raise CommandError("RemoteTDO inconsistent: Netbios[%s] DNS[%s] SID[%s]" % (
+ remote_tdo_info.netbios_name.string,
+ remote_tdo_info.domain_name.string,
+ remote_tdo_info.sid))
+
+ if local_tdo_info is not None:
+ try:
+ lsaString.string = local_tdo_info.domain_name.string
+ local_tdo_handle = \
+ local_lsa.OpenTrustedDomainByName(local_policy,
+ lsaString,
+ security.SEC_STD_DELETE)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "OpenTrustedDomainByName(%s)" % (
+ lsaString.string))
+
+ local_lsa.DeleteObject(local_tdo_handle)
+ local_tdo_handle = None
+
+ if remote_tdo_info is not None:
+ try:
+ lsaString.string = remote_tdo_info.domain_name.string
+ remote_tdo_handle = \
+ remote_lsa.OpenTrustedDomainByName(remote_policy,
+ lsaString,
+ security.SEC_STD_DELETE)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "OpenTrustedDomainByName(%s)" % (
+ lsaString.string))
+
+ if remote_tdo_handle is not None:
+ try:
+ remote_lsa.DeleteObject(remote_tdo_handle)
+ remote_tdo_handle = None
+ self.outf.write("RemoteTDO deleted.\n")
+ except RuntimeError as error:
+ self.outf.write("%s\n" % self.RemoteRuntimeError(self, error, "DeleteObject() failed"))
+
+ return
+
+
+class cmd_domain_trust_validate(DomainTrustCommand):
+ """Validate a domain trust."""
+
+ synopsis = "%prog DOMAIN [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ "localdcopts": LocalDCCredentialsOptions,
+ }
+
+ takes_options = [
+ Option("--validate-location", type="choice", metavar="LOCATION",
+ choices=["local", "both"],
+ help="Where to validate the trusted domain object: 'local' or 'both'.",
+ dest='validate_location',
+ default="both"),
+ ]
+
+ takes_args = ["domain"]
+
+ def run(self, domain, sambaopts=None, versionopts=None, credopts=None, localdcopts=None,
+ validate_location=None):
+
+ local_policy_access = lsa.LSA_POLICY_VIEW_LOCAL_INFORMATION
+
+ local_server = self.setup_local_server(sambaopts, localdcopts)
+ try:
+ local_lsa = self.new_local_lsa_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect lsa server")
+
+ try:
+ (local_policy, local_lsa_info) = self.get_lsa_info(local_lsa, local_policy_access)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to query LSA_POLICY_INFO_DNS")
+
+ self.outf.write("LocalDomain Netbios[%s] DNS[%s] SID[%s]\n" % (
+ local_lsa_info.name.string,
+ local_lsa_info.dns_domain.string,
+ local_lsa_info.sid))
+
+ try:
+ lsaString = lsa.String()
+ lsaString.string = domain
+ local_tdo_info = \
+ local_lsa.QueryTrustedDomainInfoByName(local_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX)
+ except NTSTATUSError as error:
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise CommandError("trusted domain object does not exist for domain [%s]" % domain)
+
+ raise self.LocalRuntimeError(self, error, "QueryTrustedDomainInfoByName(INFO_EX) failed")
+
+ self.outf.write("LocalTDO Netbios[%s] DNS[%s] SID[%s]\n" % (
+ local_tdo_info.netbios_name.string,
+ local_tdo_info.domain_name.string,
+ local_tdo_info.sid))
+
+ try:
+ local_netlogon = self.new_local_netlogon_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect netlogon server")
+
+ try:
+ local_trust_verify = \
+ local_netlogon.netr_LogonControl2Ex(local_server,
+ netlogon.NETLOGON_CONTROL_TC_VERIFY,
+ 2,
+ local_tdo_info.domain_name.string)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "NETLOGON_CONTROL_TC_VERIFY failed")
+
+ local_trust_status = self._uint32(local_trust_verify.pdc_connection_status[0])
+ local_conn_status = self._uint32(local_trust_verify.tc_connection_status[0])
+
+ if local_trust_verify.flags & netlogon.NETLOGON_VERIFY_STATUS_RETURNED:
+ local_validation = "LocalValidation: DC[%s] CONNECTION[%s] TRUST[%s] VERIFY_STATUS_RETURNED" % (
+ local_trust_verify.trusted_dc_name,
+ local_trust_verify.tc_connection_status[1],
+ local_trust_verify.pdc_connection_status[1])
+ else:
+ local_validation = "LocalValidation: DC[%s] CONNECTION[%s] TRUST[%s]" % (
+ local_trust_verify.trusted_dc_name,
+ local_trust_verify.tc_connection_status[1],
+ local_trust_verify.pdc_connection_status[1])
+
+ if local_trust_status != werror.WERR_SUCCESS or local_conn_status != werror.WERR_SUCCESS:
+ raise CommandError(local_validation)
+ else:
+ self.outf.write("OK: %s\n" % local_validation)
+
+ try:
+ server = local_trust_verify.trusted_dc_name.replace('\\', '')
+ domain_and_server = "%s\\%s" % (local_tdo_info.domain_name.string, server)
+ local_trust_rediscover = \
+ local_netlogon.netr_LogonControl2Ex(local_server,
+ netlogon.NETLOGON_CONTROL_REDISCOVER,
+ 2,
+ domain_and_server)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "NETLOGON_CONTROL_REDISCOVER failed")
+
+ local_conn_status = self._uint32(local_trust_rediscover.tc_connection_status[0])
+ local_rediscover = "LocalRediscover: DC[%s] CONNECTION[%s]" % (
+ local_trust_rediscover.trusted_dc_name,
+ local_trust_rediscover.tc_connection_status[1])
+
+ if local_conn_status != werror.WERR_SUCCESS:
+ raise CommandError(local_rediscover)
+ else:
+ self.outf.write("OK: %s\n" % local_rediscover)
+
+ if validate_location != "local":
+ try:
+ remote_server = self.setup_remote_server(credopts, domain, require_pdc=False)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to locate remote server")
+
+ try:
+ remote_netlogon = self.new_remote_netlogon_connection()
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "failed to connect netlogon server")
+
+ try:
+ remote_trust_verify = \
+ remote_netlogon.netr_LogonControl2Ex(remote_server,
+ netlogon.NETLOGON_CONTROL_TC_VERIFY,
+ 2,
+ local_lsa_info.dns_domain.string)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "NETLOGON_CONTROL_TC_VERIFY failed")
+
+ remote_trust_status = self._uint32(remote_trust_verify.pdc_connection_status[0])
+ remote_conn_status = self._uint32(remote_trust_verify.tc_connection_status[0])
+
+ if remote_trust_verify.flags & netlogon.NETLOGON_VERIFY_STATUS_RETURNED:
+ remote_validation = "RemoteValidation: DC[%s] CONNECTION[%s] TRUST[%s] VERIFY_STATUS_RETURNED" % (
+ remote_trust_verify.trusted_dc_name,
+ remote_trust_verify.tc_connection_status[1],
+ remote_trust_verify.pdc_connection_status[1])
+ else:
+ remote_validation = "RemoteValidation: DC[%s] CONNECTION[%s] TRUST[%s]" % (
+ remote_trust_verify.trusted_dc_name,
+ remote_trust_verify.tc_connection_status[1],
+ remote_trust_verify.pdc_connection_status[1])
+
+ if remote_trust_status != werror.WERR_SUCCESS or remote_conn_status != werror.WERR_SUCCESS:
+ raise CommandError(remote_validation)
+ else:
+ self.outf.write("OK: %s\n" % remote_validation)
+
+ try:
+ server = remote_trust_verify.trusted_dc_name.replace('\\', '')
+ domain_and_server = "%s\\%s" % (local_lsa_info.dns_domain.string, server)
+ remote_trust_rediscover = \
+ remote_netlogon.netr_LogonControl2Ex(remote_server,
+ netlogon.NETLOGON_CONTROL_REDISCOVER,
+ 2,
+ domain_and_server)
+ except RuntimeError as error:
+ raise self.RemoteRuntimeError(self, error, "NETLOGON_CONTROL_REDISCOVER failed")
+
+ remote_conn_status = self._uint32(remote_trust_rediscover.tc_connection_status[0])
+
+ remote_rediscover = "RemoteRediscover: DC[%s] CONNECTION[%s]" % (
+ remote_trust_rediscover.trusted_dc_name,
+ remote_trust_rediscover.tc_connection_status[1])
+
+ if remote_conn_status != werror.WERR_SUCCESS:
+ raise CommandError(remote_rediscover)
+ else:
+ self.outf.write("OK: %s\n" % remote_rediscover)
+
+ return
+
+
+class cmd_domain_trust_namespaces(DomainTrustCommand):
+ """Manage forest trust namespaces."""
+
+ synopsis = "%prog [DOMAIN] [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "localdcopts": LocalDCCredentialsOptions,
+ }
+
+ takes_options = [
+ Option("--refresh", type="choice", metavar="check|store",
+ choices=["check", "store", None],
+ help="List and maybe store refreshed forest trust information: 'check' or 'store'.",
+ dest='refresh',
+ default=None),
+ Option("--enable-all", action="store_true",
+ help="Try to update disabled entries, not allowed with --refresh=check.",
+ dest='enable_all',
+ default=False),
+ Option("--enable-tln", action="append", metavar='DNSDOMAIN',
+ help="Enable a top level name entry. Can be specified multiple times.",
+ dest='enable_tln',
+ default=[]),
+ Option("--disable-tln", action="append", metavar='DNSDOMAIN',
+ help="Disable a top level name entry. Can be specified multiple times.",
+ dest='disable_tln',
+ default=[]),
+ Option("--add-tln-ex", action="append", metavar='DNSDOMAIN',
+ help="Add a top level exclusion entry. Can be specified multiple times.",
+ dest='add_tln_ex',
+ default=[]),
+ Option("--delete-tln-ex", action="append", metavar='DNSDOMAIN',
+ help="Delete a top level exclusion entry. Can be specified multiple times.",
+ dest='delete_tln_ex',
+ default=[]),
+ Option("--enable-nb", action="append", metavar='NETBIOSDOMAIN',
+ help="Enable a netbios name in a domain entry. Can be specified multiple times.",
+ dest='enable_nb',
+ default=[]),
+ Option("--disable-nb", action="append", metavar='NETBIOSDOMAIN',
+ help="Disable a netbios name in a domain entry. Can be specified multiple times.",
+ dest='disable_nb',
+ default=[]),
+ Option("--enable-sid", action="append", metavar='DOMAINSID',
+ help="Enable a SID in a domain entry. Can be specified multiple times.",
+ dest='enable_sid_str',
+ default=[]),
+ Option("--disable-sid", action="append", metavar='DOMAINSID',
+ help="Disable a SID in a domain entry. Can be specified multiple times.",
+ dest='disable_sid_str',
+ default=[]),
+ Option("--add-upn-suffix", action="append", metavar='DNSDOMAIN',
+ help="Add a new uPNSuffixes attribute for the local forest. Can be specified multiple times.",
+ dest='add_upn',
+ default=[]),
+ Option("--delete-upn-suffix", action="append", metavar='DNSDOMAIN',
+ help="Delete an existing uPNSuffixes attribute of the local forest. Can be specified multiple times.",
+ dest='delete_upn',
+ default=[]),
+ Option("--add-spn-suffix", action="append", metavar='DNSDOMAIN',
+ help="Add a new msDS-SPNSuffixes attribute for the local forest. Can be specified multiple times.",
+ dest='add_spn',
+ default=[]),
+ Option("--delete-spn-suffix", action="append", metavar='DNSDOMAIN',
+ help="Delete an existing msDS-SPNSuffixes attribute of the local forest. Can be specified multiple times.",
+ dest='delete_spn',
+ default=[]),
+ ]
+
+ takes_args = ["domain?"]
+
+ def run(self, domain=None, sambaopts=None, localdcopts=None, versionopts=None,
+ refresh=None, enable_all=False,
+ enable_tln=None, disable_tln=None, add_tln_ex=None, delete_tln_ex=None,
+ enable_sid_str=None, disable_sid_str=None, enable_nb=None, disable_nb=None,
+ add_upn=None, delete_upn=None, add_spn=None, delete_spn=None):
+
+ if enable_tln is None:
+ enable_tln = []
+ if disable_tln is None:
+ disable_tln = []
+ if add_tln_ex is None:
+ add_tln_ex = []
+ if delete_tln_ex is None:
+ delete_tln_ex = []
+ if enable_sid_str is None:
+ enable_sid_str = []
+ if disable_sid_str is None:
+ disable_sid_str = []
+ if enable_nb is None:
+ enable_nb = []
+ if disable_nb is None:
+ disable_nb = []
+ if add_upn is None:
+ add_upn = []
+ if delete_upn is None:
+ delete_upn = []
+ if add_spn is None:
+ add_spn = []
+ if delete_spn is None:
+ delete_spn = []
+
+ require_update = False
+
+ if domain is None:
+ if refresh == "store":
+ raise CommandError("--refresh=%s not allowed without DOMAIN" % refresh)
+
+ if enable_all:
+ raise CommandError("--enable-all not allowed without DOMAIN")
+
+ if len(enable_tln) > 0:
+ raise CommandError("--enable-tln not allowed without DOMAIN")
+ if len(disable_tln) > 0:
+ raise CommandError("--disable-tln not allowed without DOMAIN")
+
+ if len(add_tln_ex) > 0:
+ raise CommandError("--add-tln-ex not allowed without DOMAIN")
+ if len(delete_tln_ex) > 0:
+ raise CommandError("--delete-tln-ex not allowed without DOMAIN")
+
+ if len(enable_nb) > 0:
+ raise CommandError("--enable-nb not allowed without DOMAIN")
+ if len(disable_nb) > 0:
+ raise CommandError("--disable-nb not allowed without DOMAIN")
+
+ if len(enable_sid_str) > 0:
+ raise CommandError("--enable-sid not allowed without DOMAIN")
+ if len(disable_sid_str) > 0:
+ raise CommandError("--disable-sid not allowed without DOMAIN")
+
+ if len(add_upn) > 0:
+ for n in add_upn:
+ if not n.startswith("*."):
+ continue
+ raise CommandError("value[%s] specified for --add-upn-suffix should not include with '*.'" % n)
+ require_update = True
+ if len(delete_upn) > 0:
+ for n in delete_upn:
+ if not n.startswith("*."):
+ continue
+ raise CommandError("value[%s] specified for --delete-upn-suffix should not include with '*.'" % n)
+ require_update = True
+ for a in add_upn:
+ for d in delete_upn:
+ if a.lower() != d.lower():
+ continue
+ raise CommandError("value[%s] specified for --add-upn-suffix and --delete-upn-suffix" % a)
+
+ if len(add_spn) > 0:
+ for n in add_spn:
+ if not n.startswith("*."):
+ continue
+ raise CommandError("value[%s] specified for --add-spn-suffix should not include with '*.'" % n)
+ require_update = True
+ if len(delete_spn) > 0:
+ for n in delete_spn:
+ if not n.startswith("*."):
+ continue
+ raise CommandError("value[%s] specified for --delete-spn-suffix should not include with '*.'" % n)
+ require_update = True
+ for a in add_spn:
+ for d in delete_spn:
+ if a.lower() != d.lower():
+ continue
+ raise CommandError("value[%s] specified for --add-spn-suffix and --delete-spn-suffix" % a)
+ else:
+ if len(add_upn) > 0:
+ raise CommandError("--add-upn-suffix not allowed together with DOMAIN")
+ if len(delete_upn) > 0:
+ raise CommandError("--delete-upn-suffix not allowed together with DOMAIN")
+ if len(add_spn) > 0:
+ raise CommandError("--add-spn-suffix not allowed together with DOMAIN")
+ if len(delete_spn) > 0:
+ raise CommandError("--delete-spn-suffix not allowed together with DOMAIN")
+
+ if refresh is not None:
+ if refresh == "store":
+ require_update = True
+
+ if enable_all and refresh != "store":
+ raise CommandError("--enable-all not allowed together with --refresh=%s" % refresh)
+
+ if len(enable_tln) > 0:
+ raise CommandError("--enable-tln not allowed together with --refresh")
+ if len(disable_tln) > 0:
+ raise CommandError("--disable-tln not allowed together with --refresh")
+
+ if len(add_tln_ex) > 0:
+ raise CommandError("--add-tln-ex not allowed together with --refresh")
+ if len(delete_tln_ex) > 0:
+ raise CommandError("--delete-tln-ex not allowed together with --refresh")
+
+ if len(enable_nb) > 0:
+ raise CommandError("--enable-nb not allowed together with --refresh")
+ if len(disable_nb) > 0:
+ raise CommandError("--disable-nb not allowed together with --refresh")
+
+ if len(enable_sid_str) > 0:
+ raise CommandError("--enable-sid not allowed together with --refresh")
+ if len(disable_sid_str) > 0:
+ raise CommandError("--disable-sid not allowed together with --refresh")
+ else:
+ if enable_all:
+ require_update = True
+
+ if len(enable_tln) > 0:
+ raise CommandError("--enable-tln not allowed together with --enable-all")
+
+ if len(enable_nb) > 0:
+ raise CommandError("--enable-nb not allowed together with --enable-all")
+
+ if len(enable_sid_str) > 0:
+ raise CommandError("--enable-sid not allowed together with --enable-all")
+
+ if len(enable_tln) > 0:
+ require_update = True
+ if len(disable_tln) > 0:
+ require_update = True
+ for e in enable_tln:
+ for d in disable_tln:
+ if e.lower() != d.lower():
+ continue
+ raise CommandError("value[%s] specified for --enable-tln and --disable-tln" % e)
+
+ if len(add_tln_ex) > 0:
+ for n in add_tln_ex:
+ if not n.startswith("*."):
+ continue
+ raise CommandError("value[%s] specified for --add-tln-ex should not include with '*.'" % n)
+ require_update = True
+ if len(delete_tln_ex) > 0:
+ for n in delete_tln_ex:
+ if not n.startswith("*."):
+ continue
+ raise CommandError("value[%s] specified for --delete-tln-ex should not include with '*.'" % n)
+ require_update = True
+ for a in add_tln_ex:
+ for d in delete_tln_ex:
+ if a.lower() != d.lower():
+ continue
+ raise CommandError("value[%s] specified for --add-tln-ex and --delete-tln-ex" % a)
+
+ if len(enable_nb) > 0:
+ require_update = True
+ if len(disable_nb) > 0:
+ require_update = True
+ for e in enable_nb:
+ for d in disable_nb:
+ if e.upper() != d.upper():
+ continue
+ raise CommandError("value[%s] specified for --enable-nb and --disable-nb" % e)
+
+ enable_sid = []
+ for s in enable_sid_str:
+ try:
+ sid = security.dom_sid(s)
+ except (ValueError, TypeError):
+ raise CommandError("value[%s] specified for --enable-sid is not a valid SID" % s)
+ enable_sid.append(sid)
+ disable_sid = []
+ for s in disable_sid_str:
+ try:
+ sid = security.dom_sid(s)
+ except (ValueError, TypeError):
+ raise CommandError("value[%s] specified for --disable-sid is not a valid SID" % s)
+ disable_sid.append(sid)
+ if len(enable_sid) > 0:
+ require_update = True
+ if len(disable_sid) > 0:
+ require_update = True
+ for e in enable_sid:
+ for d in disable_sid:
+ if e != d:
+ continue
+ raise CommandError("value[%s] specified for --enable-sid and --disable-sid" % e)
+
+ local_policy_access = lsa.LSA_POLICY_VIEW_LOCAL_INFORMATION
+ if require_update:
+ local_policy_access |= lsa.LSA_POLICY_TRUST_ADMIN
+
+ local_server = self.setup_local_server(sambaopts, localdcopts)
+ try:
+ local_lsa = self.new_local_lsa_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect lsa server")
+
+ try:
+ (local_policy, local_lsa_info) = self.get_lsa_info(local_lsa, local_policy_access)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to query LSA_POLICY_INFO_DNS")
+
+ self.outf.write("LocalDomain Netbios[%s] DNS[%s] SID[%s]\n" % (
+ local_lsa_info.name.string,
+ local_lsa_info.dns_domain.string,
+ local_lsa_info.sid))
+
+ if domain is None:
+ try:
+ local_netlogon = self.new_local_netlogon_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect netlogon server")
+
+ try:
+ local_netlogon_info = self.get_netlogon_dc_info(local_netlogon, local_server)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to get netlogon dc info")
+
+ if local_netlogon_info.domain_name != local_netlogon_info.forest_name:
+ raise CommandError("The local domain [%s] is not the forest root [%s]" % (
+ local_netlogon_info.domain_name,
+ local_netlogon_info.forest_name))
+
+ try:
+ # get all information about our own forest
+ own_forest_info = local_netlogon.netr_DsRGetForestTrustInformation(local_netlogon_info.dc_unc,
+ None, 0)
+ except RuntimeError as error:
+ if self.check_runtime_error(error, werror.WERR_RPC_S_PROCNUM_OUT_OF_RANGE):
+ raise CommandError("LOCAL_DC[%s]: netr_DsRGetForestTrustInformation() not supported." % (
+ local_server))
+
+ if self.check_runtime_error(error, werror.WERR_INVALID_FUNCTION):
+ raise CommandError("LOCAL_DC[%s]: netr_DsRGetForestTrustInformation() not supported." % (
+ local_server))
+
+ if self.check_runtime_error(error, werror.WERR_NERR_ACFNOTLOADED):
+ raise CommandError("LOCAL_DC[%s]: netr_DsRGetForestTrustInformation() not supported." % (
+ local_server))
+
+ raise self.LocalRuntimeError(self, error, "netr_DsRGetForestTrustInformation() failed")
+
+ self.outf.write("Own forest trust information...\n")
+ self.write_forest_trust_info(own_forest_info,
+ tln=local_lsa_info.dns_domain.string)
+
+ try:
+ local_samdb = self.new_local_ldap_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect to SamDB")
+
+ local_partitions_dn = "CN=Partitions,%s" % str(local_samdb.get_config_basedn())
+ attrs = ['uPNSuffixes', 'msDS-SPNSuffixes']
+ try:
+ msgs = local_samdb.search(base=local_partitions_dn,
+ scope=ldb.SCOPE_BASE,
+ expression="(objectClass=crossRefContainer)",
+ attrs=attrs)
+ stored_msg = msgs[0]
+ except ldb.LdbError as error:
+ raise self.LocalLdbError(self, error, "failed to search partition dn")
+
+ stored_upn_vals = []
+ if 'uPNSuffixes' in stored_msg:
+ stored_upn_vals.extend(stored_msg['uPNSuffixes'])
+
+ stored_spn_vals = []
+ if 'msDS-SPNSuffixes' in stored_msg:
+ stored_spn_vals.extend(stored_msg['msDS-SPNSuffixes'])
+
+ self.outf.write("Stored uPNSuffixes attributes[%d]:\n" % len(stored_upn_vals))
+ for v in stored_upn_vals:
+ self.outf.write("TLN: %-32s DNS[*.%s]\n" % ("", v))
+ self.outf.write("Stored msDS-SPNSuffixes attributes[%d]:\n" % len(stored_spn_vals))
+ for v in stored_spn_vals:
+ self.outf.write("TLN: %-32s DNS[*.%s]\n" % ("", v))
+
+ if not require_update:
+ return
+
+ replace_upn = False
+ update_upn_vals = []
+ update_upn_vals.extend(stored_upn_vals)
+
+ replace_spn = False
+ update_spn_vals = []
+ update_spn_vals.extend(stored_spn_vals)
+
+ for upn in add_upn:
+ for v in update_upn_vals:
+ if str(v).lower() == upn.lower():
+ raise CommandError("Entry already present for "
+ "value[%s] specified for "
+ "--add-upn-suffix" % upn)
+ update_upn_vals.append(upn)
+ replace_upn = True
+
+ for upn in delete_upn:
+ idx = None
+ for i, v in enumerate(update_upn_vals):
+ if str(v).lower() != upn.lower():
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --delete-upn-suffix" % upn)
+
+ update_upn_vals.pop(idx)
+ replace_upn = True
+
+ for spn in add_spn:
+ for v in update_spn_vals:
+ if str(v).lower() == spn.lower():
+ raise CommandError("Entry already present for "
+ "value[%s] specified for "
+ "--add-spn-suffix" % spn)
+ update_spn_vals.append(spn)
+ replace_spn = True
+
+ for spn in delete_spn:
+ idx = None
+ for i, v in enumerate(update_spn_vals):
+ if str(v).lower() != spn.lower():
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --delete-spn-suffix" % spn)
+
+ update_spn_vals.pop(idx)
+ replace_spn = True
+
+ self.outf.write("Update uPNSuffixes attributes[%d]:\n" % len(update_upn_vals))
+ for v in update_upn_vals:
+ self.outf.write("TLN: %-32s DNS[*.%s]\n" % ("", v))
+ self.outf.write("Update msDS-SPNSuffixes attributes[%d]:\n" % len(update_spn_vals))
+ for v in update_spn_vals:
+ self.outf.write("TLN: %-32s DNS[*.%s]\n" % ("", v))
+
+ update_msg = ldb.Message()
+ update_msg.dn = stored_msg.dn
+
+ if replace_upn:
+ update_msg['uPNSuffixes'] = ldb.MessageElement(update_upn_vals,
+ ldb.FLAG_MOD_REPLACE,
+ 'uPNSuffixes')
+ if replace_spn:
+ update_msg['msDS-SPNSuffixes'] = ldb.MessageElement(update_spn_vals,
+ ldb.FLAG_MOD_REPLACE,
+ 'msDS-SPNSuffixes')
+ try:
+ local_samdb.modify(update_msg)
+ except ldb.LdbError as error:
+ raise self.LocalLdbError(self, error, "failed to update partition dn")
+
+ try:
+ stored_forest_info = local_netlogon.netr_DsRGetForestTrustInformation(local_netlogon_info.dc_unc,
+ None, 0)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "netr_DsRGetForestTrustInformation() failed")
+
+ self.outf.write("Stored forest trust information...\n")
+ self.write_forest_trust_info(stored_forest_info,
+ tln=local_lsa_info.dns_domain.string)
+ return
+
+ try:
+ lsaString = lsa.String()
+ lsaString.string = domain
+ local_tdo_info = \
+ local_lsa.QueryTrustedDomainInfoByName(local_policy,
+ lsaString,
+ lsa.LSA_TRUSTED_DOMAIN_INFO_INFO_EX)
+ except NTSTATUSError as error:
+ if self.check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND):
+ raise CommandError("trusted domain object does not exist for domain [%s]" % domain)
+
+ raise self.LocalRuntimeError(self, error, "QueryTrustedDomainInfoByName(INFO_EX) failed")
+
+ self.outf.write("LocalTDO Netbios[%s] DNS[%s] SID[%s]\n" % (
+ local_tdo_info.netbios_name.string,
+ local_tdo_info.domain_name.string,
+ local_tdo_info.sid))
+
+ if not local_tdo_info.trust_attributes & lsa.LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE:
+ raise CommandError("trusted domain object for domain [%s] is not marked as FOREST_TRANSITIVE." % domain)
+
+ if refresh is not None:
+ try:
+ local_netlogon = self.new_local_netlogon_connection()
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to connect netlogon server")
+
+ try:
+ local_netlogon_info = self.get_netlogon_dc_info(local_netlogon, local_server)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "failed to get netlogon dc info")
+
+ lsa_update_check = 1
+ if refresh == "store":
+ netlogon_update_tdo = netlogon.DS_GFTI_UPDATE_TDO
+ if enable_all:
+ lsa_update_check = 0
+ else:
+ netlogon_update_tdo = 0
+
+ try:
+ # get all information about the remote trust
+ # this triggers netr_GetForestTrustInformation to the remote domain
+ # and lsaRSetForestTrustInformation() locally, but new top level
+ # names are disabled by default.
+ fresh_forest_info = \
+ local_netlogon.netr_DsRGetForestTrustInformation(local_netlogon_info.dc_unc,
+ local_tdo_info.domain_name.string,
+ netlogon_update_tdo)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "netr_DsRGetForestTrustInformation() failed")
+
+ try:
+ fresh_forest_collision = \
+ local_lsa.lsaRSetForestTrustInformation(local_policy,
+ local_tdo_info.domain_name,
+ lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
+ fresh_forest_info,
+ lsa_update_check)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "lsaRSetForestTrustInformation() failed")
+
+ self.outf.write("Fresh forest trust information...\n")
+ self.write_forest_trust_info(fresh_forest_info,
+ tln=local_tdo_info.domain_name.string,
+ collisions=fresh_forest_collision)
+
+ if refresh == "store":
+ try:
+ lsaString = lsa.String()
+ lsaString.string = local_tdo_info.domain_name.string
+ stored_forest_info = \
+ local_lsa.lsaRQueryForestTrustInformation(local_policy,
+ lsaString,
+ lsa.LSA_FOREST_TRUST_DOMAIN_INFO)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "lsaRQueryForestTrustInformation() failed")
+
+ self.outf.write("Stored forest trust information...\n")
+ self.write_forest_trust_info(stored_forest_info,
+ tln=local_tdo_info.domain_name.string)
+
+ return
+
+ #
+ # The none --refresh path
+ #
+
+ try:
+ lsaString = lsa.String()
+ lsaString.string = local_tdo_info.domain_name.string
+ local_forest_info = \
+ local_lsa.lsaRQueryForestTrustInformation(local_policy,
+ lsaString,
+ lsa.LSA_FOREST_TRUST_DOMAIN_INFO)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "lsaRQueryForestTrustInformation() failed")
+
+ self.outf.write("Local forest trust information...\n")
+ self.write_forest_trust_info(local_forest_info,
+ tln=local_tdo_info.domain_name.string)
+
+ if not require_update:
+ return
+
+ entries = []
+ entries.extend(local_forest_info.entries)
+ update_forest_info = lsa.ForestTrustInformation()
+ update_forest_info.count = len(entries)
+ update_forest_info.entries = entries
+
+ if enable_all:
+ for r in update_forest_info.entries:
+ if r.type != lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ continue
+ if r.flags == 0:
+ continue
+ r.time = 0
+ r.flags &= ~lsa.LSA_TLN_DISABLED_MASK
+ for r in update_forest_info.entries:
+ if r.type != lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
+ continue
+ if r.flags == 0:
+ continue
+ r.time = 0
+ r.flags &= ~lsa.LSA_NB_DISABLED_MASK
+ r.flags &= ~lsa.LSA_SID_DISABLED_MASK
+
+ for tln in enable_tln:
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ continue
+ if r.forest_trust_data.string.lower() != tln.lower():
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --enable-tln" % tln)
+ if not update_forest_info.entries[idx].flags & lsa.LSA_TLN_DISABLED_MASK:
+ raise CommandError("Entry found for value[%s] specified for --enable-tln is already enabled" % tln)
+ update_forest_info.entries[idx].time = 0
+ update_forest_info.entries[idx].flags &= ~lsa.LSA_TLN_DISABLED_MASK
+
+ for tln in disable_tln:
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ continue
+ if r.forest_trust_data.string.lower() != tln.lower():
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --disable-tln" % tln)
+ if update_forest_info.entries[idx].flags & lsa.LSA_TLN_DISABLED_ADMIN:
+ raise CommandError("Entry found for value[%s] specified for --disable-tln is already disabled" % tln)
+ update_forest_info.entries[idx].time = 0
+ update_forest_info.entries[idx].flags &= ~lsa.LSA_TLN_DISABLED_MASK
+ update_forest_info.entries[idx].flags |= lsa.LSA_TLN_DISABLED_ADMIN
+
+ for tln_ex in add_tln_ex:
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ continue
+ if r.forest_trust_data.string.lower() != tln_ex.lower():
+ continue
+ idx = i
+ break
+ if idx is not None:
+ raise CommandError("Entry already present for value[%s] specified for --add-tln-ex" % tln_ex)
+
+ tln_dot = ".%s" % tln_ex.lower()
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ continue
+ r_dot = ".%s" % r.forest_trust_data.string.lower()
+ if tln_dot == r_dot:
+ raise CommandError("TLN entry present for value[%s] specified for --add-tln-ex" % tln_ex)
+ if not tln_dot.endswith(r_dot):
+ continue
+ idx = i
+ break
+
+ if idx is None:
+ raise CommandError("No TLN parent present for value[%s] specified for --add-tln-ex" % tln_ex)
+
+ r = lsa.ForestTrustRecord()
+ r.type = lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX
+ r.flags = 0
+ r.time = 0
+ r.forest_trust_data.string = tln_ex
+
+ entries = []
+ entries.extend(update_forest_info.entries)
+ entries.insert(idx + 1, r)
+ update_forest_info.count = len(entries)
+ update_forest_info.entries = entries
+
+ for tln_ex in delete_tln_ex:
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ continue
+ if r.forest_trust_data.string.lower() != tln_ex.lower():
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --delete-tln-ex" % tln_ex)
+
+ entries = []
+ entries.extend(update_forest_info.entries)
+ entries.pop(idx)
+ update_forest_info.count = len(entries)
+ update_forest_info.entries = entries
+
+ for nb in enable_nb:
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
+ continue
+ if r.forest_trust_data.netbios_domain_name.string.upper() != nb.upper():
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --enable-nb" % nb)
+ if not update_forest_info.entries[idx].flags & lsa.LSA_NB_DISABLED_MASK:
+ raise CommandError("Entry found for value[%s] specified for --enable-nb is already enabled" % nb)
+ update_forest_info.entries[idx].time = 0
+ update_forest_info.entries[idx].flags &= ~lsa.LSA_NB_DISABLED_MASK
+
+ for nb in disable_nb:
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
+ continue
+ if r.forest_trust_data.netbios_domain_name.string.upper() != nb.upper():
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --delete-nb" % nb)
+ if update_forest_info.entries[idx].flags & lsa.LSA_NB_DISABLED_ADMIN:
+ raise CommandError("Entry found for value[%s] specified for --disable-nb is already disabled" % nb)
+ update_forest_info.entries[idx].time = 0
+ update_forest_info.entries[idx].flags &= ~lsa.LSA_NB_DISABLED_MASK
+ update_forest_info.entries[idx].flags |= lsa.LSA_NB_DISABLED_ADMIN
+
+ for sid in enable_sid:
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
+ continue
+ if r.forest_trust_data.domain_sid != sid:
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --enable-sid" % sid)
+ if not update_forest_info.entries[idx].flags & lsa.LSA_SID_DISABLED_MASK:
+ raise CommandError("Entry found for value[%s] specified for --enable-sid is already enabled" % nb)
+ update_forest_info.entries[idx].time = 0
+ update_forest_info.entries[idx].flags &= ~lsa.LSA_SID_DISABLED_MASK
+
+ for sid in disable_sid:
+ idx = None
+ for i, r in enumerate(update_forest_info.entries):
+ if r.type != lsa.LSA_FOREST_TRUST_DOMAIN_INFO:
+ continue
+ if r.forest_trust_data.domain_sid != sid:
+ continue
+ idx = i
+ break
+ if idx is None:
+ raise CommandError("Entry not found for value[%s] specified for --delete-sid" % sid)
+ if update_forest_info.entries[idx].flags & lsa.LSA_SID_DISABLED_ADMIN:
+ raise CommandError("Entry found for value[%s] specified for --disable-sid is already disabled" % nb)
+ update_forest_info.entries[idx].time = 0
+ update_forest_info.entries[idx].flags &= ~lsa.LSA_SID_DISABLED_MASK
+ update_forest_info.entries[idx].flags |= lsa.LSA_SID_DISABLED_ADMIN
+
+ try:
+ update_forest_collision = local_lsa.lsaRSetForestTrustInformation(local_policy,
+ local_tdo_info.domain_name,
+ lsa.LSA_FOREST_TRUST_DOMAIN_INFO,
+ update_forest_info, 0)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "lsaRSetForestTrustInformation() failed")
+
+ self.outf.write("Updated forest trust information...\n")
+ self.write_forest_trust_info(update_forest_info,
+ tln=local_tdo_info.domain_name.string,
+ collisions=update_forest_collision)
+
+ try:
+ lsaString = lsa.String()
+ lsaString.string = local_tdo_info.domain_name.string
+ stored_forest_info = local_lsa.lsaRQueryForestTrustInformation(local_policy,
+ lsaString,
+ lsa.LSA_FOREST_TRUST_DOMAIN_INFO)
+ except RuntimeError as error:
+ raise self.LocalRuntimeError(self, error, "lsaRQueryForestTrustInformation() failed")
+
+ self.outf.write("Stored forest trust information...\n")
+ self.write_forest_trust_info(stored_forest_info,
+ tln=local_tdo_info.domain_name.string)
+ return
+
+
+class cmd_domain_trust(SuperCommand):
+ """Domain and forest trust management."""
+
+ subcommands = {}
+ subcommands["list"] = cmd_domain_trust_list()
+ subcommands["show"] = cmd_domain_trust_show()
+ subcommands["create"] = cmd_domain_trust_create()
+ subcommands["modify"] = cmd_domain_trust_modify()
+ subcommands["delete"] = cmd_domain_trust_delete()
+ subcommands["validate"] = cmd_domain_trust_validate()
+ subcommands["namespaces"] = cmd_domain_trust_namespaces()