summaryrefslogtreecommitdiffstats
path: root/python/samba/netcmd/pso.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 17:20:00 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 17:20:00 +0000
commit8daa83a594a2e98f39d764422bfbdbc62c9efd44 (patch)
tree4099e8021376c7d8c05bdf8503093d80e9c7bad0 /python/samba/netcmd/pso.py
parentInitial commit. (diff)
downloadsamba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.tar.xz
samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.zip
Adding upstream version 2:4.20.0+dfsg.upstream/2%4.20.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'python/samba/netcmd/pso.py')
-rw-r--r--python/samba/netcmd/pso.py794
1 files changed, 794 insertions, 0 deletions
diff --git a/python/samba/netcmd/pso.py b/python/samba/netcmd/pso.py
new file mode 100644
index 0000000..d260e3b
--- /dev/null
+++ b/python/samba/netcmd/pso.py
@@ -0,0 +1,794 @@
+# Manages Password Settings Objects
+#
+# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
+#
+# 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
+import ldb
+from samba.samdb import SamDB
+from samba.netcmd import (Command, CommandError, Option, SuperCommand)
+from samba.dcerpc.samr import (DOMAIN_PASSWORD_COMPLEX,
+ DOMAIN_PASSWORD_STORE_CLEARTEXT)
+from samba.auth import system_session
+from samba.netcmd.common import (NEVER_TIMESTAMP,
+ timestamp_to_mins,
+ timestamp_to_days)
+
+
+def pso_container(samdb):
+ return "CN=Password Settings Container,CN=System,%s" % samdb.domain_dn()
+
+
+def mins_to_timestamp(mins):
+ """Converts a value in minutes to -100 nanosecond units"""
+ timestamp = -int((1e7) * 60 * mins)
+ return str(timestamp)
+
+
+def days_to_timestamp(days):
+ """Converts a value in days to -100 nanosecond units"""
+ timestamp = mins_to_timestamp(days * 60 * 24)
+ return str(timestamp)
+
+
+def show_pso_by_dn(outf, samdb, dn, show_applies_to=True):
+ """Displays the password settings for a PSO specified by DN"""
+
+ # map from the boolean LDB value to the CLI string the user sees
+ on_off_str = {"TRUE": "on", "FALSE": "off"}
+
+ pso_attrs = ['name', 'msDS-PasswordSettingsPrecedence',
+ 'msDS-PasswordReversibleEncryptionEnabled',
+ 'msDS-PasswordHistoryLength', 'msDS-MinimumPasswordLength',
+ 'msDS-PasswordComplexityEnabled', 'msDS-MinimumPasswordAge',
+ 'msDS-MaximumPasswordAge', 'msDS-LockoutObservationWindow',
+ 'msDS-LockoutThreshold', 'msDS-LockoutDuration',
+ 'msDS-PSOAppliesTo']
+
+ res = samdb.search(dn, scope=ldb.SCOPE_BASE, attrs=pso_attrs)
+ pso_res = res[0]
+ outf.write("Password information for PSO '%s'\n" % pso_res['name'])
+ outf.write("\n")
+
+ outf.write("Precedence (lowest is best): %s\n" %
+ pso_res['msDS-PasswordSettingsPrecedence'])
+ bool_str = str(pso_res['msDS-PasswordComplexityEnabled'])
+ outf.write("Password complexity: %s\n" % on_off_str[bool_str])
+ bool_str = str(pso_res['msDS-PasswordReversibleEncryptionEnabled'])
+ outf.write("Store plaintext passwords: %s\n" % on_off_str[bool_str])
+ outf.write("Password history length: %s\n" %
+ pso_res['msDS-PasswordHistoryLength'])
+ outf.write("Minimum password length: %s\n" %
+ pso_res['msDS-MinimumPasswordLength'])
+ outf.write("Minimum password age (days): %d\n" %
+ timestamp_to_days(pso_res['msDS-MinimumPasswordAge'][0]))
+ outf.write("Maximum password age (days): %d\n" %
+ timestamp_to_days(pso_res['msDS-MaximumPasswordAge'][0]))
+ outf.write("Account lockout duration (mins): %d\n" %
+ timestamp_to_mins(pso_res['msDS-LockoutDuration'][0]))
+ outf.write("Account lockout threshold (attempts): %s\n" %
+ pso_res['msDS-LockoutThreshold'])
+ outf.write("Reset account lockout after (mins): %d\n" %
+ timestamp_to_mins(pso_res['msDS-LockoutObservationWindow'][0]))
+
+ if show_applies_to:
+ if 'msDS-PSOAppliesTo' in pso_res:
+ outf.write("\nPSO applies directly to %d groups/users:\n" %
+ len(pso_res['msDS-PSOAppliesTo']))
+ for dn in pso_res['msDS-PSOAppliesTo']:
+ outf.write(" %s\n" % dn)
+ else:
+ outf.write("\nNote: PSO does not apply to any users or groups.\n")
+
+
+def check_pso_valid(samdb, pso_dn, name):
+ """Gracefully bail out if we can't view/modify the PSO specified"""
+ # the base scope search for the PSO throws an error if it doesn't exist
+ try:
+ res = samdb.search(pso_dn, scope=ldb.SCOPE_BASE,
+ attrs=['msDS-PasswordSettingsPrecedence'])
+ except ldb.LdbError as e:
+ if e.args[0] == ldb.ERR_NO_SUCH_OBJECT:
+ raise CommandError("Unable to find PSO '%s'" % name)
+ raise
+
+ # users need admin permission to modify/view a PSO. In this case, the
+ # search succeeds, but it doesn't return any attributes
+ if 'msDS-PasswordSettingsPrecedence' not in res[0]:
+ raise CommandError("You may not have permission to view/modify PSOs")
+
+
+def show_pso_for_user(outf, samdb, username):
+ """Displays the password settings for a specific user"""
+
+ search_filter = "(&(sAMAccountName=%s)(objectClass=user))" % username
+
+ res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter,
+ attrs=['msDS-ResultantPSO', 'msDS-PSOApplied'])
+
+ if len(res) == 0:
+ outf.write("User '%s' not found.\n" % username)
+ elif 'msDS-ResultantPSO' not in res[0]:
+ outf.write("No PSO applies to user '%s'. "
+ "The default domain settings apply.\n" % username)
+ outf.write("Refer to 'samba-tool domain passwordsettings show'.\n")
+ else:
+ # sanity-check user has permissions to view PSO details (non-admin
+ # users can view msDS-ResultantPSO, but not the actual PSO details)
+ check_pso_valid(samdb, res[0]['msDS-ResultantPSO'][0], "???")
+ outf.write("The following PSO settings apply to user '%s'.\n\n" %
+ username)
+ show_pso_by_dn(outf, samdb, res[0]['msDS-ResultantPSO'][0],
+ show_applies_to=False)
+ # PSOs that apply directly to a user don't necessarily have the best
+ # precedence, which could be a little confusing for PSO management
+ if 'msDS-PSOApplied' in res[0]:
+ outf.write("\nNote: PSO applies directly to user "
+ "(any group PSOs are overridden)\n")
+ else:
+ outf.write("\nPSO applies to user via group membership.\n")
+
+
+def msg_add_attr(msg, attr_name, value, ldb_oper):
+ msg[attr_name] = ldb.MessageElement(value, ldb_oper, attr_name)
+
+
+def make_pso_ldb_msg(outf, samdb, pso_dn, create, lockout_threshold=None,
+ complexity=None, precedence=None, store_plaintext=None,
+ history_length=None, min_pwd_length=None,
+ min_pwd_age=None, max_pwd_age=None, lockout_duration=None,
+ reset_lockout_after=None):
+ """Packs the given PSO settings into an LDB message"""
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, pso_dn)
+
+ if create:
+ ldb_oper = ldb.FLAG_MOD_ADD
+ m["msDS-objectClass"] = ldb.MessageElement("msDS-PasswordSettings",
+ ldb_oper, "objectClass")
+ else:
+ ldb_oper = ldb.FLAG_MOD_REPLACE
+
+ if precedence is not None:
+ msg_add_attr(m, "msDS-PasswordSettingsPrecedence", str(precedence),
+ ldb_oper)
+
+ if complexity is not None:
+ bool_str = "TRUE" if complexity == "on" else "FALSE"
+ msg_add_attr(m, "msDS-PasswordComplexityEnabled", bool_str, ldb_oper)
+
+ if store_plaintext is not None:
+ bool_str = "TRUE" if store_plaintext == "on" else "FALSE"
+ msg_add_attr(m, "msDS-PasswordReversibleEncryptionEnabled",
+ bool_str, ldb_oper)
+
+ if history_length is not None:
+ msg_add_attr(m, "msDS-PasswordHistoryLength", str(history_length),
+ ldb_oper)
+
+ if min_pwd_length is not None:
+ msg_add_attr(m, "msDS-MinimumPasswordLength", str(min_pwd_length),
+ ldb_oper)
+
+ if min_pwd_age is not None:
+ min_pwd_age_ticks = days_to_timestamp(min_pwd_age)
+ msg_add_attr(m, "msDS-MinimumPasswordAge", min_pwd_age_ticks,
+ ldb_oper)
+
+ if max_pwd_age is not None:
+ # Windows won't let you set max-pwd-age to zero. Here we take zero to
+ # mean 'never expire' and use the timestamp corresponding to 'never'
+ if max_pwd_age == 0:
+ max_pwd_age_ticks = str(NEVER_TIMESTAMP)
+ else:
+ max_pwd_age_ticks = days_to_timestamp(max_pwd_age)
+ msg_add_attr(m, "msDS-MaximumPasswordAge", max_pwd_age_ticks, ldb_oper)
+
+ if lockout_duration is not None:
+ lockout_duration_ticks = mins_to_timestamp(lockout_duration)
+ msg_add_attr(m, "msDS-LockoutDuration", lockout_duration_ticks,
+ ldb_oper)
+
+ if lockout_threshold is not None:
+ msg_add_attr(m, "msDS-LockoutThreshold", str(lockout_threshold),
+ ldb_oper)
+
+ if reset_lockout_after is not None:
+ msg_add_attr(m, "msDS-LockoutObservationWindow",
+ mins_to_timestamp(reset_lockout_after), ldb_oper)
+
+ return m
+
+
+def check_pso_constraints(min_pwd_length=None, history_length=None,
+ min_pwd_age=None, max_pwd_age=None):
+ """Checks PSO settings fall within valid ranges"""
+
+ # check values as per section 3.1.1.5.2.2 Constraints in MS-ADTS spec
+ if history_length is not None and history_length > 1024:
+ raise CommandError("Bad password history length: "
+ "valid range is 0 to 1024")
+
+ if min_pwd_length is not None and min_pwd_length > 255:
+ raise CommandError("Bad minimum password length: "
+ "valid range is 0 to 255")
+
+ if min_pwd_age is not None and max_pwd_age is not None:
+ # note max-age=zero is a special case meaning 'never expire'
+ if min_pwd_age >= max_pwd_age and max_pwd_age != 0:
+ raise CommandError("Minimum password age must be less than "
+ "maximum age")
+
+
+# the same args are used for both create and set commands
+pwd_settings_options = [
+ Option("--complexity", type="choice", choices=["on", "off"],
+ help="The password complexity (on | off)."),
+ Option("--store-plaintext", type="choice", choices=["on", "off"],
+ help="Store plaintext passwords where account have "
+ "'store passwords with reversible encryption' set (on | off)."),
+ Option("--history-length",
+ help="The password history length (<integer>).", type=int),
+ Option("--min-pwd-length",
+ help="The minimum password length (<integer>).", type=int),
+ Option("--min-pwd-age",
+ help=("The minimum password age (<integer in days>). "
+ "Default is domain setting."), type=int),
+ Option("--max-pwd-age",
+ help=("The maximum password age (<integer in days>). "
+ "Default is domain setting."), type=int),
+ Option("--account-lockout-duration", type=int,
+ help=("The length of time an account is locked out after exceeding "
+ "the limit on bad password attempts (<integer in mins>). "
+ "Default is domain setting")),
+ Option("--account-lockout-threshold", type=int,
+ help=("The number of bad password attempts allowed before locking "
+ "out the account (<integer>). Default is domain setting.")),
+ Option("--reset-account-lockout-after",
+ help=("After this time is elapsed, the recorded number of attempts "
+ "restarts from zero (<integer in mins>). "
+ "Default is domain setting."), type=int)]
+
+
+def num_options_in_args(options, args):
+ """
+ Returns the number of options specified that are present in the args.
+ (There can be other args besides just the ones we're interested in, which
+ is why argc on its own is not enough)
+ """
+ num_opts = 0
+ for opt in options:
+ for arg in args:
+ # The option should be a sub-string of the CLI argument for a match
+ if str(opt) in arg:
+ num_opts += 1
+ return num_opts
+
+
+class cmd_domain_pwdsettings_pso_create(Command):
+ """Creates a new Password Settings Object (PSO).
+
+ PSOs are a way to tailor different password settings (lockout policy,
+ minimum password length, etc) for specific users or groups.
+
+ The psoname is a unique name for the new Password Settings Object.
+ When multiple PSOs apply to a user, the precedence determines which PSO
+ will take effect. The PSO with the lowest precedence will take effect.
+
+ For most arguments, the default value (if unspecified) is the current
+ domain passwordsettings value. To see these values, enter the command
+ 'samba-tool domain passwordsettings show'.
+
+ To apply the new PSO to user(s) or group(s), enter the command
+ 'samba-tool domain passwordsettings pso apply'.
+ """
+
+ synopsis = "%prog <psoname> <precedence> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = pwd_settings_options + [
+ Option("-H", "--URL", help="LDB URL for database or target server",
+ metavar="URL", dest="H", type=str)
+ ]
+ takes_args = ["psoname", "precedence"]
+
+ def run(self, psoname, precedence, H=None, min_pwd_age=None,
+ max_pwd_age=None, 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)
+
+ try:
+ precedence = int(precedence)
+ except ValueError:
+ raise CommandError("The PSO's precedence should be "
+ "a numerical value. Try --help")
+
+ # sanity-check that the PSO doesn't already exist
+ pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
+ try:
+ res = samdb.search(pso_dn, scope=ldb.SCOPE_BASE)
+ except ldb.LdbError as e:
+ if e.args[0] == ldb.ERR_NO_SUCH_OBJECT:
+ pass
+ else:
+ raise
+ else:
+ raise CommandError("PSO '%s' already exists" % psoname)
+
+ # we expect the user to specify at least one password-policy setting,
+ # otherwise there's no point in creating a PSO
+ num_pwd_args = num_options_in_args(pwd_settings_options, self.raw_argv)
+ if num_pwd_args == 0:
+ raise CommandError("Please specify at least one password policy "
+ "setting. Try --help")
+
+ # it's unlikely that the user will specify all 9 password policy
+ # settings on the CLI - current domain password-settings as the default
+ # values for unspecified arguments
+ if num_pwd_args < len(pwd_settings_options):
+ self.message("Not all password policy options "
+ "have been specified.")
+ self.message("For unspecified options, the current domain password"
+ " settings will be used as the default values.")
+
+ # lookup the current domain password-settings
+ res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_BASE,
+ attrs=["pwdProperties", "pwdHistoryLength", "minPwdLength",
+ "minPwdAge", "maxPwdAge", "lockoutDuration",
+ "lockoutThreshold", "lockOutObservationWindow"])
+ assert(len(res) == 1)
+
+ # use the domain settings for any missing arguments
+ pwd_props = int(res[0]["pwdProperties"][0])
+ if complexity is None:
+ prop_flag = DOMAIN_PASSWORD_COMPLEX
+ complexity = "on" if pwd_props & prop_flag else "off"
+
+ if store_plaintext is None:
+ prop_flag = DOMAIN_PASSWORD_STORE_CLEARTEXT
+ store_plaintext = "on" if pwd_props & prop_flag else "off"
+
+ if history_length is None:
+ history_length = int(res[0]["pwdHistoryLength"][0])
+
+ if min_pwd_length is None:
+ min_pwd_length = int(res[0]["minPwdLength"][0])
+
+ if min_pwd_age is None:
+ min_pwd_age = timestamp_to_days(res[0]["minPwdAge"][0])
+
+ if max_pwd_age is None:
+ max_pwd_age = timestamp_to_days(res[0]["maxPwdAge"][0])
+
+ if account_lockout_duration is None:
+ account_lockout_duration = \
+ timestamp_to_mins(res[0]["lockoutDuration"][0])
+
+ if account_lockout_threshold is None:
+ account_lockout_threshold = int(res[0]["lockoutThreshold"][0])
+
+ if reset_account_lockout_after is None:
+ reset_account_lockout_after = \
+ timestamp_to_mins(res[0]["lockOutObservationWindow"][0])
+
+ check_pso_constraints(max_pwd_age=max_pwd_age, min_pwd_age=min_pwd_age,
+ history_length=history_length,
+ min_pwd_length=min_pwd_length)
+
+ # pack the settings into an LDB message
+ m = make_pso_ldb_msg(self.outf, samdb, pso_dn, create=True,
+ complexity=complexity, precedence=precedence,
+ store_plaintext=store_plaintext,
+ history_length=history_length,
+ min_pwd_length=min_pwd_length,
+ min_pwd_age=min_pwd_age, max_pwd_age=max_pwd_age,
+ lockout_duration=account_lockout_duration,
+ lockout_threshold=account_lockout_threshold,
+ reset_lockout_after=reset_account_lockout_after)
+
+ # create the new PSO
+ try:
+ samdb.add(m)
+ self.message("PSO successfully created: %s" % pso_dn)
+ # display the new PSO's settings
+ show_pso_by_dn(self.outf, samdb, pso_dn, show_applies_to=False)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ if num == ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS:
+ raise CommandError("Administrator permissions are needed "
+ "to create a PSO.")
+ else:
+ raise CommandError("Failed to create PSO '%s': %s" % (pso_dn,
+ msg))
+
+
+class cmd_domain_pwdsettings_pso_set(Command):
+ """Modifies a Password Settings Object (PSO)."""
+
+ synopsis = "%prog <psoname> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptions,
+ }
+
+ takes_options = pwd_settings_options + [
+ Option("--precedence", type=int,
+ help=("This PSO's precedence relative to other PSOs. "
+ "Lower precedence is better (<integer>).")),
+ Option("-H", "--URL", help="LDB URL for database or target server",
+ type=str, metavar="URL", dest="H"),
+ ]
+ takes_args = ["psoname"]
+
+ def run(self, psoname, H=None, precedence=None, min_pwd_age=None,
+ max_pwd_age=None, 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)
+
+ # sanity-check the PSO exists
+ pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
+ check_pso_valid(samdb, pso_dn, psoname)
+
+ # we expect the user to specify at least one password-policy setting
+ num_pwd_args = num_options_in_args(pwd_settings_options, self.raw_argv)
+ if num_pwd_args == 0 and precedence is None:
+ raise CommandError("Please specify at least one password policy "
+ "setting. Try --help")
+
+ if min_pwd_age is not None or max_pwd_age is not None:
+ # if we're modifying either the max or min pwd-age, check the max
+ # is always larger. We may have to fetch the PSO's setting to
+ # verify this
+ res = samdb.search(pso_dn, scope=ldb.SCOPE_BASE,
+ attrs=['msDS-MinimumPasswordAge',
+ 'msDS-MaximumPasswordAge'])
+ if min_pwd_age is None:
+ min_pwd_ticks = res[0]['msDS-MinimumPasswordAge'][0]
+ min_pwd_age = timestamp_to_days(min_pwd_ticks)
+
+ if max_pwd_age is None:
+ max_pwd_ticks = res[0]['msDS-MaximumPasswordAge'][0]
+ max_pwd_age = timestamp_to_days(max_pwd_ticks)
+
+ check_pso_constraints(max_pwd_age=max_pwd_age, min_pwd_age=min_pwd_age,
+ history_length=history_length,
+ min_pwd_length=min_pwd_length)
+
+ # pack the settings into an LDB message
+ m = make_pso_ldb_msg(self.outf, samdb, pso_dn, create=False,
+ complexity=complexity, precedence=precedence,
+ store_plaintext=store_plaintext,
+ history_length=history_length,
+ min_pwd_length=min_pwd_length,
+ min_pwd_age=min_pwd_age, max_pwd_age=max_pwd_age,
+ lockout_duration=account_lockout_duration,
+ lockout_threshold=account_lockout_threshold,
+ reset_lockout_after=reset_account_lockout_after)
+
+ # update the PSO
+ try:
+ samdb.modify(m)
+ self.message("Successfully updated PSO: %s" % pso_dn)
+ # display the new PSO's settings
+ show_pso_by_dn(self.outf, samdb, pso_dn, show_applies_to=False)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ raise CommandError("Failed to update PSO '%s': %s" % (pso_dn, msg))
+
+
+class cmd_domain_pwdsettings_pso_delete(Command):
+ """Deletes a Password Settings Object (PSO)."""
+
+ synopsis = "%prog <psoname> [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",
+ metavar="URL", dest="H", type=str)
+ ]
+ takes_args = ["psoname"]
+
+ def run(self, psoname, 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)
+
+ pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
+ # sanity-check the PSO exists
+ check_pso_valid(samdb, pso_dn, psoname)
+
+ samdb.delete(pso_dn)
+ self.message("Deleted PSO %s" % psoname)
+
+
+def pso_key(a):
+ a_precedence = int(a['msDS-PasswordSettingsPrecedence'][0])
+ return a_precedence
+
+
+class cmd_domain_pwdsettings_pso_list(Command):
+ """Lists all Password Settings Objects (PSOs)."""
+
+ 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",
+ metavar="URL", dest="H", type=str)
+ ]
+
+ 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)
+
+ res = samdb.search(pso_container(samdb), scope=ldb.SCOPE_SUBTREE,
+ attrs=['name', 'msDS-PasswordSettingsPrecedence'],
+ expression="(objectClass=msDS-PasswordSettings)")
+
+ # an unprivileged search against Windows returns nothing here. On Samba
+ # we get the PSO names, but not their attributes
+ if len(res) == 0 or 'msDS-PasswordSettingsPrecedence' not in res[0]:
+ self.outf.write("No PSOs are present, or you don't have permission"
+ " to view them.\n")
+ return
+
+ # sort the PSOs so they're displayed in order of precedence
+ pso_list = sorted(res, key=pso_key)
+
+ self.outf.write("Precedence | PSO name\n")
+ self.outf.write("--------------------------------------------------\n")
+
+ for pso in pso_list:
+ precedence = pso['msDS-PasswordSettingsPrecedence']
+ self.outf.write("%-10s | %s\n" % (precedence, pso['name']))
+
+
+class cmd_domain_pwdsettings_pso_show(Command):
+ """Display a Password Settings Object's details."""
+
+ synopsis = "%prog <psoname> [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",
+ metavar="URL", dest="H", type=str)
+ ]
+ takes_args = ["psoname"]
+
+ def run(self, psoname, 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)
+
+ pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
+ check_pso_valid(samdb, pso_dn, psoname)
+ show_pso_by_dn(self.outf, samdb, pso_dn)
+
+
+class cmd_domain_pwdsettings_pso_show_user(Command):
+ """Displays the Password Settings that apply to a user."""
+
+ synopsis = "%prog <username> [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",
+ metavar="URL", dest="H", type=str)
+ ]
+ takes_args = ["username"]
+
+ def run(self, username, 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)
+
+ show_pso_for_user(self.outf, samdb, username)
+
+
+class cmd_domain_pwdsettings_pso_apply(Command):
+ """Applies a PSO's password policy to a user or group.
+
+ When a PSO is applied to a group, it will apply to all users (and groups)
+ that are members of that group. If a PSO applies directly to a user, it
+ will override any group membership PSOs for that user.
+
+ When multiple PSOs apply to a user, either directly or through group
+ membership, the PSO with the lowest precedence will take effect.
+ """
+
+ synopsis = "%prog <psoname> <user-or-group-name> [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",
+ metavar="URL", dest="H", type=str)
+ ]
+ takes_args = ["psoname", "user_or_group"]
+
+ def run(self, psoname, user_or_group, 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)
+
+ pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
+ # sanity-check the PSO exists
+ check_pso_valid(samdb, pso_dn, psoname)
+
+ # lookup the user/group by account-name to gets its DN
+ search_filter = "(sAMAccountName=%s)" % user_or_group
+ res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter)
+
+ if len(res) == 0:
+ raise CommandError("The specified user or group '%s' was not found"
+ % user_or_group)
+
+ # modify the PSO to apply to the user/group specified
+ target_dn = str(res[0].dn)
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, pso_dn)
+ m["msDS-PSOAppliesTo"] = ldb.MessageElement(target_dn,
+ ldb.FLAG_MOD_ADD,
+ "msDS-PSOAppliesTo")
+ try:
+ samdb.modify(m)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ # most likely error - PSO already applies to that user/group
+ if num == ldb.ERR_ATTRIBUTE_OR_VALUE_EXISTS:
+ raise CommandError("PSO '%s' already applies to '%s'"
+ % (psoname, user_or_group))
+ else:
+ raise CommandError("Failed to update PSO '%s': %s" % (psoname,
+ msg))
+
+ self.message("PSO '%s' applied to '%s'" % (psoname, user_or_group))
+
+
+class cmd_domain_pwdsettings_pso_unapply(Command):
+ """Updates a PSO to no longer apply to a user or group."""
+
+ synopsis = "%prog <psoname> <user-or-group-name> [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",
+ metavar="URL", dest="H", type=str),
+ ]
+ takes_args = ["psoname", "user_or_group"]
+
+ def run(self, psoname, user_or_group, 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)
+
+ pso_dn = "CN=%s,%s" % (psoname, pso_container(samdb))
+ # sanity-check the PSO exists
+ check_pso_valid(samdb, pso_dn, psoname)
+
+ # lookup the user/group by account-name to gets its DN
+ search_filter = "(sAMAccountName=%s)" % user_or_group
+ res = samdb.search(samdb.domain_dn(), scope=ldb.SCOPE_SUBTREE,
+ expression=search_filter)
+
+ if len(res) == 0:
+ raise CommandError("The specified user or group '%s' was not found"
+ % user_or_group)
+
+ # modify the PSO to apply to the user/group specified
+ target_dn = str(res[0].dn)
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, pso_dn)
+ m["msDS-PSOAppliesTo"] = ldb.MessageElement(target_dn,
+ ldb.FLAG_MOD_DELETE,
+ "msDS-PSOAppliesTo")
+ try:
+ samdb.modify(m)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ # most likely error - PSO doesn't apply to that user/group
+ if num == ldb.ERR_NO_SUCH_ATTRIBUTE:
+ raise CommandError("PSO '%s' doesn't apply to '%s'"
+ % (psoname, user_or_group))
+ else:
+ raise CommandError("Failed to update PSO '%s': %s" % (psoname,
+ msg))
+ self.message("PSO '%s' no longer applies to '%s'" % (psoname,
+ user_or_group))
+
+
+class cmd_domain_passwordsettings_pso(SuperCommand):
+ """Manage fine-grained Password Settings Objects (PSOs)."""
+
+ subcommands = {}
+ subcommands["apply"] = cmd_domain_pwdsettings_pso_apply()
+ subcommands["create"] = cmd_domain_pwdsettings_pso_create()
+ subcommands["delete"] = cmd_domain_pwdsettings_pso_delete()
+ subcommands["list"] = cmd_domain_pwdsettings_pso_list()
+ subcommands["set"] = cmd_domain_pwdsettings_pso_set()
+ subcommands["show"] = cmd_domain_pwdsettings_pso_show()
+ subcommands["show-user"] = cmd_domain_pwdsettings_pso_show_user()
+ subcommands["unapply"] = cmd_domain_pwdsettings_pso_unapply()