diff --git a/src/providers/ldap/ldap_auth.c b/src/providers/ldap/ldap_auth.c index 859b6bd94fc..a6a747528d4 100644 --- a/src/providers/ldap/ldap_auth.c +++ b/src/providers/ldap/ldap_auth.c @@ -157,6 +157,12 @@ static errno_t check_pwexpire_shadow(struct spwd *spwd, time_t now, if (spwd->sp_lstchg <= 0) { DEBUG(SSSDBG_CONF_SETTINGS, "Last change day is not set, new password needed.\n"); + if (pd != NULL) { + ret = add_expired_warning(pd, 0); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "add_expired_warning failed.\n"); + } + } return ERR_PASSWORD_EXPIRED; } diff --git a/src/providers/ldap/sdap_access.c b/src/providers/ldap/sdap_access.c index cfdde8c014c..cc3c2baa755 100644 --- a/src/providers/ldap/sdap_access.c +++ b/src/providers/ldap/sdap_access.c @@ -116,6 +116,7 @@ struct sdap_access_req_ctx { struct ldb_message *user_entry; size_t current_rule; enum sdap_access_control_type ac_type; + bool pw_expire_warn_seen; }; static errno_t sdap_access_check_next_rule(struct sdap_access_req_ctx *state, @@ -150,6 +151,7 @@ sdap_access_send(TALLOC_CTX *mem_ctx, state->access_ctx = access_ctx; state->conn = conn; state->current_rule = 0; + state->pw_expire_warn_seen = false; DEBUG(SSSDBG_TRACE_FUNC, "Performing access check for user [%s]\n", pd->user); @@ -212,8 +214,8 @@ static errno_t sdap_access_check_next_rule(struct sdap_access_req_ctx *state, while (ret == EOK) { switch (state->access_ctx->access_rule[state->current_rule]) { case LDAP_ACCESS_EMPTY: - /* we are done with no errors */ - return EOK; + /* all rules passed; preserve warn semantics if seen */ + return state->pw_expire_warn_seen ? ERR_PASSWORD_EXPIRED_WARN : EOK; /* This option is deprecated by LDAP_ACCESS_PPOLICY */ case LDAP_ACCESS_LOCKOUT: @@ -295,7 +297,8 @@ static errno_t sdap_access_check_next_rule(struct sdap_access_req_ctx *state, state->access_ctx->type, state->access_ctx->id_ctx->opts); if (ret == ERR_PASSWORD_EXPIRED) { - ret = ERR_PASSWORD_EXPIRED_WARN; + state->pw_expire_warn_seen = true; + ret = EOK; } break; diff --git a/src/tests/system/tests/test_ipa.py b/src/tests/system/tests/test_ipa.py index adf489e2508..6633161681d 100644 --- a/src/tests/system/tests/test_ipa.py +++ b/src/tests/system/tests/test_ipa.py @@ -1256,3 +1256,72 @@ def test_ipa__prohibited_short_names(ipa: IPA): assert result is not None, "Test failure - no result of SSSD restart!" assert result.rc != 0, "SSSD should refuse to start with full_name_format set to '%1$s'!" + + +@pytest.mark.importance("low") +@pytest.mark.topology(KnownTopology.IPA) +def test_ipa__password_expiration_warning_does_not_overwrite_errors(client: Client, ipa: IPA): + """ + :title: SSSD warns that a password is about to expire + :setup: + 1. Create user with expired password + 2. Lock the user by disabling it + 3. Set 'ipa_access_order = pwd_expire_policy_warn, expire' and restart SSSD + 4. Create SSH key for the user and store it one the server + :steps: + 1. SSH into the local host with the SSH key of the user + 2. Change 'ldap_access_order' to 'pwd_expire_policy_warn, expire' + 3. SSH into the local host with the SSH key of the user + :expectedresults: + 1. SSH is successful and shows 'Your password has expired.' on standard error + 2. Config change is successful + 3. SSH fails because the 'expire' policy rejects the locked user + :customerscenario: False + """ + + user = ipa.user("user1").add(password="Secret123", require_password_reset=True) + + result = ipa.host.conn.run(f"ipa user-disable {user.name}") + assert result.rc == 0, "'ipa user-disable' failed!" + assert f'Disabled user account "{user.name}"' in result.stdout, "Unexpected output from 'ipa user-disable'!" + + client.sssd.domain["ipa_access_order"] = "pwd_expire_policy_warn" + client.sssd.enable_responder("ssh") + client.sssd.restart() + + pwd = client.tools.getent.passwd(user.name) + assert pwd is not None, "User not found!" + assert pwd.name is not None, "User name is missing!" + assert pwd.home is not None, "home directory is missing!" + + key = client.tools.sshkey.generate(pwd.name, pwd.home)[0] + user.modify(sshpubkey=key) + # remove password from keyfile to make non-interactive login work + client.host.conn.run(f"ssh-keygen -p -P ' ' -N '' -f {pwd.home}/.ssh/id_rsa") + + client.sssd.restart(clean=True) + + result = client.host.conn.run( + f"sudo -u {user.name} ssh -o StrictHostKeychecking=no " + "-o PubkeyAuthentication=yes -o GSSAPIAuthentication=no " + "-o KbdInteractiveAuthentication=no -o PasswordAuthentication=no " + f"-l {user.name} localhost whoami", + raise_on_error=False, + ) + assert result.rc == 0, "SSH with pubkey failed!" + assert result.stdout == f"{user.name}", "Missing user name output!" + assert "Your password has expired." in result.stderr, "Missing 'Your password has expired.' in error output!" + + client.sssd.domain["ipa_access_order"] = "pwd_expire_policy_warn, expire" + client.sssd.restart() + + result = client.host.conn.run( + f"sudo -u {user.name} ssh -o StrictHostKeychecking=no " + "-o PubkeyAuthentication=yes -o GSSAPIAuthentication=no " + "-o KbdInteractiveAuthentication=no -o PasswordAuthentication=no " + f"-l {user.name} localhost whoami", + raise_on_error=False, + ) + assert result.rc != 0, "SSH was successful but should have failed!" + assert result.stdout == "", "Output is not empty!" + assert "Your password has expired." in result.stderr, "Missing 'Your password has expired.' in error output!" diff --git a/src/tests/system/tests/test_ldap.py b/src/tests/system/tests/test_ldap.py index 11ebdb8d849..c981926bdc2 100644 --- a/src/tests/system/tests/test_ldap.py +++ b/src/tests/system/tests/test_ldap.py @@ -1108,3 +1108,73 @@ def test_ldap__search_base_limits_rfc2307bis_posixgroup_scope(client: Client, ld assert ( client.tools.getent.group("outside_group") is None ), "outside_group should not be visible outside search base!" + + +@pytest.mark.topology(KnownTopology.LDAP) +@pytest.mark.importance("low") +def test_ldap__password_expiration_warning_does_not_overwrite_errors(client: Client, provider: LDAP): + """ + :title: SSSD warns that a password is about to expire + :setup: + 1. Create user with expired password + 3. Set 'ldap_access_order = pwd_expire_policy_warn' and restart SSSD + 4. Create SSH key for the user and write it to 'authorizred_keys' + :steps: + 1. SSH into the local host with the SSH key of the user + 2. Change 'ldap_access_order' to 'pwd_expire_policy_warn, expire' + 3. SSH into the local host with the SSH key of the user + :expectedresults: + 1. SSH is successful and shows 'Your password has expired.' on standard error + 2. Config change is successful + 3. SSH fails because the 'expire' policy rejects the locked user + :customerscenario: False + """ + + user = provider.user("user1").add( + shadowMin=0, shadowMax=99999, shadowWarning=7, shadowLastChange=0, shadowExpire=1, password="Secret123" + ) + + client.sssd.domain["auto_private_groups"] = "True" + client.sssd.domain["access_provider"] = "ldap" + client.sssd.domain["ldap_pwd_policy"] = "shadow" + client.sssd.domain["ldap_account_expire_policy"] = "shadow" + client.sssd.domain["ldap_access_order"] = "pwd_expire_policy_warn" + client.sssd.restart() + + pwd = client.tools.getent.passwd(user.name) + assert pwd is not None, "User not found!" + assert pwd.name is not None, "User name is missing!" + assert pwd.home is not None, "home directory is missing!" + + client.host.conn.run(f"su - {user.name}", raise_on_error=False) + key = client.tools.sshkey.generate(pwd.name, pwd.home)[0] + client.fs.write(f"{pwd.home}/.ssh/authorized_keys", key) + # remove password from keyfile to make non-interactive login work + client.host.conn.run(f"ssh-keygen -p -P ' ' -N '' -f {pwd.home}/.ssh/id_rsa") + + client.sssd.restart(clean=True) + + result = client.host.conn.run( + f"sudo -u {user.name} ssh -o StrictHostKeychecking=no " + "-o PubkeyAuthentication=yes -o GSSAPIAuthentication=no " + "-o KbdInteractiveAuthentication=no -o PasswordAuthentication=no " + f"-l {user.name} localhost whoami", + raise_on_error=False, + ) + assert result.rc == 0, "SSH with pubkey failed!" + assert result.stdout == f"{user.name}", "Missing user name output!" + assert "Your password has expired." in result.stderr, "Missing 'Your password has expired.' in error output!" + + client.sssd.domain["ldap_access_order"] = "pwd_expire_policy_warn, expire" + client.sssd.restart(clean=True) + + result = client.host.conn.run( + f"sudo -u {user.name} ssh -o StrictHostKeychecking=no " + "-o PubkeyAuthentication=yes -o GSSAPIAuthentication=no " + "-o KbdInteractiveAuthentication=no -o PasswordAuthentication=no " + f"-l {user.name} localhost whoami", + raise_on_error=False, + ) + assert result.rc != 0, "SSH was successful but should have failed!" + assert result.stdout == "", "Output is not empty!" + assert "Your password has expired." in result.stderr, "Missing 'Your password has expired.' in error output!"