The branch, master has been updated via 2fa2f13 dsdb: Avoid calculating the PSO multiple times via 3779367 dsdb: Avoid performance hit if PSOs aren't actually used via b7d1c5a tests: Add tests for domain pwdHistoryLength via c10e1af tests: Extend passwordsettings tests to cover PSO command options via de131c1 netcmd: Add samba-tool support for managing PSOs via 1ebfe69 dsdb: Use PSO maxPwdAge for operational msDS-PasswordExpiryTimeComputed via 3b849f8 dsdb: Update password_hash to use PSO settings for password changes via 0ac464d dsdb: Move anonymous domain_data struct via e40af27 dsdb: Lookup PSO's lockout settings for password_hash modifies via 05e25a7 rpc/samr: Fix PSO support in SAMR password_change RPC via 7060702 dsdb/rpc: Update effective badPwdCount to use PSO settings via 5246d48 dsdb: PSO support for msDS-User-Account-Control-Computed via 442a38c dsdb/auth: Use PSO settings for lockOutThreshold/Duration via 6f82161 tests: Extend PSO tests to cover password-history/length/complexity via 4c42d3f dsdb: Add msDS-ResultantPSO constructed attribute support from 754a840 autobuild: build ldb --without-ldb-lmdb
https://git.samba.org/?p=samba.git;a=shortlog;h=master - Log ----------------------------------------------------------------- commit 2fa2f132ae3de9a403b2d93d586570f59250de23 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Wed May 16 09:45:32 2018 +1200 dsdb: Avoid calculating the PSO multiple times In a typical user login query, the code tries to work out the PSO 2-3 times - once for the msDS-ResultantPSO attribute, and then again for the msDS-User-Account-Control-Computed & msDS-UserPasswordExpiryTimeComputed constructed attributes. The PSO calculation is reasonably expensive, mostly due to the nested groups calculation. If we've already constructed the msDS-ResultantPSO attribute, then we can save ourselves extra work by just re-fetching the result directly, rather than expanding the nested groups again from scratch. The previous patch improves efficiency when there are no PSOs in the system. This should improve the case where there are PSOs that apply to the users. (Unfortunately, it won't help where there are some PSOs in the system, but no PSO applies to the user being queried). Also updated sam.c so the msDS-ResultantPSO gets calculated first, before the other constructed attributes. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> Autobuild-User(master): Garming Sam <garm...@samba.org> Autobuild-Date(master): Wed May 23 10:09:11 CEST 2018 on sn-devel-144 commit 3779367329646215065e1608ef065930b581e854 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Tue May 15 14:02:32 2018 +1200 dsdb: Avoid performance hit if PSOs aren't actually used The new PSO code adds some additional overhead in extra lookups. To avoid penalizing existing setups, we can short-circuit the PSO processing and return early if there are no actual PSO objects in the DB. The one-level search should be very quick, and it avoids the need to do more complicated PSO processing (i.e. expanding the nested groups). The longer-term plan is to rework the tokenGroups lookup so that it only gets done once, and the result can then be reused by the resultant-PSO code (rather than computing the nested-groups again). However, in the short-term, a slight decrease in performance is the price for any users that want to deploy PSOs. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit b7d1c5aae8530126f1e561911ae3adaa527c189f Author: Tim Beale <timbe...@catalyst.net.nz> Date: Fri May 11 09:29:01 2018 +1200 tests: Add tests for domain pwdHistoryLength This is not related to PSOs at all, but there's a minor discrepancy between Windows and Samba password-history-length behaviour that I noticed during PSO testing. When the pwdHistoryLength changes from zero to non-zero, Windows includes the user's current password as invalid immediately, whereas Samba only includes it as invalid *after* it next changes. It's a fairly obscure corner-case, and we might not care enough about it to fix it. However, I've added a test case to highlight the difference and marked it as a known-fail for now. I also added a general pwdHistoryLength test case to show that the basics work (this didn't seem to be tested anywhere else). Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit c10e1af005270d6d212f8e703d0fb7c5ff3430bc Author: Tim Beale <timbe...@catalyst.net.nz> Date: Fri May 11 11:49:23 2018 +1200 tests: Extend passwordsettings tests to cover PSO command options Add test cases for the new PSO samba-tool command options. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit de131c16a6b8505f8f11783537e9490554de44b2 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Mon Apr 23 10:47:21 2018 +1200 netcmd: Add samba-tool support for managing PSOs Add a new command 'samba-tool domain passwordsettings pso', with the sub-command options: create, delete, set, list, show, show-user, apply, unapply. The apply and unapply options apply the PSO to a user or group. The show-user option shows the actual PSO (and its settings) that will take effect for a given user. The new commands are pretty self-contained in a new pso.py file. We decided to add these new commands under the existing 'samba-tool domain passwordsettings' command, as that's what users would be already familiar with. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 1ebfe6957fd8995e7c7690c842e196e6c649b6db Author: Tim Beale <timbe...@catalyst.net.nz> Date: Thu Apr 19 13:51:36 2018 +1200 dsdb: Use PSO maxPwdAge for operational msDS-PasswordExpiryTimeComputed When calculating the Password-Expiry-Time, we should use the PSO's max-password-age setting, if one applies to the user. This is code may be inefficient, as it may repeat the PSO-lookup work several times (once for each constructed attribute that tries to use it). For now, I've gone for the simplest code change, and efficiency can be addressed in a subsequent patch (once we have a good test to measure it). Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 3b849f87f7237a3677338075309abb1355a4d9ef Author: Tim Beale <timbe...@catalyst.net.nz> Date: Thu Apr 19 10:46:48 2018 +1200 dsdb: Update password_hash to use PSO settings for password changes Honour the settings in the PSO when changing the password, i.e. msDS-PasswordComplexityEnabled, msDS-PasswordHistoryLength, etc. The password_hash code populates dsdb_control_password_change_status's domain_data with the password settings to use - these are currently based on the settings for the domain. Now, if the password_hash code has worked out that a PSO applies to the user, we override the domain settings with the PSO's values. This change means the password_settings tests now pass. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 0ac464df4543154ee8a1cbf03684d8b99bcb92b3 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Thu Apr 19 09:47:42 2018 +1200 dsdb: Move anonymous domain_data struct Anonymous structs and 80 character line-lengths don't mix well. Allow the struct to be referenced directly. With the introduction of PSOs, the password-settings are now calculated per-user rather than per-domain. I've tried to reflect this in the struct name. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit e40af276f8d0eb8fd7e38094101b1874177ea6b0 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Wed Apr 18 14:21:46 2018 +1200 dsdb: Lookup PSO's lockout settings for password_hash modifies When a user's password-hash is modified, we need the PSO settings for that user, so that any lockout settings get applied correctly. To do this, we query the msDS-ResultantPSO in the user search. Then, if a PSO applies to the user, we add in a extra search to retrieve the PSO's settings. Once the PSO search completes, we continue with the modify operation. In the event of error cases, I've tried to fallback to logging the problem and continuing with the default domain settings. However, unusual internal errors will still fail the operation. We can pass the PSO result into dsdb_update_bad_pwd_count(), which means the PSO's lockout-threshold and observation-window are now used. This is enough to get the remaining lockout tests passing. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 05e25a728c9260fe1696500ed26a7c4f9ad85c57 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Tue May 8 16:07:54 2018 +1200 rpc/samr: Fix PSO support in SAMR password_change RPC To get the SAMR password_lockout test passing, we now just need to query the msDS-ResultantPSO attribute for the user in the SAMR code. The common code will then determine that a PSO applies to the user, and use the PSO's lockout settings. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 706070274da9054bd0fbd7732b8304dee1d30f20 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Wed Apr 11 10:33:21 2018 +1200 dsdb/rpc: Update effective badPwdCount to use PSO settings The lockOutObservationWindow is used to calculate the badPwdCount. When a PSO applies to a user, we want to use the PSO's lockout-observation window rather the the default domain setting. This is finally enough to get some of the PSO password_lockout tests to pass. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 5246d480b1e3aba4921b27293f2573bbee380570 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Tue May 8 15:11:30 2018 +1200 dsdb: PSO support for msDS-User-Account-Control-Computed msDS-User-Account-Control-Computed uses the effective-lockoutDuration to determine if a user is locked out or not. If a PSO applies to the user, then the effective-lockoutDuration is the PSO's msDS-LockoutDuration setting. Otherwise it is the domain default lockoutDuration value. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 442a38c918ae1666b35285013365553b39837f14 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Fri Apr 6 16:42:50 2018 +1200 dsdb/auth: Use PSO settings for lockOutThreshold/Duration If a PSO applies to a user, use its lockOutThreshold/Duration settings instead of the domain setting. When we lookup a user, we now include the msDS-ResultantPSO attribute. If the attribute is present for a user, then we lookup the corresponding PSO object to get the lockOutThreshold/ Duration settings. Note: This is not quite enough to make the PSO lockout tests pass, as msDS-User-Account-Control-Computed is still constructed based on the domain lockoutDuration setting rather than the PSO. Updating the password_hash.c code properly will be done in a subsequent commit. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 6f82161caf299059c6d35bf28b9dfd8c1e4ddb30 Author: Tim Beale <timbe...@catalyst.net.nz> Date: Tue May 8 14:45:17 2018 +1200 tests: Extend PSO tests to cover password-history/length/complexity Unhobble the PSO test cases so that they not only check the msDS-ResultantPSO constructed attribute, but also that the corresponding PSO's password-history, minimum password length, and complexity settings are actually used. The tests now fail once more, as actually using the PSO's settings isn't implemented yet. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> commit 4c42d3f7165e7532cd95645b7b27173a32fa53df Author: Tim Beale <timbe...@catalyst.net.nz> Date: Wed Mar 21 10:45:38 2018 +1300 dsdb: Add msDS-ResultantPSO constructed attribute support Add support for the msDS-ResultantPSO constructed attribute, which indicates the PSO (if any) that should apply to a given user. First we consider any PSOs that apply directly to a user. If none apply directly, we consider PSOs that apply to any groups the user is a member of. (PSO lookups are done by finding any 'msDS-PSOAppliesTo' links that apply to the user or group SIDs we're interested in. Note: the PSO should be selected based on the RevMembGetAccountGroups membership, which doesn't include builtin groups. Looking at the spec, it appears that perhaps our tokenGroups implementation should also exclude builtin groups. However, in the short-term, I've added a new ACCOUNT_GROUPS option to the enum, which is only used internally for PSOs. The PSO test cases (which are currently only checking the constructed attribute) now pass, showing that the correct msDS-ResultantPSO value is being returned, even if the corresponding password-policy settings are not yet being applied. Signed-off-by: Tim Beale <timbe...@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abart...@samba.org> Reviewed-by: Garming Sam <garm...@catalyst.net.nz> ----------------------------------------------------------------------- Summary of changes: docs-xml/manpages/samba-tool.8.xml | 45 ++ python/samba/netcmd/domain.py | 3 + python/samba/netcmd/pso.py | 766 ++++++++++++++++++++++ python/samba/tests/samba_tool/passwordsettings.py | 369 +++++++++++ selftest/knownfail.d/password_hash_gpgme | 2 - selftest/knownfail.d/password_lockout | 6 - selftest/knownfail.d/password_settings | 10 +- source4/auth/sam.c | 55 +- source4/dsdb/common/util.c | 104 ++- source4/dsdb/samdb/ldb_modules/operational.c | 482 +++++++++++++- source4/dsdb/samdb/ldb_modules/password_hash.c | 213 +++++- source4/dsdb/samdb/samdb.h | 24 +- source4/dsdb/tests/python/password_settings.py | 67 +- source4/rpc_server/samr/dcesrv_samr.c | 2 + source4/rpc_server/samr/samr_password.c | 2 + 15 files changed, 2099 insertions(+), 51 deletions(-) create mode 100644 python/samba/netcmd/pso.py delete mode 100644 selftest/knownfail.d/password_hash_gpgme delete mode 100644 selftest/knownfail.d/password_lockout Changeset truncated at 500 lines: diff --git a/docs-xml/manpages/samba-tool.8.xml b/docs-xml/manpages/samba-tool.8.xml index 3173083..f2154b9 100644 --- a/docs-xml/manpages/samba-tool.8.xml +++ b/docs-xml/manpages/samba-tool.8.xml @@ -334,6 +334,51 @@ </refsect3> <refsect3> + <title>domain passwordsettings pso</title> + <para>Manage fine-grained Password Settings Objects (PSOs).</para> +</refsect3> + +<refsect3> + <title>domain passwordsettings pso apply <replaceable>pso-name</replaceable> <replaceable>user-or-group-name</replaceable> [options]</title> + <para>Applies a PSO's password policy to a user or group.</para> +</refsect3> + +<refsect3> + <title>domain passwordsettings pso create <replaceable>pso-name</replaceable> <replaceable>precedence</replaceable> [options]</title> + <para>Creates a new Password Settings Object (PSO).</para> +</refsect3> + +<refsect3> + <title>domain passwordsettings pso delete <replaceable>pso-name</replaceable> [options]</title> + <para>Deletes a Password Settings Object (PSO).</para> +</refsect3> + +<refsect3> + <title>domain passwordsettings pso list [options]</title> + <para>Lists all Password Settings Objects (PSOs).</para> +</refsect3> + +<refsect3> + <title>domain passwordsettings pso set <replaceable>pso-name</replaceable> [options]</title> + <para>Modifies a Password Settings Object (PSO).</para> +</refsect3> + +<refsect3> + <title>domain passwordsettings pso show <replaceable>user-name</replaceable> [options]</title> + <para>Displays a Password Settings Object (PSO).</para> +</refsect3> + +<refsect3> + <title>domain passwordsettings pso show-user <replaceable>pso-name</replaceable> [options]</title> + <para>Displays the Password Settings that apply to a user.</para> +</refsect3> + +<refsect3> + <title>domain passwordsettings pso unapply <replaceable>pso-name</replaceable> <replaceable>user-or-group-name</replaceable> [options]</title> + <para>Updates a PSO to no longer apply to a user or group.</para> +</refsect3> + +<refsect3> <title>domain provision</title> <para>Promote an existing domain member or NT4 PDC to an AD DC.</para> </refsect3> diff --git a/python/samba/netcmd/domain.py b/python/samba/netcmd/domain.py index f4a689b..6698fc9 100644 --- a/python/samba/netcmd/domain.py +++ b/python/samba/netcmd/domain.py @@ -99,6 +99,8 @@ from samba.provision.common import ( FILL_DRS ) +from samba.netcmd.pso import cmd_domain_passwordsettings_pso + string_version_to_constant = { "2008_R2" : DS_DOMAIN_FUNCTION_2008_R2, "2012": DS_DOMAIN_FUNCTION_2012, @@ -1530,6 +1532,7 @@ 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/pso.py b/python/samba/netcmd/pso.py new file mode 100644 index 0000000..b12f00d --- /dev/null +++ b/python/samba/netcmd/pso.py @@ -0,0 +1,766 @@ +# Manages Password Settings Objects +# +# Copyright (C) Andrew Bartlett <abart...@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 +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 + +import pdb + +NEVER_TIMESTAMP = int(-0x8000000000000000) + +def pso_container(samdb): + return "CN=Password Settings Container,CN=System,%s" % samdb.domain_dn() + +def timestamp_to_mins(timestamp_str): + """Converts a timestamp in -100 nanosecond units to minutes""" + # treat a timestamp of 'never' the same as zero (this should work OK for + # most settings, and it displays better than trying to convert + # -0x8000000000000000 to minutes) + if int(timestamp_str) == NEVER_TIMESTAMP: + return 0 + else: + return abs(int(timestamp_str)) / (1e7 * 60) + +def timestamp_to_days(timestamp_str): + """Converts a timestamp in -100 nanosecond units to days""" + return timestamp_to_mins(timestamp_str) / (60 * 24) + +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 Exception as e: + raise CommandError("Unable to find PSO '%s'" % name) + + # 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 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_account_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: + m["msDS-PasswordSettingsPrecedence"] = ldb.MessageElement(str(precedence), + ldb_oper, "msDS-PasswordSettingsPrecedence") + + if complexity is not None: + bool_str = "TRUE" if complexity == "on" else "FALSE" + m["msDS-PasswordComplexityEnabled"] = ldb.MessageElement(bool_str, + ldb_oper, "msDS-PasswordComplexityEnabled") + + if store_plaintext is not None: + bool_str = "TRUE" if store_plaintext == "on" else "FALSE" + m["msDS-msDS-PasswordReversibleEncryptionEnabled"] = \ + ldb.MessageElement(bool_str, ldb_oper, + "msDS-PasswordReversibleEncryptionEnabled") + + if history_length is not None: + m["msDS-PasswordHistoryLength"] = ldb.MessageElement(str(history_length), + ldb_oper, "msDS-PasswordHistoryLength") + + if min_pwd_length is not None: + m["msDS-MinimumPasswordLength"] = ldb.MessageElement(str(min_pwd_length), + ldb_oper, "msDS-MinimumPasswordLength") + + if min_pwd_age is not None: + min_pwd_age_ticks = days_to_timestamp(min_pwd_age) + m["msDS-MinimumPasswordAge"] = ldb.MessageElement(min_pwd_age_ticks, + ldb_oper, "msDS-MinimumPasswordAge") + + 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) + m["msDS-MaximumPasswordAge"] = ldb.MessageElement(max_pwd_age_ticks, + ldb_oper, "msDS-MaximumPasswordAge") + + if lockout_duration is not None: + lockout_duration_ticks = mins_to_timestamp(lockout_duration) + m["msDS-LockoutDuration"] = ldb.MessageElement(lockout_duration_ticks, + ldb_oper, "msDS-LockoutDuration") + + if lockout_threshold is not None: + m["msDS-LockoutThreshold"] = ldb.MessageElement(str(lockout_threshold), + ldb_oper, "msDS-LockoutThreshold") + + if reset_account_lockout_after is not None: + observation_window_ticks = mins_to_timestamp(reset_account_lockout_after) + m["msDS-LockoutObservationWindow"] = ldb.MessageElement(observation_window_ticks, + ldb_oper, "msDS-LockoutObservationWindow") + + 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 the 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", + help="The the length of time an account is locked out after exeeding the limit on bad password attempts (<integer in mins>). Default is domain setting", type=int), + Option("--account-lockout-threshold", + help="The number of bad password attempts allowed before locking out the account (<integer>). Default is domain setting.", type=int), + 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", type=str, + metavar="URL", dest="H"), + ] + 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 Exception as e: + pass + 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_account_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]" + -- Samba Shared Repository