diff --git a/src/man/sssd-ldap.5.xml b/src/man/sssd-ldap.5.xml
index a6f9b1c97b9..d50aa65b2c1 100644
--- a/src/man/sssd-ldap.5.xml
+++ b/src/man/sssd-ldap.5.xml
@@ -234,6 +234,17 @@
userPassword (not recommended).
+
+
+ exop_force - Try Password Modify
+ Extended Operation (RFC 3062) even if
+ there are no grace logins left.
+ Depending on the type and configuration
+ of the LDAP server the password change
+ might fail because an authenticated bind
+ is not possible.
+
+
diff --git a/src/providers/ipa/ipa_auth.c b/src/providers/ipa/ipa_auth.c
index e238d0623de..db1cd6ad39f 100644
--- a/src/providers/ipa/ipa_auth.c
+++ b/src/providers/ipa/ipa_auth.c
@@ -397,7 +397,8 @@ static void ipa_pam_auth_handler_connect_done(struct tevent_req *subreq)
SDAP_USE_PPOLICY);
subreq = sdap_auth_send(state, state->ev, sh, NULL, NULL, dn,
- state->pd->authtok, timeout, use_ppolicy);
+ state->pd->authtok, timeout, use_ppolicy,
+ state->auth_ctx->sdap_auth_ctx->opts->pwmodify_mode);
if (subreq == NULL) {
goto done;
}
diff --git a/src/providers/ldap/ldap_auth.c b/src/providers/ldap/ldap_auth.c
index 9ccbdabdbee..370cdf17188 100644
--- a/src/providers/ldap/ldap_auth.c
+++ b/src/providers/ldap/ldap_auth.c
@@ -914,7 +914,8 @@ static void auth_do_bind(struct tevent_req *req)
subreq = sdap_auth_send(state, state->ev, state->sh,
NULL, NULL, state->dn,
state->authtok,
- timeout, use_ppolicy);
+ timeout, use_ppolicy,
+ state->ctx->opts->pwmodify_mode);
if (!subreq) {
tevent_req_error(req, ENOMEM);
return;
@@ -1208,6 +1209,7 @@ sdap_pam_change_password_send(TALLOC_CTX *mem_ctx,
switch (opts->pwmodify_mode) {
case SDAP_PWMODIFY_EXOP:
+ case SDAP_PWMODIFY_EXOP_FORCE:
use_ppolicy = dp_opt_get_bool(opts->basic, SDAP_USE_PPOLICY);
subreq = sdap_exop_modify_passwd_send(state, ev, sh, user_dn,
password, new_password,
@@ -1252,6 +1254,7 @@ static void sdap_pam_change_password_done(struct tevent_req *subreq)
switch (state->mode) {
case SDAP_PWMODIFY_EXOP:
+ case SDAP_PWMODIFY_EXOP_FORCE:
ret = sdap_exop_modify_passwd_recv(subreq, state,
&state->user_error_message);
break;
diff --git a/src/providers/ldap/ldap_options.c b/src/providers/ldap/ldap_options.c
index 277bcb529fe..72a95300d74 100644
--- a/src/providers/ldap/ldap_options.c
+++ b/src/providers/ldap/ldap_options.c
@@ -294,6 +294,8 @@ int ldap_get_options(TALLOC_CTX *memctx,
opts->pwmodify_mode = SDAP_PWMODIFY_EXOP;
} else if (strcasecmp(pwmodify, "ldap_modify") == 0) {
opts->pwmodify_mode = SDAP_PWMODIFY_LDAP;
+ } else if (strcasecmp(pwmodify, "exop_force") == 0) {
+ opts->pwmodify_mode = SDAP_PWMODIFY_EXOP_FORCE;
} else {
DEBUG(SSSDBG_FATAL_FAILURE, "Unrecognized pwmodify mode: %s\n", pwmodify);
ret = EINVAL;
diff --git a/src/providers/ldap/sdap.h b/src/providers/ldap/sdap.h
index d66ca156afe..35a4d5e1c96 100644
--- a/src/providers/ldap/sdap.h
+++ b/src/providers/ldap/sdap.h
@@ -550,8 +550,9 @@ struct sdap_options {
/* password modify mode */
enum pwmodify_mode {
- SDAP_PWMODIFY_EXOP = 1, /* pwmodify extended operation */
- SDAP_PWMODIFY_LDAP = 2 /* ldap_modify of userPassword */
+ SDAP_PWMODIFY_EXOP = 1, /* pwmodify extended operation */
+ SDAP_PWMODIFY_LDAP = 2, /* ldap_modify of userPassword */
+ SDAP_PWMODIFY_EXOP_FORCE = 3 /* forced pwmodify extended operation */
} pwmodify_mode;
/* The search bases for the domain or its subdomain */
diff --git a/src/providers/ldap/sdap_async.h b/src/providers/ldap/sdap_async.h
index a78a1157ccc..700cd6f9c44 100644
--- a/src/providers/ldap/sdap_async.h
+++ b/src/providers/ldap/sdap_async.h
@@ -147,7 +147,8 @@ struct tevent_req *sdap_auth_send(TALLOC_CTX *memctx,
const char *user_dn,
struct sss_auth_token *authtok,
int simple_bind_timeout,
- bool use_ppolicy);
+ bool use_ppolicy,
+ enum pwmodify_mode pwmodify_mode);
errno_t sdap_auth_recv(struct tevent_req *req,
TALLOC_CTX *memctx,
diff --git a/src/providers/ldap/sdap_async_connection.c b/src/providers/ldap/sdap_async_connection.c
index a6d4ee4438b..67c09835b79 100644
--- a/src/providers/ldap/sdap_async_connection.c
+++ b/src/providers/ldap/sdap_async_connection.c
@@ -646,6 +646,7 @@ struct simple_bind_state {
struct tevent_context *ev;
struct sdap_handle *sh;
const char *user_dn;
+ enum pwmodify_mode pwmodify_mode;
struct sdap_op *op;
@@ -663,7 +664,8 @@ static struct tevent_req *simple_bind_send(TALLOC_CTX *memctx,
int timeout,
const char *user_dn,
struct berval *pw,
- bool use_ppolicy)
+ bool use_ppolicy,
+ enum pwmodify_mode pwmodify_mode)
{
struct tevent_req *req;
struct simple_bind_state *state;
@@ -686,6 +688,7 @@ static struct tevent_req *simple_bind_send(TALLOC_CTX *memctx,
state->ev = ev;
state->sh = sh;
state->user_dn = user_dn;
+ state->pwmodify_mode = pwmodify_mode;
if (use_ppolicy) {
ret = sss_ldap_control_create(LDAP_CONTROL_PASSWORDPOLICYREQUEST,
@@ -872,7 +875,12 @@ static void simple_bind_done(struct sdap_op *op,
* Grace Authentications". */
DEBUG(SSSDBG_TRACE_LIBS,
"Password expired, grace logins exhausted.\n");
- ret = ERR_AUTH_FAILED;
+ if (state->pwmodify_mode == SDAP_PWMODIFY_EXOP_FORCE) {
+ DEBUG(SSSDBG_TRACE_LIBS, "Password change forced.\n");
+ ret = ERR_PASSWORD_EXPIRED;
+ } else {
+ ret = ERR_AUTH_FAILED;
+ }
}
} else if (strcmp(response_controls[c]->ldctl_oid,
LDAP_CONTROL_PWEXPIRED) == 0) {
@@ -885,7 +893,12 @@ static void simple_bind_done(struct sdap_op *op,
if (result == LDAP_INVALID_CREDENTIALS) {
DEBUG(SSSDBG_TRACE_LIBS,
"Password expired, grace logins exhausted.\n");
- ret = ERR_AUTH_FAILED;
+ if (state->pwmodify_mode == SDAP_PWMODIFY_EXOP_FORCE) {
+ DEBUG(SSSDBG_TRACE_LIBS, "Password change forced.\n");
+ ret = ERR_PASSWORD_EXPIRED;
+ } else {
+ ret = ERR_AUTH_FAILED;
+ }
} else {
DEBUG(SSSDBG_TRACE_LIBS,
"Password expired, user must set a new password.\n");
@@ -1365,7 +1378,8 @@ struct tevent_req *sdap_auth_send(TALLOC_CTX *memctx,
const char *user_dn,
struct sss_auth_token *authtok,
int simple_bind_timeout,
- bool use_ppolicy)
+ bool use_ppolicy,
+ enum pwmodify_mode pwmodify_mode)
{
struct tevent_req *req, *subreq;
struct sdap_auth_state *state;
@@ -1404,7 +1418,7 @@ struct tevent_req *sdap_auth_send(TALLOC_CTX *memctx,
pw.bv_len = pwlen;
state->is_sasl = false;
- subreq = simple_bind_send(state, ev, sh, simple_bind_timeout, user_dn, &pw, use_ppolicy);
+ subreq = simple_bind_send(state, ev, sh, simple_bind_timeout, user_dn, &pw, use_ppolicy, pwmodify_mode);
if (!subreq) {
tevent_req_error(req, ENOMEM);
return tevent_req_post(req, ev);
@@ -1981,7 +1995,8 @@ static void sdap_cli_auth_step(struct tevent_req *req)
dp_opt_get_int(state->opts->basic,
SDAP_OPT_TIMEOUT),
dp_opt_get_bool(state->opts->basic,
- SDAP_USE_PPOLICY));
+ SDAP_USE_PPOLICY),
+ state->opts->pwmodify_mode);
talloc_free(authtok);
if (!subreq) {
tevent_req_error(req, ENOMEM);
diff --git a/src/tests/system/tests/test_ldap.py b/src/tests/system/tests/test_ldap.py
index 3d8b35a451c..251663e5f65 100644
--- a/src/tests/system/tests/test_ldap.py
+++ b/src/tests/system/tests/test_ldap.py
@@ -16,7 +16,7 @@
@pytest.mark.ticket(bz=[795044, 1695574])
@pytest.mark.importance("critical")
-@pytest.mark.parametrize("modify_mode", ["exop", "ldap_modify"])
+@pytest.mark.parametrize("modify_mode", ["exop", "ldap_modify", "exop_force"])
@pytest.mark.parametrize("use_ppolicy", ["true", "false"])
@pytest.mark.parametrize("sssd_service_user", ("root", "sssd"))
@pytest.mark.topology(KnownTopology.LDAP)
@@ -75,7 +75,7 @@ def test_ldap__password_change_using_ppolicy(
@pytest.mark.ticket(bz=[795044, 1695574])
@pytest.mark.importance("critical")
-@pytest.mark.parametrize("modify_mode", ["exop", "ldap_modify"])
+@pytest.mark.parametrize("modify_mode", ["exop", "ldap_modify", "exop_force"])
@pytest.mark.parametrize("use_ppolicy", ["true", "false"])
@pytest.mark.topology(KnownTopology.LDAP)
@pytest.mark.builtwith("ldap_use_ppolicy")
@@ -109,7 +109,7 @@ def test_ldap__password_change_new_passwords_do_not_match_using_ppolicy(
@pytest.mark.ticket(bz=[795044, 1695574, 1795220])
@pytest.mark.importance("critical")
-@pytest.mark.parametrize("modify_mode", ["exop", "ldap_modify"])
+@pytest.mark.parametrize("modify_mode", ["exop", "ldap_modify", "exop_force"])
@pytest.mark.parametrize("use_ppolicy", ["true", "false"])
@pytest.mark.topology(KnownTopology.LDAP)
@pytest.mark.builtwith("ldap_use_ppolicy")
@@ -150,7 +150,7 @@ def test_ldap__password_change_new_password_does_not_meet_complexity_requirement
@pytest.mark.ticket(bz=[1695574, 1795220])
@pytest.mark.importance("critical")
-@pytest.mark.parametrize("modify_mode", ["exop", "ldap_modify"])
+@pytest.mark.parametrize("modify_mode", ["exop", "ldap_modify", "exop_force"])
@pytest.mark.parametrize("use_ppolicy", ["true", "false"])
@pytest.mark.topology(KnownTopology.LDAP)
@pytest.mark.builtwith("ldap_use_ppolicy")
@@ -452,3 +452,53 @@ def test_ldap__lookup_and_authenticate_as_user_with_different_object_search_base
assert result is not None, "User is not found!"
assert result.name == user.name, "Username is not correct!"
assert client.auth.ssh.password(user.name, "Secret123"), "User login failed!"
+
+
+@pytest.mark.ticket(jira="RHEL-55993")
+@pytest.mark.importance("critical")
+@pytest.mark.parametrize(
+ "modify_mode, expected, err_msg",
+ [("exop", 1, "Expected login failure"), ("exop_force", 3, "Expected password change request")],
+)
+@pytest.mark.parametrize("method", ["su", "ssh"])
+@pytest.mark.topology(KnownTopology.LDAP)
+def test_ldap__password_change_no_grace_logins_left(
+ client: Client, ldap: LDAP, modify_mode: str, expected: int, err_msg: str, method: str
+):
+ """
+ :title: Password change when no grace logins left
+ :description: Typically the LDAP extended operation to change a password
+ requires an authenticated bind, even if the data send with the extended
+ operation contains the old password. If the old password is expired and
+ there are no grace logins left an authenticated bind is not possible anymore
+ and as a result it is not possible for the user to change their password.
+ With 'exop' SSSD will not try to ask the user for new credentials while with
+ 'exop_force' SSSD will ask for new credentials and will try to run the password
+ change extended operation.
+ :setup:
+ 1. Set "passwordExp" to "on"
+ 2. Set "passwordMaxAge" to "1"
+ 3. Set "passwordGraceLimit" to "0"
+ 4. Add a user to LDAP
+ 5. Wait until the password is expired
+ 6. Set "ldap_pwmodify_mode"
+ 7. Start SSSD
+ :steps:
+ 1. Authenticate as the user with 'exop_force' set
+ 2. Authenticate as the user with 'exop' set
+ :expectedresults:
+ 1. With 'exop_force' expect a request to change the password
+ 2. With 'exop' expect just a failed login
+ :customerscenario: False
+ """
+ ldap.ldap.modify("cn=config", replace={"passwordExp": "on", "passwordMaxAge": "1", "passwordGraceLimit": "0"})
+ ldap.user("user1").add(password="Secret123")
+
+ # make sure the password is expired
+ time.sleep(3)
+
+ client.sssd.domain["ldap_pwmodify_mode"] = modify_mode
+ client.sssd.start()
+
+ rc, _, _, _ = client.auth.parametrize(method).password_with_output("user1", "Secret123")
+ assert rc == expected, err_msg