diff options
Diffstat (limited to 'src/auth/auth-request.c')
-rw-r--r-- | src/auth/auth-request.c | 2580 |
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, ×tamp) < 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); +} |