xref: /freebsd/contrib/pam-krb5/tests/pam-util/options-t.c (revision bf6873c5786e333d679a7838d28812febf479a8a)
1 /*
2  * PAM option parsing test suite.
3  *
4  * The canonical version of this file is maintained in the rra-c-util package,
5  * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
6  *
7  * Written by Russ Allbery <eagle@eyrie.org>
8  * Copyright 2020 Russ Allbery <eagle@eyrie.org>
9  * Copyright 2010-2014
10  *     The Board of Trustees of the Leland Stanford Junior University
11  *
12  * Permission is hereby granted, free of charge, to any person obtaining a
13  * copy of this software and associated documentation files (the "Software"),
14  * to deal in the Software without restriction, including without limitation
15  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
16  * and/or sell copies of the Software, and to permit persons to whom the
17  * Software is furnished to do so, subject to the following conditions:
18  *
19  * The above copyright notice and this permission notice shall be included in
20  * all copies or substantial portions of the Software.
21  *
22  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
25  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
28  * DEALINGS IN THE SOFTWARE.
29  *
30  * SPDX-License-Identifier: MIT
31  */
32 
33 #include <config.h>
34 #include <portable/pam.h>
35 #include <portable/system.h>
36 
37 #include <syslog.h>
38 
39 #include <pam-util/args.h>
40 #include <pam-util/options.h>
41 #include <pam-util/vector.h>
42 #include <tests/fakepam/pam.h>
43 #include <tests/tap/basic.h>
44 #include <tests/tap/string.h>
45 
46 /* The configuration struct we will use for testing. */
47 struct pam_config {
48     struct vector *cells;
49     bool debug;
50 #ifdef HAVE_KRB5
51     krb5_deltat expires;
52 #else
53     long expires;
54 #endif
55     bool ignore_root;
56     long minimum_uid;
57     char *program;
58 };
59 
60 #define K(name) (#name), offsetof(struct pam_config, name)
61 
62 /* The rules specifying the configuration options. */
63 static struct option options[] = {
64     /* clang-format off */
65     { K(cells),       true,  LIST   (NULL)  },
66     { K(debug),       true,  BOOL   (false) },
67     { K(expires),     true,  TIME   (10)    },
68     { K(ignore_root), false, BOOL   (true)  },
69     { K(minimum_uid), true,  NUMBER (0)     },
70     { K(program),     true,  STRING (NULL)  },
71     /* clang-format on */
72 };
73 static const size_t optlen = sizeof(options) / sizeof(options[0]);
74 
75 /*
76  * A macro used to parse the various ways of spelling booleans.  This reuses
77  * the argv_bool variable, setting it to the first value provided and then
78  * calling putil_args_parse() on it.  It then checks whether the provided
79  * config option is set to the expected value.
80  */
81 #define TEST_BOOL(a, c, v)                                              \
82     do {                                                                \
83         argv_bool[0] = (a);                                             \
84         status = putil_args_parse(args, 1, argv_bool, options, optlen); \
85         ok(status, "Parse of %s", (a));                                 \
86         is_int((v), (c), "...and value is correct");                    \
87         ok(pam_output() == NULL, "...and no output");                   \
88     } while (0)
89 
90 /*
91  * A macro used to test error reporting from putil_args_parse().  This reuses
92  * the argv_err variable, setting it to the first value provided and then
93  * calling putil_args_parse() on it.  It then recovers the error message and
94  * expects it to match the severity and error message given.
95  */
96 #define TEST_ERROR(a, p, e)                                                  \
97     do {                                                                     \
98         argv_err[0] = (a);                                                   \
99         status = putil_args_parse(args, 1, argv_err, options, optlen);       \
100         ok(status, "Parse of %s", (a));                                      \
101         seen = pam_output();                                                 \
102         if (seen == NULL)                                                    \
103             ok_block(2, false, "...no error output");                        \
104         else {                                                               \
105             is_int((p), seen->lines[0].priority, "...priority for %s", (a)); \
106             is_string((e), seen->lines[0].line, "...error for %s", (a));     \
107         }                                                                    \
108         pam_output_free(seen);                                               \
109     } while (0)
110 
111 
112 /*
113  * Allocate and initialize a new struct config.
114  */
115 static struct pam_config *
config_new(void)116 config_new(void)
117 {
118     return bcalloc(1, sizeof(struct pam_config));
119 }
120 
121 
122 /*
123  * Free a struct config and all of its members.
124  */
125 static void
config_free(struct pam_config * config)126 config_free(struct pam_config *config)
127 {
128     if (config == NULL)
129         return;
130     vector_free(config->cells);
131     free(config->program);
132     free(config);
133 }
134 
135 
136 int
main(void)137 main(void)
138 {
139     pam_handle_t *pamh;
140     struct pam_args *args;
141     struct pam_conv conv = {NULL, NULL};
142     bool status;
143     struct vector *cells;
144     char *program;
145     struct output *seen;
146     const char *argv_bool[2] = {NULL, NULL};
147     const char *argv_err[2] = {NULL, NULL};
148     const char *argv_empty[] = {NULL};
149 #ifdef HAVE_KRB5
150     const char *argv_all[] = {"cells=stanford.edu,ir.stanford.edu",
151                               "debug",
152                               "expires=1d",
153                               "ignore_root",
154                               "minimum_uid=1000",
155                               "program=/bin/true"};
156     char *krb5conf;
157 #else
158     const char *argv_all[] = {"cells=stanford.edu,ir.stanford.edu",
159                               "debug",
160                               "expires=86400",
161                               "ignore_root",
162                               "minimum_uid=1000",
163                               "program=/bin/true"};
164 #endif
165 
166     if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
167         sysbail("cannot create pam_handle_t");
168     args = putil_args_new(pamh, 0);
169     if (args == NULL)
170         bail("cannot create PAM argument struct");
171 
172     plan(161);
173 
174     /* First, check just the defaults. */
175     args->config = config_new();
176     status = putil_args_defaults(args, options, optlen);
177     ok(status, "Setting the defaults");
178     ok(args->config->cells == NULL, "...cells default");
179     is_int(false, args->config->debug, "...debug default");
180     is_int(10, args->config->expires, "...expires default");
181     is_int(true, args->config->ignore_root, "...ignore_root default");
182     is_int(0, args->config->minimum_uid, "...minimum_uid default");
183     ok(args->config->program == NULL, "...program default");
184 
185     /* Now parse an empty set of PAM arguments.  Nothing should change. */
186     status = putil_args_parse(args, 0, argv_empty, options, optlen);
187     ok(status, "Parse of empty argv");
188     ok(args->config->cells == NULL, "...cells still default");
189     is_int(false, args->config->debug, "...debug still default");
190     is_int(10, args->config->expires, "...expires default");
191     is_int(true, args->config->ignore_root, "...ignore_root still default");
192     is_int(0, args->config->minimum_uid, "...minimum_uid still default");
193     ok(args->config->program == NULL, "...program still default");
194 
195     /* Now, check setting everything. */
196     status = putil_args_parse(args, 6, argv_all, options, optlen);
197     ok(status, "Parse of full argv");
198     if (args->config->cells == NULL)
199         ok_block(4, false, "...cells is set");
200     else {
201         ok(args->config->cells != NULL, "...cells is set");
202         is_int(2, args->config->cells->count, "...with two cells");
203         is_string("stanford.edu", args->config->cells->strings[0],
204                   "...first is stanford.edu");
205         is_string("ir.stanford.edu", args->config->cells->strings[1],
206                   "...second is ir.stanford.edu");
207     }
208     is_int(true, args->config->debug, "...debug is set");
209     is_int(86400, args->config->expires, "...expires is set");
210     is_int(true, args->config->ignore_root, "...ignore_root is set");
211     is_int(1000, args->config->minimum_uid, "...minimum_uid is set");
212     is_string("/bin/true", args->config->program, "...program is set");
213     config_free(args->config);
214     args->config = NULL;
215 
216     /* Test deep copying of defaults. */
217     cells = vector_new();
218     if (cells == NULL)
219         sysbail("cannot allocate memory");
220     vector_add(cells, "foo.com");
221     vector_add(cells, "bar.com");
222     options[0].defaults.list = cells;
223     program = strdup("/bin/false");
224     if (program == NULL)
225         sysbail("cannot allocate memory");
226     options[5].defaults.string = program;
227     args->config = config_new();
228     status = putil_args_defaults(args, options, optlen);
229     ok(status, "Setting defaults with new defaults");
230     if (args->config->cells == NULL)
231         ok_block(4, false, "...cells is set");
232     else {
233         ok(args->config->cells != NULL, "...cells is set");
234         is_int(2, args->config->cells->count, "...with two cells");
235         is_string("foo.com", args->config->cells->strings[0],
236                   "...first is foo.com");
237         is_string("bar.com", args->config->cells->strings[1],
238                   "...second is bar.com");
239     }
240     is_string("/bin/false", args->config->program, "...program is /bin/false");
241     status = putil_args_parse(args, 6, argv_all, options, optlen);
242     ok(status, "Parse of full argv after defaults");
243     if (args->config->cells == NULL)
244         ok_block(4, false, "...cells is set");
245     else {
246         ok(args->config->cells != NULL, "...cells is set");
247         is_int(2, args->config->cells->count, "...with two cells");
248         is_string("stanford.edu", args->config->cells->strings[0],
249                   "...first is stanford.edu");
250         is_string("ir.stanford.edu", args->config->cells->strings[1],
251                   "...second is ir.stanford.edu");
252     }
253     is_int(true, args->config->debug, "...debug is set");
254     is_int(86400, args->config->expires, "...expires is set");
255     is_int(true, args->config->ignore_root, "...ignore_root is set");
256     is_int(1000, args->config->minimum_uid, "...minimum_uid is set");
257     is_string("/bin/true", args->config->program, "...program is set");
258     is_string("foo.com", cells->strings[0], "...first cell after parse");
259     is_string("bar.com", cells->strings[1], "...second cell after parse");
260     is_string("/bin/false", program, "...string after parse");
261     config_free(args->config);
262     args->config = NULL;
263     is_string("foo.com", cells->strings[0], "...first cell after free");
264     is_string("bar.com", cells->strings[1], "...second cell after free");
265     is_string("/bin/false", program, "...string after free");
266     options[0].defaults.list = NULL;
267     options[5].defaults.string = NULL;
268     vector_free(cells);
269     free(program);
270 
271     /* Test specifying the default for a vector parameter as a string. */
272     options[0].type = TYPE_STRLIST;
273     options[0].defaults.string = "foo.com,bar.com";
274     args->config = config_new();
275     status = putil_args_defaults(args, options, optlen);
276     ok(status, "Setting defaults with string default for vector");
277     if (args->config->cells == NULL)
278         ok_block(4, false, "...cells is set");
279     else {
280         ok(args->config->cells != NULL, "...cells is set");
281         is_int(2, args->config->cells->count, "...with two cells");
282         is_string("foo.com", args->config->cells->strings[0],
283                   "...first is foo.com");
284         is_string("bar.com", args->config->cells->strings[1],
285                   "...second is bar.com");
286     }
287     config_free(args->config);
288     args->config = NULL;
289     options[0].type = TYPE_LIST;
290     options[0].defaults.string = NULL;
291 
292     /* Should be no errors so far. */
293     ok(pam_output() == NULL, "No errors so far");
294 
295     /* Test various ways of spelling booleans. */
296     args->config = config_new();
297     TEST_BOOL("debug", args->config->debug, true);
298     TEST_BOOL("debug=false", args->config->debug, false);
299     TEST_BOOL("debug=true", args->config->debug, true);
300     TEST_BOOL("debug=no", args->config->debug, false);
301     TEST_BOOL("debug=yes", args->config->debug, true);
302     TEST_BOOL("debug=off", args->config->debug, false);
303     TEST_BOOL("debug=on", args->config->debug, true);
304     TEST_BOOL("debug=0", args->config->debug, false);
305     TEST_BOOL("debug=1", args->config->debug, true);
306     TEST_BOOL("debug=False", args->config->debug, false);
307     TEST_BOOL("debug=trUe", args->config->debug, true);
308     TEST_BOOL("debug=No", args->config->debug, false);
309     TEST_BOOL("debug=Yes", args->config->debug, true);
310     TEST_BOOL("debug=OFF", args->config->debug, false);
311     TEST_BOOL("debug=ON", args->config->debug, true);
312     config_free(args->config);
313     args->config = NULL;
314 
315     /* Test for various parsing errors. */
316     args->config = config_new();
317     TEST_ERROR("debug=", LOG_ERR, "invalid boolean in setting: debug=");
318     TEST_ERROR("debug=truth", LOG_ERR,
319                "invalid boolean in setting: debug=truth");
320     TEST_ERROR("minimum_uid", LOG_ERR, "value missing for option minimum_uid");
321     TEST_ERROR("minimum_uid=", LOG_ERR,
322                "value missing for option minimum_uid=");
323     TEST_ERROR("minimum_uid=foo", LOG_ERR,
324                "invalid number in setting: minimum_uid=foo");
325     TEST_ERROR("minimum_uid=1000foo", LOG_ERR,
326                "invalid number in setting: minimum_uid=1000foo");
327     TEST_ERROR("program", LOG_ERR, "value missing for option program");
328     TEST_ERROR("cells", LOG_ERR, "value missing for option cells");
329     config_free(args->config);
330     args->config = NULL;
331 
332 #ifdef HAVE_KRB5
333 
334     /* Test for Kerberos krb5.conf option parsing. */
335     krb5conf = test_file_path("data/krb5-pam.conf");
336     if (krb5conf == NULL)
337         bail("cannot find data/krb5-pam.conf");
338     if (setenv("KRB5_CONFIG", krb5conf, 1) < 0)
339         sysbail("cannot set KRB5_CONFIG");
340     krb5_free_context(args->ctx);
341     status = krb5_init_context(&args->ctx);
342     if (status != 0)
343         bail("cannot parse test krb5.conf file");
344     args->config = config_new();
345     status = putil_args_defaults(args, options, optlen);
346     ok(status, "Setting the defaults");
347     status = putil_args_krb5(args, "testing", options, optlen);
348     ok(status, "Options from krb5.conf");
349     ok(args->config->cells == NULL, "...cells default");
350     is_int(true, args->config->debug, "...debug set from krb5.conf");
351     is_int(1800, args->config->expires, "...expires set from krb5.conf");
352     is_int(true, args->config->ignore_root, "...ignore_root default");
353     is_int(1000, args->config->minimum_uid,
354            "...minimum_uid set from krb5.conf");
355     ok(args->config->program == NULL, "...program default");
356     status = putil_args_krb5(args, "other-test", options, optlen);
357     ok(status, "Options from krb5.conf (other-test)");
358     is_int(-1000, args->config->minimum_uid,
359            "...minimum_uid set from krb5.conf other-test");
360 
361     /* Test with a realm set, which should expose more settings. */
362     krb5_free_context(args->ctx);
363     status = krb5_init_context(&args->ctx);
364     if (status != 0)
365         bail("cannot parse test krb5.conf file");
366     args->realm = strdup("FOO.COM");
367     if (args->realm == NULL)
368         sysbail("cannot allocate memory");
369     status = putil_args_krb5(args, "testing", options, optlen);
370     ok(status, "Options from krb5.conf with FOO.COM");
371     is_int(2, args->config->cells->count, "...cells count from krb5.conf");
372     is_string("foo.com", args->config->cells->strings[0],
373               "...first cell from krb5.conf");
374     is_string("bar.com", args->config->cells->strings[1],
375               "...second cell from krb5.conf");
376     is_int(true, args->config->debug, "...debug set from krb5.conf");
377     is_int(1800, args->config->expires, "...expires set from krb5.conf");
378     is_int(true, args->config->ignore_root, "...ignore_root default");
379     is_int(1000, args->config->minimum_uid,
380            "...minimum_uid set from krb5.conf");
381     is_string("/bin/false", args->config->program,
382               "...program from krb5.conf");
383 
384     /* Test with a different realm. */
385     free(args->realm);
386     args->realm = strdup("BAR.COM");
387     if (args->realm == NULL)
388         sysbail("cannot allocate memory");
389     status = putil_args_krb5(args, "testing", options, optlen);
390     ok(status, "Options from krb5.conf with BAR.COM");
391     is_int(2, args->config->cells->count, "...cells count from krb5.conf");
392     is_string("bar.com", args->config->cells->strings[0],
393               "...first cell from krb5.conf");
394     is_string("foo.com", args->config->cells->strings[1],
395               "...second cell from krb5.conf");
396     is_int(true, args->config->debug, "...debug set from krb5.conf");
397     is_int(1800, args->config->expires, "...expires set from krb5.conf");
398     is_int(true, args->config->ignore_root, "...ignore_root default");
399     is_int(1000, args->config->minimum_uid,
400            "...minimum_uid set from krb5.conf");
401     is_string("echo /bin/true", args->config->program,
402               "...program from krb5.conf");
403     config_free(args->config);
404     args->config = config_new();
405     status = putil_args_krb5(args, "other-test", options, optlen);
406     ok(status, "Options from krb5.conf (other-test with realm)");
407     ok(args->config->cells == NULL, "...cells is NULL");
408     is_string("echo /bin/true", args->config->program,
409               "...program from krb5.conf");
410     config_free(args->config);
411     args->config = NULL;
412 
413     /* Test for time parsing errors. */
414     args->config = config_new();
415     TEST_ERROR("expires=ft87", LOG_ERR,
416                "bad time value in setting: expires=ft87");
417     config_free(args->config);
418 
419     /* Test error reporting from the krb5.conf parser. */
420     args->config = config_new();
421     status = putil_args_krb5(args, "bad-number", options, optlen);
422     ok(status, "Options from krb5.conf (bad-number)");
423     seen = pam_output();
424     is_string("invalid number in krb5.conf setting for minimum_uid: 1000foo",
425               seen->lines[0].line, "...and correct error reported");
426     is_int(LOG_ERR, seen->lines[0].priority, "...with correct priority");
427     pam_output_free(seen);
428     config_free(args->config);
429     args->config = NULL;
430 
431     /* Test error reporting on times from the krb5.conf parser. */
432     args->config = config_new();
433     status = putil_args_krb5(args, "bad-time", options, optlen);
434     ok(status, "Options from krb5.conf (bad-time)");
435     seen = pam_output();
436     if (seen == NULL)
437         ok_block(2, false, "...no error output");
438     else {
439         is_string("invalid time in krb5.conf setting for expires: ft87",
440                   seen->lines[0].line, "...and correct error reported");
441         is_int(LOG_ERR, seen->lines[0].priority, "...with correct priority");
442     }
443     pam_output_free(seen);
444     config_free(args->config);
445     args->config = NULL;
446 
447     test_file_path_free(krb5conf);
448 
449 #else /* !HAVE_KRB5 */
450 
451     skip_block(37, "Kerberos support not configured");
452 
453 #endif
454 
455     putil_args_free(args);
456     pam_end(pamh, 0);
457     return 0;
458 }
459