]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-settings, config: Add support for including groups
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Tue, 27 Feb 2024 20:47:54 +0000 (22:47 +0200)
committerAki Tuomi <aki.tuomi@open-xchange.com>
Fri, 17 Jan 2025 08:39:59 +0000 (10:39 +0200)
It works like:

group @label name {
  ..any settings here..
}

any filter {
  @label = name
}

For example:

group @mailboxes defaults {
  mailbox Sent {
    special_use = \Sent
    auto = subscribe
  }
}
namespace inbox {
  @mailboxes = defaults
}

src/config/config-dump-full.c
src/config/config-filter.h
src/config/config-parser-private.h
src/config/config-parser.c
src/config/config-parser.h
src/config/doveconf.c
src/config/old-set-parser.c
src/lib-master/test-master-service-settings.c
src/lib-settings/settings.c
src/lib-settings/settings.h

index 702e40caf7675d467465e8c4ea3a70985764814c..e5005030ded6846851cdf9629a28a2702f5eac60 100644 (file)
@@ -1,6 +1,7 @@
 /* Copyright (c) 2022 Dovecot authors, see the included COPYING file */
 
 #include "lib.h"
+#include "array.h"
 #include "str.h"
 #include "strescape.h"
 #include "wildcard-match.h"
        <NUL-terminated string: error string - if client attempts to access this
                               settings block, it must fail with this error.
                               NUL = no error, followed by settings>
+       <32bit big-endian: include group count>
+       Repeat for "include group count":
+         <NUL-terminated string: group label>
+         <NUL-terminated string: group name>
        Repeat until "filter settings size" is reached:
          <32bit big-endian: key index number>
         [+|$ <strlist/boollist key>]
      Repeat for "filter count":
        <64bit big-endian: filter settings offset>
      <trailing safety NUL>
+
+   The order of filters is important in the output. lib-settings applies the
+   settings in the same order. The applying is done in reverse order, so last
+   filter is applied first. Note that lib-settings uses the value from the
+   first matching filter and won't change it afterwards. (This is especially
+   important because we want to avoid expanding %variables multiple times for
+   the same setting. And what to do then if the expansion fails? A later value
+   expansion could still work. This is avoided by doing the expansion always
+   just once.)
+
+   The filters are written in the same order as they are defined in the config
+   file. This automatically causes the more specific filters to be written
+   after the less specific ones.
+
+   Groups
+   ------
+
+   The group definition order is different from group include order. They can't
+   be the same, because the same groups can be included from different places,
+   and also the groups can be changed by -o / userdb overrides.
+
+   The group definitions are placed all at the end of the written filters. When
+   the parsing code sees a filter that includes groups, it immediately
+   processes all the group filters and applies any matches. This is needed,
+   because group includes can exist hierarchically so that the most specific
+   (innermost filter) includes are fully applied before the less epecific
+   (outermost filter / global) includes. So if there is e.g. a global
+   @group=foo and namespace { @group=bar } which both modify the same setting,
+   the @group=bar must be applied first to get the expected value. If the same
+   filter has multiple group includes, their include order doesn't matter much,
+   but the behavior should be consistent.
 */
 
 struct dump_context {
@@ -58,6 +94,7 @@ struct dump_context {
 
        const struct setting_parser_info *info;
 
+       const ARRAY_TYPE(config_include_group) *include_groups;
        const struct config_filter *filter;
        unsigned int filter_idx;
        bool filter_written;
@@ -111,15 +148,21 @@ config_dump_full_append_filter_query(string_t *str,
                const char *p = strchr(filter->filter_name, '/');
                i_assert(p != NULL);
                const char *filter_key = t_strdup_until(filter->filter_name, p);
+               bool group = filter_key[0] == SETTINGS_INCLUDE_GROUP_PREFIX;
                if (strcmp(filter_key, SETTINGS_EVENT_MAILBOX_NAME_WITH_PREFIX) == 0)
                        filter_key = SETTINGS_EVENT_MAILBOX_NAME_WITHOUT_PREFIX;
-               str_printfa(str, "(%s=\"%s\"", filter_key, str_escape(p + 1));
+               if (!group) {
+                       str_printfa(str, "(%s=\"%s\" OR ",
+                                   filter_key, str_escape(p + 1));
+               }
                /* the filter_name is used by settings_get_filter() for
                   finding a specific filter without wildcards messing
                   up the lookups. */
-               str_printfa(str, " OR "SETTINGS_EVENT_FILTER_NAME
-                           "=\"%s/%s\")", filter_key,
+               str_printfa(str, SETTINGS_EVENT_FILTER_NAME
+                           "=\"%s/%s\"", filter_key,
                            wildcard_str_escape(settings_section_escape(p + 1)));
+               if (!group)
+                       str_append_c(str, ')');
                str_append(str, " AND ");
        } else if (filter->filter_name != NULL) {
                const char *filter_name = filter->filter_name;
@@ -203,6 +246,14 @@ static void config_dump_full_stdout_write_filter(struct dump_context *ctx)
        if (ctx->filter != NULL)
                config_dump_full_append_filter(str, ctx->filter, TRUE);
        str_append_c(str, '\n');
+
+       if (ctx->include_groups != NULL) {
+               const struct config_include_group *group;
+               array_foreach(ctx->include_groups, group) {
+                       str_printfa(str, ":INCLUDE @%s %s\n",
+                                   group->label, group->name);
+               }
+       }
        o_stream_nsend(ctx->output, str_data(str), str_len(str));
 }
 
@@ -251,6 +302,27 @@ config_dump_full_stdout_callback(const struct config_export_setting *set,
        } T_END;
 }
 
+static void config_include_groups_dump(struct dump_context *ctx)
+{
+       uint32_t include_count_be32 = 0;
+       if (ctx->include_groups == NULL) {
+               o_stream_nsend(ctx->output, &include_count_be32,
+                              sizeof(include_count_be32));
+       } else {
+               include_count_be32 = cpu32_to_be(array_count(ctx->include_groups));
+               o_stream_nsend(ctx->output, &include_count_be32,
+                              sizeof(include_count_be32));
+
+               const struct config_include_group *group;
+               array_foreach(ctx->include_groups, group) {
+                       o_stream_nsend(ctx->output, group->label,
+                                      strlen(group->label) + 1);
+                       o_stream_nsend(ctx->output, group->name,
+                                      strlen(group->name) + 1);
+               }
+       }
+}
+
 static void config_dump_full_write_filter(struct dump_context *ctx)
 {
        if (ctx->filter_written)
@@ -262,6 +334,8 @@ static void config_dump_full_write_filter(struct dump_context *ctx)
        /* Start by assuming there is no error. If there is, the error
           handling code path truncates the file and writes the error. */
        o_stream_nsend(ctx->output, "", 1);
+
+       config_include_groups_dump(ctx);
 }
 
 static void config_dump_full_callback(const struct config_export_setting *set,
@@ -349,14 +423,18 @@ config_dump_full_handle_error(struct dump_context *dump_ctx,
        }
 
        size_t error_len = strlen(error) + 1;
-       uint64_t blob_size = cpu64_to_be(error_len);
+       uint64_t blob_size = cpu64_to_be(error_len + 4);
        o_stream_nsend(output, &blob_size, sizeof(blob_size));
        o_stream_nsend(output, error, error_len);
+       uint32_t include_group_count = 0;
+       o_stream_nsend(output, &include_group_count,
+                      sizeof(include_group_count));
        dump_ctx->filter_written = TRUE;
        return 0;
 }
 
 struct config_dump_full_context {
+       struct config_parsed *config;
        struct ostream *output;
        enum config_dump_full_dest dest;
 
@@ -367,12 +445,28 @@ struct config_dump_full_context {
        uint64_t *filter_offsets_be64;
 };
 
+enum config_dump_type {
+       CONFIG_DUMP_TYPE_DEFAULTS,
+       CONFIG_DUMP_TYPE_EXPLICIT,
+       CONFIG_DUMP_TYPE_GROUPS,
+};
+
+static bool filter_is_group(const struct config_filter *filter)
+{
+       for (; filter != NULL; filter = filter->parent) {
+               if (filter->filter_name_array &&
+                   filter->filter_name[0] == SETTINGS_INCLUDE_GROUP_PREFIX)
+                       return TRUE;
+       }
+       return FALSE;
+}
+
 static int
 config_dump_full_sections(struct config_dump_full_context *ctx,
                          unsigned int parser_idx,
                          const struct setting_parser_info *info,
                          const string_t *delayed_filter,
-                         bool dump_defaults)
+                         enum config_dump_type dump_type)
 {
        struct ostream *output = ctx->output;
        enum config_dump_full_dest dest = ctx->dest;
@@ -383,16 +477,45 @@ config_dump_full_sections(struct config_dump_full_context *ctx,
                .output = output,
                .info = info,
        };
+       ARRAY_TYPE(config_include_group) groups;
+       t_array_init(&groups, 8);
 
        for (unsigned int i = 1; ctx->filters[i] != NULL && ret == 0; i++) {
                const struct config_filter_parser *filter = ctx->filters[i];
                uoff_t start_offset = output->offset;
 
-               if (filter->filter.default_settings != dump_defaults)
-                       continue;
-               if (filter->module_parsers[parser_idx].settings == NULL &&
-                   filter->module_parsers[parser_idx].delayed_error == NULL)
+               if (filter_is_group(&filter->filter)) {
+                       /* This is a group filter. Are we dumping groups?
+                          Handle default groups the same as non-default
+                          groups. */
+                       if (dump_type != CONFIG_DUMP_TYPE_GROUPS)
+                               continue;
+               } else {
+                       /* This is not a group filter. */
+                       switch (dump_type) {
+                       case CONFIG_DUMP_TYPE_DEFAULTS:
+                               if (!filter->filter.default_settings)
+                                       continue;
+                               break;
+                       case CONFIG_DUMP_TYPE_EXPLICIT:
+                               if (filter->filter.default_settings)
+                                       continue;
+                               break;
+                       case CONFIG_DUMP_TYPE_GROUPS:
+                               continue;
+                       }
+               }
+
+               if (config_parsed_get_includes(ctx->config, filter,
+                                              parser_idx, &groups)) {
+                       dump_ctx.include_groups = &groups;
+               } else if (filter->module_parsers[parser_idx].settings == NULL &&
+                          filter->module_parsers[parser_idx].delayed_error == NULL) {
+                       /* nothing to export in this filter */
                        continue;
+               } else {
+                       dump_ctx.include_groups = NULL;
+               }
 
                dump_ctx.filter = &filter->filter;
                dump_ctx.filter_idx = i;
@@ -417,6 +540,12 @@ config_dump_full_sections(struct config_dump_full_context *ctx,
 
                const char *error;
                ret = config_export_parser(export_ctx, parser_idx, &error);
+               if (ret == 0 && dump_ctx.include_groups != NULL) {
+                       if (dest == CONFIG_DUMP_FULL_DEST_STDOUT)
+                               config_dump_full_stdout_write_filter(&dump_ctx);
+                       else
+                               config_dump_full_write_filter(&dump_ctx);
+               }
                if (ret < 0) {
                        /* Delay the failure until the filter is accessed by
                           the config client. The error is written to the
@@ -444,9 +573,12 @@ config_dump_full_sections(struct config_dump_full_context *ctx,
                ctx->filter_offsets_be64[ctx->filter_output_count] =
                        cpu64_to_be(output->offset);
 
-               uint64_t blob_size = cpu64_to_be(1 + str_len(delayed_filter));
+               uint64_t blob_size = cpu64_to_be(5 + str_len(delayed_filter));
                o_stream_nsend(output, &blob_size, sizeof(blob_size));
                o_stream_nsend(output, "", 1); /* no error */
+               uint32_t include_group_count = 0;
+               o_stream_nsend(output, &include_group_count,
+                              sizeof(include_group_count));
                o_stream_nsend(output, str_data(delayed_filter),
                               str_len(delayed_filter));
                ctx->filter_output_count++;
@@ -536,6 +668,7 @@ int config_dump_full(struct config_parsed *config,
        }
 
        struct config_dump_full_context ctx = {
+               .config = config,
                .output = output,
                .dest = dest,
                .filters = config_parsed_get_filter_parsers(config),
@@ -551,6 +684,9 @@ int config_dump_full(struct config_parsed *config,
        ctx.filter_indexes_be32 = t_new(uint32_t, max_filter_count);
        ctx.filter_offsets_be64 = t_new(uint64_t, max_filter_count);
 
+       ARRAY_TYPE(config_include_group) groups;
+       t_array_init(&groups, 8);
+
        unsigned int i, parser_count =
                config_export_get_parser_count(export_ctx);
        for (i = 0; i < parser_count; i++) {
@@ -578,23 +714,30 @@ int config_dump_full(struct config_parsed *config,
                                       sizeof(filter_count));
                }
 
-               /* Write default settings filters */
+               /* 1. Write built-in default settings */
                int ret;
                T_BEGIN {
-                       ret = config_dump_full_sections(&ctx, i, info, NULL, TRUE);
+                       ret = config_dump_full_sections(&ctx, i, info, NULL,
+                                       CONFIG_DUMP_TYPE_DEFAULTS);
                } T_END;
                if (ret < 0)
                        break;
 
                uoff_t blob_size_offset = output->offset;
-               /* Write base settings - add it as an empty filter */
+               /* 2. Write global settings in config - use an empty filter */
                ctx.filter_indexes_be32[ctx.filter_output_count] = 0;
                ctx.filter_offsets_be64[ctx.filter_output_count] =
                        cpu64_to_be(blob_size_offset);
                ctx.filter_output_count++;
 
+               if (config_parsed_get_includes(config, filter_parser,
+                                              i, &groups))
+                       dump_ctx.include_groups = &groups;
+               else
+                       dump_ctx.include_groups = NULL;
+
                if (dest != CONFIG_DUMP_FULL_DEST_STDOUT) {
-                       /* Write a filter for the base settings, even if there
+                       /* Write a filter for the global settings, even if there
                           are no settings. This allows lib-settings to apply
                           setting overrides at the proper position before
                           defaults. */
@@ -603,6 +746,7 @@ int config_dump_full(struct config_parsed *config,
                           the error handling code path truncates the file
                           and writes the error. */
                        o_stream_nsend(output, "", 1);
+                       config_include_groups_dump(&dump_ctx);
                        dump_ctx.filter_written = TRUE;
                } else {
                        /* Make :FILTER visible */
@@ -614,15 +758,31 @@ int config_dump_full(struct config_parsed *config,
                                        blob_size_offset, error) < 0)
                                break;
                }
+               if (dump_ctx.include_groups != NULL) {
+                       if (dest == CONFIG_DUMP_FULL_DEST_STDOUT)
+                               config_dump_full_stdout_write_filter(&dump_ctx);
+                       else
+                               config_dump_full_write_filter(&dump_ctx);
+               }
                if (dest != CONFIG_DUMP_FULL_DEST_STDOUT) {
                        if (output_blob_size(output, blob_size_offset) < 0)
                                break;
                }
 
-               /* Write non-default settings filters */
+               /* 3. Write filter settings in config */
+               T_BEGIN {
+                       ret = config_dump_full_sections(&ctx, i, info,
+                                       dump_ctx.delayed_output,
+                                       CONFIG_DUMP_TYPE_EXPLICIT);
+               } T_END;
+               if (ret < 0)
+                       break;
+
+               /* 4. Write group filters */
                T_BEGIN {
                        ret = config_dump_full_sections(&ctx, i, info,
-                               dump_ctx.delayed_output, FALSE);
+                                       dump_ctx.delayed_output,
+                                       CONFIG_DUMP_TYPE_GROUPS);
                } T_END;
                if (ret < 0)
                        break;
index 915b5e6b56e312937e33915fa372c43d3e4f0823..65b5c6b469a8fc2170441dcefb5ff78afbfca24b 100644 (file)
@@ -28,6 +28,12 @@ struct config_filter {
        bool default_settings;
 };
 
+struct config_include_group {
+       const char *label;
+       const char *name;
+};
+ARRAY_DEFINE_TYPE(config_include_group, struct config_include_group);
+
 /* Each unique config_filter (including its parents in hierarchy) has its own
    config_filter_parser. */
 struct config_filter_parser {
@@ -36,6 +42,8 @@ struct config_filter_parser {
        struct config_filter_parser *parent;
        struct config_filter_parser *children_head, *children_tail, *prev, *next;
 
+       /* When this filter is used, it includes settings from these groups. */
+       ARRAY_TYPE(config_include_group) include_groups;
        /* Filter for this parser. Its parent filters must also match. */
        struct config_filter filter;
        /* NULL-terminated array of parsers for settings. All parsers have the
index e0f6c634c8e7c821616eb7430ba80c77b4a0dfd5..64e1635360489b505e14dba1f6b8bc0502b85524 100644 (file)
@@ -22,6 +22,8 @@ enum config_line_type {
        CONFIG_LINE_TYPE_SECTION_BEGIN,
        /* } (key = "}", value = "") */
        CONFIG_LINE_TYPE_SECTION_END,
+       /* group @key value { */
+       CONFIG_LINE_TYPE_GROUP_SECTION_BEGIN,
        /* !include value (key = "!include") */
        CONFIG_LINE_TYPE_INCLUDE,
        /* !include_try value (key = "!include_try") */
index f2ee1f4e8d77b836b6a215518dbe7c2df64eb464..68938f8cb46e124d1c28a3c1967d09148ffbaad8 100644 (file)
@@ -44,6 +44,11 @@ struct config_parser_key {
        unsigned int define_idx;
 };
 
+struct config_include_group_filters {
+       const char *label;
+       ARRAY(struct config_filter_parser *) filters;
+};
+
 struct config_parsed {
        pool_t pool;
        const char *dovecot_config_version;
@@ -51,6 +56,7 @@ struct config_parsed {
        struct config_module_parser *module_parsers;
        ARRAY_TYPE(const_string) errors;
        HASH_TABLE(const char *, const struct setting_define *) key_hash;
+       HASH_TABLE(const char *, struct config_include_group_filters *) include_groups;
 };
 
 ARRAY_DEFINE_TYPE(setting_parser_info_p, const struct setting_parser_info *);
@@ -331,6 +337,16 @@ config_apply_error(struct config_parser_context *ctx, const char *key)
        return 0;
 }
 
+static bool config_filter_has_include_group(const struct config_filter *filter)
+{
+       for (; filter != NULL; filter = filter->parent) {
+               if (filter->filter_name_array &&
+                   filter->filter_name[0] == SETTINGS_INCLUDE_GROUP_PREFIX)
+                       return TRUE;
+       }
+       return FALSE;
+}
+
 static int
 settings_value_check(struct config_parser_context *ctx,
                     const struct setting_parser_info *info,
@@ -737,6 +753,11 @@ static bool replace_filter_prefix(struct config_parser_context *ctx,
        return TRUE;
 }
 
+static const char *filter_key_skip_group_prefix(const char *key)
+{
+       return key[0] == SETTINGS_INCLUDE_GROUP_PREFIX ? key + 1 : key;
+}
+
 static int
 config_apply_line_full(struct config_parser_context *ctx,
                       const struct config_line *line,
@@ -817,8 +838,8 @@ again:
            ctx->cur_section->filter_parser->filter.filter_name_array) {
                /* For named list filters, try filter name { key } ->
                   filter_name_key first before anything else. */
-               const char *filter_key =
-                       t_str_replace(ctx->cur_section->filter_parser->filter.filter_name, '/', '_');
+               const char *filter_key = filter_key_skip_group_prefix(
+                       t_str_replace(ctx->cur_section->filter_parser->filter.filter_name, '/', '_'));
                const char *key2 = t_strdup_printf("%s_%s", filter_key, key);
                struct config_filter_parser *last_filter_parser =
                        ctx->cur_section->filter_parser;
@@ -835,8 +856,8 @@ again:
                /* first try the filter name-specific prefix, so e.g.
                   inet_listener { ssl=yes } won't try to change the global
                   ssl setting. */
-               const char *filter_key =
-                       t_strcut(ctx->cur_section->filter_parser->filter.filter_name, '/');
+               const char *filter_key = filter_key_skip_group_prefix(
+                       t_strcut(ctx->cur_section->filter_parser->filter.filter_name, '/'));
                const char *key2 = t_strdup_printf("%s_%s", filter_key, key);
                struct config_filter_parser *last_filter_parser =
                        ctx->cur_section->filter_parser;
@@ -1023,7 +1044,15 @@ config_filter_add_new_filter(struct config_parser_context *ctx,
        i_zero(&filter);
        filter.parent = parent;
 
-       if (strcmp(key, "protocol") == 0) {
+       if (key[0] == SETTINGS_INCLUDE_GROUP_PREFIX) {
+               if (!config_filter_is_empty(parent)) {
+                       ctx->error = "groups must defined at top-level, not under filters";
+                       return TRUE;
+               }
+               filter.filter_name =
+                       p_strdup_printf(ctx->pool, "%s/%s", key, value);
+               filter.filter_name_array = TRUE;
+       } else if (strcmp(key, "protocol") == 0) {
                if (parent->service != NULL)
                        ctx->error = "Nested protocol { protocol { .. } } block not allowed";
                else if (parent->filter_name != NULL)
@@ -1667,9 +1696,42 @@ config_parse_line(struct config_parser_context *ctx,
        /* b) + errors */
        line[-1] = '\0';
 
-       if (*line == '{')
+       if (*line == '{') {
                config_line_r->value = "";
-       else {
+               config_line_r->type = CONFIG_LINE_TYPE_SECTION_BEGIN;
+       } else if (strcmp(key, "group") == 0) {
+               /* group @group name { */
+               config_line_r->key = line;
+               while (!IS_WHITE(*line) && *line != '\0')
+                       line++;
+               if (*line == '\0') {
+                       config_line_r->value = "Expecting group name";
+                       config_line_r->type = CONFIG_LINE_TYPE_ERROR;
+                       return;
+               }
+               *line++ = '\0';
+               while (IS_WHITE(*line))
+                       line++;
+
+               config_line_r->value = line;
+               while (!IS_WHITE(*line) && *line != '\0')
+                       line++;
+               if (*line == '\0') {
+                       config_line_r->value = "Expecting '{'";
+                       config_line_r->type = CONFIG_LINE_TYPE_ERROR;
+                       return;
+               }
+               *line++ = '\0';
+               while (IS_WHITE(*line))
+                       line++;
+               if (*line != '{') {
+                       config_line_r->value = "Expecting '{'";
+                       config_line_r->type = CONFIG_LINE_TYPE_ERROR;
+                       return;
+               }
+
+               config_line_r->type = CONFIG_LINE_TYPE_GROUP_SECTION_BEGIN;
+       } else {
                /* get section name */
                if (*line != '"') {
                        config_line_r->value = line;
@@ -1697,13 +1759,46 @@ config_parse_line(struct config_parser_context *ctx,
                        config_line_r->type = CONFIG_LINE_TYPE_ERROR;
                        return;
                }
+               config_line_r->type = CONFIG_LINE_TYPE_SECTION_BEGIN;
        }
        if (line[1] != '\0') {
                config_line_r->value = "Garbage after '{'";
                config_line_r->type = CONFIG_LINE_TYPE_ERROR;
-               return;
        }
-       config_line_r->type = CONFIG_LINE_TYPE_SECTION_BEGIN;
+}
+
+static void config_parse_finish_includes(struct config_parsed *config)
+{
+       hash_table_create(&config->include_groups, config->pool, 0,
+                         str_hash, strcmp);
+
+       for (unsigned int i = 0; config->filter_parsers[i] != NULL; i++) {
+               struct config_filter_parser *filter = config->filter_parsers[i];
+
+               if (!filter->filter.filter_name_array ||
+                   filter->filter.filter_name[0] != SETTINGS_INCLUDE_GROUP_PREFIX)
+                       continue;
+
+               /* This is a group filter's root (which may have child
+                  filters) */
+               T_BEGIN {
+                       const char *include_group =
+                               t_strcut(filter->filter.filter_name + 1, '/');
+                       struct config_include_group_filters *group =
+                               hash_table_lookup(config->include_groups,
+                                                 include_group);
+                       if (group == NULL) {
+                               group = p_new(config->pool,
+                                             struct config_include_group_filters, 1);
+                               group->label = p_strdup(config->pool,
+                                                       include_group);
+                               p_array_init(&group->filters, config->pool, 4);
+                               hash_table_insert(config->include_groups,
+                                                 group->label, group);
+                       }
+                       array_push_back(&group->filters, &filter);
+               } T_END;
+       }
 }
 
 static int
@@ -1730,6 +1825,8 @@ config_parse_finish(struct config_parser_context *ctx,
        new_config->filter_parsers = array_front(&ctx->all_filter_parsers);
        new_config->module_parsers = ctx->root_module_parsers;
 
+       config_parse_finish_includes(new_config);
+
        if (ret < 0)
                ;
        else if ((ret = config_all_parsers_check(ctx, new_config, &error)) < 0) {
@@ -2000,6 +2097,28 @@ static bool config_parser_get_version(struct config_parser_context *ctx,
        return TRUE;
 }
 
+static void
+config_parser_include_add_or_update(struct config_parser_context *ctx,
+                                   const char *group, const char *name)
+{
+       struct config_filter_parser *filter_parser =
+               ctx->cur_section->filter_parser;
+       struct config_include_group *include_group;
+
+       if (!array_is_created(&filter_parser->include_groups))
+               p_array_init(&filter_parser->include_groups, ctx->pool, 4);
+       array_foreach_modifiable(&filter_parser->include_groups, include_group) {
+               if (strcmp(include_group->label, group) == 0) {
+                       /* preserve original position */
+                       include_group->name = p_strdup(ctx->pool, name);
+                       return;
+               }
+       }
+       include_group = array_append_space(&filter_parser->include_groups);
+       include_group->label = p_strdup(ctx->pool, group);
+       include_group->name = p_strdup(ctx->pool, name);
+}
+
 void config_parser_apply_line(struct config_parser_context *ctx,
                              const struct config_line *line)
 {
@@ -2020,6 +2139,13 @@ void config_parser_apply_line(struct config_parser_context *ctx,
                if (config_write_value(ctx, line) < 0) {
                        if (config_apply_error(ctx, line->key) < 0)
                                break;
+               } else if (line->key[0] == SETTINGS_INCLUDE_GROUP_PREFIX) {
+                       if (config_filter_has_include_group(&ctx->cur_section->filter_parser->filter)) {
+                               ctx->error = "Recursive include groups not allowed";
+                               break;
+                       }
+                       config_parser_include_add_or_update(ctx, line->key + 1,
+                                                           str_c(ctx->value));
                } else {
                        /* Either a global key or list/key */
                        const char *key_with_path =
@@ -2039,6 +2165,13 @@ void config_parser_apply_line(struct config_parser_context *ctx,
                        }
                }
                break;
+       case CONFIG_LINE_TYPE_GROUP_SECTION_BEGIN:
+               ctx->cur_section = config_add_new_section(ctx);
+               ctx->cur_section->key = "group";
+
+               (void)config_filter_add_new_filter(ctx, line->key, line->value,
+                                                  FALSE);
+               break;
        case CONFIG_LINE_TYPE_SECTION_BEGIN: {
                /* See if we need to prefix the key with filter name */
                const struct config_filter *cur_filter =
@@ -2408,6 +2541,60 @@ config_parsed_key_lookup(struct config_parsed *config, const char *key)
        return hash_table_lookup(config->key_hash, key);
 }
 
+static bool config_filter_tree_has_settings(struct config_filter_parser *filter,
+                                           unsigned int parser_idx)
+{
+       if (filter->module_parsers[parser_idx].settings != NULL)
+               return TRUE;
+       for (filter = filter->children_head; filter != NULL; filter = filter->next) {
+               if (config_filter_tree_has_settings(filter, parser_idx))
+                       return TRUE;
+       }
+       return FALSE;
+}
+
+static bool
+config_include_group_filters_have_settings(
+       struct config_include_group_filters *group_filters,
+       unsigned int parser_idx)
+{
+       struct config_filter_parser *group_filter;
+
+       /* See if this group modifies the wanted parser. Check the group's
+          root filter and all of its child filters. For example
+          group @foo bar { namespace inbox { separator=/ } } needs to
+          returns TRUE for namespace parser, which is modified in the child
+          namespace filter. */
+       array_foreach_elem(&group_filters->filters, group_filter) {
+               if (config_filter_tree_has_settings(group_filter, parser_idx))
+                       return TRUE;
+       }
+       return FALSE;
+}
+
+bool config_parsed_get_includes(struct config_parsed *config,
+                               const struct config_filter_parser *filter,
+                               unsigned int parser_idx,
+                               ARRAY_TYPE(config_include_group) *groups)
+{
+       array_clear(groups);
+
+       if (!array_is_created(&filter->include_groups))
+               return FALSE;
+
+       const struct config_include_group *group;
+       array_foreach(&filter->include_groups, group) {
+               struct config_include_group_filters *group_filters =
+                       hash_table_lookup(config->include_groups, group->label);
+               if (group_filters == NULL)
+                       continue;
+
+               if (config_include_group_filters_have_settings(group_filters, parser_idx))
+                       array_push_back(groups, group);
+       }
+       return array_count(groups) > 0;
+}
+
 void config_parsed_free(struct config_parsed **_config)
 {
        struct config_parsed *config = *_config;
@@ -2416,6 +2603,7 @@ void config_parsed_free(struct config_parsed **_config)
                return;
        *_config = NULL;
 
+       hash_table_destroy(&config->include_groups);
        hash_table_destroy(&config->key_hash);
        pool_unref(&config->pool);
 }
index a29ec3d2e622b11cd806f726d4a599070695f23d..95df5a7c05d4952a2aaa09b70eea2fe0e2e680b3 100644 (file)
@@ -1,6 +1,8 @@
 #ifndef CONFIG_PARSER_H
 #define CONFIG_PARSER_H
 
+#include "config-filter.h"
+
 #define CONFIG_MODULE_DIR MODULEDIR"/settings"
 
 #define IS_WHITE(c) ((c) == ' ' || (c) == '\t')
@@ -94,6 +96,13 @@ config_module_parsers_get_setting(const struct config_module_parser *module_pars
 /* Lookup setting with the specified key. */
 const struct setting_define *
 config_parsed_key_lookup(struct config_parsed *config, const char *key);
+/* Get the list of filter's include groups that have any settings in the given
+   module parser index. Returns TRUE if any groups were returned. */
+bool config_parsed_get_includes(struct config_parsed *config,
+                               const struct config_filter_parser *filter,
+                               unsigned int parser_idx,
+                               ARRAY_TYPE(config_include_group) *groups);
+
 void config_parsed_free(struct config_parsed **config);
 
 void config_parse_load_modules(void);
index 1449c8a4edbff8d2228375416cb9814167de790c..6c0b242499f963bd886c345f19fa53c5b66ef802 100644 (file)
@@ -12,6 +12,7 @@
 #include "str.h"
 #include "strescape.h"
 #include "settings-parser.h"
+#include "settings.h"
 #include "master-interface.h"
 #include "master-service.h"
 #include "all-settings.h"
@@ -141,6 +142,29 @@ static struct prefix_stack prefix_stack_pop(ARRAY_TYPE(prefix_stack) *stack)
        return sc;
 }
 
+static bool
+config_dump_human_include_group(struct config_filter_parser *filter_parser,
+                               struct ostream *output,
+                               const string_t *list_prefix,
+                               unsigned int indent)
+{
+       const struct config_include_group *group;
+
+       if (array_is_empty(&filter_parser->include_groups))
+               return FALSE;
+
+       if (list_prefix != NULL) {
+               o_stream_nsend(output, str_data(list_prefix),
+                              str_len(list_prefix));
+       }
+       array_foreach(&filter_parser->include_groups, group) {
+               o_stream_nsend(output, indent_str, indent*2);
+               o_stream_nsend_str(output, t_strdup_printf(
+                       "@%s = %s\n", group->label, group->name));
+       }
+       return TRUE;
+}
+
 static struct config_dump_human_context *
 config_dump_human_init(enum config_dump_scope scope,
                       struct config_filter_parser *filter_parser)
@@ -620,6 +644,8 @@ config_dump_named_filters(string_t *str, unsigned int *indent,
                str_printfa(str, "%s {\n", filter->filter_name);
        else {
                /* SET_FILTER_ARRAY */
+               if (filter->filter_name[0] == SETTINGS_INCLUDE_GROUP_PREFIX)
+                       str_append(str, "group ");
                str_printfa(str, "%s %s {\n",
                            t_strdup_until(filter->filter_name, p),
                            filter_name_escaped(p+1));
@@ -776,6 +802,12 @@ config_dump_human_filter_path(enum config_dump_scope scope,
                                         strip_prefix, strip_prefix2);
 
                bool sub_list_prefix_sent = ctx->list_prefix_sent;
+               if (set_name_filter == NULL) {
+                       if (config_dump_human_include_group(filter_parser, output,
+                                                           sub_list_prefix_sent ? NULL :
+                                                           list_prefix, sub_indent))
+                               sub_list_prefix_sent = TRUE;
+               }
                if (sub_list_prefix_sent) {
                        *list_prefix_sent = TRUE;
                        str_truncate(list_prefix, 0);
@@ -834,6 +866,8 @@ config_dump_human(enum config_dump_scope scope,
                                      list_prefix, &list_prefix_sent,
                                      hide_key, hide_passwords);
 
+       if (setting_name_filter == NULL)
+               config_dump_human_include_group(filter_parser, output, NULL, 0);
        if (hide_key && output->offset == 0)
                o_stream_nsend(output, "\n", 1);
        /* flush output before writing errors */
index 715f026afbfa50e958353f912c3df1d17d53f69d..b289f05ce7e91e418032e26cfd1a6801785dbfdc 100644 (file)
@@ -641,6 +641,7 @@ bool old_settings_handle(struct config_parser_context *ctx,
        case CONFIG_LINE_TYPE_INCLUDE:
        case CONFIG_LINE_TYPE_INCLUDE_TRY:
        case CONFIG_LINE_TYPE_KEYVARIABLE:
+       case CONFIG_LINE_TYPE_GROUP_SECTION_BEGIN:
                break;
        case CONFIG_LINE_TYPE_KEYFILE:
        case CONFIG_LINE_TYPE_KEYVALUE:
index 12f782b9ced795f6b6425124a8562d0b07338d41..287b28e4cff7e33074465bccc73a0f4d1a9a18ab 100644 (file)
@@ -172,19 +172,95 @@ static const struct {
               "\x00\x00\x00\x00\x00\x00\x00\x00" // filter settings offset
               "\x00"), // safety NUL
          "'filter error string' points outside area" },
+       /* include group count is truncated */
+       { DATA("DOVECOT-CONFIG\t1.0\n"
+              "\x00\x00\x00\x00\x00\x00\x00\x40" // full size
+              "\x00\x00\x00\x01" // event filter count
+              "\x00" // event filter[0]
+              "\x00" // override event filter[0]
+              "\x00\x00\x00\x00\x00\x00\x00\x32" // block size
+              "master_service\x00" // block name
+              "\x00\x00\x00\x01" // settings count
+              "K\x00" // setting[0] key
+              "\x00\x00\x00\x01" // filter count
+              "\x00\x00\x00\x00\x00\x00\x00\x04" // filter settings size
+              "\x00" // filter error string
+              "\x00\x00\x00" // include group count
+              "\x00\x00\x00\x00" // event filter index
+              "\x00\x00\x00\x00\x00\x00\x00\x00" // filter settings offset
+              "\x00"), // safety NUL
+         "Area too small when reading uint of 'include group count'" },
+       /* include group count is too large */
+       { DATA("DOVECOT-CONFIG\t1.0\n"
+              "\x00\x00\x00\x00\x00\x00\x00\x41" // full size
+              "\x00\x00\x00\x01" // event filter count
+              "\x00" // event filter[0]
+              "\x00" // override event filter[0]
+              "\x00\x00\x00\x00\x00\x00\x00\x33" // block size
+              "master_service\x00" // block name
+              "\x00\x00\x00\x01" // settings count
+              "K\x00" // setting[0] key
+              "\x00\x00\x00\x01" // filter count
+              "\x00\x00\x00\x00\x00\x00\x00\x05" // filter settings size
+              "\x00" // filter error string
+              "\x00\x00\x00\x01" // include group count
+              "\x00\x00\x00\x00" // event filter index
+              "\x00\x00\x00\x00\x00\x00\x00\x00" // filter settings offset
+              "\x00"), // safety NUL
+         "'group label string' points outside area" },
+       /* group label not NUL-terminated */
+       { DATA("DOVECOT-CONFIG\t1.0\n"
+              "\x00\x00\x00\x00\x00\x00\x00\x42" // full size
+              "\x00\x00\x00\x01" // event filter count
+              "\x00" // event filter[0]
+              "\x00" // override event filter[0]
+              "\x00\x00\x00\x00\x00\x00\x00\x34" // block size
+              "master_service\x00" // block name
+              "\x00\x00\x00\x01" // settings count
+              "K\x00" // setting[0] key
+              "\x00\x00\x00\x01" // filter count
+              "\x00\x00\x00\x00\x00\x00\x00\x06" // filter settings size
+              "\x00" // filter error string
+              "\x00\x00\x00\x01" // include group count
+              "G" // group label
+              "\x00\x00\x00\x00" // event filter index
+              "\x00\x00\x00\x00\x00\x00\x00\x00" // filter settings offset
+              "\x00"), // safety NUL
+         "'group label string' points outside area" },
+       /* group name not NUL-terminated */
+       { DATA("DOVECOT-CONFIG\t1.0\n"
+              "\x00\x00\x00\x00\x00\x00\x00\x44" // full size
+              "\x00\x00\x00\x01" // event filter count
+              "\x00" // event filter[0]
+              "\x00" // override event filter[0]
+              "\x00\x00\x00\x00\x00\x00\x00\x36" // block size
+              "master_service\x00" // block name
+              "\x00\x00\x00\x01" // settings count
+              "K\x00" // setting[0] key
+              "\x00\x00\x00\x01" // filter count
+              "\x00\x00\x00\x00\x00\x00\x00\x08" // filter settings size
+              "\x00" // filter error string
+              "\x00\x00\x00\x01" // include group count
+              "G\x00" // group label
+              "N" // group name
+              "\x00\x00\x00\x00" // event filter index
+              "\x00\x00\x00\x00\x00\x00\x00\x00" // filter settings offset
+              "\x00"), // safety NUL
+         "'group name string' points outside area" },
        /* invalid filter string */
        { DATA("DOVECOT-CONFIG\t1.0\n"
-              "\x00\x00\x00\x00\x00\x00\x00\x32" // full size
+              "\x00\x00\x00\x00\x00\x00\x00\x36" // full size
               "\x00\x00\x00\x01" // event filter count
               "F\x00" // event filter[0]
               "F\x00" // override event filter[0]
-              "\x00\x00\x00\x00\x00\x00\x00\x22" // block size
+              "\x00\x00\x00\x00\x00\x00\x00\x26" // block size
               "N\x00" // block name
               "\x00\x00\x00\x01" // settings count
               "K\x00" // setting[0] key
               "\x00\x00\x00\x01" // filter count
-              "\x00\x00\x00\x00\x00\x00\x00\x01" // filter settings size
+              "\x00\x00\x00\x00\x00\x00\x00\x05" // filter settings size
               "\x00" // filter error string
+              "\x00\x00\x00\x00" // include group count
               "\x00\x00\x00\x00" // event filter index
               "\x00\x00\x00\x00\x00\x00\x00\x00" // filter settings offset
               "\x00"), // safety NUL
@@ -192,17 +268,18 @@ static const struct {
 
        /* Duplicate block name */
        { DATA("DOVECOT-CONFIG\t1.0\n"
-              "\x00\x00\x00\x00\x00\x00\x00\x3A" // full size
+              "\x00\x00\x00\x00\x00\x00\x00\x3E" // full size
               "\x00\x00\x00\x01" // event filter count
               "\x00" // event filter[0]
               "\x00" // override event filter[0]
-              "\x00\x00\x00\x00\x00\x00\x00\x22" // block size
+              "\x00\x00\x00\x00\x00\x00\x00\x26" // block size
               "N\x00" // block name
               "\x00\x00\x00\x01" // settings count
               "K\x00" // setting[0] key
               "\x00\x00\x00\x01" // filter count
-              "\x00\x00\x00\x00\x00\x00\x00\x01" // filter settings size
+              "\x00\x00\x00\x00\x00\x00\x00\x05" // filter settings size
               "\x00" // filter error string
+              "\x00\x00\x00\x00" // include group count
               "\x00\x00\x00\x00" // event filter index
               "\x00\x00\x00\x00\x00\x00\x00\x00" // filter settings offset
               "\x00" // safety NUL
index dd308c2aade38d27dea3f84e89c4b557fd543c55..d99c18971f91002a95df105ecbf0ceb014f99fa1 100644 (file)
@@ -76,6 +76,12 @@ struct settings_override {
 ARRAY_DEFINE_TYPE(settings_override, struct settings_override);
 ARRAY_DEFINE_TYPE(settings_override_p, struct settings_override *);
 
+struct settings_group {
+       const char *label;
+       const char *name;
+};
+ARRAY_DEFINE_TYPE(settings_group, struct settings_group);
+
 struct settings_mmap_block {
        const char *name;
        size_t block_end_offset;
@@ -388,6 +394,7 @@ settings_block_read(struct settings_mmap *mmap, size_t *_offset,
                                &filter_settings_size, error_r) < 0)
                        return -1;
 
+               /* <error string> */
                size_t tmp_offset = offset;
                size_t filter_end_offset = offset + filter_settings_size;
                if (settings_block_read_str(mmap, &tmp_offset,
@@ -396,6 +403,31 @@ settings_block_read(struct settings_mmap *mmap, size_t *_offset,
                                            error_r) < 0)
                        return -1;
 
+               uint32_t include_count;
+               if (settings_block_read_uint32(mmap, &tmp_offset,
+                                              filter_end_offset,
+                                              "include group count",
+                                              &include_count, error_r) < 0)
+                       return -1;
+
+               for (uint32_t i = 0; i < include_count; i++) {
+                       /* <group label> */
+                       const char *group_label;
+                       if (settings_block_read_str(mmap, &tmp_offset,
+                                                   filter_end_offset,
+                                                   "group label string",
+                                                   &group_label, error_r) < 0)
+                               return -1;
+
+                       /* <group name> */
+                       const char *group_name;
+                       if (settings_block_read_str(mmap, &tmp_offset,
+                                                   filter_end_offset,
+                                                   "group name string",
+                                                   &group_name, error_r) < 0)
+                               return -1;
+               }
+
                /* skip over the filter contents for now */
                offset += filter_settings_size;
        }
@@ -784,9 +816,15 @@ settings_mmap_apply(struct settings_apply_ctx *ctx, const char **error_r)
                .type = LOG_TYPE_DEBUG,
        };
 
+       ARRAY_TYPE(settings_group) include_groups;
+       t_array_init(&include_groups, 4);
+
        /* So through the filters in reverse sorted order, so we always set the
           setting just once, never overriding anything. A filter for the base
           settings is expected to always exist. */
+       struct event *event = ctx->event;
+       bool filters_matched[block->filter_count + 1];
+       memset(filters_matched, 0, block->filter_count + 1);
        for (uint32_t i = block->filter_count; i > 0; ) {
                i--;
                uint32_t event_filter_idx = be32_to_cpu_unaligned(
@@ -802,8 +840,11 @@ settings_mmap_apply(struct settings_apply_ctx *ctx, const char **error_r)
                        mmap->event_filters[event_filter_idx];
                if (event_filter == EVENT_FILTER_MATCH_NEVER)
                        ;
-               else if (event_filter == EVENT_FILTER_MATCH_ALWAYS ||
-                        event_filter_match(event_filter, ctx->event, &failure_ctx)) {
+               else if (filters_matched[i]) {
+                       /* Group include restarted the filter matching. We
+                          already applied this filter, so skip checking it. */
+               } else if (event_filter == EVENT_FILTER_MATCH_ALWAYS ||
+                          event_filter_match(event_filter, event, &failure_ctx)) {
                        uint64_t filter_offset = be64_to_cpu_unaligned(
                                CONST_PTR_OFFSET(mmap->mmap_base,
                                                 block->filter_offsets_start_offset +
@@ -823,6 +864,25 @@ settings_mmap_apply(struct settings_apply_ctx *ctx, const char **error_r)
                        }
                        filter_offset += strlen(filter_error) + 1;
 
+                       uint32_t include_count = be32_to_cpu_unaligned(
+                               CONST_PTR_OFFSET(mmap->mmap_base, filter_offset));
+                       filter_offset += sizeof(include_count);
+
+                       array_clear(&include_groups);
+                       for (uint32_t j = 0; j < include_count; j++) {
+                               struct settings_group *include_group =
+                                       array_append_space(&include_groups);
+                               include_group->label =
+                                       CONST_PTR_OFFSET(mmap->mmap_base,
+                                                        filter_offset);
+                               filter_offset += strlen(include_group->label) + 1;
+
+                               include_group->name =
+                                       CONST_PTR_OFFSET(mmap->mmap_base,
+                                                        filter_offset);
+                               filter_offset += strlen(include_group->name) + 1;
+                       }
+
                        if (ctx->filter_name != NULL && !ctx->seen_filter &&
                            event_filter != EVENT_FILTER_MATCH_ALWAYS) {
                                bool op_not;
@@ -858,6 +918,23 @@ settings_mmap_apply(struct settings_apply_ctx *ctx, const char **error_r)
                                        filter_offset, filter_end_offset,
                                        error_r) < 0)
                                return -1;
+
+                       /* Don't try to apply this filter again if group
+                          including restarts the filter processing */
+                       filters_matched[i] = TRUE;
+                       const struct settings_group *include_group;
+                       array_foreach(&include_groups, include_group) {
+                               /* Add @<group label>/<group name> to
+                                  matching filters and restart the filter
+                                  processing. */
+                               char *filter_value = p_strdup_printf(
+                                       event_get_pool(ctx->event), "@%s/%s",
+                                       include_group->label, include_group->name);
+                               event_strlist_append(ctx->event,
+                                                    SETTINGS_EVENT_FILTER_NAME,
+                                                    filter_value);
+                               i = block->filter_count;
+                       }
                }
        }
        return ctx->seen_filter ? 1 : 0;
index f236a782ba12332dfa7532bfed244f147d61f34b..f4610792ab4de208d6208b7b18240a0af9800e87 100644 (file)
@@ -58,6 +58,9 @@ struct settings_get_params {
        enum settings_get_flags flags;
 };
 
+/* Setting name prefix that is used as include group. */
+#define SETTINGS_INCLUDE_GROUP_PREFIX '@'
+
 /* Set struct settings_instance to events so settings_get() can
    use it to get instance-specific settings. */
 #define SETTINGS_EVENT_INSTANCE "settings_instance"