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