From 79042f8c2ec1778528584c064b164d1ebcdde16b Mon Sep 17 00:00:00 2001 From: Timo Sirainen Date: Sun, 8 Dec 2013 19:00:31 +0200 Subject: [PATCH] auth: passdb/userdb dict rewrite to support more complex configuration. See the new doc/example-config/dovecot-dict-auth.conf.ext for explanation how it works. The old configuration format will also stay functional. --- doc/example-config/dovecot-dict-auth.conf.ext | 56 +- src/auth/Makefile.am | 8 +- src/auth/db-dict-cache-key.c | 65 +++ src/auth/db-dict.c | 528 ++++++++++++++++-- src/auth/db-dict.h | 57 +- src/auth/passdb-dict.c | 49 +- src/auth/test-db-dict.c | 51 ++ src/auth/userdb-dict.c | 42 +- 8 files changed, 732 insertions(+), 124 deletions(-) create mode 100644 src/auth/db-dict-cache-key.c create mode 100644 src/auth/test-db-dict.c diff --git a/doc/example-config/dovecot-dict-auth.conf.ext b/doc/example-config/dovecot-dict-auth.conf.ext index 50617bb396..79f43de6ee 100644 --- a/doc/example-config/dovecot-dict-auth.conf.ext +++ b/doc/example-config/dovecot-dict-auth.conf.ext @@ -1,24 +1,54 @@ -# This file is commonly accessed via dict {} section in dovecot.conf +# This file is commonly accessed via passdb {} or userdb {} section in +# conf.d/auth-dict.conf.ext # Dictionary URI #uri = -# Key for passdb lookups -password_key = dovecot/passdb/%u - -# Key for userdb lookups -user_key = dovecot/userdb/%u - -# How to parse the value for key=value pairs. Currently we support only JSON -# format with { "key": "value", ... } object. -#value_format = json +# Default password scheme +default_pass_scheme = MD5 # Username iteration prefix. Keys under this are assumed to contain usernames. -iterate_prefix = dovecot/userdb/ +iterate_prefix = userdb/ # Should iteration be disabled for this userdb? If this userdb acts only as a # cache there's no reason to try to iterate the (partial & duplicate) users. #iterate_disable = no -# Default password scheme -default_pass_scheme = MD5 +# The example here shows how to do multiple dict lookups and merge the replies. +# The "passdb" and "userdb" keys are JSON objects containing key/value pairs, +# for example: { "uid": 1000, "gid": 1000, "home": "/home/user" } + +key passdb { + key = passdb/%u + format = json +} +key userdb { + key = userdb/%u + format = json +} +key quota { + key = userdb/%u/quota + #format = value + # The default_value is used if the key isn't found. If default_value setting + # isn't specified at all (even as empty), the passdb/userdb lookup fails with + # "user doesn't exist". + default_value = 100M +} + +# Space separated list of keys whose values contain key/value paired objects. +# All the key/value pairs inside the object are added as passdb fields. +passdb_objects = passdb + +#passdb_fields { +#} + +# Userdb key/value object list. +userdb_objects = userdb + +userdb_fields { + # dict: refers to key names + quota_rule = *:storage=%{dict:quota} + + # dict:. refers to the objkey inside (JSON) object + mail = maildir:%{dict:userdb.home}/Maildir +} diff --git a/src/auth/Makefile.am b/src/auth/Makefile.am index 6b91d5c87a..f485953a1a 100644 --- a/src/auth/Makefile.am +++ b/src/auth/Makefile.am @@ -77,6 +77,7 @@ auth_SOURCES = \ auth-worker-server.c \ db-checkpassword.c \ db-dict.c \ + db-dict-cache-key.c \ db-sql.c \ db-passwd-file.c \ main.c \ @@ -193,7 +194,8 @@ checkpassword_reply_sources = \ checkpassword-reply.c test_programs = \ - test-auth-cache + test-auth-cache \ + test-db-dict noinst_PROGRAMS = $(test_programs) @@ -205,6 +207,10 @@ test_auth_cache_SOURCES = test-auth-cache.c test_auth_cache_LDADD = auth-cache.o $(test_libs) test_auth_cache_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs) +test_db_dict_SOURCES = test-db-dict.c +test_db_dict_LDADD = db-dict-cache-key.o $(test_libs) +test_db_dict_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(test_libs) + check: check-am check-test check-test: all-am for bin in $(test_programs); do \ diff --git a/src/auth/db-dict-cache-key.c b/src/auth/db-dict-cache-key.c new file mode 100644 index 0000000000..b562ac15f3 --- /dev/null +++ b/src/auth/db-dict-cache-key.c @@ -0,0 +1,65 @@ +/* Copyright (c) 2013 Dovecot authors, see the included COPYING file */ + +#include "auth-common.h" +#include "array.h" +#include "str.h" +#include "var-expand.h" +#include "db-dict.h" + +const struct db_dict_key * +db_dict_set_key_find(const ARRAY_TYPE(db_dict_key) *keys, const char *name) +{ + const struct db_dict_key *key; + + array_foreach(keys, key) { + if (strcmp(key->name, name) == 0) + return key; + } + return NULL; +} + + +const char * +db_dict_parse_cache_key(const ARRAY_TYPE(db_dict_key) *keys, + const ARRAY_TYPE(db_dict_field) *fields, + const ARRAY_TYPE(db_dict_key_p) *objects) +{ + const struct db_dict_field *field; + const struct db_dict_key *key; + const struct db_dict_key *const *keyp; + const char *p, *name; + unsigned int idx, size; + string_t *str = t_str_new(128); + + array_foreach(fields, field) { + for (p = field->value; *p != '\0'; ) { + if (*p != '%') { + p++; + continue; + } + + var_get_key_range(++p, &idx, &size); + if (size == 0) { + /* broken %variable ending too early */ + break; + } + p += idx; + if (size > 5 && memcmp(p, "dict:", 5) == 0) { + name = t_strcut(t_strndup(p+5, size-5), ':'); + key = db_dict_set_key_find(keys, name); + if (key != NULL) + str_printfa(str, "\t%s", key->key); + } else if (size == 1) { + str_printfa(str, "\t%%%c", p[0]); + } else { + str_append(str, "\t%{"); + str_append_n(str, p, size); + str_append_c(str, '}'); + } + p += size; + } + } + array_foreach(objects, keyp) + str_printfa(str, "\t%s", (*keyp)->key); + return str_c(str); +} diff --git a/src/auth/db-dict.c b/src/auth/db-dict.c index dc8892b610..1ca61e60c6 100644 --- a/src/auth/db-dict.c +++ b/src/auth/db-dict.c @@ -2,11 +2,12 @@ #include "auth-common.h" -#include "settings.h" -#include "dict.h" -#include "json-parser.h" +#include "array.h" #include "istream.h" #include "str.h" +#include "json-parser.h" +#include "settings.h" +#include "dict.h" #include "auth-request.h" #include "auth-worker-client.h" #include "db-dict.h" @@ -14,29 +15,80 @@ #include #include +enum dict_settings_section { + DICT_SETTINGS_SECTION_ROOT = 0, + DICT_SETTINGS_SECTION_KEY, + DICT_SETTINGS_SECTION_PASSDB, + DICT_SETTINGS_SECTION_USERDB +}; + +struct dict_settings_parser_ctx { + struct dict_connection *conn; + enum dict_settings_section section; + struct db_dict_key *cur_key; +}; + +struct db_dict_iter_key { + const struct db_dict_key *key; + bool used; + const char *value; +}; + +struct db_dict_value_iter { + pool_t pool; + struct auth_request *auth_request; + struct dict_connection *conn; + const struct var_expand_table *var_expand_table; + ARRAY(struct db_dict_iter_key) keys; + + const ARRAY_TYPE(db_dict_field) *fields; + const ARRAY_TYPE(db_dict_key_p) *objects; + unsigned int field_idx; + unsigned int object_idx; + + struct json_parser *json_parser; + string_t *tmpstr; + const char *error; +}; + #define DEF_STR(name) DEF_STRUCT_STR(name, db_dict_settings) #define DEF_BOOL(name) DEF_STRUCT_BOOL(name, db_dict_settings) - static struct setting_def setting_defs[] = { DEF_STR(uri), - DEF_STR(password_key), - DEF_STR(user_key), + DEF_STR(default_pass_scheme), DEF_STR(iterate_prefix), - DEF_STR(value_format), DEF_BOOL(iterate_disable), - DEF_STR(default_pass_scheme), + DEF_STR(passdb_objects), + DEF_STR(userdb_objects), { 0, NULL, 0 } }; static struct db_dict_settings default_dict_settings = { .uri = NULL, - .password_key = "", - .user_key = "", + .default_pass_scheme = "MD5", .iterate_prefix = "", .iterate_disable = FALSE, - .value_format = "json", - .default_pass_scheme = "MD5" + .passdb_objects = "", + .userdb_objects = "" +}; + +#undef DEF_STR +#define DEF_STR(name) DEF_STRUCT_STR(name, db_dict_key) +static struct setting_def key_setting_defs[] = { + DEF_STR(name), + DEF_STR(key), + DEF_STR(format), + DEF_STR(default_value), + + { 0, NULL, 0 } +}; + +static struct db_dict_key default_key_settings = { + .name = NULL, + .key = "", + .format = "value", + .default_value = NULL }; static struct dict_connection *connections = NULL; @@ -53,15 +105,167 @@ static struct dict_connection *dict_conn_find(const char *config_path) return NULL; } +static bool +parse_obsolete_setting(const char *key, const char *value, + struct dict_settings_parser_ctx *ctx, + const char **error_r) +{ + const struct db_dict_key *dbkey; + + if (strcmp(key, "password_key") == 0) { + /* key passdb { key= format=json } + passdb_objects = passdb */ + ctx->cur_key = array_append_space(&ctx->conn->set.keys); + *ctx->cur_key = default_key_settings; + ctx->cur_key->name = "passdb"; + ctx->cur_key->format = "json"; + ctx->cur_key->parsed_format = DB_DICT_VALUE_FORMAT_JSON; + ctx->cur_key->key = p_strdup(ctx->conn->pool, value); + + dbkey = ctx->cur_key; + array_append(&ctx->conn->set.parsed_passdb_objects, &dbkey, 1); + return TRUE; + } + if (strcmp(key, "user_key") == 0) { + /* key userdb { key= format=json } + userdb_objects = userdb */ + ctx->cur_key = array_append_space(&ctx->conn->set.keys); + *ctx->cur_key = default_key_settings; + ctx->cur_key->name = "userdb"; + ctx->cur_key->format = "json"; + ctx->cur_key->parsed_format = DB_DICT_VALUE_FORMAT_JSON; + ctx->cur_key->key = p_strdup(ctx->conn->pool, value); + + dbkey = ctx->cur_key; + array_append(&ctx->conn->set.parsed_userdb_objects, &dbkey, 1); + return TRUE; + } + if (strcmp(key, "value_format") == 0) { + if (strcmp(value, "json") == 0) + return TRUE; + *error_r = "Deprecated value_format must be 'json'"; + return FALSE; + } + return FALSE; +} + static const char *parse_setting(const char *key, const char *value, - struct dict_connection *conn) + struct dict_settings_parser_ctx *ctx) +{ + struct db_dict_field *field; + const char *error = NULL; + + switch (ctx->section) { + case DICT_SETTINGS_SECTION_ROOT: + if (parse_obsolete_setting(key, value, ctx, &error)) + return NULL; + if (error != NULL) + return error; + return parse_setting_from_defs(ctx->conn->pool, setting_defs, + &ctx->conn->set, key, value); + case DICT_SETTINGS_SECTION_KEY: + return parse_setting_from_defs(ctx->conn->pool, key_setting_defs, + ctx->cur_key, key, value); + case DICT_SETTINGS_SECTION_PASSDB: + field = array_append_space(&ctx->conn->set.passdb_fields); + field->name = p_strdup(ctx->conn->pool, key); + field->value = p_strdup(ctx->conn->pool, value); + return NULL; + case DICT_SETTINGS_SECTION_USERDB: + field = array_append_space(&ctx->conn->set.userdb_fields); + field->name = p_strdup(ctx->conn->pool, key); + field->value = p_strdup(ctx->conn->pool, value); + return NULL; + } + return t_strconcat("Unknown setting: ", key, NULL); +} + +static bool parse_section(const char *type, const char *name, + struct dict_settings_parser_ctx *ctx, + const char **errormsg) { - return parse_setting_from_defs(conn->pool, setting_defs, - &conn->set, key, value); + if (type == NULL) { + ctx->section = DICT_SETTINGS_SECTION_ROOT; + if (ctx->cur_key != NULL) { + if (strcmp(ctx->cur_key->format, "value") == 0) { + ctx->cur_key->parsed_format = + DB_DICT_VALUE_FORMAT_VALUE; + } else if (strcmp(ctx->cur_key->format, "json") == 0) { + ctx->cur_key->parsed_format = + DB_DICT_VALUE_FORMAT_JSON; + } else { + return t_strconcat("Unknown key format: ", + ctx->cur_key->format, NULL); + } + } + ctx->cur_key = NULL; + return TRUE; + } + if (ctx->section != DICT_SETTINGS_SECTION_ROOT) { + *errormsg = "Nested sections not supported"; + return FALSE; + } + if (strcmp(type, "key") == 0) { + if (name == NULL) + return "Key section is missing name"; + if (strchr(name, '.') != NULL) + return "Key section names must not contain '.'"; + ctx->section = DICT_SETTINGS_SECTION_KEY; + ctx->cur_key = array_append_space(&ctx->conn->set.keys); + *ctx->cur_key = default_key_settings; + ctx->cur_key->name = p_strdup(ctx->conn->pool, name); + return TRUE; + } + if (strcmp(type, "passdb_fields") == 0) { + ctx->section = DICT_SETTINGS_SECTION_PASSDB; + return TRUE; + } + if (strcmp(type, "userdb_fields") == 0) { + ctx->section = DICT_SETTINGS_SECTION_USERDB; + return TRUE; + } + *errormsg = "Unknown section"; + return FALSE; +} + +static void +db_dict_settings_parse(struct db_dict_settings *set) +{ + const struct db_dict_key *key; + const char *const *tmp; + + tmp = t_strsplit_spaces(set->passdb_objects, " "); + for (; *tmp != NULL; tmp++) { + key = db_dict_set_key_find(&set->keys, *tmp); + if (key == NULL) { + i_fatal("dict: passdb_objects refers to key %s, " + "which doesn't exist", *tmp); + } + if (key->parsed_format == DB_DICT_VALUE_FORMAT_VALUE) { + i_fatal("dict: passdb_objects refers to key %s, " + "but it's in value-only format", *tmp); + } + array_append(&set->parsed_passdb_objects, &key, 1); + } + + tmp = t_strsplit_spaces(set->userdb_objects, " "); + for (; *tmp != NULL; tmp++) { + key = db_dict_set_key_find(&set->keys, *tmp); + if (key == NULL) { + i_fatal("dict: userdb_objects refers to key %s, " + "which doesn't exist", *tmp); + } + if (key->parsed_format == DB_DICT_VALUE_FORMAT_VALUE) { + i_fatal("dict: userdb_objects refers to key %s, " + "but it's in value-only format", *tmp); + } + array_append(&set->parsed_userdb_objects, &key, 1); + } } struct dict_connection *db_dict_init(const char *config_path) { + struct dict_settings_parser_ctx ctx; struct dict_connection *conn; const char *error; pool_t pool; @@ -83,15 +287,21 @@ struct dict_connection *db_dict_init(const char *config_path) conn->config_path = p_strdup(pool, config_path); conn->set = default_dict_settings; - if (!settings_read_nosection(config_path, parse_setting, conn, &error)) + p_array_init(&conn->set.keys, pool, 8); + p_array_init(&conn->set.passdb_fields, pool, 8); + p_array_init(&conn->set.userdb_fields, pool, 8); + p_array_init(&conn->set.parsed_passdb_objects, pool, 2); + p_array_init(&conn->set.parsed_userdb_objects, pool, 2); + + memset(&ctx, 0, sizeof(ctx)); + ctx.conn = conn; + if (!settings_read(config_path, NULL, parse_setting, + parse_section, &ctx, &error)) i_fatal("dict %s: %s", config_path, error); + db_dict_settings_parse(&conn->set); if (conn->set.uri == NULL) i_fatal("dict %s: Empty uri setting", config_path); - if (strcmp(conn->set.value_format, "json") != 0) { - i_fatal("dict %s: Unsupported value_format %s in ", - config_path, conn->set.value_format); - } if (dict_init(conn->set.uri, DICT_DATA_TYPE_STRING, "", global_auth_settings->base_dir, &conn->dict, &error) < 0) i_fatal("dict %s: Failed to init dict: %s", config_path, error); @@ -113,37 +323,160 @@ void db_dict_unref(struct dict_connection **_conn) pool_unref(&conn->pool); } -struct db_dict_value_iter { - struct json_parser *parser; - string_t *key; - const char *error; -}; +static struct db_dict_iter_key * +db_dict_iter_find_key(struct db_dict_value_iter *iter, const char *name) +{ + struct db_dict_iter_key *key; + + array_foreach_modifiable(&iter->keys, key) { + if (strcmp(key->key->name, name) == 0) + return key; + } + return NULL; +} -struct db_dict_value_iter * -db_dict_value_iter_init(struct dict_connection *conn, const char *value) +static void db_dict_iter_find_used_keys(struct db_dict_value_iter *iter) { - struct db_dict_value_iter *iter; - struct istream *input; + const struct db_dict_field *field; + struct db_dict_iter_key *key; + const char *p, *name; + unsigned int idx, size; + + array_foreach(iter->fields, field) { + for (p = field->value; *p != '\0'; ) { + if (*p != '%') { + p++; + continue; + } + + var_get_key_range(++p, &idx, &size); + if (size == 0) { + /* broken %variable ending too early */ + break; + } + p += idx; + if (size > 5 && memcmp(p, "dict:", 5) == 0) { + name = t_strcut(t_strndup(p+5, size-5), ':'); + key = db_dict_iter_find_key(iter, name); + if (key != NULL) + key->used = TRUE; + } + p += size; + } + } +} + +static void db_dict_iter_find_used_objects(struct db_dict_value_iter *iter) +{ + const struct db_dict_key *const *keyp; + struct db_dict_iter_key *key; + + array_foreach(iter->objects, keyp) { + key = db_dict_iter_find_key(iter, (*keyp)->name); + i_assert(key != NULL); /* checked at init */ + i_assert(key->key->parsed_format != DB_DICT_VALUE_FORMAT_VALUE); + key->used = TRUE; + } +} - i_assert(strcmp(conn->set.value_format, "json") == 0); +static int +db_dict_iter_key_cmp(const struct db_dict_iter_key *k1, + const struct db_dict_iter_key *k2) +{ + return null_strcmp(k1->key->default_value, k2->key->default_value); +} - /* hardcoded for now for JSON value. make it more modular when other - value types are supported. */ - iter = i_new(struct db_dict_value_iter, 1); - iter->key = str_new(default_pool, 64); - input = i_stream_create_from_data(value, strlen(value)); - iter->parser = json_parser_init(input); - i_stream_unref(&input); - return iter; +static int db_dict_iter_lookup_key_values(struct db_dict_value_iter *iter) +{ + struct db_dict_iter_key *key; + string_t *path; + int ret; + + /* sort the keys so that we'll first lookup the keys without + default value. if their lookup fails, the user doesn't exist. */ + array_sort(&iter->keys, db_dict_iter_key_cmp); + + path = t_str_new(128); + str_append(path, DICT_PATH_SHARED); + + array_foreach_modifiable(&iter->keys, key) { + if (!key->used) + continue; + + str_truncate(path, strlen(DICT_PATH_SHARED)); + var_expand(path, key->key->key, iter->var_expand_table); + ret = dict_lookup(iter->conn->dict, iter->pool, + str_c(path), &key->value); + if (ret > 0) { + auth_request_log_debug(iter->auth_request, "dict", + "Lookup: %s = %s", str_c(path), + key->value); + } else if (ret < 0) { + auth_request_log_error(iter->auth_request, "dict", + "Failed to lookup key %s", str_c(path)); + return -1; + } else if (key->key->default_value != NULL) { + auth_request_log_debug(iter->auth_request, "dict", + "Lookup: %s not found, using default value %s", + str_c(path), key->key->default_value); + key->value = key->key->default_value; + } else { + return 0; + } + } + return 1; } -bool db_dict_value_iter_next(struct db_dict_value_iter *iter, +int db_dict_value_iter_init(struct dict_connection *conn, + struct auth_request *auth_request, + const ARRAY_TYPE(db_dict_field) *fields, + const ARRAY_TYPE(db_dict_key_p) *objects, + struct db_dict_value_iter **iter_r) +{ + struct db_dict_value_iter *iter; + struct db_dict_iter_key *iterkey; + const struct db_dict_key *key; + pool_t pool; + int ret; + + pool = pool_alloconly_create("auth dict lookup", 1024); + iter = p_new(pool, struct db_dict_value_iter, 1); + iter->pool = pool; + iter->conn = conn; + iter->fields = fields; + iter->objects = objects; + iter->tmpstr = str_new(pool, 128); + iter->auth_request = auth_request; + iter->var_expand_table = auth_request_get_var_expand_table(auth_request, NULL); + + /* figure out what keys we need to lookup, and lookup them */ + p_array_init(&iter->keys, pool, array_count(&conn->set.keys)); + array_foreach(&conn->set.keys, key) { + iterkey = array_append_space(&iter->keys); + iterkey->key = key; + } + T_BEGIN { + db_dict_iter_find_used_keys(iter); + db_dict_iter_find_used_objects(iter); + ret = db_dict_iter_lookup_key_values(iter); + } T_END; + if (ret <= 0) { + pool_unref(&pool); + return ret; + } + *iter_r = iter; + return 1; +} + +static bool +db_dict_value_iter_json_next(struct db_dict_value_iter *iter, + string_t *tmpstr, const char **key_r, const char **value_r) { enum json_type type; const char *value; - if (json_parse_next(iter->parser, &type, &value) < 0) + if (json_parse_next(iter->json_parser, &type, &value) < 0) return FALSE; if (type != JSON_TYPE_OBJECT_KEY) { iter->error = "Object expected"; @@ -153,10 +486,10 @@ bool db_dict_value_iter_next(struct db_dict_value_iter *iter, iter->error = "Empty object key"; return FALSE; } - str_truncate(iter->key, 0); - str_append(iter->key, value); + str_truncate(tmpstr, 0); + str_append(tmpstr, value); - if (json_parse_next(iter->parser, &type, &value) < 0) { + if (json_parse_next(iter->json_parser, &type, &value) < 0) { iter->error = "Missing value"; return FALSE; } @@ -164,11 +497,106 @@ bool db_dict_value_iter_next(struct db_dict_value_iter *iter, iter->error = "Nested objects not supported"; return FALSE; } - *key_r = str_c(iter->key); + *key_r = str_c(tmpstr); *value_r = value; return TRUE; } +static void +db_dict_value_iter_json_init(struct db_dict_value_iter *iter, const char *data) +{ + struct istream *input; + + i_assert(iter->json_parser == NULL); + + input = i_stream_create_from_data(data, strlen(data)); + iter->json_parser = json_parser_init(input); + i_stream_unref(&input); +} + +static bool +db_dict_value_iter_object_next(struct db_dict_value_iter *iter, + const char **key_r, const char **value_r) +{ + const struct db_dict_key *const *keyp; + struct db_dict_iter_key *key; + + if (iter->json_parser != NULL) + return db_dict_value_iter_json_next(iter, iter->tmpstr, key_r, value_r); + if (iter->object_idx == array_count(iter->objects)) + return FALSE; + + keyp = array_idx(iter->objects, iter->object_idx); + key = db_dict_iter_find_key(iter, (*keyp)->name); + i_assert(key != NULL); /* checked at init */ + + switch (key->key->parsed_format) { + case DB_DICT_VALUE_FORMAT_VALUE: + i_unreached(); + case DB_DICT_VALUE_FORMAT_JSON: + db_dict_value_iter_json_init(iter, key->value); + return db_dict_value_iter_json_next(iter, iter->tmpstr, key_r, value_r); + } + i_unreached(); +} + +static const char * +db_dict_field_find(const char *data, void *context) +{ + struct db_dict_value_iter *iter = context; + struct db_dict_iter_key *key; + const char *name, *value, *ret, *dotname = strchr(data, '.'); + string_t *tmpstr; + + if (dotname != NULL) + data = t_strdup_until(data, dotname++); + key = db_dict_iter_find_key(iter, data); + if (key == NULL) + return NULL; + + switch (key->key->parsed_format) { + case DB_DICT_VALUE_FORMAT_VALUE: + return dotname != NULL ? NULL : + (key->value == NULL ? "" : key->value); + case DB_DICT_VALUE_FORMAT_JSON: + if (dotname == NULL) + return NULL; + db_dict_value_iter_json_init(iter, key->value); + ret = ""; + tmpstr = t_str_new(64); + while (db_dict_value_iter_json_next(iter, tmpstr, &name, &value)) { + if (strcmp(name, dotname) == 0) { + ret = t_strdup(value); + break; + } + } + (void)json_parser_deinit(&iter->json_parser, &iter->error); + return ret; + } + i_unreached(); +} + +bool db_dict_value_iter_next(struct db_dict_value_iter *iter, + const char **key_r, const char **value_r) +{ + static struct var_expand_func_table var_funcs_table[] = { + { "dict", db_dict_field_find }, + { NULL, NULL } + }; + const struct db_dict_field *field; + + if (iter->field_idx == array_count(iter->fields)) + return db_dict_value_iter_object_next(iter, key_r, value_r); + field = array_idx(iter->fields, iter->field_idx++); + + str_truncate(iter->tmpstr, 0); + var_expand_with_funcs(iter->tmpstr, field->value, + iter->var_expand_table, var_funcs_table, iter); + *key_r = field->name; + *value_r = str_c(iter->tmpstr); + return TRUE; +} + int db_dict_value_iter_deinit(struct db_dict_value_iter **_iter, const char **error_r) { @@ -177,10 +605,12 @@ int db_dict_value_iter_deinit(struct db_dict_value_iter **_iter, *_iter = NULL; *error_r = iter->error; - if (json_parser_deinit(&iter->parser, &iter->error) < 0 && - *error_r == NULL) - *error_r = iter->error; - str_free(&iter->key); - i_free(iter); + if (iter->json_parser != NULL) { + if (json_parser_deinit(&iter->json_parser, &iter->error) < 0 && + *error_r == NULL) + *error_r = iter->error; + } + + pool_unref(&iter->pool); return *error_r != NULL ? -1 : 0; } diff --git a/src/auth/db-dict.h b/src/auth/db-dict.h index dbba4209dc..f4feb0aee2 100644 --- a/src/auth/db-dict.h +++ b/src/auth/db-dict.h @@ -3,14 +3,46 @@ #include "sql-api.h" +struct auth_request; +struct db_dict_value_iter; + +enum db_dict_value_format { + DB_DICT_VALUE_FORMAT_VALUE = 0, + DB_DICT_VALUE_FORMAT_JSON +}; + +struct db_dict_key { + const char *name; + const char *key; + const char *format; + const char *default_value; + + enum db_dict_value_format parsed_format; +}; +ARRAY_DEFINE_TYPE(db_dict_key, struct db_dict_key); +ARRAY_DEFINE_TYPE(db_dict_key_p, const struct db_dict_key *); + +struct db_dict_field { + const char *name; + const char *value; +}; +ARRAY_DEFINE_TYPE(db_dict_field, struct db_dict_field); + struct db_dict_settings { const char *uri; - const char *password_key; - const char *user_key; + const char *default_pass_scheme; const char *iterate_prefix; bool iterate_disable; - const char *value_format; - const char *default_pass_scheme; + + ARRAY_TYPE(db_dict_key) keys; + + const char *passdb_objects; + const char *userdb_objects; + ARRAY_TYPE(db_dict_field) passdb_fields; + ARRAY_TYPE(db_dict_field) userdb_fields; + + ARRAY_TYPE(db_dict_key_p) parsed_passdb_objects; + ARRAY_TYPE(db_dict_key_p) parsed_userdb_objects; }; struct dict_connection { @@ -27,11 +59,24 @@ struct dict_connection { struct dict_connection *db_dict_init(const char *config_path); void db_dict_unref(struct dict_connection **conn); -struct db_dict_value_iter * -db_dict_value_iter_init(struct dict_connection *conn, const char *value); +/* Returns 1 if ok, 0 if a key without default_value wasn't returned + ("user doesn't exist"), -1 if internal error */ +int db_dict_value_iter_init(struct dict_connection *conn, + struct auth_request *auth_request, + const ARRAY_TYPE(db_dict_field) *fields, + const ARRAY_TYPE(db_dict_key_p) *objects, + struct db_dict_value_iter **iter_r); bool db_dict_value_iter_next(struct db_dict_value_iter *iter, const char **key_r, const char **value_r); int db_dict_value_iter_deinit(struct db_dict_value_iter **iter, const char **error_r); +const char *db_dict_parse_cache_key(const ARRAY_TYPE(db_dict_key) *keys, + const ARRAY_TYPE(db_dict_field) *fields, + const ARRAY_TYPE(db_dict_key_p) *objects); + +/* private: */ +const struct db_dict_key * +db_dict_set_key_find(const ARRAY_TYPE(db_dict_key) *keys, const char *name); + #endif diff --git a/src/auth/passdb-dict.c b/src/auth/passdb-dict.c index f4b4dc7019..cb322cedf2 100644 --- a/src/auth/passdb-dict.c +++ b/src/auth/passdb-dict.c @@ -3,6 +3,7 @@ #include "auth-common.h" #include "passdb.h" +#include "array.h" #include "str.h" #include "var-expand.h" #include "dict.h" @@ -29,12 +30,11 @@ struct passdb_dict_request { static int dict_query_save_results(struct auth_request *auth_request, - struct dict_connection *conn, const char *result) + struct dict_connection *conn, + struct db_dict_value_iter *iter) { - struct db_dict_value_iter *iter; const char *key, *value, *error; - iter = db_dict_value_iter_init(conn, result); while (db_dict_value_iter_next(iter, &key, &value)) { if (value != NULL) { auth_request_set_field(auth_request, key, value, @@ -42,9 +42,7 @@ dict_query_save_results(struct auth_request *auth_request, } } if (db_dict_value_iter_deinit(&iter, &error) < 0) { - auth_request_log_error(auth_request, "dict", - "Value '%s' not in valid %s format: %s", - result, conn->set.value_format, error); + auth_request_log_error(auth_request, "dict", "%s", error); return -1; } return 0; @@ -52,24 +50,22 @@ dict_query_save_results(struct auth_request *auth_request, static enum passdb_result passdb_dict_lookup_key(struct auth_request *auth_request, - struct dict_passdb_module *module, const char *key) + struct dict_passdb_module *module) { - const char *value; + struct db_dict_value_iter *iter; int ret; - auth_request_log_debug(auth_request, "dict", "lookup %s", key); - ret = dict_lookup(module->conn->dict, pool_datastack_create(), - key, &value); - if (ret < 0) { - auth_request_log_error(auth_request, "dict", "Lookup failed"); + ret = db_dict_value_iter_init(module->conn, auth_request, + &module->conn->set.passdb_fields, + &module->conn->set.parsed_passdb_objects, + &iter); + if (ret < 0) return PASSDB_RESULT_INTERNAL_FAILURE; - } else if (ret == 0) { + else if (ret == 0) { auth_request_log_unknown_user(auth_request, "dict"); return PASSDB_RESULT_USER_UNKNOWN; } else { - auth_request_log_debug(auth_request, "dict", - "result: %s", value); - if (dict_query_save_results(auth_request, module->conn, value) < 0) + if (dict_query_save_results(auth_request, module->conn, iter) < 0) return PASSDB_RESULT_INTERNAL_FAILURE; if (auth_request->passdb_password == NULL && @@ -89,23 +85,17 @@ static void passdb_dict_lookup_pass(struct passdb_dict_request *dict_request) struct passdb_module *_module = auth_request->passdb->passdb; struct dict_passdb_module *module = (struct dict_passdb_module *)_module; - string_t *key; const char *password = NULL, *scheme = NULL; enum passdb_result passdb_result; int ret; - key = t_str_new(512); - str_append(key, DICT_PATH_SHARED); - var_expand(key, module->conn->set.password_key, - auth_request_get_var_expand_table(auth_request, NULL)); - - if (*module->conn->set.password_key == '\0') { + if (array_count(&module->conn->set.passdb_fields) == 0 && + array_count(&module->conn->set.parsed_passdb_objects) == 0) { auth_request_log_error(auth_request, "dict", - "password_key not specified"); + "No passdb_objects or passdb_fields specified"); passdb_result = PASSDB_RESULT_INTERNAL_FAILURE; } else { - passdb_result = passdb_dict_lookup_key(auth_request, module, - str_c(key)); + passdb_result = passdb_dict_lookup_key(auth_request, module); } if (passdb_result == PASSDB_RESULT_OK) { @@ -170,8 +160,9 @@ passdb_dict_preinit(pool_t pool, const char *args) module->conn = conn = db_dict_init(args); module->module.blocking = TRUE; - module->module.cache_key = - auth_cache_parse_key(pool, conn->set.password_key); + module->module.cache_key = auth_cache_parse_key(pool, + db_dict_parse_cache_key(&conn->set.keys, &conn->set.passdb_fields, + &conn->set.parsed_passdb_objects)); module->module.default_pass_scheme = conn->set.default_pass_scheme; return &module->module; } diff --git a/src/auth/test-db-dict.c b/src/auth/test-db-dict.c new file mode 100644 index 0000000000..f5ea33ffc0 --- /dev/null +++ b/src/auth/test-db-dict.c @@ -0,0 +1,51 @@ +/* Copyright (c) 2013 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "db-dict.h" +#include "test-common.h" + +static void test_db_dict_parse_cache_key(void) +{ + struct db_dict_key keys[] = { + { "key0", "%d and %n", NULL, NULL, 0 }, + { "key1", "%{foo}%r%{bar}", NULL, NULL, 0 }, + { "key2", "%{test1}/path", NULL, NULL, 0 }, + { "key3", "path2/%{test2}", NULL, NULL, 0 }, + { "key4", "%{plop}", NULL, NULL, 0 }, + { "key5", "%{unused}", NULL, NULL, 0 } + }; + struct db_dict_field fields[] = { + { "name1", "hello %{dict:key0} %l and %{dict:key1}" }, + { "name2", "%{dict:key2} also %{extra} plus" } + }; + const struct db_dict_key *objects[] = { + &keys[3], &keys[4] + }; + buffer_t keybuf, fieldbuf, objectbuf; + ARRAY_TYPE(db_dict_key) keyarr; + ARRAY_TYPE(db_dict_field) fieldarr; + ARRAY_TYPE(db_dict_key_p) objectarr; + + test_begin("db dict parse cache key"); + + buffer_create_from_const_data(&keybuf, keys, sizeof(keys)); + buffer_create_from_const_data(&fieldbuf, fields, sizeof(fields)); + buffer_create_from_const_data(&objectbuf, objects, sizeof(objects)); + array_create_from_buffer(&keyarr, &keybuf, sizeof(keys[0])); + array_create_from_buffer(&fieldarr, &fieldbuf, sizeof(fields[0])); + array_create_from_buffer(&objectarr, &objectbuf, sizeof(objects[0])); + + test_assert(strcmp(db_dict_parse_cache_key(&keyarr, &fieldarr, &objectarr), + "\t%d and %n\t%l\t%{foo}%r%{bar}\t%{test1}/path\t%{extra}\tpath2/%{test2}\t%{plop}") == 0); + test_end(); +} + +int main(void) +{ + static void (*test_functions[])(void) = { + test_db_dict_parse_cache_key, + NULL + }; + return test_run(test_functions); +} diff --git a/src/auth/userdb-dict.c b/src/auth/userdb-dict.c index 169b5ccb3e..8ceedf1df7 100644 --- a/src/auth/userdb-dict.c +++ b/src/auth/userdb-dict.c @@ -30,22 +30,18 @@ struct dict_userdb_iterate_context { static int dict_query_save_results(struct auth_request *auth_request, - struct dict_connection *conn, const char *result) + struct db_dict_value_iter *iter) { - struct db_dict_value_iter *iter; const char *key, *value, *error; auth_request_init_userdb_reply(auth_request); - iter = db_dict_value_iter_init(conn, result); while (db_dict_value_iter_next(iter, &key, &value)) { if (value != NULL) auth_request_set_userdb_field(auth_request, key, value); } if (db_dict_value_iter_deinit(&iter, &error) < 0) { - auth_request_log_error(auth_request, "dict", - "Value '%s' not in valid %s format: %s", - result, conn->set.value_format, error); + auth_request_log_error(auth_request, "dict", "%s", error); return -1; } return 0; @@ -57,36 +53,29 @@ static void userdb_dict_lookup(struct auth_request *auth_request, struct userdb_module *_module = auth_request->userdb->userdb; struct dict_userdb_module *module = (struct dict_userdb_module *)_module; - string_t *key; + struct db_dict_value_iter *iter; enum userdb_result userdb_result; - const char *value; int ret; - if (*module->conn->set.user_key == '\0') { + if (array_count(&module->conn->set.userdb_fields) == 0 && + array_count(&module->conn->set.parsed_userdb_objects) == 0) { auth_request_log_error(auth_request, "dict", - "user_key not specified"); + "No userdb_objects or userdb_fields specified"); callback(USERDB_RESULT_INTERNAL_FAILURE, auth_request); return; } - key = t_str_new(512); - str_append(key, DICT_PATH_SHARED); - var_expand(key, module->conn->set.user_key, - auth_request_get_var_expand_table(auth_request, NULL)); - - auth_request_log_debug(auth_request, "dict", "lookup %s", str_c(key)); - ret = dict_lookup(module->conn->dict, pool_datastack_create(), - str_c(key), &value); - if (ret < 0) { - auth_request_log_error(auth_request, "dict", "Lookup failed"); + ret = db_dict_value_iter_init(module->conn, auth_request, + &module->conn->set.userdb_fields, + &module->conn->set.parsed_userdb_objects, + &iter); + if (ret < 0) userdb_result = USERDB_RESULT_INTERNAL_FAILURE; - } else if (ret == 0) { + else if (ret == 0) { auth_request_log_unknown_user(auth_request, "dict"); userdb_result = USERDB_RESULT_USER_UNKNOWN; } else { - auth_request_log_debug(auth_request, "dict", - "result: %s", value); - if (dict_query_save_results(auth_request, module->conn, value) < 0) + if (dict_query_save_results(auth_request, iter) < 0) userdb_result = USERDB_RESULT_INTERNAL_FAILURE; else userdb_result = USERDB_RESULT_OK; @@ -178,8 +167,9 @@ userdb_dict_preinit(pool_t pool, const char *args) module->conn = conn = db_dict_init(args); module->module.blocking = TRUE; - module->module.cache_key = - auth_cache_parse_key(pool, conn->set.user_key); + module->module.cache_key = auth_cache_parse_key(pool, + db_dict_parse_cache_key(&conn->set.keys, &conn->set.userdb_fields, + &conn->set.parsed_userdb_objects)); return &module->module; } -- 2.47.3