From ce81747874bb003c78e95c4dcf6239b9251ceeaa Mon Sep 17 00:00:00 2001
From: Sumit Bose <sbose@redhat.com>
Date: Wed, 15 Jun 2022 09:07:33 +0200
Subject: [PATCH] ldap: allow password changes with shadow pwd policy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Currently a password change is rejected if
"ldap_pwd_policy = shadow" is used because it was not clear if the
corresponding shadow LDAP attributes get updates as well. But with
commit c975031 SSSD can update the attribute on its own so there is no
need to reject the password change.

Since it is important for SSSD to know if the LDAP server can update the
shadow LDAP attribute automatically or not it is checked if the
ldap_chpass_update_last_change option is set explicitly in sssd.conf. If
not there will be a log message.

Resolves: https://github.com/SSSD/sssd/issues/6220

Reviewed-by: Iker Pedrosa <ipedrosa@redhat.com>
Reviewed-by: Tomáš Halman <thalman@redhat.com>
---
 src/man/sssd-ldap.5.xml        |  10 +++
 src/providers/ldap/ldap_auth.c | 108 ++++++++++++++++++++++++++-------
 2 files changed, 96 insertions(+), 22 deletions(-)

Index: sssd-2.5.2/src/man/sssd-ldap.5.xml
===================================================================
--- sssd-2.5.2.orig/src/man/sssd-ldap.5.xml
+++ sssd-2.5.2/src/man/sssd-ldap.5.xml
@@ -1103,6 +1103,8 @@ host/*
                             <citerefentry><refentrytitle>shadow</refentrytitle>
                             <manvolnum>5</manvolnum></citerefentry> style
                             attributes to evaluate if the password has expired.
+                            Please see option "ldap_chpass_update_last_change"
+                            as well.
                         </para>
                         <para>
                             <emphasis>mit_kerberos</emphasis> - Use the attributes
@@ -1189,6 +1191,14 @@ host/*
                             operation.
                         </para>
                         <para>
+                            It is recommend to set this option explicitly if
+                            "ldap_pwd_policy = shadow" is used to let SSSD
+                            know if the LDAP server will update
+                            shadowLastChange LDAP attribute automatically
+                            after a password change or if SSSD has to update
+                            it.
+                        </para>
+                        <para>
                             Default: False
                         </para>
                     </listitem>
Index: sssd-2.5.2/src/providers/ldap/ldap_auth.c
===================================================================
--- sssd-2.5.2.orig/src/providers/ldap/ldap_auth.c
+++ sssd-2.5.2/src/providers/ldap/ldap_auth.c
@@ -1243,6 +1243,10 @@ struct sdap_pam_chpass_handler_state {
 };
 
 static void sdap_pam_chpass_handler_auth_done(struct tevent_req *subreq);
+static int
+sdap_pam_chpass_handler_change_step(struct sdap_pam_chpass_handler_state *state,
+                                    struct tevent_req *req,
+                                    enum pwexpire pw_expire_type);
 static void sdap_pam_chpass_handler_chpass_done(struct tevent_req *subreq);
 static void sdap_pam_chpass_handler_last_done(struct tevent_req *subreq);
 
@@ -1311,6 +1315,26 @@ immediately:
     return req;
 }
 
+static bool confdb_is_set_explicit(struct confdb_ctx *cdb, const char *section,
+                                   const char *attribute)
+{
+    int ret;
+    char **vals = NULL;
+    bool update_option_set_explictly = false;
+
+    ret = confdb_get_param(cdb, NULL, section, attribute, &vals);
+    if (ret != EOK) {
+        DEBUG(SSSDBG_OP_FAILURE, "Failed to check if [%s] is set "
+              "explicitly in sssd.conf, assuming it is not set.\n",
+              attribute);
+    } else if (vals != NULL && vals[0] != NULL) {
+        update_option_set_explictly = true;
+    }
+    talloc_free(vals);
+
+    return update_option_set_explictly;
+}
+
 static void sdap_pam_chpass_handler_auth_done(struct tevent_req *subreq)
 {
     struct sdap_pam_chpass_handler_state *state;
@@ -1368,30 +1392,13 @@ static void sdap_pam_chpass_handler_auth
         case ERR_PASSWORD_EXPIRED:
             DEBUG(SSSDBG_TRACE_LIBS,
                   "user [%s] successfully authenticated.\n", state->dn);
-            if (pw_expire_type == PWEXPIRE_SHADOW) {
-                /* TODO: implement async ldap modify request */
-                DEBUG(SSSDBG_CRIT_FAILURE,
-                      "Changing shadow password attributes not implemented.\n");
-                state->pd->pam_status = PAM_MODULE_UNKNOWN;
+            ret = sdap_pam_chpass_handler_change_step(state, req, pw_expire_type);
+            if (ret != EOK) {
+                DEBUG(SSSDBG_OP_FAILURE,
+                      "sdap_pam_chpass_handler_change_step() failed.\n");
                 goto done;
-            } else {
-                subreq = sdap_pam_change_password_send(state, state->ev,
-                                                       state->sh,
-                                                       state->auth_ctx->opts,
-                                                       state->pd,
-                                                       state->dn);
-                if (subreq == NULL) {
-                    DEBUG(SSSDBG_OP_FAILURE, "Failed to change password for "
-                          "%s\n", state->pd->user);
-                    state->pd->pam_status = PAM_SYSTEM_ERR;
-                    goto done;
-                }
-
-                tevent_req_set_callback(subreq,
-                                        sdap_pam_chpass_handler_chpass_done,
-                                        req);
-                return;
             }
+            return;
             break;
         case ERR_AUTH_DENIED:
         case ERR_AUTH_FAILED:
@@ -1424,6 +1431,63 @@ done:
     tevent_req_done(req);
 }
 
+static int
+sdap_pam_chpass_handler_change_step(struct sdap_pam_chpass_handler_state *state,
+                                    struct tevent_req *req,
+                                    enum pwexpire pw_expire_type)
+{
+    int ret;
+    struct tevent_req *subreq;
+    bool update_option_set_explictly = false;
+
+    if (pw_expire_type == PWEXPIRE_SHADOW) {
+
+        update_option_set_explictly = confdb_is_set_explicit(
+                   state->be_ctx->cdb, state->be_ctx->conf_path,
+                   state->auth_ctx->opts->basic[SDAP_CHPASS_UPDATE_LAST_CHANGE].opt_name);
+
+        if (!dp_opt_get_bool(state->auth_ctx->opts->basic,
+                             SDAP_CHPASS_UPDATE_LAST_CHANGE)
+                    && !update_option_set_explictly) {
+            DEBUG(SSSDBG_CRIT_FAILURE,
+               "Shadow password policy is selected but "
+               "ldap_chpass_update_last_change is not set, please "
+               "make sure your LDAP server can update the [%s] "
+               "attribute automatically. Otherwise SSSD might "
+               "consider your password as expired.\n",
+               state->auth_ctx->opts->user_map[SDAP_AT_SP_LSTCHG].name);
+        }
+        ret = sysdb_invalidate_cache_entry(state->be_ctx->domain,
+                                           state->pd->user, true);
+        if (ret != EOK) {
+            DEBUG(SSSDBG_OP_FAILURE,
+               "Failed to invalidate cache entry for user [%s] with error code "
+               "[%d][%s]. The last changed attribute [%s] might not get "
+               "refreshed after the password and the password might still be "
+               "considered as expired. Call sss_cache for this user "
+               "to expire the entry manually in this case.\n",
+               state->pd->user, ret, sss_strerror(ret),
+               state->auth_ctx->opts->user_map[SDAP_AT_SP_LSTCHG].name);
+        }
+    }
+    subreq = sdap_pam_change_password_send(state, state->ev,
+                                           state->sh,
+                                           state->auth_ctx->opts,
+                                           state->pd,
+                                           state->dn);
+    if (subreq == NULL) {
+        DEBUG(SSSDBG_OP_FAILURE, "Failed to change password for "
+              "%s\n", state->pd->user);
+        state->pd->pam_status = PAM_SYSTEM_ERR;
+        return ENOMEM;
+    }
+
+    tevent_req_set_callback(subreq,
+                            sdap_pam_chpass_handler_chpass_done,
+                            req);
+    return EOK;
+}
+
 static void sdap_pam_chpass_handler_chpass_done(struct tevent_req *subreq)
 {
     struct sdap_pam_chpass_handler_state *state;
