[SCM] Samba Shared Repository - branch master updated

Garming Sam garming at samba.org
Wed May 23 08:10:02 UTC 2018


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 <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>
    
    Autobuild-User(master): Garming Sam <garming at samba.org>
    Autobuild-Date(master): Wed May 23 10:09:11 CEST 2018 on sn-devel-144

commit 3779367329646215065e1608ef065930b581e854
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit b7d1c5aae8530126f1e561911ae3adaa527c189f
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit c10e1af005270d6d212f8e703d0fb7c5ff3430bc
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit de131c16a6b8505f8f11783537e9490554de44b2
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 1ebfe6957fd8995e7c7690c842e196e6c649b6db
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 3b849f87f7237a3677338075309abb1355a4d9ef
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 0ac464df4543154ee8a1cbf03684d8b99bcb92b3
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit e40af276f8d0eb8fd7e38094101b1874177ea6b0
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 05e25a728c9260fe1696500ed26a7c4f9ad85c57
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 706070274da9054bd0fbd7732b8304dee1d30f20
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 5246d480b1e3aba4921b27293f2573bbee380570
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 442a38c918ae1666b35285013365553b39837f14
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 6f82161caf299059c6d35bf28b9dfd8c1e4ddb30
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at catalyst.net.nz>

commit 4c42d3f7165e7532cd95645b7b27173a32fa53df
Author: Tim Beale <timbeale at 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 <timbeale at catalyst.net.nz>
    Reviewed-by: Andrew Bartlett <abartlet at samba.org>
    Reviewed-by: Garming Sam <garming at 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 <abartlet at 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



More information about the samba-cvs mailing list