From 72a7fd0ded236a16b00bb4e26221f7e23b702a53 Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Fri, 13 Sep 2024 15:45:59 +0200 Subject: [PATCH 1/2] ldap: add 'exop_force' value for ldap_pwmodify_mode In case the LDAP server allows to run the extended operation to change a password even if an authenticated bind fails due to missing grace logins the new option 'exop_force' can be used to run the extended operation to change the password anyways. :config: Added `exop_force` value for configuration option `ldap_pwmodify_mode`. This can be used to force a password change even if no grace logins are left. Depending on the configuration of the LDAP server it might be expected that the password change will fail. --- src/man/sssd-ldap.5.xml | 11 +++++++++ src/providers/ipa/ipa_auth.c | 3 ++- src/providers/ldap/ldap_auth.c | 5 +++- src/providers/ldap/ldap_options.c | 2 ++ src/providers/ldap/sdap.h | 5 ++-- src/providers/ldap/sdap_async.h | 3 ++- src/providers/ldap/sdap_async_connection.c | 27 +++++++++++++++++----- 7 files changed, 45 insertions(+), 11 deletions(-) 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); From 7c8564c1a9cb06a840398712a81d2ad9e87c4365 Mon Sep 17 00:00:00 2001 From: Sumit Bose Date: Fri, 27 Sep 2024 16:54:42 +0200 Subject: [PATCH 2/2] tests: add 'expo_force' tests The new value for the ldap_pwmodify_mode option 'exop_force' is added to existing test. A new test to illustrate the different behavior of 'exop' and 'exop_force' is added. --- src/tests/system/tests/test_ldap.py | 58 +++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) 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