# Manages Password Settings Objects # # Copyright (C) Andrew Bartlett 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 . # 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 ().", type=int), Option("--min-pwd-length", help="The minimum password length ().", type=int), Option("--min-pwd-age", help=("The minimum password age (). " "Default is domain setting."), type=int), Option("--max-pwd-age", help=("The maximum password age (). " "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 (). " "Default is domain setting")), Option("--account-lockout-threshold", type=int, help=("The number of bad password attempts allowed before locking " "out the account (). Default is domain setting.")), Option("--reset-account-lockout-after", help=("After this time is elapsed, the recorded number of attempts " "restarts from zero (). " "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 [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 [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 ().")), 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 [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 [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 [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 [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 [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()