summaryrefslogtreecommitdiffstats
path: root/src/auth/auth-request.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
commitf7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch)
treea3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/auth/auth-request.c
parentInitial commit. (diff)
downloaddovecot-upstream.tar.xz
dovecot-upstream.zip
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/auth/auth-request.c')
-rw-r--r--src/auth/auth-request.c2580
1 files changed, 2580 insertions, 0 deletions
diff --git a/src/auth/auth-request.c b/src/auth/auth-request.c
new file mode 100644
index 0000000..ee89e75
--- /dev/null
+++ b/src/auth/auth-request.c
@@ -0,0 +1,2580 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "auth-common.h"
+#include "ioloop.h"
+#include "buffer.h"
+#include "hash.h"
+#include "sha1.h"
+#include "hex-binary.h"
+#include "str.h"
+#include "array.h"
+#include "safe-memset.h"
+#include "str-sanitize.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "dns-lookup.h"
+#include "auth-cache.h"
+#include "auth-request.h"
+#include "auth-request-handler.h"
+#include "auth-request-handler-private.h"
+#include "auth-request-stats.h"
+#include "auth-client-connection.h"
+#include "auth-master-connection.h"
+#include "auth-policy.h"
+#include "passdb.h"
+#include "passdb-blocking.h"
+#include "passdb-cache.h"
+#include "passdb-template.h"
+#include "userdb-blocking.h"
+#include "userdb-template.h"
+#include "password-scheme.h"
+#include "wildcard-match.h"
+
+#include <sys/stat.h>
+
+#define AUTH_SUBSYS_PROXY "proxy"
+#define AUTH_DNS_SOCKET_PATH "dns-client"
+#define AUTH_DNS_DEFAULT_TIMEOUT_MSECS (1000*10)
+#define AUTH_DNS_WARN_MSECS 500
+#define AUTH_REQUEST_MAX_DELAY_SECS (60*5)
+#define CACHED_PASSWORD_SCHEME "SHA1"
+
+struct auth_request_proxy_dns_lookup_ctx {
+ struct auth_request *request;
+ auth_request_proxy_cb_t *callback;
+ struct dns_lookup *dns_lookup;
+};
+
+struct auth_policy_check_ctx {
+ enum {
+ AUTH_POLICY_CHECK_TYPE_PLAIN,
+ AUTH_POLICY_CHECK_TYPE_LOOKUP,
+ AUTH_POLICY_CHECK_TYPE_SUCCESS,
+ } type;
+ struct auth_request *request;
+
+ buffer_t *success_data;
+
+ verify_plain_callback_t *callback_plain;
+ lookup_credentials_callback_t *callback_lookup;
+};
+
+const char auth_default_subsystems[2];
+
+unsigned int auth_request_state_count[AUTH_REQUEST_STATE_MAX];
+
+static void get_log_identifier(string_t *str, struct auth_request *auth_request);
+static void
+auth_request_userdb_import(struct auth_request *request, const char *args);
+
+static
+void auth_request_lookup_credentials_policy_continue(struct auth_request *request,
+ lookup_credentials_callback_t *callback);
+static
+void auth_request_policy_check_callback(int result, void *context);
+
+static const char *get_log_prefix_mech(struct auth_request *auth_request)
+{
+ string_t *str = t_str_new(64);
+ auth_request_get_log_prefix(str, auth_request, AUTH_SUBSYS_MECH);
+ return str_c(str);
+}
+
+const char *auth_request_get_log_prefix_db(struct auth_request *auth_request)
+{
+ string_t *str = t_str_new(64);
+ auth_request_get_log_prefix(str, auth_request, AUTH_SUBSYS_DB);
+ return str_c(str);
+}
+
+static struct event *get_request_event(struct auth_request *request,
+ const char *subsystem)
+{
+ if (subsystem == AUTH_SUBSYS_DB)
+ return authdb_event(request);
+ else if (subsystem == AUTH_SUBSYS_MECH)
+ return request->mech_event;
+ else
+ return request->event;
+}
+
+static void auth_request_post_alloc_init(struct auth_request *request, struct event *parent_event)
+{
+ enum log_type level;
+ request->state = AUTH_REQUEST_STATE_NEW;
+ auth_request_state_count[AUTH_REQUEST_STATE_NEW]++;
+ request->refcount = 1;
+ request->last_access = ioloop_time;
+ request->session_pid = (pid_t)-1;
+ request->set = global_auth_settings;
+ request->event = event_create(parent_event);
+ request->mech_event = event_create(request->event);
+ auth_request_fields_init(request);
+
+ level = request->set->verbose ? LOG_TYPE_INFO : LOG_TYPE_WARNING;
+ event_set_min_log_level(request->event, level);
+ event_set_min_log_level(request->mech_event, level);
+
+ p_array_init(&request->authdb_event, request->pool, 2);
+ event_set_log_prefix_callback(request->mech_event, FALSE, get_log_prefix_mech,
+ request);
+ event_set_forced_debug(request->event, request->set->debug);
+ event_add_category(request->event, &event_category_auth);
+}
+
+struct auth_request *
+auth_request_new(const struct mech_module *mech, struct event *parent_event)
+{
+ struct auth_request *request;
+
+ request = mech->auth_new();
+ request->mech = mech;
+ auth_request_post_alloc_init(request, parent_event);
+
+ return request;
+}
+
+struct auth_request *auth_request_new_dummy(struct event *parent_event)
+{
+ struct auth_request *request;
+ pool_t pool;
+
+ pool = pool_alloconly_create(MEMPOOL_GROWING"auth_request", 1024);
+ request = p_new(pool, struct auth_request, 1);
+ request->pool = pool;
+
+ auth_request_post_alloc_init(request, parent_event);
+ return request;
+}
+
+void auth_request_set_state(struct auth_request *request,
+ enum auth_request_state state)
+{
+ if (request->state == state)
+ return;
+
+ i_assert(request->to_penalty == NULL);
+
+ i_assert(auth_request_state_count[request->state] > 0);
+ auth_request_state_count[request->state]--;
+ auth_request_state_count[state]++;
+
+ request->state = state;
+ auth_refresh_proctitle();
+}
+
+void auth_request_init(struct auth_request *request)
+{
+ struct auth *auth;
+
+ auth = auth_request_get_auth(request);
+ request->set = auth->set;
+ request->passdb = auth->passdbs;
+ request->userdb = auth->userdbs;
+}
+
+struct auth *auth_request_get_auth(struct auth_request *request)
+{
+ return auth_find_service(request->fields.service);
+}
+
+void auth_request_success(struct auth_request *request,
+ const void *data, size_t data_size)
+{
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (!request->set->policy_check_after_auth) {
+ struct auth_policy_check_ctx *ctx =
+ p_new(request->pool, struct auth_policy_check_ctx, 1);
+ ctx->success_data = buffer_create_dynamic(request->pool, 1);
+ ctx->request = request;
+ ctx->type = AUTH_POLICY_CHECK_TYPE_SUCCESS;
+ auth_request_policy_check_callback(0, ctx);
+ return;
+ }
+
+ /* perform second policy lookup here */
+ struct auth_policy_check_ctx *ctx = p_new(request->pool, struct auth_policy_check_ctx, 1);
+ ctx->request = request;
+ ctx->success_data = buffer_create_dynamic(request->pool, data_size);
+ buffer_append(ctx->success_data, data, data_size);
+ ctx->type = AUTH_POLICY_CHECK_TYPE_SUCCESS;
+ auth_policy_check(request, request->mech_password, auth_request_policy_check_callback, ctx);
+}
+
+struct event_passthrough *
+auth_request_finished_event(struct auth_request *request, struct event *event)
+{
+ struct event_passthrough *e = event_create_passthrough(event);
+
+ if (request->failed) {
+ if (request->internal_failure) {
+ e->add_str("error", "internal failure");
+ } else {
+ e->add_str("error", "authentication failed");
+ }
+ } else if (request->fields.successful) {
+ e->add_str("success", "yes");
+ }
+ if (request->userdb_lookup) {
+ return e;
+ }
+ if (request->policy_penalty > 0)
+ e->add_int("policy_penalty", request->policy_penalty);
+ if (request->policy_refusal) {
+ e->add_str("policy_result", "refused");
+ } else if (request->policy_processed && request->policy_penalty > 0) {
+ e->add_str("policy_result", "delayed");
+ e->add_int("policy_penalty", request->policy_penalty);
+ } else if (request->policy_processed) {
+ e->add_str("policy_result", "ok");
+ }
+ return e;
+}
+
+void auth_request_log_finished(struct auth_request *request)
+{
+ if (request->event_finished_sent)
+ return;
+ request->event_finished_sent = TRUE;
+ string_t *str = t_str_new(64);
+ auth_request_get_log_prefix(str, request, "auth");
+ struct event_passthrough *e =
+ auth_request_finished_event(request, request->event)->
+ set_name("auth_request_finished");
+ e_debug(e->event(), "%sAuth request finished", str_c(str));
+}
+
+static
+void auth_request_success_continue(struct auth_policy_check_ctx *ctx)
+{
+ struct auth_request *request = ctx->request;
+ struct auth_stats *stats;
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ timeout_remove(&request->to_penalty);
+
+ if (request->failed || !request->passdb_success) {
+ /* password was valid, but some other check failed. */
+ auth_request_fail(request);
+ return;
+ }
+ auth_request_set_auth_successful(request);
+
+ /* log before delay */
+ auth_request_log_finished(request);
+
+ if (request->delay_until > ioloop_time) {
+ unsigned int delay_secs = request->delay_until - ioloop_time;
+ request->to_penalty = timeout_add(delay_secs * 1000,
+ auth_request_success_continue, ctx);
+ return;
+ }
+
+ if (ctx->success_data->used > 0 && !request->fields.final_resp_ok) {
+ /* we'll need one more SASL round, since client doesn't support
+ the final SASL response */
+ auth_request_handler_reply_continue(request,
+ ctx->success_data->data, ctx->success_data->used);
+ return;
+ }
+
+ if (request->set->stats) {
+ stats = auth_request_stats_get(request);
+ stats->auth_success_count++;
+ if (request->fields.master_user != NULL)
+ stats->auth_master_success_count++;
+ }
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED);
+ auth_request_refresh_last_access(request);
+ auth_request_handler_reply(request, AUTH_CLIENT_RESULT_SUCCESS,
+ ctx->success_data->data, ctx->success_data->used);
+}
+
+void auth_request_fail(struct auth_request *request)
+{
+ struct auth_stats *stats;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (request->set->stats) {
+ stats = auth_request_stats_get(request);
+ stats->auth_failure_count++;
+ }
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_FINISHED);
+ auth_request_refresh_last_access(request);
+ auth_request_log_finished(request);
+ auth_request_handler_reply(request, AUTH_CLIENT_RESULT_FAILURE, "", 0);
+}
+
+void auth_request_internal_failure(struct auth_request *request)
+{
+ request->internal_failure = TRUE;
+ auth_request_fail(request);
+}
+
+void auth_request_ref(struct auth_request *request)
+{
+ request->refcount++;
+}
+
+void auth_request_unref(struct auth_request **_request)
+{
+ struct auth_request *request = *_request;
+
+ *_request = NULL;
+ i_assert(request->refcount > 0);
+ if (--request->refcount > 0)
+ return;
+
+ i_assert(array_count(&request->authdb_event) == 0);
+
+ if (request->handler_pending_reply)
+ auth_request_handler_abort(request);
+
+ event_unref(&request->mech_event);
+ event_unref(&request->event);
+ auth_request_stats_send(request);
+ auth_request_state_count[request->state]--;
+ auth_refresh_proctitle();
+
+ if (request->mech_password != NULL) {
+ safe_memset(request->mech_password, 0,
+ strlen(request->mech_password));
+ }
+
+ if (request->dns_lookup_ctx != NULL)
+ dns_lookup_abort(&request->dns_lookup_ctx->dns_lookup);
+ timeout_remove(&request->to_abort);
+ timeout_remove(&request->to_penalty);
+
+ if (request->mech != NULL)
+ request->mech->auth_free(request);
+ else
+ pool_unref(&request->pool);
+}
+
+bool auth_request_import_master(struct auth_request *request,
+ const char *key, const char *value)
+{
+ pid_t pid;
+
+ i_assert(value != NULL);
+
+ /* master request lookups may set these */
+ if (strcmp(key, "session_pid") == 0) {
+ if (str_to_pid(value, &pid) == 0)
+ request->session_pid = pid;
+ } else if (strcmp(key, "request_auth_token") == 0)
+ request->request_auth_token = TRUE;
+ else
+ return FALSE;
+ return TRUE;
+}
+
+static bool auth_request_fail_on_nuls(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ if ((request->mech->flags & MECH_SEC_ALLOW_NULS) != 0)
+ return FALSE;
+ if (memchr(data, '\0', data_size) != NULL) {
+ e_debug(request->mech_event, "Unexpected NUL in auth data");
+ auth_request_fail(request);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+void auth_request_initial(struct auth_request *request)
+{
+ i_assert(request->state == AUTH_REQUEST_STATE_NEW);
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (auth_request_fail_on_nuls(request, request->initial_response,
+ request->initial_response_len))
+ return;
+
+ request->mech->auth_initial(request, request->initial_response,
+ request->initial_response_len);
+}
+
+void auth_request_continue(struct auth_request *request,
+ const unsigned char *data, size_t data_size)
+{
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (request->fields.successful) {
+ auth_request_success(request, "", 0);
+ return;
+ }
+
+ if (auth_request_fail_on_nuls(request, data, data_size))
+ return;
+
+ auth_request_refresh_last_access(request);
+ request->mech->auth_continue(request, data, data_size);
+}
+
+static void auth_request_save_cache(struct auth_request *request,
+ enum passdb_result result)
+{
+ struct auth_passdb *passdb = request->passdb;
+ const char *encoded_password;
+ string_t *str;
+ struct password_generate_params gen_params = {
+ .user = request->fields.user,
+ .rounds = 0
+ };
+
+ switch (result) {
+ case PASSDB_RESULT_USER_UNKNOWN:
+ case PASSDB_RESULT_PASSWORD_MISMATCH:
+ case PASSDB_RESULT_OK:
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ /* can be cached */
+ break;
+ case PASSDB_RESULT_NEXT:
+ case PASSDB_RESULT_USER_DISABLED:
+ case PASSDB_RESULT_PASS_EXPIRED:
+ /* FIXME: we can't cache this now, or cache lookup would
+ return success. */
+ return;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ i_unreached();
+ }
+
+ if (passdb_cache == NULL || passdb->cache_key == NULL)
+ return;
+
+ if (result < 0) {
+ /* lookup failed. */
+ if (result == PASSDB_RESULT_USER_UNKNOWN) {
+ auth_cache_insert(passdb_cache, request,
+ passdb->cache_key, "", FALSE);
+ }
+ return;
+ }
+
+ if (request->passdb_password == NULL &&
+ !auth_fields_exists(request->fields.extra_fields, "nopassword")) {
+ /* passdb didn't provide the correct password */
+ if (result != PASSDB_RESULT_OK ||
+ request->mech_password == NULL)
+ return;
+
+ /* we can still cache valid password lookups though.
+ strdup() it so that mech_password doesn't get
+ cleared too early. */
+ if (!password_generate_encoded(request->mech_password,
+ &gen_params,
+ CACHED_PASSWORD_SCHEME,
+ &encoded_password))
+ i_unreached();
+ request->passdb_password =
+ p_strconcat(request->pool, "{"CACHED_PASSWORD_SCHEME"}",
+ encoded_password, NULL);
+ }
+
+ /* save all except the currently given password in cache */
+ str = t_str_new(256);
+ if (request->passdb_password != NULL) {
+ if (*request->passdb_password != '{') {
+ /* cached passwords must have a known scheme */
+ str_append_c(str, '{');
+ str_append(str, passdb->passdb->default_pass_scheme);
+ str_append_c(str, '}');
+ }
+ str_append_tabescaped(str, request->passdb_password);
+ }
+
+ if (!auth_fields_is_empty(request->fields.extra_fields)) {
+ str_append_c(str, '\t');
+ /* add only those extra fields to cache that were set by this
+ passdb lookup. the CHANGED flag does this, because we
+ snapshotted the extra_fields before the current passdb
+ lookup. */
+ auth_fields_append(request->fields.extra_fields, str,
+ AUTH_FIELD_FLAG_CHANGED,
+ AUTH_FIELD_FLAG_CHANGED);
+ }
+ auth_cache_insert(passdb_cache, request, passdb->cache_key, str_c(str),
+ result == PASSDB_RESULT_OK);
+}
+
+static bool
+auth_request_mechanism_accepted(const char *const *mechs,
+ const struct mech_module *mech)
+{
+ /* no filter specified, anything goes */
+ if (mechs == NULL) return TRUE;
+ /* request has no mechanism, see if none is accepted */
+ if (mech == NULL)
+ return str_array_icase_find(mechs, "none");
+ /* check if request mechanism is accepted */
+ return str_array_icase_find(mechs, mech->mech_name);
+}
+
+/**
+
+Check if username is included in the filter. Logic is that if the username
+is not excluded by anything, and is included by something, it will be accepted.
+By default, all usernames are included, unless there is a inclusion item, when
+username will be excluded if there is no inclusion for it.
+
+Exclusions are denoted with a ! in front of the pattern.
+*/
+bool auth_request_username_accepted(const char *const *filter, const char *username)
+{
+ bool have_includes = FALSE;
+ bool matched_inc = FALSE;
+
+ for(;*filter != NULL; filter++) {
+ /* if filter has ! it means the pattern will be refused */
+ bool exclude = (**filter == '!');
+ if (!exclude)
+ have_includes = TRUE;
+ if (wildcard_match(username, (*filter)+(exclude?1:0))) {
+ if (exclude) {
+ return FALSE;
+ } else {
+ matched_inc = TRUE;
+ }
+ }
+ }
+
+ return matched_inc || !have_includes;
+}
+
+static bool
+auth_request_want_skip_passdb(struct auth_request *request,
+ struct auth_passdb *passdb)
+{
+ /* if mechanism is not supported, skip */
+ const char *const *mechs = passdb->passdb->mechanisms;
+ const char *const *username_filter = passdb->passdb->username_filter;
+ const char *username;
+
+ username = request->fields.user;
+
+ if (!auth_request_mechanism_accepted(mechs, request->mech)) {
+ auth_request_log_debug(request,
+ request->mech != NULL ? AUTH_SUBSYS_MECH
+ : "none",
+ "skipping passdb: mechanism filtered");
+ return TRUE;
+ }
+
+ if (passdb->passdb->username_filter != NULL &&
+ !auth_request_username_accepted(username_filter, username)) {
+ auth_request_log_debug(request,
+ request->mech != NULL ? AUTH_SUBSYS_MECH
+ : "none",
+ "skipping passdb: username filtered");
+ return TRUE;
+ }
+
+ /* skip_password_check basically specifies if authentication is
+ finished */
+ bool authenticated = request->fields.skip_password_check;
+
+ switch (passdb->skip) {
+ case AUTH_PASSDB_SKIP_NEVER:
+ return FALSE;
+ case AUTH_PASSDB_SKIP_AUTHENTICATED:
+ return authenticated;
+ case AUTH_PASSDB_SKIP_UNAUTHENTICATED:
+ return !authenticated;
+ }
+ i_unreached();
+}
+
+static bool
+auth_request_want_skip_userdb(struct auth_request *request,
+ struct auth_userdb *userdb)
+{
+ switch (userdb->skip) {
+ case AUTH_USERDB_SKIP_NEVER:
+ return FALSE;
+ case AUTH_USERDB_SKIP_FOUND:
+ return request->userdb_success;
+ case AUTH_USERDB_SKIP_NOTFOUND:
+ return !request->userdb_success;
+ }
+ i_unreached();
+}
+
+static const char *
+auth_request_cache_result_to_str(enum auth_request_cache_result result)
+{
+ switch(result) {
+ case AUTH_REQUEST_CACHE_NONE:
+ return "none";
+ case AUTH_REQUEST_CACHE_HIT:
+ return "hit";
+ case AUTH_REQUEST_CACHE_MISS:
+ return "miss";
+ default:
+ i_unreached();
+ }
+}
+
+void auth_request_passdb_lookup_begin(struct auth_request *request)
+{
+ struct event *event;
+ const char *name;
+
+ i_assert(request->passdb != NULL);
+ i_assert(!request->userdb_lookup);
+
+ request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE;
+
+ name = (request->passdb->set->name[0] != '\0' ?
+ request->passdb->set->name :
+ request->passdb->passdb->iface.name);
+
+ event = event_create(request->event);
+ event_add_str(event, "passdb_id", dec2str(request->passdb->passdb->id));
+ event_add_str(event, "passdb_name", name);
+ event_add_str(event, "passdb", request->passdb->passdb->iface.name);
+ event_set_log_prefix_callback(event, FALSE,
+ auth_request_get_log_prefix_db, request);
+
+ /* check if we should enable verbose logging here */
+ if (*request->passdb->set->auth_verbose == 'y')
+ event_set_min_log_level(event, LOG_TYPE_INFO);
+ else if (*request->passdb->set->auth_verbose == 'n')
+ event_set_min_log_level(event, LOG_TYPE_WARNING);
+
+ e_debug(event_create_passthrough(event)->
+ set_name("auth_passdb_request_started")->
+ event(),
+ "Performing passdb lookup");
+ array_push_back(&request->authdb_event, &event);
+}
+
+void auth_request_passdb_lookup_end(struct auth_request *request,
+ enum passdb_result result)
+{
+ i_assert(array_count(&request->authdb_event) > 0);
+ struct event *event = authdb_event(request);
+ struct event_passthrough *e =
+ event_create_passthrough(event)->
+ set_name("auth_passdb_request_finished")->
+ add_str("result", passdb_result_to_string(result));
+ if (request->passdb_cache_result != AUTH_REQUEST_CACHE_NONE &&
+ request->set->cache_ttl != 0 && request->set->cache_size != 0)
+ e->add_str("cache", auth_request_cache_result_to_str(request->passdb_cache_result));
+ e_debug(e->event(), "Finished passdb lookup");
+ event_unref(&event);
+ array_pop_back(&request->authdb_event);
+}
+
+void auth_request_userdb_lookup_begin(struct auth_request *request)
+{
+ struct event *event;
+ const char *name;
+
+ i_assert(request->userdb != NULL);
+ i_assert(request->userdb_lookup);
+
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_NONE;
+
+ name = (request->userdb->set->name[0] != '\0' ?
+ request->userdb->set->name :
+ request->userdb->userdb->iface->name);
+
+ event = event_create(request->event);
+ event_add_str(event, "userdb_id", dec2str(request->userdb->userdb->id));
+ event_add_str(event, "userdb_name", name);
+ event_add_str(event, "userdb", request->userdb->userdb->iface->name);
+ event_set_log_prefix_callback(event, FALSE,
+ auth_request_get_log_prefix_db, request);
+
+ /* check if we should enable verbose logging here*/
+ if (*request->userdb->set->auth_verbose == 'y')
+ event_set_min_log_level(event, LOG_TYPE_INFO);
+ else if (*request->userdb->set->auth_verbose == 'n')
+ event_set_min_log_level(event, LOG_TYPE_WARNING);
+
+ e_debug(event_create_passthrough(event)->
+ set_name("auth_userdb_request_started")->
+ event(),
+ "Performing userdb lookup");
+ array_push_back(&request->authdb_event, &event);
+}
+
+void auth_request_userdb_lookup_end(struct auth_request *request,
+ enum userdb_result result)
+{
+ i_assert(array_count(&request->authdb_event) > 0);
+ struct event *event = authdb_event(request);
+ struct event_passthrough *e =
+ event_create_passthrough(event)->
+ set_name("auth_userdb_request_finished")->
+ add_str("result", userdb_result_to_string(result));
+ if (request->userdb_cache_result != AUTH_REQUEST_CACHE_NONE &&
+ request->set->cache_ttl != 0 && request->set->cache_size != 0)
+ e->add_str("cache", auth_request_cache_result_to_str(request->userdb_cache_result));
+ e_debug(e->event(), "Finished userdb lookup");
+ event_unref(&event);
+ array_pop_back(&request->authdb_event);
+}
+
+static bool
+auth_request_handle_passdb_callback(enum passdb_result *result,
+ struct auth_request *request)
+{
+ struct auth_passdb *next_passdb;
+ enum auth_db_rule result_rule;
+ bool passdb_continue = FALSE;
+
+ if (request->passdb_password != NULL) {
+ safe_memset(request->passdb_password, 0,
+ strlen(request->passdb_password));
+ }
+
+ auth_request_passdb_lookup_end(request, *result);
+
+ if (request->passdb->set->deny &&
+ *result != PASSDB_RESULT_USER_UNKNOWN) {
+ /* deny passdb. we can get through this step only if the
+ lookup returned that user doesn't exist in it. internal
+ errors are fatal here. */
+ if (*result != PASSDB_RESULT_INTERNAL_FAILURE) {
+ e_info(authdb_event(request),
+ "User found from deny passdb");
+ *result = PASSDB_RESULT_USER_DISABLED;
+ }
+ return TRUE;
+ }
+ if (request->failed) {
+ /* The passdb didn't fail, but something inside it failed
+ (e.g. allow_nets mismatch). Make sure we'll fail this
+ lookup, but reset the failure so the next passdb can
+ succeed. */
+ if (*result == PASSDB_RESULT_OK)
+ *result = PASSDB_RESULT_USER_UNKNOWN;
+ request->failed = FALSE;
+ }
+
+ /* users that exist but can't log in are special. we don't try to match
+ any of the success/failure rules to them. they'll always fail. */
+ switch (*result) {
+ case PASSDB_RESULT_USER_DISABLED:
+ return TRUE;
+ case PASSDB_RESULT_PASS_EXPIRED:
+ auth_request_set_field(request, "reason",
+ "Password expired", NULL);
+ return TRUE;
+
+ case PASSDB_RESULT_OK:
+ result_rule = request->passdb->result_success;
+ break;
+ case PASSDB_RESULT_INTERNAL_FAILURE:
+ result_rule = request->passdb->result_internalfail;
+ break;
+ case PASSDB_RESULT_NEXT:
+ e_debug(authdb_event(request),
+ "Not performing authentication (noauthenticate set)");
+ result_rule = AUTH_DB_RULE_CONTINUE;
+ break;
+ case PASSDB_RESULT_SCHEME_NOT_AVAILABLE:
+ case PASSDB_RESULT_USER_UNKNOWN:
+ case PASSDB_RESULT_PASSWORD_MISMATCH:
+ default:
+ result_rule = request->passdb->result_failure;
+ break;
+ }
+
+ switch (result_rule) {
+ case AUTH_DB_RULE_RETURN:
+ break;
+ case AUTH_DB_RULE_RETURN_OK:
+ request->passdb_success = TRUE;
+ break;
+ case AUTH_DB_RULE_RETURN_FAIL:
+ request->passdb_success = FALSE;
+ break;
+ case AUTH_DB_RULE_CONTINUE:
+ passdb_continue = TRUE;
+ if (*result == PASSDB_RESULT_OK) {
+ /* password was successfully verified. don't bother
+ checking it again. */
+ auth_request_set_password_verified(request);
+ }
+ break;
+ case AUTH_DB_RULE_CONTINUE_OK:
+ passdb_continue = TRUE;
+ request->passdb_success = TRUE;
+ /* password was successfully verified. don't bother
+ checking it again. */
+ auth_request_set_password_verified(request);
+ break;
+ case AUTH_DB_RULE_CONTINUE_FAIL:
+ passdb_continue = TRUE;
+ request->passdb_success = FALSE;
+ break;
+ }
+ /* nopassword check is specific to a single passdb and shouldn't leak
+ to the next one. we already added it to cache. */
+ auth_fields_remove(request->fields.extra_fields, "nopassword");
+ auth_fields_remove(request->fields.extra_fields, "noauthenticate");
+
+ if (request->fields.requested_login_user != NULL &&
+ *result == PASSDB_RESULT_OK) {
+ auth_request_master_user_login_finish(request);
+ /* if the passdb lookup continues, it continues with non-master
+ passdbs for the requested_login_user. */
+ next_passdb = auth_request_get_auth(request)->passdbs;
+ } else {
+ next_passdb = request->passdb->next;
+ }
+
+ while (next_passdb != NULL &&
+ auth_request_want_skip_passdb(request, next_passdb))
+ next_passdb = next_passdb->next;
+
+ if (*result == PASSDB_RESULT_OK || *result == PASSDB_RESULT_NEXT) {
+ /* this passdb lookup succeeded, preserve its extra fields */
+ auth_fields_snapshot(request->fields.extra_fields);
+ request->snapshot_have_userdb_prefetch_set =
+ request->userdb_prefetch_set;
+ if (request->fields.userdb_reply != NULL)
+ auth_fields_snapshot(request->fields.userdb_reply);
+ } else {
+ /* this passdb lookup failed, remove any extra fields it set */
+ auth_fields_rollback(request->fields.extra_fields);
+ if (request->fields.userdb_reply != NULL) {
+ auth_fields_rollback(request->fields.userdb_reply);
+ request->userdb_prefetch_set =
+ request->snapshot_have_userdb_prefetch_set;
+ }
+ }
+
+ if (passdb_continue && next_passdb != NULL) {
+ /* try next passdb. */
+ request->passdb = next_passdb;
+ request->passdb_password = NULL;
+
+ if (*result == PASSDB_RESULT_USER_UNKNOWN) {
+ /* remember that we did at least one successful
+ passdb lookup */
+ request->passdbs_seen_user_unknown = TRUE;
+ } else if (*result == PASSDB_RESULT_INTERNAL_FAILURE) {
+ /* remember that we have had an internal failure. at
+ the end return internal failure if we couldn't
+ successfully login. */
+ request->passdbs_seen_internal_failure = TRUE;
+ }
+ return FALSE;
+ } else if (*result == PASSDB_RESULT_NEXT) {
+ /* admin forgot to put proper passdb last */
+ e_error(request->event,
+ "%sLast passdb had noauthenticate field, cannot authenticate user",
+ auth_request_get_log_prefix_db(request));
+ *result = PASSDB_RESULT_INTERNAL_FAILURE;
+ } else if (request->passdb_success) {
+ /* either this or a previous passdb lookup succeeded. */
+ *result = PASSDB_RESULT_OK;
+ } else if (request->passdbs_seen_internal_failure) {
+ /* last passdb lookup returned internal failure. it may have
+ had the correct password, so return internal failure
+ instead of plain failure. */
+ *result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ return TRUE;
+}
+
+void
+auth_request_verify_plain_callback_finish(enum passdb_result result,
+ struct auth_request *request)
+{
+ const char *error;
+
+ if (passdb_template_export(request->passdb->override_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand override_fields: %s", error);
+ result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ if (!auth_request_handle_passdb_callback(&result, request)) {
+ /* try next passdb */
+ auth_request_verify_plain(request, request->mech_password,
+ request->private_callback.verify_plain);
+ } else {
+ auth_request_ref(request);
+ request->passdb_result = result;
+ request->private_callback.verify_plain(request->passdb_result, request);
+ auth_request_unref(&request);
+ }
+}
+
+void auth_request_verify_plain_callback(enum passdb_result result,
+ struct auth_request *request)
+{
+ struct auth_passdb *passdb = request->passdb;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_PASSDB);
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (result == PASSDB_RESULT_OK &&
+ auth_fields_exists(request->fields.extra_fields, "noauthenticate"))
+ result = PASSDB_RESULT_NEXT;
+
+ if (result != PASSDB_RESULT_INTERNAL_FAILURE)
+ auth_request_save_cache(request, result);
+ else {
+ /* lookup failed. if we're looking here only because the
+ request was expired in cache, fallback to using cached
+ expired record. */
+ const char *cache_key = passdb->cache_key;
+
+ auth_request_stats_add_tempfail(request);
+ if (passdb_cache_verify_plain(request, cache_key,
+ request->mech_password,
+ &result, TRUE)) {
+ e_info(authdb_event(request),
+ "Falling back to expired data from cache");
+ return;
+ }
+ }
+
+ auth_request_verify_plain_callback_finish(result, request);
+}
+
+static bool password_has_illegal_chars(const char *password)
+{
+ for (; *password != '\0'; password++) {
+ switch (*password) {
+ case '\001':
+ case '\t':
+ case '\r':
+ case '\n':
+ /* these characters have a special meaning in internal
+ protocols, make sure the password doesn't
+ accidentally get there unescaped. */
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+static bool auth_request_is_disabled_master_user(struct auth_request *request)
+{
+ if (request->fields.requested_login_user == NULL ||
+ request->passdb != NULL)
+ return FALSE;
+
+ /* no masterdbs, master logins not supported */
+ e_info(request->mech_event,
+ "Attempted master login with no master passdbs "
+ "(trying to log in as user: %s)",
+ request->fields.requested_login_user);
+ return TRUE;
+}
+
+static
+void auth_request_policy_penalty_finish(void *context)
+{
+ struct auth_policy_check_ctx *ctx = context;
+
+ timeout_remove(&ctx->request->to_penalty);
+
+ i_assert(ctx->request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ switch(ctx->type) {
+ case AUTH_POLICY_CHECK_TYPE_PLAIN:
+ ctx->request->handler->verify_plain_continue_callback(ctx->request, ctx->callback_plain);
+ return;
+ case AUTH_POLICY_CHECK_TYPE_LOOKUP:
+ auth_request_lookup_credentials_policy_continue(ctx->request, ctx->callback_lookup);
+ return;
+ case AUTH_POLICY_CHECK_TYPE_SUCCESS:
+ auth_request_success_continue(ctx);
+ return;
+ default:
+ i_unreached();
+ }
+}
+
+static
+void auth_request_policy_check_callback(int result, void *context)
+{
+ struct auth_policy_check_ctx *ctx = context;
+
+ ctx->request->policy_processed = TRUE;
+ /* It's possible that multiple policy lookups return a penalty.
+ Sum them all up to the event. */
+ ctx->request->policy_penalty += result < 0 ? 0 : result;
+
+ if (ctx->request->set->policy_log_only && result != 0) {
+ auth_request_policy_penalty_finish(context);
+ return;
+ }
+ if (result < 0) {
+ /* fail it right here and now */
+ auth_request_fail(ctx->request);
+ } else if (ctx->type != AUTH_POLICY_CHECK_TYPE_SUCCESS && result > 0 &&
+ !ctx->request->fields.no_penalty) {
+ ctx->request->to_penalty = timeout_add(result * 1000,
+ auth_request_policy_penalty_finish,
+ context);
+ } else {
+ auth_request_policy_penalty_finish(context);
+ }
+}
+
+void auth_request_verify_plain(struct auth_request *request,
+ const char *password,
+ verify_plain_callback_t *callback)
+{
+ struct auth_policy_check_ctx *ctx;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (request->mech_password == NULL)
+ request->mech_password = p_strdup(request->pool, password);
+ else
+ i_assert(request->mech_password == password);
+ request->user_changed_by_lookup = FALSE;
+
+ if (request->policy_processed || !request->set->policy_check_before_auth) {
+ request->handler->verify_plain_continue_callback(request,
+ callback);
+ } else {
+ ctx = p_new(request->pool, struct auth_policy_check_ctx, 1);
+ ctx->request = request;
+ ctx->callback_plain = callback;
+ ctx->type = AUTH_POLICY_CHECK_TYPE_PLAIN;
+ auth_policy_check(request, request->mech_password, auth_request_policy_check_callback, ctx);
+ }
+}
+
+void auth_request_default_verify_plain_continue(struct auth_request *request,
+ verify_plain_callback_t *callback)
+{
+ struct auth_passdb *passdb;
+ enum passdb_result result;
+ const char *cache_key, *error;
+ const char *password = request->mech_password;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (auth_request_is_disabled_master_user(request)) {
+ callback(PASSDB_RESULT_USER_UNKNOWN, request);
+ return;
+ }
+
+ if (password_has_illegal_chars(password)) {
+ e_info(authdb_event(request),
+ "Attempted login with password having illegal chars");
+ callback(PASSDB_RESULT_USER_UNKNOWN, request);
+ return;
+ }
+
+ passdb = request->passdb;
+
+ while (passdb != NULL && auth_request_want_skip_passdb(request, passdb))
+ passdb = passdb->next;
+
+ request->passdb = passdb;
+
+ if (passdb == NULL) {
+ auth_request_log_error(request,
+ request->mech != NULL ? AUTH_SUBSYS_MECH : "none",
+ "All password databases were skipped");
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+
+ auth_request_passdb_lookup_begin(request);
+ request->private_callback.verify_plain = callback;
+
+ cache_key = passdb_cache == NULL ? NULL : passdb->cache_key;
+ if (passdb_cache_verify_plain(request, cache_key, password,
+ &result, FALSE)) {
+ return;
+ }
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_PASSDB);
+ /* In case this request had already done a credentials lookup (is it
+ even possible?), make sure wanted_credentials_scheme is cleared
+ so passdbs don't think we're doing a credentials lookup. */
+ request->wanted_credentials_scheme = NULL;
+
+ if (passdb->passdb->iface.verify_plain == NULL) {
+ /* we're deinitializing and just want to get rid of this
+ request */
+ auth_request_verify_plain_callback(
+ PASSDB_RESULT_INTERNAL_FAILURE, request);
+ } else if (passdb->passdb->blocking) {
+ passdb_blocking_verify_plain(request);
+ } else if (passdb_template_export(passdb->default_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand default_fields: %s", error);
+ auth_request_verify_plain_callback(
+ PASSDB_RESULT_INTERNAL_FAILURE, request);
+ } else {
+ passdb->passdb->iface.verify_plain(request, password,
+ auth_request_verify_plain_callback);
+ }
+}
+
+static void
+auth_request_lookup_credentials_finish(enum passdb_result result,
+ const unsigned char *credentials,
+ size_t size,
+ struct auth_request *request)
+{
+ const char *error;
+
+ if (passdb_template_export(request->passdb->override_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand override_fields: %s", error);
+ result = PASSDB_RESULT_INTERNAL_FAILURE;
+ }
+ if (!auth_request_handle_passdb_callback(&result, request)) {
+ /* try next passdb */
+ if (request->fields.skip_password_check &&
+ request->fields.delayed_credentials == NULL && size > 0) {
+ /* passdb continue* rule after a successful lookup.
+ remember these credentials and use them later on. */
+ auth_request_set_delayed_credentials(request,
+ credentials, size);
+ }
+ auth_request_lookup_credentials(request,
+ request->wanted_credentials_scheme,
+ request->private_callback.lookup_credentials);
+ } else {
+ if (request->fields.delayed_credentials != NULL && size == 0) {
+ /* we did multiple passdb lookups, but the last one
+ didn't provide any credentials (e.g. just wanted to
+ add some extra fields). so use the first passdb's
+ credentials instead. */
+ credentials = request->fields.delayed_credentials;
+ size = request->fields.delayed_credentials_size;
+ }
+ if (request->set->debug_passwords &&
+ result == PASSDB_RESULT_OK) {
+ e_debug(authdb_event(request),
+ "Credentials: %s",
+ binary_to_hex(credentials, size));
+ }
+ if (result == PASSDB_RESULT_SCHEME_NOT_AVAILABLE &&
+ request->passdbs_seen_user_unknown) {
+ /* one of the passdbs accepted the scheme,
+ but the user was unknown there */
+ result = PASSDB_RESULT_USER_UNKNOWN;
+ }
+ request->passdb_result = result;
+ request->private_callback.
+ lookup_credentials(result, credentials, size, request);
+ }
+}
+
+void auth_request_lookup_credentials_callback(enum passdb_result result,
+ const unsigned char *credentials,
+ size_t size,
+ struct auth_request *request)
+{
+ struct auth_passdb *passdb = request->passdb;
+ const char *cache_cred, *cache_scheme;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_PASSDB);
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (result == PASSDB_RESULT_OK &&
+ auth_fields_exists(request->fields.extra_fields, "noauthenticate"))
+ result = PASSDB_RESULT_NEXT;
+
+ if (result != PASSDB_RESULT_INTERNAL_FAILURE)
+ auth_request_save_cache(request, result);
+ else {
+ /* lookup failed. if we're looking here only because the
+ request was expired in cache, fallback to using cached
+ expired record. */
+ const char *cache_key = passdb->cache_key;
+
+ auth_request_stats_add_tempfail(request);
+ if (passdb_cache_lookup_credentials(request, cache_key,
+ &cache_cred, &cache_scheme,
+ &result, TRUE)) {
+ e_info(authdb_event(request),
+ "Falling back to expired data from cache");
+ passdb_handle_credentials(
+ result, cache_cred, cache_scheme,
+ auth_request_lookup_credentials_finish,
+ request);
+ return;
+ }
+ }
+
+ auth_request_lookup_credentials_finish(result, credentials, size,
+ request);
+}
+
+void auth_request_lookup_credentials(struct auth_request *request,
+ const char *scheme,
+ lookup_credentials_callback_t *callback)
+{
+ struct auth_policy_check_ctx *ctx;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+
+ if (request->wanted_credentials_scheme == NULL)
+ request->wanted_credentials_scheme =
+ p_strdup(request->pool, scheme);
+ request->user_changed_by_lookup = FALSE;
+
+ if (request->policy_processed || !request->set->policy_check_before_auth)
+ auth_request_lookup_credentials_policy_continue(request, callback);
+ else {
+ ctx = p_new(request->pool, struct auth_policy_check_ctx, 1);
+ ctx->request = request;
+ ctx->callback_lookup = callback;
+ ctx->type = AUTH_POLICY_CHECK_TYPE_LOOKUP;
+ auth_policy_check(request, ctx->request->mech_password, auth_request_policy_check_callback, ctx);
+ }
+}
+
+static
+void auth_request_lookup_credentials_policy_continue(struct auth_request *request,
+ lookup_credentials_callback_t *callback)
+{
+ struct auth_passdb *passdb;
+ const char *cache_key, *cache_cred, *cache_scheme, *error;
+ enum passdb_result result;
+
+ i_assert(request->state == AUTH_REQUEST_STATE_MECH_CONTINUE);
+ if (auth_request_is_disabled_master_user(request)) {
+ callback(PASSDB_RESULT_USER_UNKNOWN, NULL, 0, request);
+ return;
+ }
+ passdb = request->passdb;
+ while (passdb != NULL && auth_request_want_skip_passdb(request, passdb))
+ passdb = passdb->next;
+ request->passdb = passdb;
+
+ if (passdb == NULL) {
+ auth_request_log_error(request,
+ request->mech != NULL ? AUTH_SUBSYS_MECH : "none",
+ "All password databases were skipped");
+ callback(PASSDB_RESULT_INTERNAL_FAILURE, NULL, 0, request);
+ return;
+ }
+
+ auth_request_passdb_lookup_begin(request);
+ request->private_callback.lookup_credentials = callback;
+
+ cache_key = passdb_cache == NULL ? NULL : passdb->cache_key;
+ if (cache_key != NULL) {
+ if (passdb_cache_lookup_credentials(request, cache_key,
+ &cache_cred, &cache_scheme,
+ &result, FALSE)) {
+ request->passdb_cache_result = AUTH_REQUEST_CACHE_HIT;
+ passdb_handle_credentials(
+ result, cache_cred, cache_scheme,
+ auth_request_lookup_credentials_finish,
+ request);
+ return;
+ } else {
+ request->passdb_cache_result = AUTH_REQUEST_CACHE_MISS;
+ }
+ }
+
+ auth_request_set_state(request, AUTH_REQUEST_STATE_PASSDB);
+
+ if (passdb->passdb->iface.lookup_credentials == NULL) {
+ /* this passdb doesn't support credentials */
+ e_debug(authdb_event(request),
+ "passdb doesn't support credential lookups");
+ auth_request_lookup_credentials_callback(
+ PASSDB_RESULT_SCHEME_NOT_AVAILABLE,
+ uchar_empty_ptr, 0, request);
+ } else if (passdb->passdb->blocking) {
+ passdb_blocking_lookup_credentials(request);
+ } else if (passdb_template_export(passdb->default_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand default_fields: %s", error);
+ auth_request_lookup_credentials_callback(
+ PASSDB_RESULT_INTERNAL_FAILURE,
+ uchar_empty_ptr, 0, request);
+ } else {
+ passdb->passdb->iface.lookup_credentials(request,
+ auth_request_lookup_credentials_callback);
+ }
+}
+
+void auth_request_set_credentials(struct auth_request *request,
+ const char *scheme, const char *data,
+ set_credentials_callback_t *callback)
+{
+ struct auth_passdb *passdb = request->passdb;
+ const char *cache_key, *new_credentials;
+
+ cache_key = passdb_cache == NULL ? NULL : passdb->cache_key;
+ if (cache_key != NULL)
+ auth_cache_remove(passdb_cache, request, cache_key);
+
+ request->private_callback.set_credentials = callback;
+
+ new_credentials = t_strdup_printf("{%s}%s", scheme, data);
+ if (passdb->passdb->blocking)
+ passdb_blocking_set_credentials(request, new_credentials);
+ else if (passdb->passdb->iface.set_credentials != NULL) {
+ passdb->passdb->iface.set_credentials(request, new_credentials,
+ callback);
+ } else {
+ /* this passdb doesn't support credentials update */
+ callback(FALSE, request);
+ }
+}
+
+static void auth_request_userdb_save_cache(struct auth_request *request,
+ enum userdb_result result)
+{
+ struct auth_userdb *userdb = request->userdb;
+ string_t *str;
+ const char *cache_value;
+
+ if (passdb_cache == NULL || userdb->cache_key == NULL)
+ return;
+
+ if (result == USERDB_RESULT_USER_UNKNOWN)
+ cache_value = "";
+ else {
+ str = t_str_new(128);
+ auth_fields_append(request->fields.userdb_reply, str,
+ AUTH_FIELD_FLAG_CHANGED,
+ AUTH_FIELD_FLAG_CHANGED);
+ if (request->user_changed_by_lookup) {
+ /* username was changed by passdb or userdb */
+ if (str_len(str) > 0)
+ str_append_c(str, '\t');
+ str_append(str, "user=");
+ str_append_tabescaped(str, request->fields.user);
+ }
+ if (str_len(str) == 0) {
+ /* no userdb fields. but we can't save an empty string,
+ since that means "user unknown". */
+ str_append(str, AUTH_REQUEST_USER_KEY_IGNORE);
+ }
+ cache_value = str_c(str);
+ }
+ /* last_success has no meaning with userdb */
+ auth_cache_insert(passdb_cache, request, userdb->cache_key,
+ cache_value, FALSE);
+}
+
+static bool auth_request_lookup_user_cache(struct auth_request *request,
+ const char *key,
+ enum userdb_result *result_r,
+ bool use_expired)
+{
+ struct auth_stats *stats = auth_request_stats_get(request);
+ const char *value;
+ struct auth_cache_node *node;
+ bool expired, neg_expired;
+
+ value = auth_cache_lookup(passdb_cache, request, key, &node,
+ &expired, &neg_expired);
+ if (value == NULL || (expired && !use_expired)) {
+ stats->auth_cache_miss_count++;
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_MISS;
+ e_debug(request->event,
+ value == NULL ? "%suserdb cache miss" :
+ "%suserdb cache expired",
+ auth_request_get_log_prefix_db(request));
+ return FALSE;
+ }
+ stats->auth_cache_hit_count++;
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_HIT;
+ e_debug(request->event,
+ "%suserdb cache hit: %s",
+ auth_request_get_log_prefix_db(request), value);
+
+ if (*value == '\0') {
+ /* negative cache entry */
+ *result_r = USERDB_RESULT_USER_UNKNOWN;
+ auth_request_init_userdb_reply(request, FALSE);
+ return TRUE;
+ }
+
+ /* We want to preserve any userdb fields set by the earlier passdb
+ lookup, so initialize userdb_reply only if it doesn't exist.
+ Don't add userdb's default_fields, because the entire userdb part of
+ the result comes from the cache. */
+ if (request->fields.userdb_reply == NULL)
+ auth_request_init_userdb_reply(request, FALSE);
+ auth_request_userdb_import(request, value);
+ *result_r = USERDB_RESULT_OK;
+ return TRUE;
+}
+
+void auth_request_userdb_callback(enum userdb_result result,
+ struct auth_request *request)
+{
+ struct auth_userdb *userdb = request->userdb;
+ struct auth_userdb *next_userdb;
+ enum auth_db_rule result_rule;
+ const char *error;
+ bool userdb_continue = FALSE;
+
+ switch (result) {
+ case USERDB_RESULT_OK:
+ result_rule = userdb->result_success;
+ break;
+ case USERDB_RESULT_INTERNAL_FAILURE:
+ auth_request_stats_add_tempfail(request);
+ result_rule = userdb->result_internalfail;
+ break;
+ case USERDB_RESULT_USER_UNKNOWN:
+ default:
+ result_rule = userdb->result_failure;
+ break;
+ }
+
+ switch (result_rule) {
+ case AUTH_DB_RULE_RETURN:
+ break;
+ case AUTH_DB_RULE_RETURN_OK:
+ request->userdb_success = TRUE;
+ break;
+ case AUTH_DB_RULE_RETURN_FAIL:
+ request->userdb_success = FALSE;
+ break;
+ case AUTH_DB_RULE_CONTINUE:
+ userdb_continue = TRUE;
+ break;
+ case AUTH_DB_RULE_CONTINUE_OK:
+ userdb_continue = TRUE;
+ request->userdb_success = TRUE;
+ break;
+ case AUTH_DB_RULE_CONTINUE_FAIL:
+ userdb_continue = TRUE;
+ request->userdb_success = FALSE;
+ break;
+ }
+
+ auth_request_userdb_lookup_end(request, result);
+
+ next_userdb = userdb->next;
+ while (next_userdb != NULL &&
+ auth_request_want_skip_userdb(request, next_userdb))
+ next_userdb = next_userdb->next;
+
+ if (userdb_continue && next_userdb != NULL) {
+ /* try next userdb. */
+ if (result == USERDB_RESULT_INTERNAL_FAILURE)
+ request->userdbs_seen_internal_failure = TRUE;
+
+ if (result == USERDB_RESULT_OK) {
+ /* this userdb lookup succeeded, preserve its extra
+ fields */
+ if (userdb_template_export(userdb->override_fields_tmpl,
+ request, &error) < 0) {
+ e_error(request->event,
+ "%sFailed to expand override_fields: %s",
+ auth_request_get_log_prefix_db(request), error);
+ request->private_callback.userdb(
+ USERDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+ auth_fields_snapshot(request->fields.userdb_reply);
+ } else {
+ /* this userdb lookup failed, remove any extra fields
+ it set */
+ auth_fields_rollback(request->fields.userdb_reply);
+ }
+ request->user_changed_by_lookup = FALSE;
+
+ request->userdb = next_userdb;
+ auth_request_lookup_user(request,
+ request->private_callback.userdb);
+ return;
+ }
+
+ if (request->userdb_success) {
+ if (userdb_template_export(userdb->override_fields_tmpl,
+ request, &error) < 0) {
+ e_error(request->event,
+ "%sFailed to expand override_fields: %s",
+ auth_request_get_log_prefix_db(request), error);
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ } else {
+ result = USERDB_RESULT_OK;
+ }
+ } else if (request->userdbs_seen_internal_failure ||
+ result == USERDB_RESULT_INTERNAL_FAILURE) {
+ /* one of the userdb lookups failed. the user might have been
+ in there, so this is an internal failure */
+ result = USERDB_RESULT_INTERNAL_FAILURE;
+ } else if (request->client_pid != 0) {
+ /* this was an actual login attempt, the user should
+ have been found. */
+ if (auth_request_get_auth(request)->userdbs->next == NULL) {
+ e_error(request->event,
+ "%suser not found from userdb",
+ auth_request_get_log_prefix_db(request));
+ } else {
+ e_error(request->mech_event,
+ "user not found from any userdbs");
+ }
+ result = USERDB_RESULT_USER_UNKNOWN;
+ } else {
+ result = USERDB_RESULT_USER_UNKNOWN;
+ }
+
+ if (request->userdb_lookup_tempfailed) {
+ /* no caching */
+ } else if (result != USERDB_RESULT_INTERNAL_FAILURE) {
+ if (request->userdb_cache_result != AUTH_REQUEST_CACHE_HIT)
+ auth_request_userdb_save_cache(request, result);
+ } else if (passdb_cache != NULL && userdb->cache_key != NULL) {
+ /* lookup failed. if we're looking here only because the
+ request was expired in cache, fallback to using cached
+ expired record. */
+ const char *cache_key = userdb->cache_key;
+
+ if (auth_request_lookup_user_cache(request, cache_key,
+ &result, TRUE)) {
+ e_info(request->event,
+ "%sFalling back to expired data from cache",
+ auth_request_get_log_prefix_db(request));
+ }
+ }
+
+ request->private_callback.userdb(result, request);
+}
+
+void auth_request_lookup_user(struct auth_request *request,
+ userdb_callback_t *callback)
+{
+ struct auth_userdb *userdb = request->userdb;
+ const char *cache_key, *error;
+
+ request->private_callback.userdb = callback;
+ request->user_changed_by_lookup = FALSE;
+ request->userdb_lookup = TRUE;
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_NONE;
+ if (request->fields.userdb_reply == NULL)
+ auth_request_init_userdb_reply(request, TRUE);
+ else {
+ /* we still want to set default_fields. these override any
+ existing fields set by previous userdbs (because if that is
+ unwanted, ":protected" can be used). */
+ if (userdb_template_export(userdb->default_fields_tmpl,
+ request, &error) < 0) {
+ e_error(authdb_event(request),
+ "Failed to expand default_fields: %s", error);
+ auth_request_userdb_callback(
+ USERDB_RESULT_INTERNAL_FAILURE, request);
+ return;
+ }
+ }
+
+ auth_request_userdb_lookup_begin(request);
+
+ /* (for now) auth_cache is shared between passdb and userdb */
+ cache_key = passdb_cache == NULL ? NULL : userdb->cache_key;
+ if (cache_key != NULL) {
+ enum userdb_result result;
+
+ if (auth_request_lookup_user_cache(request, cache_key,
+ &result, FALSE)) {
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_HIT;
+ auth_request_userdb_callback(result, request);
+ return;
+ } else {
+ request->userdb_cache_result = AUTH_REQUEST_CACHE_MISS;
+ }
+ }
+
+ if (userdb->userdb->iface->lookup == NULL) {
+ /* we are deinitializing */
+ auth_request_userdb_callback(USERDB_RESULT_INTERNAL_FAILURE,
+ request);
+ } else if (userdb->userdb->blocking)
+ userdb_blocking_lookup(request);
+ else
+ userdb->userdb->iface->lookup(request, auth_request_userdb_callback);
+}
+
+static void
+auth_request_validate_networks(struct auth_request *request,
+ const char *name, const char *networks,
+ const struct ip_addr *remote_ip)
+{
+ const char *const *net;
+ struct ip_addr net_ip;
+ unsigned int bits;
+ bool found = FALSE;
+
+ for (net = t_strsplit_spaces(networks, ", "); *net != NULL; net++) {
+ e_debug(authdb_event(request),
+ "%s: Matching for network %s", name, *net);
+
+ if (strcmp(*net, "local") == 0) {
+ if (remote_ip->family == 0) {
+ found = TRUE;
+ break;
+ }
+ } else if (net_parse_range(*net, &net_ip, &bits) < 0) {
+ e_info(authdb_event(request),
+ "%s: Invalid network '%s'", name, *net);
+ } else if (remote_ip->family != 0 &&
+ net_is_in_network(remote_ip, &net_ip, bits)) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (found)
+ ;
+ else if (remote_ip->family == 0) {
+ e_info(authdb_event(request),
+ "%s check failed: Remote IP not known and 'local' missing", name);
+ } else {
+ e_info(authdb_event(request),
+ "%s check failed: IP %s not in allowed networks",
+ name, net_ip2addr(remote_ip));
+ }
+ if (!found)
+ request->failed = TRUE;
+}
+
+static void
+auth_request_set_password(struct auth_request *request, const char *value,
+ const char *default_scheme, bool noscheme)
+{
+ if (request->passdb_password != NULL) {
+ e_error(authdb_event(request),
+ "Multiple password values not supported");
+ return;
+ }
+
+ /* if the password starts with '{' it most likely contains
+ also '}'. check it anyway to make sure, because we
+ assert-crash later if it doesn't exist. this could happen
+ if plaintext passwords are used. */
+ if (*value == '{' && !noscheme && strchr(value, '}') != NULL)
+ request->passdb_password = p_strdup(request->pool, value);
+ else {
+ i_assert(default_scheme != NULL);
+ request->passdb_password =
+ p_strdup_printf(request->pool, "{%s}%s",
+ default_scheme, value);
+ }
+}
+
+static const char *
+get_updated_username(const char *old_username,
+ const char *name, const char *value)
+{
+ const char *p;
+
+ if (strcmp(name, "user") == 0) {
+ /* replace the whole username */
+ return value;
+ }
+
+ p = strchr(old_username, '@');
+ if (strcmp(name, "username") == 0) {
+ if (strchr(value, '@') != NULL)
+ return value;
+
+ /* preserve the current @domain */
+ return t_strconcat(value, p, NULL);
+ }
+
+ if (strcmp(name, "domain") == 0) {
+ if (p == NULL) {
+ /* add the domain */
+ return t_strconcat(old_username, "@", value, NULL);
+ } else {
+ /* replace the existing domain */
+ p = t_strdup_until(old_username, p + 1);
+ return t_strconcat(p, value, NULL);
+ }
+ }
+ return NULL;
+}
+
+static bool
+auth_request_try_update_username(struct auth_request *request,
+ const char *name, const char *value)
+{
+ const char *new_value;
+
+ new_value = get_updated_username(request->fields.user, name, value);
+ if (new_value == NULL)
+ return FALSE;
+ if (new_value[0] == '\0') {
+ e_error(authdb_event(request),
+ "username attempted to be changed to empty");
+ request->failed = TRUE;
+ return TRUE;
+ }
+
+ if (strcmp(request->fields.user, new_value) != 0) {
+ e_debug(authdb_event(request),
+ "username changed %s -> %s",
+ request->fields.user, new_value);
+ auth_request_set_username_forced(request, new_value);
+ request->user_changed_by_lookup = TRUE;
+ }
+ return TRUE;
+}
+
+static void
+auth_request_passdb_import(struct auth_request *request, const char *args,
+ const char *key_prefix, const char *default_scheme)
+{
+ const char *const *arg, *field;
+
+ for (arg = t_strsplit(args, "\t"); *arg != NULL; arg++) {
+ field = t_strconcat(key_prefix, *arg, NULL);
+ auth_request_set_field_keyvalue(request, field, default_scheme);
+ }
+}
+
+void auth_request_set_field(struct auth_request *request,
+ const char *name, const char *value,
+ const char *default_scheme)
+{
+ size_t name_len = strlen(name);
+
+ i_assert(*name != '\0');
+ i_assert(value != NULL);
+
+ i_assert(request->passdb != NULL);
+
+ if (name_len > 10 && strcmp(name+name_len-10, ":protected") == 0) {
+ /* set this field only if it hasn't been set before */
+ name = t_strndup(name, name_len-10);
+ if (auth_fields_exists(request->fields.extra_fields, name))
+ return;
+ } else if (name_len > 7 && strcmp(name+name_len-7, ":remove") == 0) {
+ /* remove this field entirely */
+ name = t_strndup(name, name_len-7);
+ auth_fields_remove(request->fields.extra_fields, name);
+ return;
+ }
+
+ if (strcmp(name, "password") == 0) {
+ auth_request_set_password(request, value,
+ default_scheme, FALSE);
+ return;
+ }
+ if (strcmp(name, "password_noscheme") == 0) {
+ auth_request_set_password(request, value, default_scheme, TRUE);
+ return;
+ }
+
+ if (auth_request_try_update_username(request, name, value)) {
+ /* don't change the original value so it gets saved correctly
+ to cache. */
+ } else if (strcmp(name, "login_user") == 0) {
+ auth_request_set_login_username_forced(request, value);
+ } else if (strcmp(name, "allow_nets") == 0) {
+ auth_request_validate_networks(request, name, value,
+ &request->fields.remote_ip);
+ } else if (strcmp(name, "fail") == 0) {
+ request->failed = TRUE;
+ } else if (strcmp(name, "delay_until") == 0) {
+ time_t timestamp;
+ unsigned int extra_secs = 0;
+ const char *p;
+
+ p = strchr(value, '+');
+ if (p != NULL) {
+ value = t_strdup_until(value, p++);
+ if (str_to_uint(p, &extra_secs) < 0) {
+ e_error(authdb_event(request),
+ "Invalid delay_until randomness number '%s'", p);
+ request->failed = TRUE;
+ } else {
+ extra_secs = i_rand_limit(extra_secs);
+ }
+ }
+ if (str_to_time(value, &timestamp) < 0) {
+ e_error(authdb_event(request),
+ "Invalid delay_until timestamp '%s'", value);
+ request->failed = TRUE;
+ } else if (timestamp <= ioloop_time) {
+ /* no more delays */
+ } else if (timestamp - ioloop_time > AUTH_REQUEST_MAX_DELAY_SECS) {
+ e_error(authdb_event(request),
+ "delay_until timestamp %s is too much in the future, failing", value);
+ request->failed = TRUE;
+ } else {
+ /* add randomness, but not too much of it */
+ timestamp += extra_secs;
+ if (timestamp - ioloop_time > AUTH_REQUEST_MAX_DELAY_SECS)
+ timestamp = ioloop_time + AUTH_REQUEST_MAX_DELAY_SECS;
+ request->delay_until = timestamp;
+ }
+ } else if (strcmp(name, "allow_real_nets") == 0) {
+ auth_request_validate_networks(request, name, value,
+ &request->fields.real_remote_ip);
+ } else if (str_begins(name, "userdb_")) {
+ /* for prefetch userdb */
+ request->userdb_prefetch_set = TRUE;
+ if (request->fields.userdb_reply == NULL)
+ auth_request_init_userdb_reply(request, TRUE);
+ if (strcmp(name, "userdb_userdb_import") == 0) {
+ /* we can't put the whole userdb_userdb_import
+ value to extra_cache_fields or it doesn't work
+ properly. so handle this explicitly. */
+ auth_request_passdb_import(request, value,
+ "userdb_", default_scheme);
+ return;
+ }
+ auth_request_set_userdb_field(request, name + 7, value);
+ } else if (strcmp(name, "noauthenticate") == 0) {
+ /* add "nopassword" also so that passdbs won't try to verify
+ the password. */
+ auth_fields_add(request->fields.extra_fields, name, value, 0);
+ auth_fields_add(request->fields.extra_fields, "nopassword", NULL, 0);
+ } else if (strcmp(name, "nopassword") == 0) {
+ /* NULL password - anything goes */
+ const char *password = request->passdb_password;
+
+ if (password != NULL &&
+ !auth_fields_exists(request->fields.extra_fields, "noauthenticate")) {
+ (void)password_get_scheme(&password);
+ if (*password != '\0') {
+ e_error(authdb_event(request),
+ "nopassword set but password is "
+ "non-empty");
+ return;
+ }
+ }
+ request->passdb_password = NULL;
+ auth_fields_add(request->fields.extra_fields, name, value, 0);
+ return;
+ } else if (strcmp(name, "passdb_import") == 0) {
+ auth_request_passdb_import(request, value, "", default_scheme);
+ return;
+ } else {
+ /* these fields are returned to client */
+ auth_fields_add(request->fields.extra_fields, name, value, 0);
+ return;
+ }
+
+ /* add the field unconditionally to extra_fields. this is required if
+ a) auth cache is used, b) if we're a worker and we'll need to send
+ this to the main auth process that can store it in the cache,
+ c) for easily checking :protected fields' existence. */
+ auth_fields_add(request->fields.extra_fields, name, value,
+ AUTH_FIELD_FLAG_HIDDEN);
+}
+
+void auth_request_set_null_field(struct auth_request *request, const char *name)
+{
+ if (str_begins(name, "userdb_")) {
+ /* make sure userdb prefetch is used even if all the fields
+ were returned as NULL. */
+ request->userdb_prefetch_set = TRUE;
+ }
+}
+
+void auth_request_set_field_keyvalue(struct auth_request *request,
+ const char *field,
+ const char *default_scheme)
+{
+ const char *key, *value;
+
+ value = strchr(field, '=');
+ if (value == NULL) {
+ key = field;
+ value = "";
+ } else {
+ key = t_strdup_until(field, value);
+ value++;
+ }
+ auth_request_set_field(request, key, value, default_scheme);
+}
+
+void auth_request_set_fields(struct auth_request *request,
+ const char *const *fields,
+ const char *default_scheme)
+{
+ for (; *fields != NULL; fields++) {
+ if (**fields == '\0')
+ continue;
+ auth_request_set_field_keyvalue(request, *fields, default_scheme);
+ }
+}
+
+static void auth_request_set_uidgid_file(struct auth_request *request,
+ const char *path_template)
+{
+ string_t *path;
+ struct stat st;
+ const char *error;
+
+ path = t_str_new(256);
+ if (auth_request_var_expand(path, path_template, request,
+ NULL, &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand uidgid_file=%s: %s", path_template, error);
+ request->userdb_lookup_tempfailed = TRUE;
+ } else if (stat(str_c(path), &st) < 0) {
+ e_error(authdb_event(request),
+ "stat(%s) failed: %m", str_c(path));
+ request->userdb_lookup_tempfailed = TRUE;
+ } else {
+ auth_fields_add(request->fields.userdb_reply,
+ "uid", dec2str(st.st_uid), 0);
+ auth_fields_add(request->fields.userdb_reply,
+ "gid", dec2str(st.st_gid), 0);
+ }
+}
+
+static void
+auth_request_userdb_import(struct auth_request *request, const char *args)
+{
+ const char *key, *value, *const *arg;
+
+ for (arg = t_strsplit(args, "\t"); *arg != NULL; arg++) {
+ value = strchr(*arg, '=');
+ if (value == NULL) {
+ key = *arg;
+ value = "";
+ } else {
+ key = t_strdup_until(*arg, value);
+ value++;
+ }
+ auth_request_set_userdb_field(request, key, value);
+ }
+}
+
+void auth_request_set_userdb_field(struct auth_request *request,
+ const char *name, const char *value)
+{
+ size_t name_len = strlen(name);
+ uid_t uid;
+ gid_t gid;
+
+ i_assert(value != NULL);
+
+ if (name_len > 10 && strcmp(name+name_len-10, ":protected") == 0) {
+ /* set this field only if it hasn't been set before */
+ name = t_strndup(name, name_len-10);
+ if (auth_fields_exists(request->fields.userdb_reply, name))
+ return;
+ } else if (name_len > 7 && strcmp(name+name_len-7, ":remove") == 0) {
+ /* remove this field entirely */
+ name = t_strndup(name, name_len-7);
+ auth_fields_remove(request->fields.userdb_reply, name);
+ return;
+ }
+
+ if (strcmp(name, "uid") == 0) {
+ uid = userdb_parse_uid(request, value);
+ if (uid == (uid_t)-1) {
+ request->userdb_lookup_tempfailed = TRUE;
+ return;
+ }
+ value = dec2str(uid);
+ } else if (strcmp(name, "gid") == 0) {
+ gid = userdb_parse_gid(request, value);
+ if (gid == (gid_t)-1) {
+ request->userdb_lookup_tempfailed = TRUE;
+ return;
+ }
+ value = dec2str(gid);
+ } else if (strcmp(name, "tempfail") == 0) {
+ request->userdb_lookup_tempfailed = TRUE;
+ return;
+ } else if (auth_request_try_update_username(request, name, value)) {
+ return;
+ } else if (strcmp(name, "uidgid_file") == 0) {
+ auth_request_set_uidgid_file(request, value);
+ return;
+ } else if (strcmp(name, "userdb_import") == 0) {
+ auth_request_userdb_import(request, value);
+ return;
+ } else if (strcmp(name, "system_user") == 0) {
+ /* FIXME: the system_user is for backwards compatibility */
+ static bool warned = FALSE;
+ if (!warned) {
+ e_warning(authdb_event(request),
+ "Replace system_user with system_groups_user");
+ warned = TRUE;
+ }
+ name = "system_groups_user";
+ } else if (strcmp(name, AUTH_REQUEST_USER_KEY_IGNORE) == 0) {
+ return;
+ }
+
+ auth_fields_add(request->fields.userdb_reply, name, value, 0);
+}
+
+void auth_request_set_userdb_field_values(struct auth_request *request,
+ const char *name,
+ const char *const *values)
+{
+ if (*values == NULL)
+ return;
+
+ if (strcmp(name, "gid") == 0) {
+ /* convert gids to comma separated list */
+ string_t *value;
+ gid_t gid;
+
+ value = t_str_new(128);
+ for (; *values != NULL; values++) {
+ gid = userdb_parse_gid(request, *values);
+ if (gid == (gid_t)-1) {
+ request->userdb_lookup_tempfailed = TRUE;
+ return;
+ }
+
+ if (str_len(value) > 0)
+ str_append_c(value, ',');
+ str_append(value, dec2str(gid));
+ }
+ auth_fields_add(request->fields.userdb_reply, name, str_c(value), 0);
+ } else {
+ /* add only one */
+ if (values[1] != NULL) {
+ e_warning(authdb_event(request),
+ "Multiple values found for '%s', "
+ "using value '%s'", name, *values);
+ }
+ auth_request_set_userdb_field(request, name, *values);
+ }
+}
+
+static bool auth_request_proxy_is_self(struct auth_request *request)
+{
+ const char *port = NULL;
+
+ /* check if the port is the same */
+ port = auth_fields_find(request->fields.extra_fields, "port");
+ if (port != NULL && !str_uint_equals(port, request->fields.local_port))
+ return FALSE;
+ /* don't check destuser. in some systems destuser is intentionally
+ changed to proxied connections, but that shouldn't affect the
+ proxying decision.
+
+ it's unlikely any systems would actually want to proxy a connection
+ to itself only to change the username, since it can already be done
+ without proxying by changing the "user" field. */
+ return TRUE;
+}
+
+static bool
+auth_request_proxy_ip_is_self(struct auth_request *request,
+ const struct ip_addr *ip)
+{
+ unsigned int i;
+
+ if (net_ip_compare(ip, &request->fields.real_local_ip))
+ return TRUE;
+
+ for (i = 0; request->set->proxy_self_ips[i].family != 0; i++) {
+ if (net_ip_compare(ip, &request->set->proxy_self_ips[i]))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+auth_request_proxy_finish_ip(struct auth_request *request,
+ bool proxy_host_is_self)
+{
+ const struct auth_request_fields *fields = &request->fields;
+
+ if (!auth_fields_exists(fields->extra_fields, "proxy_maybe")) {
+ /* proxying */
+ } else if (!proxy_host_is_self ||
+ !auth_request_proxy_is_self(request)) {
+ /* proxy destination isn't ourself - proxy */
+ auth_fields_remove(fields->extra_fields, "proxy_maybe");
+ auth_fields_add(fields->extra_fields, "proxy", NULL, 0);
+ } else {
+ /* proxying to ourself - log in without proxying by dropping
+ all the proxying fields. */
+ bool proxy_always = auth_fields_exists(fields->extra_fields,
+ "proxy_always");
+
+ auth_request_proxy_finish_failure(request);
+ if (proxy_always) {
+ /* setup where "self" refers to the local director
+ cluster, while "non-self" refers to remote clusters.
+
+ we've matched self here, so add proxy field and
+ let director fill the host. */
+ auth_fields_add(request->fields.extra_fields,
+ "proxy", NULL, 0);
+ }
+ }
+}
+
+static void
+auth_request_proxy_dns_callback(const struct dns_lookup_result *result,
+ struct auth_request_proxy_dns_lookup_ctx *ctx)
+{
+ struct auth_request *request = ctx->request;
+ const char *host;
+ unsigned int i;
+ bool proxy_host_is_self;
+
+ request->dns_lookup_ctx = NULL;
+ ctx->dns_lookup = NULL;
+
+ host = auth_fields_find(request->fields.extra_fields, "host");
+ i_assert(host != NULL);
+
+ if (result->ret != 0) {
+ auth_request_log_error(request, AUTH_SUBSYS_PROXY,
+ "DNS lookup for %s failed: %s", host, result->error);
+ request->internal_failure = TRUE;
+ auth_request_proxy_finish_failure(request);
+ } else {
+ if (result->msecs > AUTH_DNS_WARN_MSECS) {
+ auth_request_log_warning(request, AUTH_SUBSYS_PROXY,
+ "DNS lookup for %s took %u.%03u s",
+ host, result->msecs/1000, result->msecs % 1000);
+ }
+ auth_fields_add(request->fields.extra_fields, "hostip",
+ net_ip2addr(&result->ips[0]), 0);
+ proxy_host_is_self = FALSE;
+ for (i = 0; i < result->ips_count; i++) {
+ if (auth_request_proxy_ip_is_self(request,
+ &result->ips[i])) {
+ proxy_host_is_self = TRUE;
+ break;
+ }
+ }
+ auth_request_proxy_finish_ip(request, proxy_host_is_self);
+ }
+ if (ctx->callback != NULL)
+ ctx->callback(result->ret == 0, request);
+ auth_request_unref(&request);
+}
+
+static int auth_request_proxy_host_lookup(struct auth_request *request,
+ const char *host,
+ auth_request_proxy_cb_t *callback)
+{
+ struct auth_request_proxy_dns_lookup_ctx *ctx;
+ struct dns_lookup_settings dns_set;
+ const char *value;
+ unsigned int secs;
+
+ /* need to do dns lookup for the host */
+ i_zero(&dns_set);
+ dns_set.dns_client_socket_path = AUTH_DNS_SOCKET_PATH;
+ dns_set.timeout_msecs = AUTH_DNS_DEFAULT_TIMEOUT_MSECS;
+ dns_set.event_parent = request->event;
+ value = auth_fields_find(request->fields.extra_fields, "proxy_timeout");
+ if (value != NULL) {
+ if (str_to_uint(value, &secs) < 0) {
+ auth_request_log_error(request, AUTH_SUBSYS_PROXY,
+ "Invalid proxy_timeout value: %s", value);
+ } else {
+ dns_set.timeout_msecs = secs*1000;
+ }
+ }
+
+ ctx = p_new(request->pool, struct auth_request_proxy_dns_lookup_ctx, 1);
+ ctx->request = request;
+ auth_request_ref(request);
+ request->dns_lookup_ctx = ctx;
+
+ if (dns_lookup(host, &dns_set, auth_request_proxy_dns_callback, ctx,
+ &ctx->dns_lookup) < 0) {
+ /* failed early */
+ return -1;
+ }
+ ctx->callback = callback;
+ return 0;
+}
+
+int auth_request_proxy_finish(struct auth_request *request,
+ auth_request_proxy_cb_t *callback)
+{
+ const char *host, *hostip;
+ struct ip_addr ip;
+ bool proxy_host_is_self;
+
+ if (request->auth_only)
+ return 1;
+ if (!auth_fields_exists(request->fields.extra_fields, "proxy") &&
+ !auth_fields_exists(request->fields.extra_fields, "proxy_maybe"))
+ return 1;
+
+ host = auth_fields_find(request->fields.extra_fields, "host");
+ if (host == NULL) {
+ /* director can set the host. give it access to lip and lport
+ so it can also perform proxy_maybe internally */
+ proxy_host_is_self = FALSE;
+ if (request->fields.local_ip.family != 0) {
+ auth_fields_add(request->fields.extra_fields, "lip",
+ net_ip2addr(&request->fields.local_ip), 0);
+ }
+ if (request->fields.local_port != 0) {
+ auth_fields_add(request->fields.extra_fields, "lport",
+ dec2str(request->fields.local_port), 0);
+ }
+ } else if (net_addr2ip(host, &ip) == 0) {
+ proxy_host_is_self =
+ auth_request_proxy_ip_is_self(request, &ip);
+ } else {
+ hostip = auth_fields_find(request->fields.extra_fields, "hostip");
+ if (hostip != NULL && net_addr2ip(hostip, &ip) < 0) {
+ auth_request_log_error(request, AUTH_SUBSYS_PROXY,
+ "Invalid hostip in passdb: %s", hostip);
+ return -1;
+ }
+ if (hostip == NULL) {
+ /* asynchronous host lookup */
+ return auth_request_proxy_host_lookup(request, host, callback);
+ }
+ proxy_host_is_self =
+ auth_request_proxy_ip_is_self(request, &ip);
+ }
+
+ auth_request_proxy_finish_ip(request, proxy_host_is_self);
+ return 1;
+}
+
+void auth_request_proxy_finish_failure(struct auth_request *request)
+{
+ /* drop all proxying fields */
+ auth_fields_remove(request->fields.extra_fields, "proxy");
+ auth_fields_remove(request->fields.extra_fields, "proxy_maybe");
+ auth_fields_remove(request->fields.extra_fields, "proxy_always");
+ auth_fields_remove(request->fields.extra_fields, "host");
+ auth_fields_remove(request->fields.extra_fields, "port");
+ auth_fields_remove(request->fields.extra_fields, "destuser");
+}
+
+static void log_password_failure(struct auth_request *request,
+ const char *plain_password,
+ const char *crypted_password,
+ const char *scheme,
+ const struct password_generate_params *params,
+ const char *subsystem)
+{
+ struct event *event = get_request_event(request, subsystem);
+ static bool scheme_ok = FALSE;
+ string_t *str = t_str_new(256);
+ const char *working_scheme;
+
+ str_printfa(str, "%s(%s) != '%s'", scheme,
+ plain_password, crypted_password);
+
+ if (!scheme_ok) {
+ /* perhaps the scheme is wrong - see if we can find
+ a working one */
+ working_scheme = password_scheme_detect(plain_password,
+ crypted_password, params);
+ if (working_scheme != NULL) {
+ str_printfa(str, ", try %s scheme instead",
+ working_scheme);
+ }
+ }
+
+ e_debug(event, "%s", str_c(str));
+}
+
+static void
+auth_request_append_password(struct auth_request *request, string_t *str)
+{
+ const char *p, *log_type = request->set->verbose_passwords;
+ unsigned int max_len = 1024;
+
+ if (request->mech_password == NULL)
+ return;
+
+ p = strchr(log_type, ':');
+ if (p != NULL) {
+ if (str_to_uint(p+1, &max_len) < 0)
+ i_unreached();
+ log_type = t_strdup_until(log_type, p);
+ }
+
+ if (strcmp(log_type, "plain") == 0) {
+ str_printfa(str, "(given password: %s)",
+ t_strndup(request->mech_password, max_len));
+ } else if (strcmp(log_type, "sha1") == 0) {
+ unsigned char sha1[SHA1_RESULTLEN];
+
+ sha1_get_digest(request->mech_password,
+ strlen(request->mech_password), sha1);
+ str_printfa(str, "(SHA1 of given password: %s)",
+ t_strndup(binary_to_hex(sha1, sizeof(sha1)),
+ max_len));
+ } else {
+ i_unreached();
+ }
+}
+
+void auth_request_log_password_mismatch(struct auth_request *request,
+ const char *subsystem)
+{
+ auth_request_log_login_failure(request, subsystem, AUTH_LOG_MSG_PASSWORD_MISMATCH);
+}
+
+void auth_request_log_unknown_user(struct auth_request *request,
+ const char *subsystem)
+{
+ auth_request_log_login_failure(request, subsystem, "unknown user");
+}
+
+void auth_request_log_login_failure(struct auth_request *request,
+ const char *subsystem,
+ const char *message)
+{
+ struct event *event = get_request_event(request, subsystem);
+ string_t *str;
+
+ if (strcmp(request->set->verbose_passwords, "no") == 0) {
+ e_info(event, "%s", message);
+ return;
+ }
+
+ /* make sure this gets logged */
+ enum log_type orig_level = event_get_min_log_level(event);
+ event_set_min_log_level(event, LOG_TYPE_INFO);
+
+ str = t_str_new(128);
+ str_append(str, message);
+ str_append(str, " ");
+
+ auth_request_append_password(request, str);
+
+ if (request->userdb_lookup) {
+ if (request->userdb->next != NULL)
+ str_append(str, " - trying the next userdb");
+ } else {
+ if (request->passdb->next != NULL)
+ str_append(str, " - trying the next passdb");
+ }
+ e_info(event, "%s", str_c(str));
+ event_set_min_log_level(event, orig_level);
+}
+
+int auth_request_password_verify(struct auth_request *request,
+ const char *plain_password,
+ const char *crypted_password,
+ const char *scheme, const char *subsystem)
+{
+ return auth_request_password_verify_log(request, plain_password,
+ crypted_password, scheme, subsystem, TRUE);
+}
+
+int auth_request_password_verify_log(struct auth_request *request,
+ const char *plain_password,
+ const char *crypted_password,
+ const char *scheme, const char *subsystem,
+ bool log_password_mismatch)
+{
+ const unsigned char *raw_password;
+ size_t raw_password_size;
+ const char *error;
+ int ret;
+ struct password_generate_params gen_params = {
+ .user = request->fields.original_username,
+ .rounds = 0
+ };
+
+ if (request->fields.skip_password_check) {
+ /* passdb continue* rule after a successful authentication */
+ return 1;
+ }
+
+ if (request->passdb->set->deny) {
+ /* this is a deny database, we don't care about the password */
+ return 0;
+ }
+
+ if (auth_fields_exists(request->fields.extra_fields, "nopassword")) {
+ auth_request_log_debug(request, subsystem,
+ "Allowing any password");
+ return 1;
+ }
+
+ ret = password_decode(crypted_password, scheme,
+ &raw_password, &raw_password_size, &error);
+ if (ret <= 0) {
+ if (ret < 0) {
+ auth_request_log_error(request, subsystem,
+ "Password data is not valid for scheme %s: %s",
+ scheme, error);
+ } else {
+ auth_request_log_error(request, subsystem,
+ "Unknown scheme %s", scheme);
+ }
+ return -1;
+ }
+
+ /* Use original_username since it may be important for some
+ password schemes (eg. digest-md5). Otherwise the username is used
+ only for logging purposes. */
+ ret = password_verify(plain_password, &gen_params,
+ scheme, raw_password, raw_password_size, &error);
+ if (ret < 0) {
+ const char *password_str = request->set->debug_passwords ?
+ t_strdup_printf(" '%s'", crypted_password) : "";
+ auth_request_log_error(request, subsystem,
+ "Invalid password%s in passdb: %s",
+ password_str, error);
+ } else if (ret == 0) {
+ if (log_password_mismatch)
+ auth_request_log_password_mismatch(request, subsystem);
+ }
+ if (ret <= 0 && request->set->debug_passwords) T_BEGIN {
+ log_password_failure(request, plain_password,
+ crypted_password, scheme,
+ &gen_params,
+ subsystem);
+ } T_END;
+ return ret;
+}
+
+enum passdb_result auth_request_password_missing(struct auth_request *request)
+{
+ if (request->fields.skip_password_check) {
+ /* This passdb wasn't used for authentication */
+ return PASSDB_RESULT_OK;
+ }
+ e_info(authdb_event(request),
+ "No password returned (and no nopassword)");
+ return PASSDB_RESULT_PASSWORD_MISMATCH;
+}
+
+void auth_request_get_log_prefix(string_t *str, struct auth_request *auth_request,
+ const char *subsystem)
+{
+ const char *name;
+
+ if (subsystem == AUTH_SUBSYS_DB) {
+ if (!auth_request->userdb_lookup) {
+ i_assert(auth_request->passdb != NULL);
+ name = auth_request->passdb->set->name[0] != '\0' ?
+ auth_request->passdb->set->name :
+ auth_request->passdb->passdb->iface.name;
+ } else {
+ i_assert(auth_request->userdb != NULL);
+ name = auth_request->userdb->set->name[0] != '\0' ?
+ auth_request->userdb->set->name :
+ auth_request->userdb->userdb->iface->name;
+ }
+ } else if (subsystem == AUTH_SUBSYS_MECH) {
+ i_assert(auth_request->mech != NULL);
+ name = t_str_lcase(auth_request->mech->mech_name);
+ } else {
+ name = subsystem;
+ }
+ str_append(str, name);
+ str_append_c(str, '(');
+ get_log_identifier(str, auth_request);
+ str_append(str, "): ");
+}
+
+#define MAX_LOG_USERNAME_LEN 64
+static void get_log_identifier(string_t *str, struct auth_request *auth_request)
+{
+ const char *ip;
+
+ if (auth_request->fields.user == NULL)
+ str_append(str, "?");
+ else
+ str_sanitize_append(str, auth_request->fields.user,
+ MAX_LOG_USERNAME_LEN);
+
+ ip = net_ip2addr(&auth_request->fields.remote_ip);
+ if (ip[0] != '\0') {
+ str_append_c(str, ',');
+ str_append(str, ip);
+ }
+ if (auth_request->fields.requested_login_user != NULL)
+ str_append(str, ",master");
+ if (auth_request->fields.session_id != NULL)
+ str_printfa(str, ",<%s>", auth_request->fields.session_id);
+}
+
+void auth_request_log_debug(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...)
+{
+ struct event *event = get_request_event(auth_request, subsystem);
+ va_list va;
+
+ va_start(va, format);
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_vprintfa(str, format, va);
+ e_debug(event, "%s", str_c(str));
+ } T_END;
+ va_end(va);
+}
+
+void auth_request_log_info(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...)
+{
+ struct event *event = get_request_event(auth_request, subsystem);
+ va_list va;
+
+ va_start(va, format);
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_vprintfa(str, format, va);
+ e_info(event, "%s", str_c(str));
+ } T_END;
+ va_end(va);
+}
+
+void auth_request_log_warning(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...)
+{
+ struct event *event = get_request_event(auth_request, subsystem);
+ va_list va;
+
+ va_start(va, format);
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_vprintfa(str, format, va);
+ e_warning(event, "%s", str_c(str));
+ } T_END;
+ va_end(va);
+}
+
+void auth_request_log_error(struct auth_request *auth_request,
+ const char *subsystem,
+ const char *format, ...)
+{
+ struct event *event = get_request_event(auth_request, subsystem);
+ va_list va;
+
+ va_start(va, format);
+ T_BEGIN {
+ string_t *str = t_str_new(128);
+ str_vprintfa(str, format, va);
+ e_error(event, "%s", str_c(str));
+ } T_END;
+ va_end(va);
+}
+
+void auth_request_refresh_last_access(struct auth_request *request)
+{
+ request->last_access = ioloop_time;
+ if (request->to_abort != NULL)
+ timeout_reset(request->to_abort);
+}