1 /*        $NetBSD: postconf_user.c,v 1.5 2025/02/25 19:15:47 christos Exp $     */
2 
3 /*++
4 /* NAME
5 /*        postconf_user 3
6 /* SUMMARY
7 /*        support for user-defined main.cf parameter names
8 /* SYNOPSIS
9 /*        #include <postconf.h>
10 /*
11 /*        void      pcf_register_user_parameters(int mode)
12 /* DESCRIPTION
13 /*        Postfix has multiple parameter name spaces: the global
14 /*        main.cf parameter name space, and the local parameter name
15 /*        space of each master.cf entry. Parameters in local name
16 /*        spaces take precedence over global parameters.
17 /*
18 /*        There are three categories of known parameter names: built-in,
19 /*        service-defined (see postconf_service.c), and valid
20 /*        user-defined.
21 /*
22 /*        There are two categories of valid user-defined parameter
23 /*        names:
24 /*
25 /*        - Parameters whose user-defined-name appears in the value
26 /*        of smtpd_restriction_classes in main.cf or master.cf.
27 /*
28 /*        - Parameters whose $user-defined-name appear in the value
29 /*        of "name=value" entries in main.cf or master.cf.
30 /*
31 /*        - In both cases the parameters must have a
32 /*        "user-defined-name=value" entry in main.cf or master.cf.
33 /*
34 /*        Other user-defined parameter names are flagged as "unused".
35 /*
36 /*        pcf_register_user_parameters() scans the global and per-service
37 /*        name spaces for user-defined parameters and flags parameters
38 /*        as "valid" in the global name space (pcf_param_table) or
39 /*        in the per-service name space (valid_params).
40 /*
41 /*        This function also invokes pcf_register_dbms_parameters() to
42 /*        to instantiate legacy per-dbms parameters, and to examine
43 /*        per-dbms configuration files. This is limited to the content
44 /*        of global and local, built-in and per-service, parameters.
45 /*
46 /*        Arguments:
47 /* .IP mode
48 /*        Passed on to pcf_register_dbms_parameters().
49 /* DIAGNOSTICS
50 /*        Problems are reported to the standard error stream.
51 /* LICENSE
52 /* .ad
53 /* .fi
54 /*        The Secure Mailer license must be distributed with this software.
55 /* AUTHOR(S)
56 /*        Wietse Venema
57 /*        IBM T.J. Watson Research
58 /*        P.O. Box 704
59 /*        Yorktown Heights, NY 10598, USA
60 /*
61 /*        Wietse Venema
62 /*        Google, Inc.
63 /*        111 8th Avenue
64 /*        New York, NY 10011, USA
65 /*--*/
66 
67 /* System library. */
68 
69 #include <sys_defs.h>
70 #include <string.h>
71 
72 /* Utility library. */
73 
74 #include <msg.h>
75 #include <mymalloc.h>
76 #include <htable.h>
77 #include <mac_expand.h>
78 #include <stringops.h>
79 
80 /* Global library. */
81 
82 #include <mail_conf.h>
83 #include <mail_params.h>
84 
85 /* Application-specific. */
86 
87 #include <postconf.h>
88 
89  /*
90   * Hash with all user-defined names in the global smtpd_restriction_classes
91   * value. This is used when validating "-o user-defined-name=value" entries
92   * in master.cf.
93   */
94 static HTABLE *pcf_rest_class_table;
95 
96  /*
97   * SLMs.
98   */
99 #define STR(x)      vstring_str(x)
100 
101  /*
102   * Macros to make code with obscure constants more readable.
103   */
104 #define NO_SCAN_RESULT        ((VSTRING *) 0)
105 #define NO_SCAN_FILTER        ((char *) 0)
106 
107 /* SCAN_USER_PARAMETER_VALUE - examine macro names in parameter value */
108 
109 #define SCAN_USER_PARAMETER_VALUE(value, class, scope) do { \
110     PCF_PARAM_CTX _ctx; \
111     _ctx.local_scope = (scope); \
112     _ctx.param_class = (class); \
113     (void) mac_expand(NO_SCAN_RESULT, (value), MAC_EXP_FLAG_SCAN, \
114               NO_SCAN_FILTER, pcf_flag_user_parameter_wrapper, (void *) &_ctx); \
115 } while (0)
116 
117 /* pcf_convert_user_parameter - get user-defined parameter string value */
118 
pcf_convert_user_parameter(void * unused_ptr)119 static const char *pcf_convert_user_parameter(void *unused_ptr)
120 {
121     return ("");                        /* can't happen */
122 }
123 
124 /* pcf_flag_user_parameter - flag user-defined name "valid" if it has name=value */
125 
pcf_flag_user_parameter(const char * mac_name,int param_class,PCF_MASTER_ENT * local_scope)126 static const char *pcf_flag_user_parameter(const char *mac_name,
127                                                              int param_class,
128                                                           PCF_MASTER_ENT *local_scope)
129 {
130     const char *source = local_scope ? MASTER_CONF_FILE : MAIN_CONF_FILE;
131     int     user_supplied = 0;
132 
133     /*
134      * If the name=value exists in the local (or global) name space, update
135      * the local (or global) "valid" parameter name table.
136      *
137      * Do not "validate" user-defined parameters whose name appears only as
138      * macro expansion; this is how Postfix historically implements backwards
139      * compatibility after a feature name change.
140      */
141     if (local_scope && dict_get(local_scope->all_params, mac_name)) {
142           user_supplied = 1;
143           /* $name in master.cf references name=value in master.cf. */
144           if (PCF_PARAM_TABLE_LOCATE(local_scope->valid_names, mac_name) == 0) {
145               PCF_PARAM_TABLE_ENTER(local_scope->valid_names, mac_name,
146                                           param_class, PCF_PARAM_NO_DATA,
147                                           pcf_convert_user_parameter);
148               if (msg_verbose)
149                     msg_info("$%s in %s:%s validates %s=value in %s:%s",
150                                mac_name, MASTER_CONF_FILE,
151                                local_scope->name_space,
152                                mac_name, MASTER_CONF_FILE,
153                                local_scope->name_space);
154           }
155     } else if (mail_conf_lookup(mac_name) != 0) {
156           user_supplied = 1;
157           /* $name in main/master.cf references name=value in main.cf. */
158           if (PCF_PARAM_TABLE_LOCATE(pcf_param_table, mac_name) == 0) {
159               PCF_PARAM_TABLE_ENTER(pcf_param_table, mac_name, param_class,
160                                    PCF_PARAM_NO_DATA, pcf_convert_user_parameter);
161               if (msg_verbose) {
162                     if (local_scope)
163                         msg_info("$%s in %s:%s validates %s=value in %s",
164                                    mac_name, MASTER_CONF_FILE,
165                                    local_scope->name_space,
166                                    mac_name, MAIN_CONF_FILE);
167                     else
168                         msg_info("$%s in %s validates %s=value in %s",
169                                    mac_name, MAIN_CONF_FILE,
170                                    mac_name, MAIN_CONF_FILE);
171               }
172           }
173     }
174     if (local_scope == 0) {
175           for (local_scope = pcf_master_table; local_scope->argv; local_scope++) {
176               if (local_scope->all_params != 0
177                     && dict_get(local_scope->all_params, mac_name) != 0) {
178                     user_supplied = 1;
179                     /* $name in main.cf references name=value in master.cf. */
180                     if (PCF_PARAM_TABLE_LOCATE(local_scope->valid_names, mac_name) == 0) {
181                         PCF_PARAM_TABLE_ENTER(local_scope->valid_names, mac_name,
182                                                     param_class, PCF_PARAM_NO_DATA,
183                                                     pcf_convert_user_parameter);
184                         if (msg_verbose)
185                               msg_info("$%s in %s validates %s=value in %s:%s",
186                                          mac_name, MAIN_CONF_FILE,
187                                          mac_name, MASTER_CONF_FILE,
188                                          local_scope->name_space);
189                     }
190               }
191           }
192     }
193 
194     /*
195      * Warn about a $name that has no user-supplied explicit value or
196      * Postfix-supplied default value. We don't enforce this for legacy DBMS
197      * parameters because they exist only for backwards compatibility, so we
198      * don't bother to figure out which parameters come without defaults.
199      */
200     if (user_supplied == 0 && (param_class & PCF_PARAM_FLAG_DBMS) == 0
201           && PCF_PARAM_TABLE_LOCATE(pcf_param_table, mac_name) == 0)
202           msg_warn("%s/%s: undefined parameter: %s",
203                      var_config_dir, source, mac_name);
204     return (0);
205 }
206 
207 /* pcf_flag_user_parameter_wrapper - mac_expand call-back helper */
208 
pcf_flag_user_parameter_wrapper(const char * mac_name,int unused_mode,void * context)209 static const char *pcf_flag_user_parameter_wrapper(const char *mac_name,
210                                                                        int unused_mode,
211                                                                        void *context)
212 {
213     PCF_PARAM_CTX *ctx = (PCF_PARAM_CTX *) context;
214 
215     return (pcf_flag_user_parameter(mac_name, ctx->param_class, ctx->local_scope));
216 }
217 
218 /* pcf_lookup_eval - generalized mail_conf_lookup_eval */
219 
pcf_lookup_eval(const char * dict_name,const char * name)220 static const char *pcf_lookup_eval(const char *dict_name, const char *name)
221 {
222     const char *value;
223 
224 #define RECURSIVE       1
225 
226     if ((value = dict_lookup(dict_name, name)) != 0)
227           value = dict_eval(dict_name, value, RECURSIVE);
228     return (value);
229 }
230 
231 /* pcf_scan_user_parameter_namespace - scan parameters in name space */
232 
pcf_scan_user_parameter_namespace(int mode,const char * dict_name,PCF_MASTER_ENT * local_scope)233 static void pcf_scan_user_parameter_namespace(int mode, const char *dict_name,
234                                                           PCF_MASTER_ENT *local_scope)
235 {
236     const char *myname = "pcf_scan_user_parameter_namespace";
237     const char *class_list;
238     char   *saved_class_list;
239     char   *cp;
240     DICT   *dict;
241     char   *param_name;
242     int     how;
243     const char *cparam_name;
244     const char *cparam_value;
245     PCF_PARAM_NODE *node;
246     const char *source = local_scope ? MASTER_CONF_FILE : MAIN_CONF_FILE;
247 
248     /*
249      * Flag parameter names in smtpd_restriction_classes as "valid", but only
250      * if they have a "name=value" entry. If we are in not in a local name
251      * space, update the global restriction class name table, so that we can
252      * query the global table from within a local master.cf name space.
253      */
254     if ((class_list = pcf_lookup_eval(dict_name, VAR_REST_CLASSES)) != 0) {
255           cp = saved_class_list = mystrdup(class_list);
256           while ((param_name = mystrtok(&cp, CHARS_COMMA_SP)) != 0) {
257               if (local_scope == 0
258                     && htable_locate(pcf_rest_class_table, param_name) == 0)
259                     htable_enter(pcf_rest_class_table, param_name, "");
260               pcf_flag_user_parameter(param_name, PCF_PARAM_FLAG_USER, local_scope);
261           }
262           myfree(saved_class_list);
263     }
264 
265     /*
266      * For all "name=value" instances: a) if the name space is local and the
267      * name appears in the global restriction class table, flag the name as
268      * "valid" in the local name space; b) scan the value for macro
269      * expansions of unknown parameter names, and flag those parameter names
270      * as "valid" if they have a "name=value" entry.
271      *
272      * We delete name=value entries for read-only parameters, to maintain
273      * compatibility with Postfix programs that ignore such settings.
274      */
275     if ((dict = dict_handle(dict_name)) == 0)
276           msg_panic("%s: parameter dictionary %s not found",
277                       myname, dict_name);
278     if (dict->sequence == 0)
279           msg_panic("%s: parameter dictionary %s has no iterator",
280                       myname, dict_name);
281     for (how = DICT_SEQ_FUN_FIRST;
282            dict->sequence(dict, how, &cparam_name, &cparam_value) == 0;
283            how = DICT_SEQ_FUN_NEXT) {
284           if (local_scope != 0
285           && PCF_PARAM_TABLE_LOCATE(local_scope->valid_names, cparam_name) == 0
286               && htable_locate(pcf_rest_class_table, cparam_name) != 0)
287               PCF_PARAM_TABLE_ENTER(local_scope->valid_names, cparam_name,
288                                           PCF_PARAM_FLAG_USER, PCF_PARAM_NO_DATA,
289                                           pcf_convert_user_parameter);
290           if ((node = PCF_PARAM_TABLE_FIND(pcf_param_table, cparam_name)) != 0) {
291               if (PCF_READONLY_PARAMETER(node)) {
292                     msg_warn("%s/%s: read-only parameter assignment: %s=%s",
293                                var_config_dir, source, cparam_name, cparam_value);
294                     /* Can't use dict_del() with Postfix<2.10 htable_sequence(). */
295                     if (dict_del(dict, cparam_name) != 0)
296                         msg_panic("%s: can't delete %s/%s parameter entry for %s",
297                                     myname, var_config_dir, source, cparam_name);
298                     continue;
299               }
300               /* Re-label legacy parameter as user-defined, so it's printed. */
301               if (PCF_LEGACY_PARAMETER(node))
302                     PCF_PARAM_CLASS_OVERRIDE(node, PCF_PARAM_FLAG_USER);
303               /* Skip "do not expand" parameters. */
304               if (PCF_RAW_PARAMETER(node))
305                     continue;
306           }
307           SCAN_USER_PARAMETER_VALUE(cparam_value, PCF_PARAM_FLAG_USER, local_scope);
308 #ifdef LEGACY_DBMS_SUPPORT
309 
310           /*
311            * Scan global or local parameters that are built-in or per-service
312            * (when node == 0, the parameter doesn't exist in the global
313            * namespace and therefore it can't be built-in or per-service).
314            */
315           if (node != 0
316               && (PCF_BUILTIN_PARAMETER(node) || PCF_SERVICE_PARAMETER(node)))
317               pcf_register_dbms_parameters(mode, cparam_value, pcf_flag_user_parameter,
318                                                    local_scope);
319 #endif
320     }
321 }
322 
323 /* pcf_scan_default_parameter_values - scan parameters at implicit defaults */
324 
pcf_scan_default_parameter_values(HTABLE * valid_params,const char * dict_name,PCF_MASTER_ENT * local_scope)325 static void pcf_scan_default_parameter_values(HTABLE *valid_params,
326                                                                 const char *dict_name,
327                                                           PCF_MASTER_ENT *local_scope)
328 {
329     const char *myname = "pcf_scan_default_parameter_values";
330     PCF_PARAM_INFO **list;
331     PCF_PARAM_INFO **ht;
332     const char *param_value;
333 
334     list = PCF_PARAM_TABLE_LIST(valid_params);
335     for (ht = list; *ht; ht++) {
336           /* Skip "do not expand" parameters. */
337           if (PCF_RAW_PARAMETER(PCF_PARAM_INFO_NODE(*ht)))
338               continue;
339           /* Skip parameters with a non-default value. */
340           if (dict_lookup(dict_name, PCF_PARAM_INFO_NAME(*ht)))
341               continue;
342           if ((param_value = pcf_convert_param_node(PCF_SHOW_DEFS, PCF_PARAM_INFO_NAME(*ht),
343                                                       PCF_PARAM_INFO_NODE(*ht))) == 0)
344               msg_panic("%s: parameter %s has no default value",
345                           myname, PCF_PARAM_INFO_NAME(*ht));
346           SCAN_USER_PARAMETER_VALUE(param_value, PCF_PARAM_FLAG_USER, local_scope);
347           /* No need to scan default values for legacy DBMS configuration. */
348     }
349     myfree((void *) list);
350 }
351 
352 /* pcf_register_user_parameters - add parameters with user-defined names */
353 
pcf_register_user_parameters(int mode)354 void    pcf_register_user_parameters(int mode)
355 {
356     const char *myname = "pcf_register_user_parameters";
357     PCF_MASTER_ENT *masterp;
358     ARGV   *argv;
359     char   *arg;
360     char   *aval;
361     int     field;
362     char   *saved_arg;
363     char   *param_name;
364     char   *param_value;
365     DICT   *dict;
366 
367     /*
368      * Sanity checks.
369      */
370     if (pcf_param_table == 0)
371           msg_panic("%s: global parameter table is not initialized", myname);
372     if (pcf_master_table == 0)
373           msg_panic("%s: master table is not initialized", myname);
374     if (pcf_rest_class_table != 0)
375           msg_panic("%s: restriction class table is already initialized", myname);
376 
377     /*
378      * Initialize the table with global restriction class names.
379      */
380     pcf_rest_class_table = htable_create(1);
381 
382     /*
383      * Initialize the per-service parameter name spaces.
384      */
385     for (masterp = pcf_master_table; (argv = masterp->argv) != 0; masterp++) {
386           for (field = PCF_MASTER_MIN_FIELDS; argv->argv[field] != 0; field++) {
387               arg = argv->argv[field];
388               if (arg[0] != '-' || strcmp(arg, "--") == 0)
389                     break;
390               if (strchr(pcf_daemon_options_expecting_value, arg[1]) == 0
391                     || (aval = argv->argv[field + 1]) == 0)
392                     continue;
393               if (strcmp(arg, "-o") == 0) {
394                     saved_arg = mystrdup(aval);
395                     if (split_nameval(saved_arg, &param_name, &param_value) == 0)
396                         dict_update(masterp->name_space, param_name, param_value);
397                     myfree(saved_arg);
398               }
399               field += 1;
400           }
401           if ((dict = dict_handle(masterp->name_space)) != 0) {
402               masterp->all_params = dict;
403               masterp->valid_names = htable_create(1);
404           }
405     }
406 
407     /*
408      * Scan the "-o parameter=value" instances in each master.cf name space.
409      */
410     for (masterp = pcf_master_table; masterp->argv != 0; masterp++)
411           if (masterp->all_params != 0)
412               pcf_scan_user_parameter_namespace(mode, masterp->name_space, masterp);
413 
414     /*
415      * Scan parameter values that are left at their defaults in the global
416      * name space. Some defaults contain the $name of an obsolete parameter
417      * for backwards compatibility purposes. We might warn that an explicit
418      * name=value is obsolete, but we must not warn that the parameter is
419      * unused.
420      */
421     pcf_scan_default_parameter_values(pcf_param_table, CONFIG_DICT,
422                                               (PCF_MASTER_ENT *) 0);
423 
424     /*
425      * Scan the explicit name=value entries in the global name space.
426      */
427     pcf_scan_user_parameter_namespace(mode, CONFIG_DICT, (PCF_MASTER_ENT *) 0);
428 }
429