summaryrefslogtreecommitdiffstats
path: root/src/auth/auth-policy.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/auth/auth-policy.c')
-rw-r--r--src/auth/auth-policy.c620
1 files changed, 620 insertions, 0 deletions
diff --git a/src/auth/auth-policy.c b/src/auth/auth-policy.c
new file mode 100644
index 0000000..951f85e
--- /dev/null
+++ b/src/auth/auth-policy.c
@@ -0,0 +1,620 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "net.h"
+#include "str.h"
+#include "istream.h"
+#include "ioloop.h"
+#include "base64.h"
+#include "hex-binary.h"
+#include "hash-method.h"
+#include "http-url.h"
+#include "http-client.h"
+#include "json-parser.h"
+#include "master-service.h"
+#include "master-service-ssl-settings.h"
+#include "auth-request.h"
+#include "auth-penalty.h"
+#include "auth-settings.h"
+#include "auth-policy.h"
+#include "auth-common.h"
+#include "iostream-ssl.h"
+
+#define AUTH_POLICY_DNS_SOCKET_PATH "dns-client"
+
+static struct http_client_settings http_client_set = {
+ .dns_client_socket_path = AUTH_POLICY_DNS_SOCKET_PATH,
+ .max_connect_attempts = 1,
+ .max_idle_time_msecs = 10000,
+ .max_parallel_connections = 100,
+ .debug = 0,
+ .user_agent = "dovecot/auth-policy-client"
+};
+
+static char *auth_policy_json_template;
+
+static struct http_client *http_client;
+
+struct policy_lookup_ctx {
+ pool_t pool;
+ string_t *json;
+ struct auth_request *request;
+ struct http_client_request *http_request;
+ struct json_parser *parser;
+ const struct auth_settings *set;
+ const char *url;
+ bool expect_result;
+ int result;
+ const char *message;
+ auth_policy_callback_t callback;
+ void *callback_context;
+
+ struct istream *payload;
+ struct io *io;
+ struct event *event;
+
+ enum {
+ POLICY_RESULT = 0,
+ POLICY_RESULT_VALUE_STATUS,
+ POLICY_RESULT_VALUE_MESSAGE
+ } parse_state;
+
+ bool parse_error;
+};
+
+struct policy_template_keyvalue {
+ const char *key;
+ const char *value;
+};
+
+static
+int auth_policy_attribute_comparator(const struct policy_template_keyvalue *a,
+ const struct policy_template_keyvalue *b)
+{
+ return strcmp(a->key, b->key);
+}
+
+static
+int auth_policy_strptrcmp(const char *a0, const char *a1,
+ const char *b0, const char *b1)
+{
+ i_assert(a0 <= a1 && b0 <= b1);
+ return memcmp(a0, b0, I_MIN((a1-a0),(b1-b0)));
+}
+
+static
+void auth_policy_open_key(const char *key, string_t *template)
+{
+ const char *ptr;
+ while((ptr = strchr(key, '/')) != NULL) {
+ str_append_c(template,'"');
+ json_append_escaped(template, t_strndup(key, (ptr-key)));
+ str_append_c(template,'"');
+ str_append_c(template,':');
+ str_append_c(template,'{');
+ key = ptr+1;
+ }
+}
+
+static
+void auth_policy_close_key(const char *key, string_t *template)
+{
+ while((key = strchr(key, '/')) != NULL) { str_append_c(template,'}'); key++; }
+}
+
+static
+void auth_policy_open_and_close_to_key(const char *fromkey, const char *tokey, string_t *template)
+{
+ const char *fptr,*tptr,*fdash,*tdash;
+
+ fptr = strrchr(fromkey, '/');
+ tptr = strrchr(tokey, '/');
+
+ if (fptr == NULL && tptr == NULL) return; /* nothing to do */
+
+ if (fptr == NULL && tptr != NULL) {
+ auth_policy_open_key(tokey, template);
+ return;
+ }
+
+ if (fptr != NULL && tptr == NULL) {
+ str_truncate(template, str_len(template)-1);
+
+ auth_policy_close_key(fromkey, template);
+ str_append_c(template, ',');
+ return;
+ }
+
+ if (auth_policy_strptrcmp(fromkey, fptr, tokey, tptr) == 0) {
+ /* nothing to do, again */
+ return;
+ }
+
+ fptr = fromkey;
+ tptr = tokey;
+
+ while (fptr != NULL && tptr != NULL) {
+ fdash = strchr(fptr, '/');
+ tdash = strchr(tptr, '/');
+
+ if (fdash == NULL) {
+ auth_policy_open_key(tptr, template);
+ break;
+ }
+ if (tdash == NULL) {
+ str_truncate(template, str_len(template)-1);
+ auth_policy_close_key(fptr, template);
+ str_append_c(template, ',');
+ break;
+ }
+ if (auth_policy_strptrcmp(fptr, fdash, tptr, tdash) != 0) {
+ str_truncate(template, str_len(template)-1);
+ auth_policy_close_key(fptr, template);
+ str_append_c(template, ',');
+ auth_policy_open_key(tptr, template);
+ break;
+ }
+ fptr = fdash+1;
+ tptr = tdash+1;
+ }
+}
+
+void auth_policy_init(void)
+{
+ const struct master_service_ssl_settings *master_ssl_set =
+ master_service_ssl_settings_get(master_service);
+ struct ssl_iostream_settings ssl_set;
+ i_zero(&ssl_set);
+
+ http_client_set.request_absolute_timeout_msecs = global_auth_settings->policy_server_timeout_msecs;
+ if (global_auth_settings->debug)
+ http_client_set.debug = 1;
+
+ master_service_ssl_client_settings_to_iostream_set(master_ssl_set,
+ pool_datastack_create(), &ssl_set);
+ http_client_set.ssl = &ssl_set;
+ http_client_set.event_parent = auth_event;
+ http_client = http_client_init(&http_client_set);
+
+ /* prepare template */
+
+ ARRAY(struct policy_template_keyvalue) attribute_pairs;
+ const struct policy_template_keyvalue *kvptr;
+ string_t *template = t_str_new(64);
+ const char **ptr;
+ const char *key = NULL;
+ const char **list = t_strsplit_spaces(global_auth_settings->policy_request_attributes, "= ");
+
+ t_array_init(&attribute_pairs, 8);
+ for(ptr = list; *ptr != NULL; ptr++) {
+ struct policy_template_keyvalue pair;
+ if (key == NULL) {
+ key = *ptr;
+ } else {
+ pair.key = key;
+ pair.value = *ptr;
+ key = NULL;
+ array_push_back(&attribute_pairs, &pair);
+ }
+ }
+ if (key != NULL) {
+ i_fatal("auth_policy_request_attributes contains invalid value");
+ }
+
+ /* then we sort it */
+ array_sort(&attribute_pairs, auth_policy_attribute_comparator);
+
+ /* and build a template string */
+ const char *prevkey = "";
+
+ array_foreach(&attribute_pairs, kvptr) {
+ const char *kptr = strchr(kvptr->key, '/');
+ auth_policy_open_and_close_to_key(prevkey, kvptr->key, template);
+ str_append_c(template,'"');
+ json_append_escaped(template, (kptr != NULL?kptr+1:kvptr->key));
+ str_append_c(template,'"');
+ str_append_c(template,':');
+ str_append_c(template,'"');
+ str_append(template,kvptr->value);
+ str_append_c(template,'"');
+ str_append_c(template,',');
+ prevkey = kvptr->key;
+ }
+
+ auth_policy_open_and_close_to_key(prevkey, "", template);
+ str_truncate(template, str_len(template)-1);
+ auth_policy_json_template = i_strdup(str_c(template));
+
+ if (global_auth_settings->policy_log_only)
+ i_warning("auth-policy: Currently in log-only mode. Ignoring "
+ "tarpit and disconnect instructions from policy server");
+}
+
+void auth_policy_deinit(void)
+{
+ if (http_client != NULL)
+ http_client_deinit(&http_client);
+ i_free(auth_policy_json_template);
+}
+
+static
+void auth_policy_log_result(struct policy_lookup_ctx *context)
+{
+ const char *action;
+ struct event_passthrough *e = event_create_passthrough(context->event)->
+ set_name("auth_policy_request_finished");
+ if (!context->expect_result) {
+ e_debug(e->event(), "Policy report action finished");
+ return;
+ }
+ int result = context->result;
+ e->add_int("policy_response", context->result);
+ if (result < 0)
+ action = "drop connection";
+ else if (context->result == 0)
+ action = "continue";
+ else
+ action = t_strdup_printf("tarpit %d second(s)", context->result);
+ if (context->request->set->policy_log_only && result != 0)
+ e_info(e->event(), "Policy check action '%s' ignored",
+ action);
+ else if (result != 0)
+ e_info(e->event(), "Policy check action is %s",
+ action);
+ else
+ e_debug(e->event(), "Policy check action is %s",
+ action);
+}
+
+static
+void auth_policy_finish(struct policy_lookup_ctx *context)
+{
+ if (context->parser != NULL) {
+ const char *error ATTR_UNUSED;
+ (void)json_parser_deinit(&context->parser, &error);
+ }
+ http_client_request_abort(&context->http_request);
+ if (context->request != NULL)
+ auth_request_unref(&context->request);
+ event_unref(&context->event);
+ pool_unref(&context->pool);
+}
+
+static
+void auth_policy_callback(struct policy_lookup_ctx *context)
+{
+ if (context->callback != NULL)
+ context->callback(context->result, context->callback_context);
+ if (context->event != NULL)
+ auth_policy_log_result(context);
+}
+
+static
+void auth_policy_parse_response(struct policy_lookup_ctx *context)
+{
+ enum json_type type;
+ const char *value;
+ int ret;
+
+ while((ret = json_parse_next(context->parser, &type, &value)) == 1) {
+ if (context->parse_state == POLICY_RESULT) {
+ if (type != JSON_TYPE_OBJECT_KEY)
+ continue;
+ else if (strcmp(value, "status") == 0)
+ context->parse_state = POLICY_RESULT_VALUE_STATUS;
+ else if (strcmp(value, "msg") == 0)
+ context->parse_state = POLICY_RESULT_VALUE_MESSAGE;
+ else
+ continue;
+ } else if (context->parse_state == POLICY_RESULT_VALUE_STATUS) {
+ if (type != JSON_TYPE_NUMBER || str_to_int(value, &context->result) != 0)
+ break;
+ context->parse_state = POLICY_RESULT;
+ } else if (context->parse_state == POLICY_RESULT_VALUE_MESSAGE) {
+ if (type != JSON_TYPE_STRING)
+ break;
+ if (*value != '\0')
+ context->message = p_strdup(context->pool, value);
+ context->parse_state = POLICY_RESULT;
+ } else {
+ break;
+ }
+ }
+
+ if (ret == 0 && !context->payload->eof)
+ return;
+
+ context->parse_error = TRUE;
+
+ io_remove(&context->io);
+
+ if (context->payload->stream_errno != 0) {
+ e_error(context->event,
+ "Error reading policy server result: %s",
+ i_stream_get_error(context->payload));
+ } else if (ret == 0 && context->payload->eof) {
+ e_error(context->event,
+ "Policy server result was too short");
+ } else if (ret == 1) {
+ e_error(context->event,
+ "Policy server response was malformed");
+ } else {
+ const char *error = "unknown";
+ if (json_parser_deinit(&context->parser, &error) != 0)
+ e_error(context->event,
+ "Policy server response JSON parse error: %s", error);
+ else if (context->parse_state == POLICY_RESULT)
+ context->parse_error = FALSE;
+ }
+
+ if (context->parse_error) {
+ context->result = (context->set->policy_reject_on_fail ? -1 : 0);
+ }
+
+ context->request->policy_refusal = FALSE;
+
+ if (context->result < 0) {
+ if (context->message != NULL) {
+ /* set message here */
+ e_debug(context->event,
+ "Policy response %d with message: %s",
+ context->result, context->message);
+ auth_request_set_field(context->request, "reason", context->message, NULL);
+ }
+ context->request->policy_refusal = TRUE;
+ } else {
+ e_debug(context->event,
+ "Policy response %d", context->result);
+ }
+
+ if (context->request->policy_refusal == TRUE && context->set->verbose == TRUE) {
+ e_info(context->event, "Authentication failure due to policy server refusal%s%s",
+ (context->message!=NULL?": ":""),
+ (context->message!=NULL?context->message:""));
+ }
+
+ auth_policy_callback(context);
+ i_stream_unref(&context->payload);
+}
+
+static
+void auth_policy_process_response(const struct http_response *response,
+ void *ctx)
+{
+ struct policy_lookup_ctx *context = ctx;
+
+ context->payload = response->payload;
+
+ if ((response->status / 10) != 20) {
+ e_error(context->event,
+ "Policy server HTTP error: %s",
+ http_response_get_message(response));
+ auth_policy_callback(context);
+ return;
+ }
+
+ if (response->payload == NULL) {
+ if (context->expect_result)
+ e_error(context->event,
+ "Policy server result was empty");
+ auth_policy_callback(context);
+ return;
+ }
+
+ if (context->expect_result) {
+ i_stream_ref(response->payload);
+ context->io = io_add_istream(response->payload, auth_policy_parse_response, context);
+ context->parser = json_parser_init(response->payload);
+ auth_policy_parse_response(ctx);
+ } else {
+ auth_policy_callback(context);
+ }
+}
+
+static
+void auth_policy_send_request(struct policy_lookup_ctx *context)
+{
+ const char *error;
+ struct http_url *url;
+
+ auth_request_ref(context->request);
+ if (http_url_parse(context->url, NULL, HTTP_URL_ALLOW_USERINFO_PART,
+ context->pool, &url, &error) != 0) {
+ e_error(context->event,
+ "Could not parse url %s: %s", context->url, error);
+ auth_policy_callback(context);
+ auth_policy_finish(context);
+ return;
+ }
+ context->http_request = http_client_request_url(http_client,
+ "POST", url, auth_policy_process_response, (void*)context);
+ http_client_request_set_destroy_callback(context->http_request, auth_policy_finish, context);
+ http_client_request_add_header(context->http_request, "Content-Type", "application/json");
+ if (*context->set->policy_server_api_header != 0) {
+ const char *ptr;
+ if ((ptr = strstr(context->set->policy_server_api_header, ":")) != NULL) {
+ const char *header = t_strcut(context->set->policy_server_api_header, ':');
+ http_client_request_add_header(context->http_request, header, ptr + 1);
+ } else {
+ http_client_request_add_header(context->http_request,
+ "X-API-Key", context->set->policy_server_api_header);
+ }
+ }
+ if (url->user != NULL) {
+ /* allow empty password */
+ http_client_request_set_auth_simple(context->http_request, url->user,
+ (url->password != NULL ? url->password : ""));
+ }
+ struct istream *is = i_stream_create_from_buffer(context->json);
+ http_client_request_set_payload(context->http_request, is, FALSE);
+ i_stream_unref(&is);
+ http_client_request_submit(context->http_request);
+}
+
+static
+const char *auth_policy_escape_function(const char *string,
+ const struct auth_request *auth_request ATTR_UNUSED)
+{
+ string_t *tmp = t_str_new(64);
+ json_append_escaped(tmp, string);
+ return str_c(tmp);
+}
+
+static
+const struct var_expand_table *policy_get_var_expand_table(struct auth_request *auth_request,
+ const char *hashed_password, const char *requested_username)
+{
+ struct var_expand_table *table;
+ unsigned int count = 2;
+
+ table = auth_request_get_var_expand_table_full(auth_request,
+ auth_request->fields.user, auth_policy_escape_function, &count);
+ table[0].key = '\0';
+ table[0].long_key = "hashed_password";
+ table[0].value = hashed_password;
+ table[1].key = '\0';
+ table[1].long_key = "requested_username";
+ table[1].value = requested_username;
+ if (table[0].value != NULL)
+ table[0].value = auth_policy_escape_function(table[0].value, auth_request);
+ if (table[1].value != NULL)
+ table[1].value = auth_policy_escape_function(table[1].value, auth_request);
+
+ return table;
+}
+
+static
+void auth_policy_create_json(struct policy_lookup_ctx *context,
+ const char *password, bool include_success)
+{
+ const struct var_expand_table *var_table;
+ context->json = str_new(context->pool, 64);
+ unsigned char *ptr;
+ const char *requested_username;
+ const struct hash_method *digest = hash_method_lookup(context->set->policy_hash_mech);
+
+ i_assert(digest != NULL);
+
+ void *ctx = t_malloc_no0(digest->context_size);
+ buffer_t *buffer = t_buffer_create(64);
+
+ digest->init(ctx);
+ digest->loop(ctx,
+ context->set->policy_hash_nonce,
+ strlen(context->set->policy_hash_nonce));
+ if (context->request->fields.requested_login_user != NULL)
+ requested_username = context->request->fields.requested_login_user;
+ else if (context->request->fields.user != NULL)
+ requested_username = context->request->fields.user;
+ else
+ requested_username = "";
+ /* use +1 to make sure \0 gets included */
+ digest->loop(ctx, requested_username, strlen(requested_username)+1);
+ if (password != NULL)
+ digest->loop(ctx, password, strlen(password));
+ ptr = buffer_get_modifiable_data(buffer, NULL);
+ digest->result(ctx, ptr);
+ buffer_set_used_size(buffer, digest->digest_size);
+ if (context->set->policy_hash_truncate > 0) {
+ buffer_truncate_rshift_bits(buffer, context->set->policy_hash_truncate);
+ }
+ const char *hashed_password = binary_to_hex(buffer->data, buffer->used);
+ str_append_c(context->json, '{');
+ var_table = policy_get_var_expand_table(context->request, hashed_password, requested_username);
+ const char *error;
+ if (auth_request_var_expand_with_table(context->json, auth_policy_json_template,
+ context->request, var_table,
+ auth_policy_escape_function, &error) <= 0) {
+ e_error(context->event,
+ "Failed to expand auth policy template: %s", error);
+ }
+ if (include_success) {
+ str_append(context->json, ",\"success\":");
+ if (!context->request->failed &&
+ context->request->fields.successful &&
+ !context->request->internal_failure)
+ str_append(context->json, "true");
+ else
+ str_append(context->json, "false");
+ str_append(context->json, ",\"policy_reject\":");
+ str_append(context->json, context->request->policy_refusal ? "true" : "false");
+ }
+ str_append(context->json, ",\"tls\":");
+ if (context->request->fields.secured == AUTH_REQUEST_SECURED_TLS)
+ str_append(context->json, "true");
+ else
+ str_append(context->json, "false");
+ str_append_c(context->json, '}');
+ e_debug(context->event,
+ "Policy server request JSON: %s", str_c(context->json));
+}
+
+static
+void auth_policy_url(struct policy_lookup_ctx *context, const char *command)
+{
+ size_t len = strlen(context->set->policy_server_url);
+ if (context->set->policy_server_url[len-1] == '&')
+ context->url = p_strdup_printf(context->pool, "%scommand=%s",
+ context->set->policy_server_url, command);
+ else
+ context->url = p_strdup_printf(context->pool, "%s?command=%s",
+ context->set->policy_server_url, command);
+}
+
+static const char *auth_policy_get_prefix(struct auth_request *request)
+{
+ string_t *str = t_str_new(256);
+ auth_request_get_log_prefix(str, request, "policy");
+ return str_c(str);
+}
+
+void auth_policy_check(struct auth_request *request, const char *password,
+ auth_policy_callback_t cb, void *context)
+{
+ if (request->master != NULL || *(request->set->policy_server_url) == '\0') {
+ cb(0, context);
+ return;
+ }
+ pool_t pool = pool_alloconly_create("auth policy", 512);
+ struct policy_lookup_ctx *ctx = p_new(pool, struct policy_lookup_ctx, 1);
+ ctx->pool = pool;
+ ctx->request = request;
+ ctx->expect_result = TRUE;
+ ctx->callback = cb;
+ ctx->callback_context = context;
+ ctx->set = request->set;
+ ctx->event = event_create(request->event);
+ event_add_str(ctx->event, "mode", "allow");
+ event_set_append_log_prefix(ctx->event, auth_policy_get_prefix(request));
+ auth_policy_url(ctx, "allow");
+ ctx->result = (ctx->set->policy_reject_on_fail ? -1 : 0);
+ e_debug(ctx->event, "Policy request %s", ctx->url);
+ T_BEGIN {
+ auth_policy_create_json(ctx, password, FALSE);
+ } T_END;
+ auth_policy_send_request(ctx);
+}
+
+void auth_policy_report(struct auth_request *request)
+{
+ if (request->master != NULL)
+ return;
+
+ if (*(request->set->policy_server_url) == '\0')
+ return;
+ pool_t pool = pool_alloconly_create("auth policy", 512);
+ struct policy_lookup_ctx *ctx = p_new(pool, struct policy_lookup_ctx, 1);
+ ctx->pool = pool;
+ ctx->request = request;
+ ctx->expect_result = FALSE;
+ ctx->set = request->set;
+ ctx->event = event_create(request->event);
+ event_add_str(ctx->event, "mode", "report");
+ event_set_append_log_prefix(ctx->event, auth_policy_get_prefix(request));
+ auth_policy_url(ctx, "report");
+ e_debug(ctx->event, "Policy request %s", ctx->url);
+ T_BEGIN {
+ auth_policy_create_json(ctx, request->mech_password, TRUE);
+ } T_END;
+ auth_policy_send_request(ctx);
+}