From e06248ef69b33d102a57aaa31fffcde3240dad9c Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Wed, 23 Sep 2026 10:08:44 +0200 Subject: user.set_password: change your own password; TUI "Byt lösenord" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Needs the current password (wrong ones rate limited like logins) and a password session, requires at least 10 characters, closes the user's other sessions and is audited without secrets. The TUI main menu gets "Byt lösenord" with masked prompts; ^R keeps working with the new password. Masked prompt buffers are wiped before they are freed. Co-Authored-By: Claude Opus 5.5 --- tests/test_core.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) (limited to 'tests') diff --git a/tests/test_core.c b/tests/test_core.c index 071b5ce..6376517 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -1309,6 +1309,101 @@ static void test_tokens(struct tctx *t) yyjson_doc_free(d); } +static void set_pw(const char *sess, const char *cur, const char *pw, + const char *want_code) +{ + yyjson_doc *d = call(reqf( + "{\"v\":1,\"id\":\"pw\",\"cmd\":\"user.set_password\"," + "\"session\":\"%s\",\"args\":{\"current_password\":\"%s\"," + "\"new_password\":\"%s\"}}", + sess, cur, pw)); + if (want_code) + CHECK_STR(d, "error.code", want_code); + else + CHECK_OK(d); + yyjson_doc_free(d); +} + +static void test_set_password(struct tctx *t) +{ + yyjson_doc *d; + CHECK(login("admin", "secret123")); + d = call(reqf("{\"v\":1,\"id\":\"p1\",\"cmd\":\"user.create\"," + "\"session\":\"%s\",\"args\":{\"username\":\"pwuser\"," + "\"password\":\"oldpassword1\"}}", + g_session)); + CHECK_OK(d); + yyjson_doc_free(d); + + /* two sessions of the same user */ + CHECK(login("pwuser", "oldpassword1")); + char other[128]; + snprintf(other, sizeof other, "%s", g_session); + CHECK(login("pwuser", "oldpassword1")); + char mine[128]; + snprintf(mine, sizeof mine, "%s", g_session); + + set_pw(mine, "wrongpassword", "newpassword22", "AUTH_FAILED"); + set_pw(mine, "oldpassword1", "short", "INVALID_ARGS"); + set_pw(mine, "oldpassword1", "oldpassword1", "INVALID_ARGS"); + d = call(reqf("{\"v\":1,\"id\":\"p2\",\"cmd\":\"user.set_password\"," + "\"session\":\"%s\",\"args\":{\"new_password\":" + "\"newpassword22\"}}", + mine)); + CHECK_STR(d, "error.code", "INVALID_ARGS"); + yyjson_doc_free(d); + /* nothing changed yet: the other session and the old password work */ + d = call(reqf("{\"v\":1,\"id\":\"p3\",\"cmd\":\"session.whoami\"," + "\"session\":\"%s\"}", other)); + CHECK_OK(d); + yyjson_doc_free(d); + + d = call(reqf("{\"v\":1,\"id\":\"p4\",\"cmd\":\"user.set_password\"," + "\"session\":\"%s\",\"args\":{\"current_password\":" + "\"oldpassword1\",\"new_password\":\"newpassword22\"}}", + mine)); + CHECK_OK(d); + CHECK(jint(d, "result.sessions_closed") == 1); + yyjson_doc_free(d); + /* other sessions are closed, this one stays */ + d = call(reqf("{\"v\":1,\"id\":\"p5\",\"cmd\":\"session.whoami\"," + "\"session\":\"%s\"}", other)); + CHECK_STR(d, "error.code", "SESSION_EXPIRED"); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"p6\",\"cmd\":\"session.whoami\"," + "\"session\":\"%s\"}", mine)); + CHECK_OK(d); + CHECK_STR(d, "result.user.username", "pwuser"); + yyjson_doc_free(d); + CHECK(!login("pwuser", "oldpassword1")); + CHECK(login("pwuser", "newpassword22")); + snprintf(mine, sizeof mine, "%s", g_session); + + /* wrong current passwords are rate limited like logins */ + for (int i = 0; i < 5; i++) + set_pw(mine, "wrongpassword", "otherpassword3", "AUTH_FAILED"); + set_pw(mine, "newpassword22", "otherpassword3", "RATE_LIMITED"); + + /* a token session cannot change the password */ + CHECK(login("admin", "secret123")); + d = call(reqf("{\"v\":1,\"id\":\"p7\",\"cmd\":\"token.create\"," + "\"session\":\"%s\",\"org\":%d,\"args\":" + "{\"label\":\"pw-test\",\"scopes\":[\"read\"]}}", + g_session, (int)t->org_id)); + CHECK_OK(d); + char tok[128]; + snprintf(tok, sizeof tok, "%s", jstr(d, "result.token")); + yyjson_doc_free(d); + d = call(reqf("{\"v\":1,\"id\":\"p8\",\"cmd\":\"session.open\",\"args\":" + "{\"method\":\"token\",\"token\":\"%s\"}}", tok)); + CHECK_OK(d); + char tsess[128]; + snprintf(tsess, sizeof tsess, "%s", jstr(d, "result.session")); + yyjson_doc_free(d); + set_pw(tsess, "secret123", "anotherpassword4", "FORBIDDEN"); + CHECK(login("admin", "secret123")); +} + static void test_accounts(struct tctx *t) { yyjson_doc *d; @@ -5801,6 +5896,7 @@ static const struct ttest TESTS[] = { { "session_auth", test_session_auth, "" }, { "org_members", test_org_members, "session_auth" }, { "tokens", test_tokens, "org_members" }, + { "set_password", test_set_password, "org_members" }, { "accounts", test_accounts, "org_members" }, { "fiscal_years", test_fiscal_years, "org_members" }, { "vouchers", test_vouchers, "org_members" }, -- cgit v1.3