/* * PAM option parsing test suite. * * The canonical version of this file is maintained in the rra-c-util package, * which can be found at . * * Written by Russ Allbery * Copyright 2020 Russ Allbery * Copyright 2010-2014 * The Board of Trustees of the Leland Stanford Junior University * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * * SPDX-License-Identifier: MIT */ #include #include #include #include #include #include #include #include #include #include /* The configuration struct we will use for testing. */ struct pam_config { struct vector *cells; bool debug; #ifdef HAVE_KRB5 krb5_deltat expires; #else long expires; #endif bool ignore_root; long minimum_uid; char *program; }; #define K(name) (#name), offsetof(struct pam_config, name) /* The rules specifying the configuration options. */ static struct option options[] = { /* clang-format off */ { K(cells), true, LIST (NULL) }, { K(debug), true, BOOL (false) }, { K(expires), true, TIME (10) }, { K(ignore_root), false, BOOL (true) }, { K(minimum_uid), true, NUMBER (0) }, { K(program), true, STRING (NULL) }, /* clang-format on */ }; static const size_t optlen = sizeof(options) / sizeof(options[0]); /* * A macro used to parse the various ways of spelling booleans. This reuses * the argv_bool variable, setting it to the first value provided and then * calling putil_args_parse() on it. It then checks whether the provided * config option is set to the expected value. */ #define TEST_BOOL(a, c, v) \ do { \ argv_bool[0] = (a); \ status = putil_args_parse(args, 1, argv_bool, options, optlen); \ ok(status, "Parse of %s", (a)); \ is_int((v), (c), "...and value is correct"); \ ok(pam_output() == NULL, "...and no output"); \ } while (0) /* * A macro used to test error reporting from putil_args_parse(). This reuses * the argv_err variable, setting it to the first value provided and then * calling putil_args_parse() on it. It then recovers the error message and * expects it to match the severity and error message given. */ #define TEST_ERROR(a, p, e) \ do { \ argv_err[0] = (a); \ status = putil_args_parse(args, 1, argv_err, options, optlen); \ ok(status, "Parse of %s", (a)); \ seen = pam_output(); \ if (seen == NULL) \ ok_block(2, false, "...no error output"); \ else { \ is_int((p), seen->lines[0].priority, "...priority for %s", (a)); \ is_string((e), seen->lines[0].line, "...error for %s", (a)); \ } \ pam_output_free(seen); \ } while (0) /* * Allocate and initialize a new struct config. */ static struct pam_config * config_new(void) { return bcalloc(1, sizeof(struct pam_config)); } /* * Free a struct config and all of its members. */ static void config_free(struct pam_config *config) { if (config == NULL) return; vector_free(config->cells); free(config->program); free(config); } int main(void) { pam_handle_t *pamh; struct pam_args *args; struct pam_conv conv = {NULL, NULL}; bool status; struct vector *cells; char *program; struct output *seen; const char *argv_bool[2] = {NULL, NULL}; const char *argv_err[2] = {NULL, NULL}; const char *argv_empty[] = {NULL}; #ifdef HAVE_KRB5 const char *argv_all[] = {"cells=stanford.edu,ir.stanford.edu", "debug", "expires=1d", "ignore_root", "minimum_uid=1000", "program=/bin/true"}; char *krb5conf; #else const char *argv_all[] = {"cells=stanford.edu,ir.stanford.edu", "debug", "expires=86400", "ignore_root", "minimum_uid=1000", "program=/bin/true"}; #endif if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS) sysbail("cannot create pam_handle_t"); args = putil_args_new(pamh, 0); if (args == NULL) bail("cannot create PAM argument struct"); plan(161); /* First, check just the defaults. */ args->config = config_new(); status = putil_args_defaults(args, options, optlen); ok(status, "Setting the defaults"); ok(args->config->cells == NULL, "...cells default"); is_int(false, args->config->debug, "...debug default"); is_int(10, args->config->expires, "...expires default"); is_int(true, args->config->ignore_root, "...ignore_root default"); is_int(0, args->config->minimum_uid, "...minimum_uid default"); ok(args->config->program == NULL, "...program default"); /* Now parse an empty set of PAM arguments. Nothing should change. */ status = putil_args_parse(args, 0, argv_empty, options, optlen); ok(status, "Parse of empty argv"); ok(args->config->cells == NULL, "...cells still default"); is_int(false, args->config->debug, "...debug still default"); is_int(10, args->config->expires, "...expires default"); is_int(true, args->config->ignore_root, "...ignore_root still default"); is_int(0, args->config->minimum_uid, "...minimum_uid still default"); ok(args->config->program == NULL, "...program still default"); /* Now, check setting everything. */ status = putil_args_parse(args, 6, argv_all, options, optlen); ok(status, "Parse of full argv"); if (args->config->cells == NULL) ok_block(4, false, "...cells is set"); else { ok(args->config->cells != NULL, "...cells is set"); is_int(2, args->config->cells->count, "...with two cells"); is_string("stanford.edu", args->config->cells->strings[0], "...first is stanford.edu"); is_string("ir.stanford.edu", args->config->cells->strings[1], "...second is ir.stanford.edu"); } is_int(true, args->config->debug, "...debug is set"); is_int(86400, args->config->expires, "...expires is set"); is_int(true, args->config->ignore_root, "...ignore_root is set"); is_int(1000, args->config->minimum_uid, "...minimum_uid is set"); is_string("/bin/true", args->config->program, "...program is set"); config_free(args->config); args->config = NULL; /* Test deep copying of defaults. */ cells = vector_new(); if (cells == NULL) sysbail("cannot allocate memory"); vector_add(cells, "foo.com"); vector_add(cells, "bar.com"); options[0].defaults.list = cells; program = strdup("/bin/false"); if (program == NULL) sysbail("cannot allocate memory"); options[5].defaults.string = program; args->config = config_new(); status = putil_args_defaults(args, options, optlen); ok(status, "Setting defaults with new defaults"); if (args->config->cells == NULL) ok_block(4, false, "...cells is set"); else { ok(args->config->cells != NULL, "...cells is set"); is_int(2, args->config->cells->count, "...with two cells"); is_string("foo.com", args->config->cells->strings[0], "...first is foo.com"); is_string("bar.com", args->config->cells->strings[1], "...second is bar.com"); } is_string("/bin/false", args->config->program, "...program is /bin/false"); status = putil_args_parse(args, 6, argv_all, options, optlen); ok(status, "Parse of full argv after defaults"); if (args->config->cells == NULL) ok_block(4, false, "...cells is set"); else { ok(args->config->cells != NULL, "...cells is set"); is_int(2, args->config->cells->count, "...with two cells"); is_string("stanford.edu", args->config->cells->strings[0], "...first is stanford.edu"); is_string("ir.stanford.edu", args->config->cells->strings[1], "...second is ir.stanford.edu"); } is_int(true, args->config->debug, "...debug is set"); is_int(86400, args->config->expires, "...expires is set"); is_int(true, args->config->ignore_root, "...ignore_root is set"); is_int(1000, args->config->minimum_uid, "...minimum_uid is set"); is_string("/bin/true", args->config->program, "...program is set"); is_string("foo.com", cells->strings[0], "...first cell after parse"); is_string("bar.com", cells->strings[1], "...second cell after parse"); is_string("/bin/false", program, "...string after parse"); config_free(args->config); args->config = NULL; is_string("foo.com", cells->strings[0], "...first cell after free"); is_string("bar.com", cells->strings[1], "...second cell after free"); is_string("/bin/false", program, "...string after free"); options[0].defaults.list = NULL; options[5].defaults.string = NULL; vector_free(cells); free(program); /* Test specifying the default for a vector parameter as a string. */ options[0].type = TYPE_STRLIST; options[0].defaults.string = "foo.com,bar.com"; args->config = config_new(); status = putil_args_defaults(args, options, optlen); ok(status, "Setting defaults with string default for vector"); if (args->config->cells == NULL) ok_block(4, false, "...cells is set"); else { ok(args->config->cells != NULL, "...cells is set"); is_int(2, args->config->cells->count, "...with two cells"); is_string("foo.com", args->config->cells->strings[0], "...first is foo.com"); is_string("bar.com", args->config->cells->strings[1], "...second is bar.com"); } config_free(args->config); args->config = NULL; options[0].type = TYPE_LIST; options[0].defaults.string = NULL; /* Should be no errors so far. */ ok(pam_output() == NULL, "No errors so far"); /* Test various ways of spelling booleans. */ args->config = config_new(); TEST_BOOL("debug", args->config->debug, true); TEST_BOOL("debug=false", args->config->debug, false); TEST_BOOL("debug=true", args->config->debug, true); TEST_BOOL("debug=no", args->config->debug, false); TEST_BOOL("debug=yes", args->config->debug, true); TEST_BOOL("debug=off", args->config->debug, false); TEST_BOOL("debug=on", args->config->debug, true); TEST_BOOL("debug=0", args->config->debug, false); TEST_BOOL("debug=1", args->config->debug, true); TEST_BOOL("debug=False", args->config->debug, false); TEST_BOOL("debug=trUe", args->config->debug, true); TEST_BOOL("debug=No", args->config->debug, false); TEST_BOOL("debug=Yes", args->config->debug, true); TEST_BOOL("debug=OFF", args->config->debug, false); TEST_BOOL("debug=ON", args->config->debug, true); config_free(args->config); args->config = NULL; /* Test for various parsing errors. */ args->config = config_new(); TEST_ERROR("debug=", LOG_ERR, "invalid boolean in setting: debug="); TEST_ERROR("debug=truth", LOG_ERR, "invalid boolean in setting: debug=truth"); TEST_ERROR("minimum_uid", LOG_ERR, "value missing for option minimum_uid"); TEST_ERROR("minimum_uid=", LOG_ERR, "value missing for option minimum_uid="); TEST_ERROR("minimum_uid=foo", LOG_ERR, "invalid number in setting: minimum_uid=foo"); TEST_ERROR("minimum_uid=1000foo", LOG_ERR, "invalid number in setting: minimum_uid=1000foo"); TEST_ERROR("program", LOG_ERR, "value missing for option program"); TEST_ERROR("cells", LOG_ERR, "value missing for option cells"); config_free(args->config); args->config = NULL; #ifdef HAVE_KRB5 /* Test for Kerberos krb5.conf option parsing. */ krb5conf = test_file_path("data/krb5-pam.conf"); if (krb5conf == NULL) bail("cannot find data/krb5-pam.conf"); if (setenv("KRB5_CONFIG", krb5conf, 1) < 0) sysbail("cannot set KRB5_CONFIG"); krb5_free_context(args->ctx); status = krb5_init_context(&args->ctx); if (status != 0) bail("cannot parse test krb5.conf file"); args->config = config_new(); status = putil_args_defaults(args, options, optlen); ok(status, "Setting the defaults"); status = putil_args_krb5(args, "testing", options, optlen); ok(status, "Options from krb5.conf"); ok(args->config->cells == NULL, "...cells default"); is_int(true, args->config->debug, "...debug set from krb5.conf"); is_int(1800, args->config->expires, "...expires set from krb5.conf"); is_int(true, args->config->ignore_root, "...ignore_root default"); is_int(1000, args->config->minimum_uid, "...minimum_uid set from krb5.conf"); ok(args->config->program == NULL, "...program default"); status = putil_args_krb5(args, "other-test", options, optlen); ok(status, "Options from krb5.conf (other-test)"); is_int(-1000, args->config->minimum_uid, "...minimum_uid set from krb5.conf other-test"); /* Test with a realm set, which should expose more settings. */ krb5_free_context(args->ctx); status = krb5_init_context(&args->ctx); if (status != 0) bail("cannot parse test krb5.conf file"); args->realm = strdup("FOO.COM"); if (args->realm == NULL) sysbail("cannot allocate memory"); status = putil_args_krb5(args, "testing", options, optlen); ok(status, "Options from krb5.conf with FOO.COM"); is_int(2, args->config->cells->count, "...cells count from krb5.conf"); is_string("foo.com", args->config->cells->strings[0], "...first cell from krb5.conf"); is_string("bar.com", args->config->cells->strings[1], "...second cell from krb5.conf"); is_int(true, args->config->debug, "...debug set from krb5.conf"); is_int(1800, args->config->expires, "...expires set from krb5.conf"); is_int(true, args->config->ignore_root, "...ignore_root default"); is_int(1000, args->config->minimum_uid, "...minimum_uid set from krb5.conf"); is_string("/bin/false", args->config->program, "...program from krb5.conf"); /* Test with a different realm. */ free(args->realm); args->realm = strdup("BAR.COM"); if (args->realm == NULL) sysbail("cannot allocate memory"); status = putil_args_krb5(args, "testing", options, optlen); ok(status, "Options from krb5.conf with BAR.COM"); is_int(2, args->config->cells->count, "...cells count from krb5.conf"); is_string("bar.com", args->config->cells->strings[0], "...first cell from krb5.conf"); is_string("foo.com", args->config->cells->strings[1], "...second cell from krb5.conf"); is_int(true, args->config->debug, "...debug set from krb5.conf"); is_int(1800, args->config->expires, "...expires set from krb5.conf"); is_int(true, args->config->ignore_root, "...ignore_root default"); is_int(1000, args->config->minimum_uid, "...minimum_uid set from krb5.conf"); is_string("echo /bin/true", args->config->program, "...program from krb5.conf"); config_free(args->config); args->config = config_new(); status = putil_args_krb5(args, "other-test", options, optlen); ok(status, "Options from krb5.conf (other-test with realm)"); ok(args->config->cells == NULL, "...cells is NULL"); is_string("echo /bin/true", args->config->program, "...program from krb5.conf"); config_free(args->config); args->config = NULL; /* Test for time parsing errors. */ args->config = config_new(); TEST_ERROR("expires=ft87", LOG_ERR, "bad time value in setting: expires=ft87"); config_free(args->config); /* Test error reporting from the krb5.conf parser. */ args->config = config_new(); status = putil_args_krb5(args, "bad-number", options, optlen); ok(status, "Options from krb5.conf (bad-number)"); seen = pam_output(); is_string("invalid number in krb5.conf setting for minimum_uid: 1000foo", seen->lines[0].line, "...and correct error reported"); is_int(LOG_ERR, seen->lines[0].priority, "...with correct priority"); pam_output_free(seen); config_free(args->config); args->config = NULL; /* Test error reporting on times from the krb5.conf parser. */ args->config = config_new(); status = putil_args_krb5(args, "bad-time", options, optlen); ok(status, "Options from krb5.conf (bad-time)"); seen = pam_output(); if (seen == NULL) ok_block(2, false, "...no error output"); else { is_string("invalid time in krb5.conf setting for expires: ft87", seen->lines[0].line, "...and correct error reported"); is_int(LOG_ERR, seen->lines[0].priority, "...with correct priority"); } pam_output_free(seen); config_free(args->config); args->config = NULL; test_file_path_free(krb5conf); #else /* !HAVE_KRB5 */ skip_block(37, "Kerberos support not configured"); #endif putil_args_free(args); pam_end(pamh, 0); return 0; }