From a70ce7d3e2f983979e971414c5892c4e30197231 Mon Sep 17 00:00:00 2001
From: Aki Tuomi <aki.tuomi@open-xchange.com>
Date: Fri, 25 Jul 2025 08:16:52 +0300
Subject: [PATCH 1/7] auth: Use AUTH_CACHE_KEY_USER instead of per-database
 constants

Fixes cache key issue where users would end up overwriting
each other in cache due to cache key being essentially static
string because we no longer support %u.

Forgotten in 2e298e7ee98b6df61cf85117f000290d60a473b8
---
 src/auth/auth-settings.h  | 2 ++
 src/auth/passdb-bsdauth.c | 4 +---
 src/auth/passdb-oauth2.c  | 2 +-
 src/auth/passdb-pam.c     | 3 ++-
 src/auth/passdb-passwd.c  | 3 +--
 src/auth/userdb-passwd.c  | 3 +--
 6 files changed, 8 insertions(+), 9 deletions(-)

diff --git a/src/auth/auth-settings.h b/src/auth/auth-settings.h
index 1d420eceaaf..90aba17ec38 100644
--- a/src/auth/auth-settings.h
+++ b/src/auth/auth-settings.h
@@ -1,6 +1,8 @@
 #ifndef AUTH_SETTINGS_H
 #define AUTH_SETTINGS_H
 
+#define AUTH_CACHE_KEY_USER "%{user}"
+
 struct master_service;
 struct master_service_settings_output;
 
diff --git a/src/auth/passdb-bsdauth.c b/src/auth/passdb-bsdauth.c
index 68292679b7f..1b86da4053c 100644
--- a/src/auth/passdb-bsdauth.c
+++ b/src/auth/passdb-bsdauth.c
@@ -14,8 +14,6 @@
 #include <login_cap.h>
 #include <bsd_auth.h>
 
-#define BSDAUTH_CACHE_KEY "%u"
-
 struct passdb_bsdauth_settings {
 	pool_t pool;
 };
@@ -104,7 +102,7 @@ bsdauth_preinit(pool_t pool, struct event *event,
 			 &post_set, error_r) < 0)
 		return -1;
 	module->default_cache_key = auth_cache_parse_key_and_fields(
-		pool, BSDAUTH_CACHE_KEY, &post_set->fields, "bsdauth");
+		pool, AUTH_CACHE_KEY_USER, &post_set->fields, "bsdauth");
 
 	settings_free(post_set);
 	*module_r = module;
diff --git a/src/auth/passdb-oauth2.c b/src/auth/passdb-oauth2.c
index 96d902d323d..91fed060183 100644
--- a/src/auth/passdb-oauth2.c
+++ b/src/auth/passdb-oauth2.c
@@ -53,7 +53,7 @@ oauth2_preinit(pool_t pool, struct event *event, struct passdb_module **module_r
 	if (db_oauth2_init(event, TRUE, &module->db, error_r) < 0)
 		return -1;
 	module->module.default_pass_scheme = "PLAIN";
-	module->module.default_cache_key = "%u";
+	module->module.default_cache_key = AUTH_CACHE_KEY_USER;
 	*module_r = &module->module;
 	return 0;
 }
diff --git a/src/auth/passdb-pam.c b/src/auth/passdb-pam.c
index 2acbceb80a3..fdf0f573ef4 100644
--- a/src/auth/passdb-pam.c
+++ b/src/auth/passdb-pam.c
@@ -415,7 +415,8 @@ static int pam_preinit(pool_t pool, struct event *event,
 	module = p_new(pool, struct pam_passdb_module, 1);
 	module->module.default_cache_key =
 		auth_cache_parse_key_and_fields(pool,
-						t_strdup_printf("%%u/%s", set->service_name),
+						t_strdup_printf("%"AUTH_CACHE_KEY_USER"\t%s",
+								set->service_name),
 						&post_set->fields, "pam");
 	module->requests_left = set->max_requests;
 	module->pam_setcred = set->setcred;
diff --git a/src/auth/passdb-passwd.c b/src/auth/passdb-passwd.c
index 13003151f9c..22e2eae7fa3 100644
--- a/src/auth/passdb-passwd.c
+++ b/src/auth/passdb-passwd.c
@@ -10,7 +10,6 @@
 #include "safe-memset.h"
 #include "ipwd.h"
 
-#define PASSWD_CACHE_KEY "%u"
 #define PASSWD_PASS_SCHEME "CRYPT"
 
 #undef DEF
@@ -142,7 +141,7 @@ static int passwd_preinit(pool_t pool, struct event *event,
 			 &post_set, error_r) < 0)
 		return -1;
 	module->default_cache_key = auth_cache_parse_key_and_fields(pool,
-								    PASSWD_CACHE_KEY,
+								    AUTH_CACHE_KEY_USER,
 								    &post_set->fields,
 								    "passwd");
 	settings_free(post_set);
diff --git a/src/auth/userdb-passwd.c b/src/auth/userdb-passwd.c
index 5241129a0cc..14cf90a6d65 100644
--- a/src/auth/userdb-passwd.c
+++ b/src/auth/userdb-passwd.c
@@ -9,7 +9,6 @@
 #include "ipwd.h"
 #include "time-util.h"
 
-#define USER_CACHE_KEY "%u"
 #define PASSWD_SLOW_WARN_MSECS (10*1000)
 #define PASSWD_SLOW_MASTER_WARN_MSECS 50
 #define PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL 100
@@ -225,7 +224,7 @@ static int passwd_preinit(pool_t pool, struct event *event ATTR_UNUSED,
 	struct passwd_userdb_module *module =
 		p_new(pool, struct passwd_userdb_module, 1);
 
-	module->module.default_cache_key = USER_CACHE_KEY;
+	module->module.default_cache_key = AUTH_CACHE_KEY_USER;
 	*module_r = &module->module;
 	return 0;
 }

From c45ce2c073c9439a9d6366016cb4d41059d737f0 Mon Sep 17 00:00:00 2001
From: Aki Tuomi <aki.tuomi@open-xchange.com>
Date: Wed, 30 Jul 2025 09:42:20 +0300
Subject: [PATCH 2/7] auth: auth-cache - Refactor
 auth_cache_parse_key_and_fields()

Call auth_cache_parse_key_exclude() at the function end,
simplifies next commit.
---
 src/auth/auth-cache.c | 24 +++++++++++-------------
 1 file changed, 11 insertions(+), 13 deletions(-)

diff --git a/src/auth/auth-cache.c b/src/auth/auth-cache.c
index 360ad8b3f62..3ccd45ff4b9 100644
--- a/src/auth/auth-cache.c
+++ b/src/auth/auth-cache.c
@@ -129,20 +129,18 @@ char *auth_cache_parse_key_and_fields(pool_t pool, const char *query,
 				      const ARRAY_TYPE(const_string) *fields,
 				      const char *exclude_driver)
 {
-	if (array_is_empty(fields))
-		return auth_cache_parse_key_exclude(pool, query, exclude_driver);
-
-	string_t *full_query = t_str_new(128);
-	str_append(full_query, query);
-
-	unsigned int i, count;
-	const char *const *str = array_get(fields, &count);
-	for (i = 0; i < count; i += 2) {
-		str_append_c(full_query, '\t');
-		str_append(full_query, str[i + 1]);
+	if (!array_is_empty(fields)) {
+		unsigned int i, count;
+		const char *const *str = array_get(fields, &count);
+		string_t *full_query = t_str_new(128);
+		str_append(full_query, query);
+		for (i = 0; i < count; i += 2) {
+			str_append_c(full_query, '\t');
+			str_append(full_query, str[i + 1]);
+		}
+		query = str_c(full_query);
 	}
-	return auth_cache_parse_key_exclude(pool, str_c(full_query),
-					    exclude_driver);
+	return auth_cache_parse_key_exclude(pool, query, exclude_driver);
 }
 
 static void

From 759ee1af848480987d012de2f7135160156724b6 Mon Sep 17 00:00:00 2001
From: Aki Tuomi <aki.tuomi@open-xchange.com>
Date: Fri, 25 Jul 2025 11:48:43 +0300
Subject: [PATCH 3/7] auth: auth-cache - Deduplicate auth_cache_parse_key() to
 use auth_cache_parse_key_and_fields()

Simplifies following commit
---
 src/auth/auth-cache.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/auth/auth-cache.c b/src/auth/auth-cache.c
index 3ccd45ff4b9..ad8cbe50784 100644
--- a/src/auth/auth-cache.c
+++ b/src/auth/auth-cache.c
@@ -122,14 +122,14 @@ static char *auth_cache_parse_key_exclude(pool_t pool, const char *query,
 
 char *auth_cache_parse_key(pool_t pool, const char *query)
 {
-	return auth_cache_parse_key_exclude(pool, query, NULL);
+	return auth_cache_parse_key_and_fields(pool, query, NULL, NULL);
 }
 
 char *auth_cache_parse_key_and_fields(pool_t pool, const char *query,
 				      const ARRAY_TYPE(const_string) *fields,
 				      const char *exclude_driver)
 {
-	if (!array_is_empty(fields)) {
+	if (fields != NULL && !array_is_empty(fields)) {
 		unsigned int i, count;
 		const char *const *str = array_get(fields, &count);
 		string_t *full_query = t_str_new(128);

From d12bb78b5a235f31c9d5a655bd223c28d44bcadb Mon Sep 17 00:00:00 2001
From: Aki Tuomi <aki.tuomi@open-xchange.com>
Date: Fri, 25 Jul 2025 11:51:16 +0300
Subject: [PATCH 4/7] auth: auth-cache - Change auth_cache_parse_key_exclude()
 to return error

Simplifies following commit
---
 src/auth/auth-cache.c | 25 ++++++++++++++++++-------
 1 file changed, 18 insertions(+), 7 deletions(-)

diff --git a/src/auth/auth-cache.c b/src/auth/auth-cache.c
index ad8cbe50784..407e5d4aa0e 100644
--- a/src/auth/auth-cache.c
+++ b/src/auth/auth-cache.c
@@ -64,8 +64,10 @@ static void auth_cache_key_add_tab_idx(string_t *str, unsigned int i)
 	str_append_c(str, '}');
 }
 
-static char *auth_cache_parse_key_exclude(pool_t pool, const char *query,
-					  const char *exclude_driver)
+static int auth_cache_parse_key_exclude(pool_t pool, const char *query,
+					const char *exclude_driver,
+					char **cache_key_r,
+					const char **error_r)
 {
 	string_t *str;
 	bool key_seen[AUTH_REQUEST_VAR_TAB_COUNT];
@@ -76,9 +78,9 @@ static char *auth_cache_parse_key_exclude(pool_t pool, const char *query,
 
 	struct var_expand_program *prog;
 	if (var_expand_program_create(query, &prog, &error) < 0) {
-		e_debug(auth_event, "auth-cache: var_expand_program_create('%s') failed: %s",
-			query, error);
-		return p_strdup(pool, "");
+		*error_r = t_strdup_printf("var_expand_program_create(%s) failed: %s",
+					   query, error);
+		return -1;
 	}
 
 	const char *const *vars = var_expand_program_variables(prog);
@@ -117,7 +119,8 @@ static char *auth_cache_parse_key_exclude(pool_t pool, const char *query,
 
 	var_expand_program_free(&prog);
 
-	return p_strdup(pool, str_c(str));
+	*cache_key_r = p_strdup(pool, str_c(str));
+	return 0;
 }
 
 char *auth_cache_parse_key(pool_t pool, const char *query)
@@ -140,7 +143,15 @@ char *auth_cache_parse_key_and_fields(pool_t pool, const char *query,
 		}
 		query = str_c(full_query);
 	}
-	return auth_cache_parse_key_exclude(pool, query, exclude_driver);
+
+	char *cache_key;
+	const char *error;
+	if (auth_cache_parse_key_exclude(pool, query, exclude_driver,
+					 &cache_key, &error) < 0) {
+		e_debug(auth_event, "auth-cache: %s", error);
+		cache_key = p_strdup(pool, "");
+	}
+	return cache_key;
 }
 
 static void

From 20d15baa071747f91176eb3115235aa8c78a3d11 Mon Sep 17 00:00:00 2001
From: Aki Tuomi <aki.tuomi@open-xchange.com>
Date: Fri, 25 Jul 2025 11:52:36 +0300
Subject: [PATCH 5/7] auth: auth-cache - Treat cache key parsing errors as
 fatals

Avoids accidentically turning off caching
---
 src/auth/auth-cache.c | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/auth/auth-cache.c b/src/auth/auth-cache.c
index 407e5d4aa0e..be569349182 100644
--- a/src/auth/auth-cache.c
+++ b/src/auth/auth-cache.c
@@ -147,10 +147,8 @@ char *auth_cache_parse_key_and_fields(pool_t pool, const char *query,
 	char *cache_key;
 	const char *error;
 	if (auth_cache_parse_key_exclude(pool, query, exclude_driver,
-					 &cache_key, &error) < 0) {
-		e_debug(auth_event, "auth-cache: %s", error);
-		cache_key = p_strdup(pool, "");
-	}
+					 &cache_key, &error) < 0)
+		i_fatal("auth-cache: %s", error);
 	return cache_key;
 }
 

From 0172f8e8c55aff42c688633b2891cf157641366b Mon Sep 17 00:00:00 2001
From: Aki Tuomi <aki.tuomi@open-xchange.com>
Date: Fri, 25 Jul 2025 11:41:03 +0300
Subject: [PATCH 6/7] auth: auth-cache - Require cache key to contain at least
 one variable

---
 src/auth/auth-cache.c      |  7 +++++++
 src/auth/test-auth-cache.c | 37 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/src/auth/auth-cache.c b/src/auth/auth-cache.c
index be569349182..32959f5d0f4 100644
--- a/src/auth/auth-cache.c
+++ b/src/auth/auth-cache.c
@@ -86,6 +86,13 @@ static int auth_cache_parse_key_exclude(pool_t pool, const char *query,
 	const char *const *vars = var_expand_program_variables(prog);
 	str = t_str_new(32);
 
+	if (*vars == NULL && *query != '\0') {
+		var_expand_program_free(&prog);
+		*error_r = t_strdup_printf("%s: Cache key must contain at least one variable",
+					   query);
+		return -1;
+	}
+
 	for (; *vars != NULL; vars++) {
 		/* ignore any providers */
 		if (strchr(*vars, ':') != NULL &&
diff --git a/src/auth/test-auth-cache.c b/src/auth/test-auth-cache.c
index 46836defc6d..b36d83ec022 100644
--- a/src/auth/test-auth-cache.c
+++ b/src/auth/test-auth-cache.c
@@ -97,7 +97,35 @@ static void test_auth_cache_parse_key(void)
 						 tests[i].in);
 		test_assert_strcmp_idx(cache_key, tests[i].out, i);
 	}
+
+	test_end();
+}
+
+static enum fatal_test_state test_cache_key_missing_variable(unsigned int i)
+{
+	if (i == 0)
+		test_begin("auth cache missing variable");
+
+	/* ensure that we do not accept static string */
+	static const struct {
+		const char *in, *out;
+	} tests_bad[] = {
+		{ "%u", "auth-cache: %u: Cache key must contain at least one variable" },
+		{ "foobar", "auth-cache: foobar: Cache key must contain at least one variable" },
+		{ "%{test", "auth-cache: var_expand_program_create(%{test) " \
+			    "failed: syntax error, unexpected end of file, " \
+			    "expecting CCBRACE or PIPE" },
+	};
+
+	if (i < N_ELEMENTS(tests_bad)) {
+		test_expect_fatal_string(tests_bad[i].out);
+		(void)auth_cache_parse_key(pool_datastack_create(),
+					   tests_bad[i].in);
+		return FATAL_TEST_FAILURE;
+	}
+
 	test_end();
+	return FATAL_TEST_FINISHED;
 }
 
 int main(void)
@@ -108,7 +136,14 @@ int main(void)
 		test_auth_cache_parse_key,
 		NULL
 	};
-	int ret = test_run(test_functions);
+
+	static test_fatal_func_t *const fatal_functions[] = {
+		test_cache_key_missing_variable,
+		NULL,
+	};
+
+	int ret = test_run_with_fatals(test_functions, fatal_functions);
+
 	event_unref(&auth_event);
 	return ret;
 }

From 34caed79b76a7b82a2a9c94cf35371bec6c2b826 Mon Sep 17 00:00:00 2001
From: Aki Tuomi <aki.tuomi@open-xchange.com>
Date: Fri, 25 Jul 2025 12:00:57 +0300
Subject: [PATCH 7/7] auth: auth-cache - Drop auth_cache_parse_key()

It's only used by tests and can now just call
auth_cache_parse_key_and_fields().
---
 src/auth/auth-cache.c      | 5 -----
 src/auth/auth-cache.h      | 6 ++----
 src/auth/test-auth-cache.c | 8 ++++----
 3 files changed, 6 insertions(+), 13 deletions(-)

diff --git a/src/auth/auth-cache.c b/src/auth/auth-cache.c
index 32959f5d0f4..82cc0d526eb 100644
--- a/src/auth/auth-cache.c
+++ b/src/auth/auth-cache.c
@@ -130,11 +130,6 @@ static int auth_cache_parse_key_exclude(pool_t pool, const char *query,
 	return 0;
 }
 
-char *auth_cache_parse_key(pool_t pool, const char *query)
-{
-	return auth_cache_parse_key_and_fields(pool, query, NULL, NULL);
-}
-
 char *auth_cache_parse_key_and_fields(pool_t pool, const char *query,
 				      const ARRAY_TYPE(const_string) *fields,
 				      const char *exclude_driver)
diff --git a/src/auth/auth-cache.h b/src/auth/auth-cache.h
index 9bdb9185170..d63621b1a4c 100644
--- a/src/auth/auth-cache.h
+++ b/src/auth/auth-cache.h
@@ -16,10 +16,8 @@ struct auth_cache_node {
 struct auth_cache;
 struct auth_request;
 
-/* Parses all %x variables from query and compresses them into tab-separated
-   list, so it can be used as a cache key. */
-char *auth_cache_parse_key(pool_t pool, const char *query);
-/* Same as auth_cache_parse_key(), but add also variables from "fields",
+/* Parses all %variables from query and compresses them into tab-separated
+   list, so it can be used as a cache key. Adds also variables from "fields",
    except variables prefixed with <exclude_driver>":" */
 char *auth_cache_parse_key_and_fields(pool_t pool, const char *query,
 				      const ARRAY_TYPE(const_string) *fields,
diff --git a/src/auth/test-auth-cache.c b/src/auth/test-auth-cache.c
index b36d83ec022..f58c21f7afb 100644
--- a/src/auth/test-auth-cache.c
+++ b/src/auth/test-auth-cache.c
@@ -93,8 +93,8 @@ static void test_auth_cache_parse_key(void)
 	test_begin("auth cache parse key");
 
 	for (i = 0; i < N_ELEMENTS(tests); i++) {
-		cache_key = auth_cache_parse_key(pool_datastack_create(),
-						 tests[i].in);
+		cache_key = auth_cache_parse_key_and_fields(pool_datastack_create(),
+							    tests[i].in, NULL, NULL);
 		test_assert_strcmp_idx(cache_key, tests[i].out, i);
 	}
 
@@ -119,8 +119,8 @@ static enum fatal_test_state test_cache_key_missing_variable(unsigned int i)
 
 	if (i < N_ELEMENTS(tests_bad)) {
 		test_expect_fatal_string(tests_bad[i].out);
-		(void)auth_cache_parse_key(pool_datastack_create(),
-					   tests_bad[i].in);
+		(void)auth_cache_parse_key_and_fields(pool_datastack_create(),
+						      tests_bad[i].in, NULL, NULL);
 		return FATAL_TEST_FAILURE;
 	}
 
