diff options
author | Cy Schubert <cy@FreeBSD.org> | 2025-04-17 02:13:41 +0000 |
---|---|---|
committer | Cy Schubert <cy@FreeBSD.org> | 2025-05-27 16:20:06 +0000 |
commit | 24f0b4ca2d565cdbb4fe7839ff28320706bf2386 (patch) | |
tree | bc9ce87edb73f767f5580887d0fc8c643b9d7a49 /tests |
Diffstat (limited to 'tests')
176 files changed, 15173 insertions, 0 deletions
diff --git a/tests/README b/tests/README new file mode 100644 index 000000000000..186d2d5699b1 --- /dev/null +++ b/tests/README @@ -0,0 +1,252 @@ + Writing TAP Tests + +Introduction + + This is a guide for users of the C TAP Harness package or similar + TAP-based test harnesses explaining how to write tests. If your + package uses C TAP Harness as the test suite driver, you may want to + copy this document to an appropriate file name in your test suite as + documentation for contributors. + +About TAP + + TAP is the Test Anything Protocol, a protocol for communication + between test cases and a test harness. This is the protocol used by + Perl for its internal test suite and for nearly all Perl modules, + since it's the format used by the build tools for Perl modules to run + tests and report their results. + + A TAP-based test suite works with a somewhat different set of + assumptions than an xUnit test suite. In TAP, each test case is a + separate program. That program, when run, must produce output in the + following format: + + 1..4 + ok 1 - the first test + ok 2 + # a diagnostic, ignored by the harness + not ok 3 - a failing test + ok 4 # skip a skipped test + + The output should all go to standard output. The first line specifies + the number of tests to be run, and then each test produces output that + looks like either "ok <n>" or "not ok <n>" depending on whether the + test succeeded or failed. Additional information about the test can + be provided after the "ok <n>" or "not ok <n>", but is optional. + Additional diagnostics and information can be provided in lines + beginning with a "#". + + Processing directives are supported after the "ok <n>" or "not ok <n>" + and start with a "#". The main one of interest is "# skip" which says + that the test was skipped rather than successful and optionally gives + the reason. Also supported is "# todo", which normally annotates a + failing test and indicates that test is expected to fail, optionally + providing a reason for why. + + There are three more special cases. First, the initial line stating + the number of tests to run, called the plan, may appear at the end of + the output instead of the beginning. This can be useful if the number + of tests to run is not known in advance. Second, a plan in the form: + + 1..0 # skip entire test case skipped + + can be given instead, which indicates that this entire test case has + been skipped (generally because it depends on facilities or optional + configuration which is not present). Finally, if the test case + encounters a fatal error, it should print the text: + + Bail out! + + on standard output, optionally followed by an error message, and then + exit. This tells the harness that the test aborted unexpectedly. + + The exit status of a successful test case should always be 0. The + harness will report the test as "dubious" if all the tests appeared to + succeed but it exited with a non-zero status. + +Writing TAP Tests + + Environment + + One of the special features of C TAP Harness is the environment that + it sets up for your test cases. If your test program is called under + the runtests driver, the environment variables C_TAP_SOURCE and + C_TAP_BUILD will be set to the top of the test directory in the source + tree and the top of the build tree, respectively. You can use those + environment variables to locate additional test data, programs and + libraries built as part of your software build, and other supporting + information needed by tests. + + The C and shell TAP libraries support a test_file_path() function, + which looks for a file under the build tree and then under the source + tree, using the C_TAP_BUILD and C_TAP_SOURCE environment variables, + and return the full path to the file. This can be used to locate + supporting data files. They also support a test_tmpdir() function + that returns a directory that can be used for temporary files during + tests. + + Perl + + Since TAP is the native test framework for Perl, writing TAP tests in + Perl is very easy and extremely well-supported. If you've never + written tests in Perl before, start by reading the documentation for + Test::Tutorial and Test::Simple, which walks you through the basics, + including the TAP output syntax. Then, the best Perl module to use + for serious testing is Test::More, which provides a lot of additional + functions over Test::Simple including support for skipping tests, + bailing out, and not planning tests in advance. See the documentation + of Test::More for all the details and lots of examples. + + C TAP Harness can run Perl test scripts directly and interpret the + results correctly, and similarly the Perl Test::Harness module and + prove command can run TAP tests written in other languages using, for + example, the TAP library that comes with C TAP Harness. You can, if + you wish, use the library that comes with C TAP Harness but use prove + instead of runtests for running the test suite. + + C + + C TAP Harness provides a basic TAP library that takes away most of the + pain of writing TAP test cases in C. A C test case should start with + a call to plan(), passing in the number of tests to run. Then, each + test should use is_int(), is_string(), is_double(), or is_hex() as + appropriate to compare expected and seen values, or ok() to do a + simpler boolean test. The is_*() functions take expected and seen + values and then a printf-style format string explaining the test + (which may be NULL). ok() takes a boolean and then the printf-style + string. + + Here's a complete example test program that uses the C TAP library: + + #include <stddef.h> + #include <tap/basic.h> + + int + main(void) + { + plan(4); + + ok(1, "the first test"); + is_int(42, 42, NULL); + diag("a diagnostic, ignored by the harness"); + ok(0, "a failing test"); + skip("a skipped test"); + + return 0; + } + + This test program produces the output shown above in the section on + TAP and demonstrates most of the functions. The other functions of + interest are sysdiag() (like diag() but adds strerror() results), + bail() and sysbail() for fatal errors, skip_block() to skip a whole + block of tests, and skip_all() which is called instead of plan() to + skip an entire test case. + + The C TAP library also provides plan_lazy(), which can be called + instead of plan(). If plan_lazy() is called, the library will keep + track of how many test results are reported and will print out the + plan at the end of execution of the program. This should normally be + avoided since the test may appear to be successful even if it exits + prematurely, but it can make writing tests easier in some + circumstances. + + Complete API documentation for the basic C TAP library that comes with + C TAP Harness is available at: + + <https://www.eyrie.org/~eagle/software/c-tap-harness/> + + It's common to need additional test functions and utility functions + for your C tests, particularly if you have to set up and tear down a + test environment for your test programs, and it's useful to have them + all in the libtap library so that you only have to link your test + programs with one library. Rather than editing tap/basic.c and + tap/basic.h to add those additional functions, add additional *.c and + *.h files into the tap directory with the function implementations and + prototypes, and then add those additional objects to the library. + That way, you can update tap/basic.c and tap/basic.h from subsequent + releases of C TAP Harness without having to merge changes with your + own code. + + Libraries of additional useful TAP test functions are available in + rra-c-util at: + + <https://www.eyrie.org/~eagle/software/rra-c-util/> + + Some of the code there is particularly useful when testing programs + that require Kerberos keys. + + If you implement new test functions that compare an expected and seen + value, it's best to name them is_<something> and take the expected + value, the seen value, and then a printf-style format string and + possible arguments to match the calling convention of the functions + provided by C TAP Harness. + + Shell + + C TAP Harness provides a library of shell functions to make it easier + to write TAP tests in shell. That library includes much of the same + functionality as the C TAP library, but takes its parameters in a + somewhat different order to make better use of shell features. + + The libtap.sh file should be installed in a directory named tap in + your test suite area. It can then be loaded by tests written in shell + using the environment set up by runtests with: + + . "$C_TAP_SOURCE"/tap/libtap.sh + + Here is a complete test case written in shell which produces the same + output as the TAP sample above: + + #!/bin/sh + + . "$C_TAP_SOURCE"/tap/libtap.sh + cd "$C_TAP_BUILD" + + plan 4 + ok 'the first test' true + ok '' [ 42 -eq 42 ] + diag a diagnostic, ignored by the harness + ok '' false + skip 'a skipped test' + + The shell framework doesn't provide the is_* functions, so you'll use + the ok function more. It takes a string describing the text and then + treats all of its remaining arguments as a condition, evaluated the + same way as the arguments to the "if" statement. If that condition + evaluates to true, the test passes; otherwise, the test fails. + + The plan, plan_lazy, diag, and bail functions work the same as with + the C library. skip takes a string and skips the next test with that + explanation. skip_block takes a count and a string and skips that + many tests with that explanation. skip_all takes an optional reason + and skips the entire test case. + + Since it's common for shell programs to want to test the output of + commands, there's an additional function ok_program provided by the + shell test library. It takes the test description string, the + expected exit status, the expected program output, and then treats the + rest of its arguments as the program to run. That program is run with + standard error and standard output combined, and then its exit status + and output are tested against the provided values. + + A utility function, strip_colon_error, is provided that runs the + command given as its arguments and strips text following a colon and a + space from the output (unless there is no whitespace on the line + before the colon and the space, normally indicating a prefix of the + program name). This function can be used to wrap commands that are + expected to fail with output that has a system- or locale-specific + error message appended, such as the output of strerror(). + +License + + This file is part of the documentation of C TAP Harness, which can be + found at <https://www.eyrie.org/~eagle/software/c-tap-harness/>. + + Copyright 2010, 2016 Russ Allbery <eagle@eyrie.org> + + Copying and distribution of this file, with or without modification, + are permitted in any medium without royalty provided the copyright + notice and this notice are preserved. This file is offered as-is, + without any warranty. + + SPDX-License-Identifier: FSFAP diff --git a/tests/TESTS b/tests/TESTS new file mode 100644 index 000000000000..f9036b1569c4 --- /dev/null +++ b/tests/TESTS @@ -0,0 +1,46 @@ +# Test list for pam-krb5. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2011-2012 +# The Board of Trustees of the Leland Stanford Junior University +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and +# this notice are preserved. This file is offered as-is, without any +# warranty. +# +# SPDX-License-Identifier: FSFAP + +# Exclude the tests that use the pkinit.so MIT Kerberos module from valgrind +# testing (module/fast-anon and module/pkinit) because they cause valgrind to +# go into an infinite loop. + +docs/pod +docs/pod-spelling +docs/spdx-license +module/alt-auth valgrind +module/bad-authtok valgrind +module/basic valgrind +module/cache valgrind +module/cache-cleanup valgrind +module/expired valgrind +module/fast valgrind +module/fast-anon +module/long valgrind +module/no-cache valgrind +module/pam-user valgrind +module/password valgrind +module/pkinit +module/realm valgrind +module/stacked valgrind +pam-util/args valgrind +pam-util/fakepam valgrind +pam-util/logging valgrind +pam-util/options valgrind +pam-util/vector valgrind +portable/asprintf valgrind +portable/mkstemp valgrind +portable/strndup valgrind +style/obsolete-strings +valgrind/logs diff --git a/tests/config/README b/tests/config/README new file mode 100644 index 000000000000..a034b35b6b0d --- /dev/null +++ b/tests/config/README @@ -0,0 +1,70 @@ +This directory contains configuration required to run the complete +pam-krb5 test suite. If there is no configuration in this directory, many +of the tests will be skipped. To enable the full test suite, create the +following files: + +admin-keytab + + A keytab for a principal (in the same realm as the test principal + configured in password) that has admin access to inspect and modify + that test principal. For an MIT Kerberos KDC, it needs "mci" + permissions in kadm5.acl for that principal. For a Heimdal KDC, it + needs "cpw,list,modify" permissions (obviously, "all" will do). This + file is optional; if not present, the tests requiring admin + modification of a principal will be skipped. + +krb5.conf + + This is optional and not required if the Kerberos realm used for + testing is configured in DNS or your system krb5.conf file and that + file is in either /etc/krb5.conf or /usr/local/etc/krb5.conf. + Otherwise, create a krb5.conf file that contains the realm information + (KDC, kpasswd server, and admin server) for the realm you're using for + testing. You don't need to worry about setting the default realm; + this will be done automatically in the generated file used by the test + suite. + +keytab + + An optional keytab for a principal, which generally should be in the + same realm as the user configured in the password file. This is used + to test FAST support with a ticket cache. + +password + + This file should contain two lines. The first line is the + fully-qualified principal (including the realm) of a Kerberos + principal to use for testing authentication. The second line is the + password for that principal. + + If the realm of the principal is not configured in either DNS or in + your system krb5.conf file (/usr/local/etc/krb5.conf or + /etc/krb5.conf) with the KDC, kpasswd server, and admin server, you + will need to also provide a krb5.conf file in this directory. See + below. + +pkinit-cert + + Certificate and private key (concatenated together) for PKINIT + authentication for the user listed in the pkinit-principal file. + Optional; PKINIT checks will be skipped if this file isn't present. + +pkinit-principal + + Principal to use to test PKINIT authentication. Must be the Kerberos + identity corresponding to the certificate and private key given in + pkinit-cert. Optional; PKINIT checks will be skipped if this file + isn't present. + +----- + +Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org> +Copyright 2011-2012 + The Board of Trustees of the Leland Stanford Junior University + +Copying and distribution of this file, with or without modification, are +permitted in any medium without royalty provided the copyright notice and +this notice are preserved. This file is offered as-is, without any +warranty. + +SPDX-License-Identifier: FSFAP diff --git a/tests/data/cppcheck.supp b/tests/data/cppcheck.supp new file mode 100644 index 000000000000..00734778b256 --- /dev/null +++ b/tests/data/cppcheck.supp @@ -0,0 +1,72 @@ +// Suppressions file for cppcheck. -*- conf -*- +// +// This includes suppressions for all of my projects, including files that +// aren't in rra-c-util, for ease of sharing between projects. The ones that +// don't apply to a particular project should hopefully be harmless. +// +// To determine the correct suppression to add for a new error, run cppcheck +// with the --xml flag and then add a suppression for the error id, file +// location, and line. +// +// Copyright 2018-2021 Russ Allbery <eagle@eyrie.org> +// +// Copying and distribution of this file, with or without modification, are +// permitted in any medium without royalty provided the copyright notice and +// this notice are preserved. This file is offered as-is, without any +// warranty. +// +// SPDX-License-Identifier: FSFAP + +// I like declaring variables at the top of a function rather than cluttering +// every if and loop body with declarations. +variableScope + +// strlen of a constant string is more maintainable code than hard-coding the +// string length. +constArgument:tests/runtests.c:804 + +// False positive due to recursive function. +knownConditionTrueFalse:portable/getopt.c:146 + +// Bug in cppcheck 2.3. cppcheck can't see the assignment because of the +// void * cast. +knownConditionTrueFalse:portable/k_haspag.c:61 + +// False positive since the string comes from a command-line define. +knownConditionTrueFalse:tests/tap/process.c:415 +knownConditionTrueFalse:tests/tap/remctl.c:79 + +// Stored in the returned ai struct, but cppcheck can't see the assignment +// because of the struct sockaddr * cast. +memleak:portable/getaddrinfo.c:236 + +// Bug in cppcheck 1.89 (fixed in 2.3). The address of this variable is +// passed to a Windows function (albeit through a cast). +nullPointer:portable/winsock.c:61 + +// Bug in cppcheck 2.3. +nullPointerRedundantCheck:portable/krb5-profile.c:61 + +// Bug in cppcheck 2.3. +nullPointerRedundantCheck:portable/krb5-renew.c:82 +nullPointerRedundantCheck:portable/krb5-renew.c:83 + +// Setting the variable to NULL explicitly after deallocation. +redundantAssignment:tests/pam-util/options-t.c + +// (remctl) Bug in cppcheck 1.89 (fixed in 2.3). The address of these +// variables are passed to a PHP function. +uninitvar:php/php_remctl.c:119 +uninitvar:php/php_remctl.c:123 +uninitvar:php/php_remctl.c:315 +uninitvar:php/php5_remctl.c:125 +uninitvar:php/php5_remctl.c:129 +uninitvar:php/php5_remctl.c:321 + +// (remctl) Bug in cppcheck 1.82. A pointer to this array is stored in a +// struct that's passed to another function. +redundantAssignment:tests/server/acl-t.c + +// (pam-krb5) cppcheck doesn't recognize the unused attribute on labels. +unusedLabel:module/auth.c:895 +unusedLabelConfiguration:module/auth.c:895 diff --git a/tests/data/generate-krb5-conf b/tests/data/generate-krb5-conf new file mode 100755 index 000000000000..712a933d40ba --- /dev/null +++ b/tests/data/generate-krb5-conf @@ -0,0 +1,86 @@ +#!/bin/sh + +# Generate a krb5.conf file in the current directory for testing purposes. +# Takes one command-line argument: the default realm to use. Strips out the +# entire [appdefaults] section to avoid picking up any local configuration and +# sets the default realm as indicated. +# +# The canonical version of this file is maintained in the rra-c-util package, +# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2016, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2006-2008, 2010-2011 +# 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 + +set -e + +# Load the test library. +. "$C_TAP_SOURCE/tap/libtap.sh" +cd "$C_TAP_BUILD" + +# If there is no default realm specified on the command line, we leave the +# realm information alone. +realm="$1" + +# Locate the krb5.conf file to use as a base. Prefer the one in the test +# configuration area, if it exists. +krb5conf=`test_file_path config/krb5.conf` +if [ -z "$krb5conf" ] ; then + for p in /etc/krb5.conf /usr/local/etc/krb5.conf ; do + if [ -r "$p" ] ; then + krb5conf="$p" + break + fi + done +fi +if [ -z "$krb5conf" ] ; then + echo 'no krb5.conf found, see test instructions' >&2 + exit 1 +fi + +# We found a krb5.conf file. Generate our munged one. +mkdir -p tmp +awk ' + BEGIN { skip = 0 } + /^ *\[appdefaults\]/ { skip = 1 } + !/^ *\[appdefaults\]/ && / *\[/ { skip = 0 } + + { if (skip == 0) print } +' "$krb5conf" > tmp/krb5.conf.tmp +if [ -n "$realm" ] ; then + pattern='^[ ]*default_realm.*=' + if grep "$pattern" tmp/krb5.conf.tmp >/dev/null 2>/dev/null; then + sed -e "s/\\(default_realm.*=\\) .*/\\1 $realm/" \ + tmp/krb5.conf.tmp >tmp/krb5.conf + else + ( + cat tmp/krb5.conf.tmp + echo "[libdefaults]" + echo " default_realm = $realm" + ) >tmp/krb5.conf + fi + rm tmp/krb5.conf.tmp +else + mv tmp/krb5.conf.tmp tmp/krb5.conf +fi diff --git a/tests/data/krb5-pam.conf b/tests/data/krb5-pam.conf new file mode 100644 index 000000000000..57887882c954 --- /dev/null +++ b/tests/data/krb5-pam.conf @@ -0,0 +1,30 @@ +# Test krb5.conf file for PAM option parsing. + +[appdefaults] + FOO.COM = { + program = /bin/false + } + BAR.COM = { + program = echo /bin/true + } + testing = { + minimum_uid = 1000 + ignore_root = false + expires = 30m + FOO.COM = { + cells = foo.com,bar.com + } + BAR.COM = { + cells = bar.com foo.com + } + } + other-test = { + minimum_uid = -1000 + } + bad-number = { + minimum_uid = 1000foo + } + bad-time = { + expires = ft87 + } + debug = true diff --git a/tests/data/krb5.conf b/tests/data/krb5.conf new file mode 100644 index 000000000000..57887882c954 --- /dev/null +++ b/tests/data/krb5.conf @@ -0,0 +1,30 @@ +# Test krb5.conf file for PAM option parsing. + +[appdefaults] + FOO.COM = { + program = /bin/false + } + BAR.COM = { + program = echo /bin/true + } + testing = { + minimum_uid = 1000 + ignore_root = false + expires = 30m + FOO.COM = { + cells = foo.com,bar.com + } + BAR.COM = { + cells = bar.com foo.com + } + } + other-test = { + minimum_uid = -1000 + } + bad-number = { + minimum_uid = 1000foo + } + bad-time = { + expires = ft87 + } + debug = true diff --git a/tests/data/perl.conf b/tests/data/perl.conf new file mode 100644 index 000000000000..699ef3a9123a --- /dev/null +++ b/tests/data/perl.conf @@ -0,0 +1,19 @@ +# Configuration for Perl tests. -*- perl -*- + +# Ignore these top-level directories for perlcritic testing. +@CRITIC_IGNORE = qw(); + +# Add this directory (or a .libs subdirectory) relative to the top of the +# source tree to LD_LIBRARY_PATH when checking the syntax of Perl modules. +# This may be required to pick up libraries that are used by in-tree Perl +# modules. +#$LIBRARY_PATH = 'lib'; + +# Default minimum version requirement for included Perl scripts. +$MINIMUM_VERSION = '5.006'; + +# Minimum version exceptions for specific top-level directories. +%MINIMUM_VERSION = (); + +# File must end with this line. +1; diff --git a/tests/data/scripts/alt-auth/basic b/tests/data/scripts/alt-auth/basic new file mode 100644 index 000000000000..92628e98cd8f --- /dev/null +++ b/tests/data/scripts/alt-auth/basic @@ -0,0 +1,19 @@ +# Test simplest case of alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%1 force_first_pass no_ccache + account = alt_auth_map=%1 no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + +[output] + INFO user %u authenticated as %1 diff --git a/tests/data/scripts/alt-auth/basic-debug b/tests/data/scripts/alt-auth/basic-debug new file mode 100644 index 000000000000..325a8117284c --- /dev/null +++ b/tests/data/scripts/alt-auth/basic-debug @@ -0,0 +1,25 @@ +# Test simplest case of alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%1 force_first_pass no_ccache debug + account = alt_auth_map=%1 no_ccache debug + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) mapping bogus-nonexistent-account to %1 + DEBUG (user %u) alternate authentication successful + INFO user %u authenticated as %1 + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG pam_sm_acct_mgmt: exit (success) diff --git a/tests/data/scripts/alt-auth/fail b/tests/data/scripts/alt-auth/fail new file mode 100644 index 000000000000..ec2145f3098f --- /dev/null +++ b/tests/data/scripts/alt-auth/fail @@ -0,0 +1,19 @@ +# Test failure of alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=bogus force_first_pass no_ccache + account = alt_auth_map=bogus no_ccache + +[run] + authenticate = PAM_AUTHINFO_UNAVAIL + acct_mgmt = PAM_IGNORE + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/alt-auth/fail-debug b/tests/data/scripts/alt-auth/fail-debug new file mode 100644 index 000000000000..ae96bb148e6a --- /dev/null +++ b/tests/data/scripts/alt-auth/fail-debug @@ -0,0 +1,28 @@ +# Test failure of alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=bogus force_first_pass no_ccache debug + account = alt_auth_map=bogus no_ccache debug + +[run] + authenticate = PAM_AUTHINFO_UNAVAIL + acct_mgmt = PAM_IGNORE + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) mapping bogus-nonexistent-account to bogus@%2 + DEBUG /^\(user %u\) alternate authentication failed: / + DEBUG (user %u) attempting authentication as %u@%2 + DEBUG /^\(user %u\) krb5_get_init_creds_password: / + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) + DEBUG pam_sm_acct_mgmt: entry + DEBUG skipping non-Kerberos login + DEBUG pam_sm_acct_mgmt: exit (ignore) diff --git a/tests/data/scripts/alt-auth/fallback b/tests/data/scripts/alt-auth/fallback new file mode 100644 index 000000000000..a0ee7a3d4292 --- /dev/null +++ b/tests/data/scripts/alt-auth/fallback @@ -0,0 +1,25 @@ +# Test alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%%s/unknown-user no_ccache + account = alt_auth_map=%%s/unknown-user no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/alt-auth/fallback-debug b/tests/data/scripts/alt-auth/fallback-debug new file mode 100644 index 000000000000..f63741a60a16 --- /dev/null +++ b/tests/data/scripts/alt-auth/fallback-debug @@ -0,0 +1,38 @@ +# Test alternative authentication principal with debug logging. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%%s/unknown-user no_ccache debug + account = alt_auth_map=%%s/unknown-user no_ccache debug + session = no_ccache debug + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) mapping %u to %0/unknown-user@%2 + DEBUG /^\(user %u\) alternate authentication failed: / + DEBUG (user %u) attempting authentication as %u + DEBUG (user %u) mapped user %0/unknown-user@%2 does not match principal %u + INFO user %u authenticated as %u + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG (user %u) mapped user %0/unknown-user@%2 does not match principal %u + DEBUG pam_sm_acct_mgmt: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/alt-auth/fallback-realm b/tests/data/scripts/alt-auth/fallback-realm new file mode 100644 index 000000000000..0eef10fd5056 --- /dev/null +++ b/tests/data/scripts/alt-auth/fallback-realm @@ -0,0 +1,25 @@ +# Test alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%%s@BOGUS.EXAMPLE.COM no_ccache + account = alt_auth_map=%%s@BOGUS.EXAMPLE.COM no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/alt-auth/force b/tests/data/scripts/alt-auth/force new file mode 100644 index 000000000000..4ad34f6f1fe4 --- /dev/null +++ b/tests/data/scripts/alt-auth/force @@ -0,0 +1,19 @@ +# Test forced alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%1 force_alt_auth force_first_pass no_ccache + account = alt_auth_map=%1 no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + +[output] + INFO user %u authenticated as %1 diff --git a/tests/data/scripts/alt-auth/force-fail-debug b/tests/data/scripts/alt-auth/force-fail-debug new file mode 100644 index 000000000000..cc077b1a4743 --- /dev/null +++ b/tests/data/scripts/alt-auth/force-fail-debug @@ -0,0 +1,26 @@ +# Test failure of forced authentication principal (no fallback). -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%1 force_alt_auth force_first_pass no_ccache debug + account = alt_auth_map=%1 no_ccache debug + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) mapping bogus-nonexistent-account to %1 + DEBUG /^\(user %u\) alternate authentication failed: / + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) + DEBUG pam_sm_acct_mgmt: entry + DEBUG skipping non-Kerberos login + DEBUG pam_sm_acct_mgmt: exit (ignore) diff --git a/tests/data/scripts/alt-auth/force-fallback b/tests/data/scripts/alt-auth/force-fallback new file mode 100644 index 000000000000..b93b04175ed5 --- /dev/null +++ b/tests/data/scripts/alt-auth/force-fallback @@ -0,0 +1,25 @@ +# Test forced alternative authentication with fallback. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%%s/unknown-user force_alt_auth no_ccache + account = alt_auth_map=%%s/unknown-user no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/alt-auth/only b/tests/data/scripts/alt-auth/only new file mode 100644 index 000000000000..7761fc7fd0ce --- /dev/null +++ b/tests/data/scripts/alt-auth/only @@ -0,0 +1,19 @@ +# Test required alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%1 only_alt_auth force_first_pass no_ccache + account = alt_auth_map=%1 no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + +[output] + INFO user %u authenticated as %1 diff --git a/tests/data/scripts/alt-auth/only-fail b/tests/data/scripts/alt-auth/only-fail new file mode 100644 index 000000000000..5c2831614928 --- /dev/null +++ b/tests/data/scripts/alt-auth/only-fail @@ -0,0 +1,22 @@ +# Test failure of required alternative authentication. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=bogus only_alt_auth no_ccache + account = alt_auth_map=bogus no_ccache + +[run] + authenticate = PAM_USER_UNKNOWN + acct_mgmt = PAM_IGNORE + +[prompts] + echo_off = Password: |%p + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/alt-auth/username-map b/tests/data/scripts/alt-auth/username-map new file mode 100644 index 000000000000..7f28a670344b --- /dev/null +++ b/tests/data/scripts/alt-auth/username-map @@ -0,0 +1,19 @@ +# Test username mapping of alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%%s@%2 force_first_pass no_ccache + account = alt_auth_map=%%s@%2 no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + +[output] + INFO user %u authenticated as %1 diff --git a/tests/data/scripts/alt-auth/username-map-prefix b/tests/data/scripts/alt-auth/username-map-prefix new file mode 100644 index 000000000000..5e83fc888d77 --- /dev/null +++ b/tests/data/scripts/alt-auth/username-map-prefix @@ -0,0 +1,19 @@ +# Test username mapping of alternative authentication principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = alt_auth_map=%3%%s@%2 force_first_pass no_ccache + account = alt_auth_map=%3%%s@%2 no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + +[output] + INFO user %u authenticated as %1 diff --git a/tests/data/scripts/bad-authtok/no-prompt b/tests/data/scripts/bad-authtok/no-prompt new file mode 100644 index 000000000000..e0c10cc69804 --- /dev/null +++ b/tests/data/scripts/bad-authtok/no-prompt @@ -0,0 +1,25 @@ +# Defer prompting to the Kerberos library after bad authtok. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache no_prompt try_first_pass + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = /^(%u's Password|Password for %u): $/|%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/bad-authtok/try-first b/tests/data/scripts/bad-authtok/try-first new file mode 100644 index 000000000000..cde6153efaeb --- /dev/null +++ b/tests/data/scripts/bad-authtok/try-first @@ -0,0 +1,25 @@ +# Test try_first_pass with a bad initial AUTHTOK. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = try_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/bad-authtok/try-first-debug b/tests/data/scripts/bad-authtok/try-first-debug new file mode 100644 index 000000000000..c76ce7ac89dd --- /dev/null +++ b/tests/data/scripts/bad-authtok/try-first-debug @@ -0,0 +1,36 @@ +# Test try_first_pass with a bad initial AUTHTOK and debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = try_first_pass no_ccache debug + account = no_ccache debug + session = no_ccache debug + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %u + DEBUG /^\(user %u\) krb5_get_init_creds_password: / + DEBUG (user %u) attempting authentication as %u + INFO user %u authenticated as %u + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG pam_sm_acct_mgmt: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/bad-authtok/use-first b/tests/data/scripts/bad-authtok/use-first new file mode 100644 index 000000000000..62d55ca2146f --- /dev/null +++ b/tests/data/scripts/bad-authtok/use-first @@ -0,0 +1,22 @@ +# Test use_first_pass with a bad initial AUTHTOK. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = use_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/bad-authtok/use-first-debug b/tests/data/scripts/bad-authtok/use-first-debug new file mode 100644 index 000000000000..4346d2395cb0 --- /dev/null +++ b/tests/data/scripts/bad-authtok/use-first-debug @@ -0,0 +1,33 @@ +# Test use_first_pass with a bad initial AUTHTOK and debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = use_first_pass no_ccache debug + account = no_ccache debug + session = no_ccache debug + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %u + DEBUG /^\(user %u\) krb5_get_init_creds_password: / + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) + DEBUG pam_sm_acct_mgmt: entry + DEBUG skipping non-Kerberos login + DEBUG pam_sm_acct_mgmt: exit (ignore) + DEBUG pam_sm_open_session: entry + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/basic/force-first b/tests/data/scripts/basic/force-first new file mode 100644 index 000000000000..792d737ba7c3 --- /dev/null +++ b/tests/data/scripts/basic/force-first @@ -0,0 +1,22 @@ +# Test force_first_pass without an authtok. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/basic/force-first-debug b/tests/data/scripts/basic/force-first-debug new file mode 100644 index 000000000000..539345316183 --- /dev/null +++ b/tests/data/scripts/basic/force-first-debug @@ -0,0 +1,32 @@ +# Test force_first_pass without an authtok. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache debug + account = no_ccache debug + session = no_ccache debug + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) no stored password + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) + DEBUG pam_sm_acct_mgmt: entry + DEBUG skipping non-Kerberos login + DEBUG pam_sm_acct_mgmt: exit (ignore) + DEBUG pam_sm_open_session: entry + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/basic/ignore-root b/tests/data/scripts/basic/ignore-root new file mode 100644 index 000000000000..bfbfee5c86df --- /dev/null +++ b/tests/data/scripts/basic/ignore-root @@ -0,0 +1,16 @@ +# Test account and session behavior for ignored root user. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_root + password = ignore_root + +[run] + authenticate = PAM_USER_UNKNOWN + chauthtok(PRELIM_CHECK) = PAM_IGNORE diff --git a/tests/data/scripts/basic/ignore-root-debug b/tests/data/scripts/basic/ignore-root-debug new file mode 100644 index 000000000000..2ffd33c16229 --- /dev/null +++ b/tests/data/scripts/basic/ignore-root-debug @@ -0,0 +1,24 @@ +# Test account and session behavior for ignored root user. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_root debug + password = ignore_root debug + +[run] + authenticate = PAM_USER_UNKNOWN + chauthtok(PRELIM_CHECK) = PAM_IGNORE + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user root) ignoring root user + DEBUG pam_sm_authenticate: exit (failure) + DEBUG pam_sm_chauthtok: entry (prelim) + DEBUG ignoring root user + DEBUG pam_sm_chauthtok: exit (ignore) diff --git a/tests/data/scripts/basic/minimum-uid b/tests/data/scripts/basic/minimum-uid new file mode 100644 index 000000000000..e56161041306 --- /dev/null +++ b/tests/data/scripts/basic/minimum-uid @@ -0,0 +1,13 @@ +# Test account and session behavior for minimum UID. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = minimum_uid=%1 + password = minimum_uid=%1 + +[run] + authenticate = PAM_USER_UNKNOWN + chauthtok(PRELIM_CHECK) = PAM_IGNORE diff --git a/tests/data/scripts/basic/minimum-uid-debug b/tests/data/scripts/basic/minimum-uid-debug new file mode 100644 index 000000000000..c20e43d55ac8 --- /dev/null +++ b/tests/data/scripts/basic/minimum-uid-debug @@ -0,0 +1,21 @@ +# Test account and session behavior for minimum UID (debug). -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = minimum_uid=%1 debug + password = minimum_uid=%1 debug + +[run] + authenticate = PAM_USER_UNKNOWN + chauthtok(PRELIM_CHECK) = PAM_IGNORE + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) ignoring low-UID user (%0 < %1) + DEBUG pam_sm_authenticate: exit (failure) + DEBUG pam_sm_chauthtok: entry (prelim) + DEBUG ignoring low-UID user (%0 < %1) + DEBUG pam_sm_chauthtok: exit (ignore) diff --git a/tests/data/scripts/basic/no-context b/tests/data/scripts/basic/no-context new file mode 100644 index 000000000000..5629422e23d9 --- /dev/null +++ b/tests/data/scripts/basic/no-context @@ -0,0 +1,17 @@ +# Test account and session behavior with no context. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[run] + acct_mgmt = PAM_IGNORE + setcred(DELETE_CRED) = PAM_SUCCESS + setcred(ESTABLISH_CRED) = PAM_SUCCESS + setcred(REFRESH_CRED) = PAM_SUCCESS + setcred(REINITIALIZE_CRED) = PAM_SUCCESS + open_session = PAM_IGNORE + close_session = PAM_SUCCESS diff --git a/tests/data/scripts/basic/no-context-debug b/tests/data/scripts/basic/no-context-debug new file mode 100644 index 000000000000..4bdeee727ed7 --- /dev/null +++ b/tests/data/scripts/basic/no-context-debug @@ -0,0 +1,47 @@ +# Test account and session behavior with no context. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = debug + account = debug + session = debug + +[run] + acct_mgmt = PAM_IGNORE + setcred(DELETE_CRED) = PAM_SUCCESS + setcred(ESTABLISH_CRED) = PAM_SUCCESS + setcred(REFRESH_CRED) = PAM_SUCCESS + setcred(REINITIALIZE_CRED) = PAM_SUCCESS + open_session = PAM_IGNORE + close_session = PAM_SUCCESS + +[output] + DEBUG pam_sm_acct_mgmt: entry + DEBUG skipping non-Kerberos login + DEBUG pam_sm_acct_mgmt: exit (ignore) + DEBUG pam_sm_setcred: entry (delete) + DEBUG pam_sm_setcred: exit (success) + DEBUG pam_sm_setcred: entry (establish) + DEBUG no context found, creating one + DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login + DEBUG pam_sm_setcred: exit (success) + DEBUG pam_sm_setcred: entry (refresh) + DEBUG no context found, creating one + DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login + DEBUG pam_sm_setcred: exit (success) + DEBUG pam_sm_setcred: entry (reinit) + DEBUG no context found, creating one + DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login + DEBUG pam_sm_setcred: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG no context found, creating one + DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login + DEBUG pam_sm_open_session: exit (ignore) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/cache-cleanup/auth-only b/tests/data/scripts/cache-cleanup/auth-only new file mode 100644 index 000000000000..c29608f3c8da --- /dev/null +++ b/tests/data/scripts/cache-cleanup/auth-only @@ -0,0 +1,17 @@ +# Test authentication only with ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass ignore_k5login ccache_dir=FILE:%1 + +[run] + authenticate = PAM_SUCCESS + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/cache/basic b/tests/data/scripts/cache/basic new file mode 100644 index 000000000000..6b1042f3084b --- /dev/null +++ b/tests/data/scripts/cache/basic @@ -0,0 +1,21 @@ +# Test basic authentication with ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass ignore_k5login + account = ignore_k5login + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/cache/end-data-silent b/tests/data/scripts/cache/end-data-silent new file mode 100644 index 000000000000..f172008bc574 --- /dev/null +++ b/tests/data/scripts/cache/end-data-silent @@ -0,0 +1,27 @@ +# Test pam_end with PAM_DATA_SILENT. -*- conf -*- +# +# Passing PAM_DATA_SILENT to pam_end should cause the credential cache to not +# be deleted (under the assumption that pam_end is being called in a forked +# process and will be called again in the parent to clean up resources). +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020-2021 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass ignore_k5login + account = ignore_k5login + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + +[end] + flags = PAM_DATA_SILENT + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/cache/open-session b/tests/data/scripts/cache/open-session new file mode 100644 index 000000000000..83e48c36511e --- /dev/null +++ b/tests/data/scripts/cache/open-session @@ -0,0 +1,20 @@ +# Test authentication with ticket cache, open session. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass ignore_k5login + account = ignore_k5login + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/cache/search-k5login b/tests/data/scripts/cache/search-k5login new file mode 100644 index 000000000000..b87c28147edb --- /dev/null +++ b/tests/data/scripts/cache/search-k5login @@ -0,0 +1,20 @@ +# Test authentication with search_k5login, open session. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass search_k5login + account = search_k5login + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/cache/search-k5login-debug b/tests/data/scripts/cache/search-k5login-debug new file mode 100644 index 000000000000..eb50b9e47eaf --- /dev/null +++ b/tests/data/scripts/cache/search-k5login-debug @@ -0,0 +1,34 @@ +# Test authentication with search_k5login and debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass search_k5login debug + account = search_k5login debug + session = debug + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %0 + INFO user %u authenticated as %0 + DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/ + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG (user %u) retrieving principal from cache + DEBUG pam_sm_acct_mgmt: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/ + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/expired/basic-heimdal b/tests/data/scripts/expired/basic-heimdal new file mode 100644 index 000000000000..2b4f471cf247 --- /dev/null +++ b/tests/data/scripts/expired/basic-heimdal @@ -0,0 +1,31 @@ +# Test default handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login + account = ignore_k5login + password = ignore_k5login + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + info = Password has expired + info = Your password will expire at %1 + info = Changing password + echo_off = New password: |%n + echo_off = Repeat new password: |%n + info = Success: Password changed + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/expired/basic-heimdal-debug b/tests/data/scripts/expired/basic-heimdal-debug new file mode 100644 index 000000000000..a18cc00c71a9 --- /dev/null +++ b/tests/data/scripts/expired/basic-heimdal-debug @@ -0,0 +1,44 @@ +# Test default handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login debug + account = ignore_k5login debug + password = ignore_k5login debug + session = debug + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + info = Password has expired + info = Your password will expire at %1 + info = Changing password + echo_off = New password: |%n + echo_off = Repeat new password: |%n + info = Success: Password changed + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %0 + INFO user %u authenticated as %0 + DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/ + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG (user %u) retrieving principal from cache + DEBUG pam_sm_acct_mgmt: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/ + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/expired/basic-heimdal-flag-silent b/tests/data/scripts/expired/basic-heimdal-flag-silent new file mode 100644 index 000000000000..58e065b485bb --- /dev/null +++ b/tests/data/scripts/expired/basic-heimdal-flag-silent @@ -0,0 +1,27 @@ +# Test default handling of expired passwords with PAM_SILENT. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login + account = ignore_k5login + password = ignore_k5login + +[run] + authenticate(SILENT) = PAM_SUCCESS + acct_mgmt(SILENT) = PAM_SUCCESS + open_session(SILENT) = PAM_SUCCESS + close_session(SILENT) = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + echo_off = New password: |%n + echo_off = Repeat new password: |%n + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/expired/basic-heimdal-old b/tests/data/scripts/expired/basic-heimdal-old new file mode 100644 index 000000000000..dd67ec44df7c --- /dev/null +++ b/tests/data/scripts/expired/basic-heimdal-old @@ -0,0 +1,30 @@ +# Test default handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login + account = ignore_k5login + password = ignore_k5login + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + info = Your password will expire at %1 + info = Changing password + echo_off = New password: |%n + echo_off = Repeat new password: |%n + info = Success: Password changed + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/expired/basic-heimdal-old-debug b/tests/data/scripts/expired/basic-heimdal-old-debug new file mode 100644 index 000000000000..53267f5fac62 --- /dev/null +++ b/tests/data/scripts/expired/basic-heimdal-old-debug @@ -0,0 +1,43 @@ +# Test default handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login debug + account = ignore_k5login debug + password = ignore_k5login debug + session = debug + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + info = Your password will expire at %1 + info = Changing password + echo_off = New password: |%n + echo_off = Repeat new password: |%n + info = Success: Password changed + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %0 + INFO user %u authenticated as %0 + DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/ + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG (user %u) retrieving principal from cache + DEBUG pam_sm_acct_mgmt: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/ + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/expired/basic-heimdal-silent b/tests/data/scripts/expired/basic-heimdal-silent new file mode 100644 index 000000000000..028d5fe382f6 --- /dev/null +++ b/tests/data/scripts/expired/basic-heimdal-silent @@ -0,0 +1,27 @@ +# Test default handling of expired passwords with silent. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login silent + account = ignore_k5login silent + password = ignore_k5login silent + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + echo_off = New password: |%n + echo_off = Repeat new password: |%n + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/expired/basic-mit b/tests/data/scripts/expired/basic-mit new file mode 100644 index 000000000000..9611381b4ce9 --- /dev/null +++ b/tests/data/scripts/expired/basic-mit @@ -0,0 +1,28 @@ +# Test default handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login + account = ignore_k5login + password = ignore_k5login + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + info = Password expired. You must change it now. + echo_off = Enter new password: |%n + echo_off = Enter it again: |%n + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/expired/basic-mit-debug b/tests/data/scripts/expired/basic-mit-debug new file mode 100644 index 000000000000..5b58b25b8ec2 --- /dev/null +++ b/tests/data/scripts/expired/basic-mit-debug @@ -0,0 +1,41 @@ +# Test default handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login debug + account = ignore_k5login debug + password = ignore_k5login debug + session = debug + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + info = Password expired. You must change it now. + echo_off = Enter new password: |%n + echo_off = Enter it again: |%n + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %0 + INFO user %u authenticated as %0 + DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/ + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG (user %u) retrieving principal from cache + DEBUG pam_sm_acct_mgmt: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/ + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/expired/basic-mit-flag-silent b/tests/data/scripts/expired/basic-mit-flag-silent new file mode 100644 index 000000000000..a13bffdeea44 --- /dev/null +++ b/tests/data/scripts/expired/basic-mit-flag-silent @@ -0,0 +1,27 @@ +# Test default handling of expired passwords with PAM_SILENT. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login + account = ignore_k5login + password = ignore_k5login + +[run] + authenticate(SILENT) = PAM_SUCCESS + acct_mgmt(SILENT) = PAM_SUCCESS + open_session(SILENT) = PAM_SUCCESS + close_session(SILENT) = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + echo_off = Enter new password: |%n + echo_off = Enter it again: |%n + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/expired/basic-mit-silent b/tests/data/scripts/expired/basic-mit-silent new file mode 100644 index 000000000000..7dea2b7bdd4e --- /dev/null +++ b/tests/data/scripts/expired/basic-mit-silent @@ -0,0 +1,27 @@ +# Test default handling of expired passwords with silent. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login silent + account = ignore_k5login silent + password = ignore_k5login silent + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + echo_off = Enter new password: |%n + echo_off = Enter it again: |%n + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/expired/defer-mit b/tests/data/scripts/expired/defer-mit new file mode 100644 index 000000000000..7403edbfdbbf --- /dev/null +++ b/tests/data/scripts/expired/defer-mit @@ -0,0 +1,33 @@ +# Test deferring handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = defer_pwchange use_first_pass + account = ignore_k5login + password = ignore_k5login use_first_pass + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_NEW_AUTHTOK_REQD + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Current Kerberos password: |%p + echo_off = Enter new Kerberos password: |%n + echo_off = Retype new Kerberos password: |%n + +[output] + INFO user %u authenticated as %0 (expired) + INFO user %u account password is expired + INFO user %u changed Kerberos password + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/expired/defer-mit-debug b/tests/data/scripts/expired/defer-mit-debug new file mode 100644 index 000000000000..c637f39402f7 --- /dev/null +++ b/tests/data/scripts/expired/defer-mit-debug @@ -0,0 +1,57 @@ +# Test deferring handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = defer_pwchange use_first_pass debug + account = ignore_k5login debug + password = ignore_k5login use_first_pass debug + session = debug + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_NEW_AUTHTOK_REQD + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Current Kerberos password: |%p + echo_off = Enter new Kerberos password: |%n + echo_off = Retype new Kerberos password: |%n + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %0 + DEBUG (user %u) krb5_get_init_creds_password: Password has expired + DEBUG (user %u) expired account, deferring failure + INFO user %u authenticated as %0 (expired) + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + INFO user %u account password is expired + DEBUG pam_sm_acct_mgmt: exit (failure) + DEBUG pam_sm_chauthtok: entry (prelim) + DEBUG (user %u) attempting authentication as %0 for kadmin/changepw + DEBUG pam_sm_chauthtok: exit (success) + DEBUG pam_sm_chauthtok: entry (update) + INFO user %u changed Kerberos password + DEBUG (user %u) obtaining credentials with new password + DEBUG (user %u) attempting authentication as %0 + INFO user %u authenticated as %0 + DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/ + DEBUG pam_sm_chauthtok: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG (user %u) retrieving principal from cache + DEBUG pam_sm_acct_mgmt: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/ + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/expired/fail b/tests/data/scripts/expired/fail new file mode 100644 index 000000000000..566b4b9c73dc --- /dev/null +++ b/tests/data/scripts/expired/fail @@ -0,0 +1,20 @@ +# Test default handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login fail_pwchange + +[run] + authenticate = PAM_AUTH_ERR + +[prompts] + echo_off = Password: |%p + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/expired/fail-debug b/tests/data/scripts/expired/fail-debug new file mode 100644 index 000000000000..7f464b4ed89f --- /dev/null +++ b/tests/data/scripts/expired/fail-debug @@ -0,0 +1,24 @@ +# Test default handling of expired passwords. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = ignore_k5login fail_pwchange debug + +[run] + authenticate = PAM_AUTH_ERR + +[prompts] + echo_off = Password: |%p + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %0 + DEBUG /^\(user %u\) krb5_get_init_creds_password: / + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) diff --git a/tests/data/scripts/fast/anonymous b/tests/data/scripts/fast/anonymous new file mode 100644 index 000000000000..5f725ae63dcf --- /dev/null +++ b/tests/data/scripts/fast/anonymous @@ -0,0 +1,17 @@ +# Test anonymous FAST. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache anon_fast + +[run] + authenticate = PAM_SUCCESS + +[output] + INFO user %u authenticated as %0 diff --git a/tests/data/scripts/fast/anonymous-debug b/tests/data/scripts/fast/anonymous-debug new file mode 100644 index 000000000000..48fd1eadd581 --- /dev/null +++ b/tests/data/scripts/fast/anonymous-debug @@ -0,0 +1,22 @@ +# Test FAST with an existing ticket cache, with debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache anon_fast debug + +[run] + authenticate = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) anonymous authentication for FAST succeeded + DEBUG /^\(user %u\) setting FAST credential cache to MEMORY:/ + DEBUG (user %u) attempting authentication as %0 + INFO user %u authenticated as %0 + DEBUG pam_sm_authenticate: exit (success) diff --git a/tests/data/scripts/fast/ccache b/tests/data/scripts/fast/ccache new file mode 100644 index 000000000000..32e5eaa92465 --- /dev/null +++ b/tests/data/scripts/fast/ccache @@ -0,0 +1,17 @@ +# Test FAST with an existing ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache fast_ccache=%0 + +[run] + authenticate = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/fast/ccache-debug b/tests/data/scripts/fast/ccache-debug new file mode 100644 index 000000000000..f3788f2fc1c7 --- /dev/null +++ b/tests/data/scripts/fast/ccache-debug @@ -0,0 +1,21 @@ +# Test FAST with an existing ticket cache, with debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache fast_ccache=%0 debug + +[run] + authenticate = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) setting FAST credential cache to %0 + DEBUG (user %u) attempting authentication as %u + INFO user %u authenticated as %u + DEBUG pam_sm_authenticate: exit (success) diff --git a/tests/data/scripts/fast/no-ccache b/tests/data/scripts/fast/no-ccache new file mode 100644 index 000000000000..71d4e2d494cf --- /dev/null +++ b/tests/data/scripts/fast/no-ccache @@ -0,0 +1,17 @@ +# Test FAST with an existing ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache fast_ccache=%0BAD + +[run] + authenticate = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/fast/no-ccache-debug b/tests/data/scripts/fast/no-ccache-debug new file mode 100644 index 000000000000..743ad5559538 --- /dev/null +++ b/tests/data/scripts/fast/no-ccache-debug @@ -0,0 +1,21 @@ +# Test FAST with an existing ticket cache, with debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache fast_ccache=%0BAD debug + +[run] + authenticate = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG /^\(user %u\) failed to get principal from FAST ccache %0BAD: / + DEBUG (user %u) attempting authentication as %u + INFO user %u authenticated as %u + DEBUG pam_sm_authenticate: exit (success) diff --git a/tests/data/scripts/long/password b/tests/data/scripts/long/password new file mode 100644 index 000000000000..e8183976c004 --- /dev/null +++ b/tests/data/scripts/long/password @@ -0,0 +1,14 @@ +# Test authentication with an excessively long password. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[run] + authenticate = PAM_AUTH_ERR + +[prompts] + echo_off = Password: |%p + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/long/password-debug b/tests/data/scripts/long/password-debug new file mode 100644 index 000000000000..832c19340485 --- /dev/null +++ b/tests/data/scripts/long/password-debug @@ -0,0 +1,20 @@ +# Test excessively long password handling with debug logging. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = debug + +[run] + authenticate = PAM_AUTH_ERR + +[prompts] + echo_off = Password: |%p + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/ + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) diff --git a/tests/data/scripts/long/use-first b/tests/data/scripts/long/use-first new file mode 100644 index 000000000000..b68800485d04 --- /dev/null +++ b/tests/data/scripts/long/use-first @@ -0,0 +1,14 @@ +# Test use_first_pass with an excessively long password. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = use_first_pass + +[run] + authenticate = PAM_AUTH_ERR + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/long/use-first-debug b/tests/data/scripts/long/use-first-debug new file mode 100644 index 000000000000..72747e81f40c --- /dev/null +++ b/tests/data/scripts/long/use-first-debug @@ -0,0 +1,17 @@ +# Test use_first_pass with a long password and debug. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = use_first_pass debug + +[run] + authenticate = PAM_AUTH_ERR + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/ + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) diff --git a/tests/data/scripts/no-cache/no-prompt b/tests/data/scripts/no-cache/no-prompt new file mode 100644 index 000000000000..1eef2f26b4ee --- /dev/null +++ b/tests/data/scripts/no-cache/no-prompt @@ -0,0 +1,25 @@ +# Defer prompting to the Kerberos library. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache no_prompt + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = /^(%u's Password|Password for %u): $/|%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/no-cache/no-prompt-try b/tests/data/scripts/no-cache/no-prompt-try new file mode 100644 index 000000000000..1d632a96f9e6 --- /dev/null +++ b/tests/data/scripts/no-cache/no-prompt-try @@ -0,0 +1,25 @@ +# Defer prompting to the Kerberos library w/try_first_pass. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache no_prompt try_first_pass + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = /^(%u's Password|Password for %u): $/|%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/no-cache/no-prompt-use b/tests/data/scripts/no-cache/no-prompt-use new file mode 100644 index 000000000000..76ef388465d2 --- /dev/null +++ b/tests/data/scripts/no-cache/no-prompt-use @@ -0,0 +1,25 @@ +# Defer prompting to the Kerberos library w/use_first_pass. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache no_prompt + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = /^(%u's Password|Password for %u): $/|%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/no-cache/prompt b/tests/data/scripts/no-cache/prompt new file mode 100644 index 000000000000..b0eb0d9ca57b --- /dev/null +++ b/tests/data/scripts/no-cache/prompt @@ -0,0 +1,25 @@ +# Test basic auth w/prompting without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/no-cache/prompt-expose b/tests/data/scripts/no-cache/prompt-expose new file mode 100644 index 000000000000..a3365cc69754 --- /dev/null +++ b/tests/data/scripts/no-cache/prompt-expose @@ -0,0 +1,25 @@ +# Test basic auth w/prompting without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = expose_account no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password for %u: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/no-cache/prompt-fail b/tests/data/scripts/no-cache/prompt-fail new file mode 100644 index 000000000000..376b0f911997 --- /dev/null +++ b/tests/data/scripts/no-cache/prompt-fail @@ -0,0 +1,25 @@ +# Test failed password authentication. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |BAD%p + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/no-cache/prompt-fail-debug b/tests/data/scripts/no-cache/prompt-fail-debug new file mode 100644 index 000000000000..9c9a7a406b4b --- /dev/null +++ b/tests/data/scripts/no-cache/prompt-fail-debug @@ -0,0 +1,36 @@ +# Test failed password authentication with debug logging. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache debug + account = no_ccache debug + session = no_ccache debug + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |BAD%p + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %u + DEBUG /^\(user %u\) krb5_get_init_creds_password: / + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) + DEBUG pam_sm_acct_mgmt: entry + DEBUG skipping non-Kerberos login + DEBUG pam_sm_acct_mgmt: exit (ignore) + DEBUG pam_sm_open_session: entry + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/no-cache/prompt-principal b/tests/data/scripts/no-cache/prompt-principal new file mode 100644 index 000000000000..5e7278f1e92d --- /dev/null +++ b/tests/data/scripts/no-cache/prompt-principal @@ -0,0 +1,26 @@ +# Test prompting for principal without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = prompt_principal no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_on = Principal: |%u + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/no-cache/try-first b/tests/data/scripts/no-cache/try-first new file mode 100644 index 000000000000..366801e9a078 --- /dev/null +++ b/tests/data/scripts/no-cache/try-first @@ -0,0 +1,25 @@ +# Test basic auth w/no AUTHTOK and try_first_pass. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = try_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/no-cache/use-first b/tests/data/scripts/no-cache/use-first new file mode 100644 index 000000000000..028009fd7ba7 --- /dev/null +++ b/tests/data/scripts/no-cache/use-first @@ -0,0 +1,25 @@ +# Test basic auth w/no AUTHTOK and use_first_pass. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = use_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/pam-user/no-update b/tests/data/scripts/pam-user/no-update new file mode 100644 index 000000000000..36520bb4f332 --- /dev/null +++ b/tests/data/scripts/pam-user/no-update @@ -0,0 +1,20 @@ +# PAM_USER updates disabled. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache no_update_user + +[run] + authenticate = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/pam-user/update b/tests/data/scripts/pam-user/update new file mode 100644 index 000000000000..11d404a02144 --- /dev/null +++ b/tests/data/scripts/pam-user/update @@ -0,0 +1,20 @@ +# PAM_USER updates. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache + +[run] + authenticate = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %0 authenticated as %1 diff --git a/tests/data/scripts/password/authtok b/tests/data/scripts/password/authtok new file mode 100644 index 000000000000..9f6a39935b2d --- /dev/null +++ b/tests/data/scripts/password/authtok @@ -0,0 +1,21 @@ +# Test password change with new authtok set but not old. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = use_authtok + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_off = Current Kerberos password: |%p + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/authtok-force b/tests/data/scripts/password/authtok-force new file mode 100644 index 000000000000..3bc0b598521b --- /dev/null +++ b/tests/data/scripts/password/authtok-force @@ -0,0 +1,18 @@ +# Test password change with new authtok set but not old. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = use_authtok force_first_pass + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/authtok-too-long b/tests/data/scripts/password/authtok-too-long new file mode 100644 index 000000000000..df81e24977b3 --- /dev/null +++ b/tests/data/scripts/password/authtok-too-long @@ -0,0 +1,17 @@ +# Test use_authtok with an excessively long password. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = use_authtok + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR + +[prompts] + echo_off = Current Kerberos password: |%p + +[output] diff --git a/tests/data/scripts/password/authtok-too-long-debug b/tests/data/scripts/password/authtok-too-long-debug new file mode 100644 index 000000000000..cb38e8861102 --- /dev/null +++ b/tests/data/scripts/password/authtok-too-long-debug @@ -0,0 +1,23 @@ +# Test use_authtok with an excessively long password. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = use_authtok debug + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR + +[prompts] + echo_off = Current Kerberos password: |%p + +[output] + DEBUG pam_sm_chauthtok: entry (prelim) + DEBUG (user %u) attempting authentication as %0 for kadmin/changepw + DEBUG pam_sm_chauthtok: exit (success) + DEBUG pam_sm_chauthtok: entry (update) + DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/ + DEBUG pam_sm_chauthtok: exit (failure) diff --git a/tests/data/scripts/password/banner b/tests/data/scripts/password/banner new file mode 100644 index 000000000000..98c899c26af5 --- /dev/null +++ b/tests/data/scripts/password/banner @@ -0,0 +1,23 @@ +# Test password change with a modified banner. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = banner=realm + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_off = Current realm password: |%p + echo_off = Enter new realm password: |%n + echo_off = Retype new realm password: |%n + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/banner-expose b/tests/data/scripts/password/banner-expose new file mode 100644 index 000000000000..595fa0380b22 --- /dev/null +++ b/tests/data/scripts/password/banner-expose @@ -0,0 +1,23 @@ +# Test password change with banner and expose_account. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = expose_account banner=realm + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_off = Current realm password for %0: |%p + echo_off = Enter new realm password for %0: |%n + echo_off = Retype new realm password for %0: |%n + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/basic b/tests/data/scripts/password/basic new file mode 100644 index 000000000000..5cb68267ce26 --- /dev/null +++ b/tests/data/scripts/password/basic @@ -0,0 +1,20 @@ +# Test password change with prompting. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_off = Current Kerberos password: |%p + echo_off = Enter new Kerberos password: |%n + echo_off = Retype new Kerberos password: |%n + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/basic-debug b/tests/data/scripts/password/basic-debug new file mode 100644 index 000000000000..ca1c86b9c2c9 --- /dev/null +++ b/tests/data/scripts/password/basic-debug @@ -0,0 +1,28 @@ +# Test password change with prompting and debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = debug + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_off = Current Kerberos password: |%p + echo_off = Enter new Kerberos password: |%n + echo_off = Retype new Kerberos password: |%n + +[output] + DEBUG pam_sm_chauthtok: entry (prelim) + DEBUG (user %u) attempting authentication as %0 for kadmin/changepw + DEBUG pam_sm_chauthtok: exit (success) + DEBUG pam_sm_chauthtok: entry (update) + INFO user %u changed Kerberos password + DEBUG pam_sm_chauthtok: exit (success) diff --git a/tests/data/scripts/password/expose b/tests/data/scripts/password/expose new file mode 100644 index 000000000000..a82c1bd0b78d --- /dev/null +++ b/tests/data/scripts/password/expose @@ -0,0 +1,23 @@ +# Test password change with prompting and expose_account. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = expose_account + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_off = Current Kerberos password for %0: |%p + echo_off = Enter new Kerberos password for %0: |%n + echo_off = Retype new Kerberos password for %0: |%n + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/ignore b/tests/data/scripts/password/ignore new file mode 100644 index 000000000000..023cf5656f67 --- /dev/null +++ b/tests/data/scripts/password/ignore @@ -0,0 +1,18 @@ +# Test password prompt saving for ignored users. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = ignore_root + +[run] + chauthtok(PRELIM_CHECK) = PAM_IGNORE + chauthtok(UPDATE_AUTHTOK) = PAM_IGNORE + +[prompts] + echo_off = Enter new password: |%n + echo_off = Retype new password: |%n + +[output] diff --git a/tests/data/scripts/password/no-banner b/tests/data/scripts/password/no-banner new file mode 100644 index 000000000000..9cabbd8ec5f9 --- /dev/null +++ b/tests/data/scripts/password/no-banner @@ -0,0 +1,23 @@ +# Test password change with no identifying banner. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = banner= + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_off = Current password: |%p + echo_off = Enter new password: |%n + echo_off = Retype new password: |%n + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/no-banner-expose b/tests/data/scripts/password/no-banner-expose new file mode 100644 index 000000000000..3a5b944887bd --- /dev/null +++ b/tests/data/scripts/password/no-banner-expose @@ -0,0 +1,23 @@ +# Test password change with no banner and expose_account. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = expose_account banner= + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_off = Current password for %0: |%p + echo_off = Enter new password for %0: |%n + echo_off = Retype new password for %0: |%n + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/prompt-principal b/tests/data/scripts/password/prompt-principal new file mode 100644 index 000000000000..1e7274eb058e --- /dev/null +++ b/tests/data/scripts/password/prompt-principal @@ -0,0 +1,24 @@ +# Test password change with prompting and prompt_principal. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = prompt_principal + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS + +[prompts] + echo_on = Principal: |%u + echo_off = Current Kerberos password: |%p + echo_off = Enter new Kerberos password: |%n + echo_off = Retype new Kerberos password: |%n + +[output] + INFO user %u changed Kerberos password diff --git a/tests/data/scripts/password/too-long b/tests/data/scripts/password/too-long new file mode 100644 index 000000000000..4dbabd5db11e --- /dev/null +++ b/tests/data/scripts/password/too-long @@ -0,0 +1,15 @@ +# Test password change to an excessively long password. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR + +[prompts] + echo_off = Current Kerberos password: |%p + echo_off = Enter new Kerberos password: |%n + +[output] diff --git a/tests/data/scripts/password/too-long-debug b/tests/data/scripts/password/too-long-debug new file mode 100644 index 000000000000..18b4ed608612 --- /dev/null +++ b/tests/data/scripts/password/too-long-debug @@ -0,0 +1,24 @@ +# Test password change to an excessively long password. -*- conf -*- +# +# Copyright 2020 Russ Allbery <eagle@eyrie.org> +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + password = debug + +[run] + chauthtok(PRELIM_CHECK) = PAM_SUCCESS + chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR + +[prompts] + echo_off = Current Kerberos password: |%p + echo_off = Enter new Kerberos password: |%n + +[output] + DEBUG pam_sm_chauthtok: entry (prelim) + DEBUG (user %u) attempting authentication as %0 for kadmin/changepw + DEBUG pam_sm_chauthtok: exit (success) + DEBUG pam_sm_chauthtok: entry (update) + DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/ + DEBUG pam_sm_chauthtok: exit (failure) diff --git a/tests/data/scripts/pkinit/basic b/tests/data/scripts/pkinit/basic new file mode 100644 index 000000000000..713bf0af1ce1 --- /dev/null +++ b/tests/data/scripts/pkinit/basic @@ -0,0 +1,22 @@ +# Test PKINIT auth without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache use_pkinit pkinit_user=FILE:%0 + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/pkinit/basic-debug b/tests/data/scripts/pkinit/basic-debug new file mode 100644 index 000000000000..92a3fcf934d6 --- /dev/null +++ b/tests/data/scripts/pkinit/basic-debug @@ -0,0 +1,30 @@ +# Test PKINIT auth without saving a ticket cache w/debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = debug no_ccache use_pkinit pkinit_user=FILE:%0 + account = debug no_ccache + session = debug no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + INFO user %u authenticated as %u + DEBUG pam_sm_authenticate: exit (success) + DEBUG pam_sm_acct_mgmt: entry + DEBUG pam_sm_acct_mgmt: exit (success) + DEBUG pam_sm_open_session: entry + DEBUG pam_sm_open_session: exit (success) + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/pkinit/no-use-pkinit b/tests/data/scripts/pkinit/no-use-pkinit new file mode 100644 index 000000000000..ead640bcc4a0 --- /dev/null +++ b/tests/data/scripts/pkinit/no-use-pkinit @@ -0,0 +1,18 @@ +# Test for unsupported use_pkinit. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache use_pkinit + +[run] + authenticate = PAM_AUTHINFO_UNAVAIL + +[output] + ERR use_pkinit requested but PKINIT not available or cannot be enforced + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/pkinit/pin-mit b/tests/data/scripts/pkinit/pin-mit new file mode 100644 index 000000000000..9791ebc2ace6 --- /dev/null +++ b/tests/data/scripts/pkinit/pin-mit @@ -0,0 +1,20 @@ +# Test PKINIT auth with a PIN prompt. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache use_pkinit pkinit_user=PKCS12:%0 + +[run] + authenticate = PAM_SUCCESS + +[prompts] + echo_off = Pass phrase for %0: |%1 + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/pkinit/preauth-opt-mit b/tests/data/scripts/pkinit/preauth-opt-mit new file mode 100644 index 000000000000..4602d18c7556 --- /dev/null +++ b/tests/data/scripts/pkinit/preauth-opt-mit @@ -0,0 +1,17 @@ +# Test PKINIT auth with MIT preauth options. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache try_pkinit preauth_opt=X509_user_identity=FILE:%0 + +[run] + authenticate = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/pkinit/prompt-try b/tests/data/scripts/pkinit/prompt-try new file mode 100644 index 000000000000..723a228847e3 --- /dev/null +++ b/tests/data/scripts/pkinit/prompt-try @@ -0,0 +1,20 @@ +# Test try_pkinit with an initial prompt. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache try_pkinit pkinit_user=FILE:%0 pkinit_prompt + +[run] + authenticate = PAM_SUCCESS + +[prompts] + echo_off = Insert smart card if desired, then press Enter: | + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/pkinit/prompt-use b/tests/data/scripts/pkinit/prompt-use new file mode 100644 index 000000000000..0b341d5d73ce --- /dev/null +++ b/tests/data/scripts/pkinit/prompt-use @@ -0,0 +1,20 @@ +# Test use_pkinit with an initial prompt. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache use_pkinit pkinit_user=FILE:%0 pkinit_prompt + +[run] + authenticate = PAM_SUCCESS + +[prompts] + echo_off = Insert smart card and press Enter: | + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/pkinit/try-pkinit b/tests/data/scripts/pkinit/try-pkinit new file mode 100644 index 000000000000..13b7bcf76653 --- /dev/null +++ b/tests/data/scripts/pkinit/try-pkinit @@ -0,0 +1,17 @@ +# Test optional PKINIT auth without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache try_pkinit pkinit_user=FILE:%0 + +[run] + authenticate = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/pkinit/try-pkinit-debug b/tests/data/scripts/pkinit/try-pkinit-debug new file mode 100644 index 000000000000..c721395abd07 --- /dev/null +++ b/tests/data/scripts/pkinit/try-pkinit-debug @@ -0,0 +1,19 @@ +# Test optional PKINIT auth w/debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = debug no_ccache try_pkinit pkinit_user=FILE:%0 + +[run] + authenticate = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + INFO user %u authenticated as %u + DEBUG pam_sm_authenticate: exit (success) diff --git a/tests/data/scripts/pkinit/try-pkinit-debug-mit b/tests/data/scripts/pkinit/try-pkinit-debug-mit new file mode 100644 index 000000000000..2c8c966bdc03 --- /dev/null +++ b/tests/data/scripts/pkinit/try-pkinit-debug-mit @@ -0,0 +1,20 @@ +# Test optional PKINIT auth w/debug. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = debug no_ccache try_pkinit pkinit_user=FILE:%0 + +[run] + authenticate = PAM_SUCCESS + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %u + INFO user %u authenticated as %u + DEBUG pam_sm_authenticate: exit (success) diff --git a/tests/data/scripts/realm/fail-bad-user-realm b/tests/data/scripts/realm/fail-bad-user-realm new file mode 100644 index 000000000000..d30bec6f1f33 --- /dev/null +++ b/tests/data/scripts/realm/fail-bad-user-realm @@ -0,0 +1,17 @@ +# Test authentication failure with different user_realm. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache user_realm=%0 + +[run] + authenticate = PAM_AUTHINFO_UNAVAIL + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/realm/fail-no-realm b/tests/data/scripts/realm/fail-no-realm new file mode 100644 index 000000000000..87b59aab49f2 --- /dev/null +++ b/tests/data/scripts/realm/fail-no-realm @@ -0,0 +1,17 @@ +# Test authentication failure due to wrong realm. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache + +[run] + authenticate = PAM_AUTHINFO_UNAVAIL + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/realm/fail-no-realm-debug b/tests/data/scripts/realm/fail-no-realm-debug new file mode 100644 index 000000000000..5ef2ce588177 --- /dev/null +++ b/tests/data/scripts/realm/fail-no-realm-debug @@ -0,0 +1,21 @@ +# Test authentication failure due to wrong realm. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache debug + +[run] + authenticate = PAM_AUTHINFO_UNAVAIL + +[output] + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) attempting authentication as %u@%0 + DEBUG /^\(user %u\) krb5_get_init_creds_password: / + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) diff --git a/tests/data/scripts/realm/fail-realm b/tests/data/scripts/realm/fail-realm new file mode 100644 index 000000000000..6dfe6a044354 --- /dev/null +++ b/tests/data/scripts/realm/fail-realm @@ -0,0 +1,17 @@ +# Test authentication failure with different realm. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache realm=%0 + +[run] + authenticate = PAM_AUTHINFO_UNAVAIL + +[output] + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/realm/fail-user-realm b/tests/data/scripts/realm/fail-user-realm new file mode 100644 index 000000000000..c97324c2d028 --- /dev/null +++ b/tests/data/scripts/realm/fail-user-realm @@ -0,0 +1,18 @@ +# Test authentication failure with different user_realm. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache user_realm=%0 + +[run] + authenticate = PAM_AUTH_ERR + +[output] + ERR /^\(user %u\) cannot convert principal to user: / + NOTICE failed authorization check; logname=%u uid=%i euid=%i tty= ruser= rhost= diff --git a/tests/data/scripts/realm/pass-realm b/tests/data/scripts/realm/pass-realm new file mode 100644 index 000000000000..91136c9bfc1c --- /dev/null +++ b/tests/data/scripts/realm/pass-realm @@ -0,0 +1,17 @@ +# Test authentication success with different realm. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache realm=%0 + +[run] + authenticate = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u@%0 diff --git a/tests/data/scripts/realm/pass-user-realm b/tests/data/scripts/realm/pass-user-realm new file mode 100644 index 000000000000..86007c2d4d26 --- /dev/null +++ b/tests/data/scripts/realm/pass-user-realm @@ -0,0 +1,17 @@ +# Test authentication success with different user_realm. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache user_realm=%0 + +[run] + authenticate = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u@%0 diff --git a/tests/data/scripts/stacked/auth-only b/tests/data/scripts/stacked/auth-only new file mode 100644 index 000000000000..46d3308ac0e4 --- /dev/null +++ b/tests/data/scripts/stacked/auth-only @@ -0,0 +1,18 @@ +# Test basic authentication without setcred. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/stacked/basic b/tests/data/scripts/stacked/basic new file mode 100644 index 000000000000..a05640d278bf --- /dev/null +++ b/tests/data/scripts/stacked/basic @@ -0,0 +1,22 @@ +# Test basic authentication without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/stacked/prompt b/tests/data/scripts/stacked/prompt new file mode 100644 index 000000000000..b0eb0d9ca57b --- /dev/null +++ b/tests/data/scripts/stacked/prompt @@ -0,0 +1,25 @@ +# Test basic auth w/prompting without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_off = Password: |%p + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/stacked/prompt-principal b/tests/data/scripts/stacked/prompt-principal new file mode 100644 index 000000000000..b416671875c7 --- /dev/null +++ b/tests/data/scripts/stacked/prompt-principal @@ -0,0 +1,25 @@ +# Test prompting for principal without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = prompt_principal force_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[prompts] + echo_on = Principal: |%u + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/stacked/try-first b/tests/data/scripts/stacked/try-first new file mode 100644 index 000000000000..3a14b7584bc1 --- /dev/null +++ b/tests/data/scripts/stacked/try-first @@ -0,0 +1,22 @@ +# Test try_first_pass without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = try_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/stacked/use-first b/tests/data/scripts/stacked/use-first new file mode 100644 index 000000000000..29c5c5c4188d --- /dev/null +++ b/tests/data/scripts/stacked/use-first @@ -0,0 +1,22 @@ +# Test use_first_pass without saving a ticket cache. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = use_first_pass no_ccache + account = no_ccache + session = no_ccache + +[run] + authenticate = PAM_SUCCESS + acct_mgmt = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + INFO user %u authenticated as %u diff --git a/tests/data/scripts/trace/supported b/tests/data/scripts/trace/supported new file mode 100644 index 000000000000..f67c389735ff --- /dev/null +++ b/tests/data/scripts/trace/supported @@ -0,0 +1,58 @@ +# Basic test of enabling trace logging. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache trace=%0 debug + account = no_ccache trace=%0 debug + session = no_ccache trace=%0 debug + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + setcred(DELETE_CRED) = PAM_SUCCESS + setcred(ESTABLISH_CRED) = PAM_SUCCESS + setcred(REFRESH_CRED) = PAM_SUCCESS + setcred(REINITIALIZE_CRED) = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + DEBUG enabled trace logging to %0 + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) no stored password + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) + DEBUG enabled trace logging to %0 + DEBUG pam_sm_acct_mgmt: entry + DEBUG skipping non-Kerberos login + DEBUG pam_sm_acct_mgmt: exit (ignore) + DEBUG enabled trace logging to %0 + DEBUG pam_sm_setcred: entry (delete) + DEBUG pam_sm_setcred: exit (success) + DEBUG enabled trace logging to %0 + DEBUG pam_sm_setcred: entry (establish) + DEBUG no context found, creating one + DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login + DEBUG pam_sm_setcred: exit (success) + DEBUG enabled trace logging to %0 + DEBUG pam_sm_setcred: entry (refresh) + DEBUG no context found, creating one + DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login + DEBUG pam_sm_setcred: exit (success) + DEBUG enabled trace logging to %0 + DEBUG pam_sm_setcred: entry (reinit) + DEBUG no context found, creating one + DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login + DEBUG pam_sm_setcred: exit (success) + DEBUG enabled trace logging to %0 + DEBUG pam_sm_open_session: entry + DEBUG pam_sm_open_session: exit (success) + DEBUG enabled trace logging to %0 + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/scripts/trace/unsupported b/tests/data/scripts/trace/unsupported new file mode 100644 index 000000000000..2100c34fc2f5 --- /dev/null +++ b/tests/data/scripts/trace/unsupported @@ -0,0 +1,52 @@ +# Basic test of attempting trace logging when not supported. -*- conf -*- +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2010-2011 +# The Board of Trustees of the Leland Stanford Junior University +# +# SPDX-License-Identifier: BSD-3-clause or GPL-1+ + +[options] + auth = force_first_pass no_ccache trace=%0 debug + account = no_ccache trace=%0 debug + session = no_ccache trace=%0 debug + +[run] + authenticate = PAM_AUTH_ERR + acct_mgmt = PAM_IGNORE + setcred(DELETE_CRED) = PAM_SUCCESS + setcred(ESTABLISH_CRED) = PAM_SUCCESS + setcred(REFRESH_CRED) = PAM_SUCCESS + setcred(REINITIALIZE_CRED) = PAM_SUCCESS + open_session = PAM_SUCCESS + close_session = PAM_SUCCESS + +[output] + ERR trace logging requested but not supported + DEBUG pam_sm_authenticate: entry + DEBUG (user %u) no stored password + NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost= + DEBUG pam_sm_authenticate: exit (failure) + ERR trace logging requested but not supported + DEBUG pam_sm_acct_mgmt: entry + DEBUG skipping non-Kerberos login + DEBUG pam_sm_acct_mgmt: exit (ignore) + ERR trace logging requested but not supported + DEBUG pam_sm_setcred: entry (delete) + DEBUG pam_sm_setcred: exit (success) + ERR trace logging requested but not supported + DEBUG pam_sm_setcred: entry (establish) + DEBUG pam_sm_setcred: exit (success) + ERR trace logging requested but not supported + DEBUG pam_sm_setcred: entry (refresh) + DEBUG pam_sm_setcred: exit (success) + ERR trace logging requested but not supported + DEBUG pam_sm_setcred: entry (reinit) + DEBUG pam_sm_setcred: exit (success) + ERR trace logging requested but not supported + DEBUG pam_sm_open_session: entry + DEBUG pam_sm_open_session: exit (success) + ERR trace logging requested but not supported + DEBUG pam_sm_close_session: entry + DEBUG pam_sm_close_session: exit (success) diff --git a/tests/data/valgrind.supp b/tests/data/valgrind.supp new file mode 100644 index 000000000000..6e987803f5e2 --- /dev/null +++ b/tests/data/valgrind.supp @@ -0,0 +1,242 @@ +# -*- conf -*- +# +# This is a valgrind suppression file for analysis of test suite results. +# +# Suppress a variety of apparent memory leaks in various Kerberos +# implementations due to one-time instantiation of data, and a few other +# artifacts of the test suite for rra-c-util portability and utility code +# and related software. +# +# The canonical version of this file is maintained in the rra-c-util package, +# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2017-2018, 2020 Russ Allbery <eagle@eyrie.org> +# Copyright 2011-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 + +{ + dlopen-dlerror + Memcheck:Leak + fun:calloc + fun:_dlerror_run +} +{ + fakeroot-msgsnd + Memcheck:Param + msgsnd(msgp->mtext) + fun:msgsnd + fun:send_fakem + fun:send_get_fakem + obj:*/libfakeroot-sysv.so +} +{ + heimdal-base-once + Memcheck:Leak + fun:*alloc + ... + fun:heim_base_once_f +} +{ + heimdal-gss-config + Memcheck:Leak + fun:*alloc + ... + fun:krb5_config_parse_debug +} +{ + heimdal-gss-config-2 + Memcheck:Leak + fun:*alloc + fun:_krb5_config_get_entry +} +{ + heimdal-gss-cred + Memcheck:Leak + fun:calloc + obj:*libgssapi.so.* + obj:*libgssapi.so.* + fun:gss_acquire_cred +} +{ + heimdal-gss-krb5-init + Memcheck:Leak + fun:*alloc + ... + fun:_gsskrb5_init +} +{ + heimdal-gss-load-mech + Memcheck:Leak + fun:*alloc + ... + fun:_gss_load_mech +} +{ + heimdal-krb5-init-context-once + Memcheck:Leak + fun:*alloc + ... + fun:init_context_once +} +{ + heimdal-krb5-reg-plugins-once + Memcheck:Leak + fun:*alloc + ... + fun:krb5_plugin_register + fun:reg_def_plugins_once +} +{ + heimdal-krb5-openssl-init + Memcheck:Leak + fun:*alloc + obj:* + fun:CRYPTO_*alloc +} +{ + mit-gss-ccache + Memcheck:Leak + fun:*alloc + fun:krb5int_setspecific + fun:kg_set_ccache_name + fun:gss_krb5int_ccache_name +} +{ + mit-gss-ccache-2 + Memcheck:Leak + fun:*alloc + fun:strdup + fun:kg_set_ccache_name + fun:gss_krb5int_ccache_name +} +{ + mit-gss-error + Memcheck:Leak + fun:*alloc + ... + fun:krb5_gss_save_error_string +} +{ + mit-gss-mechs + Memcheck:Leak + fun:glob + fun:loadConfigFiles + fun:updateMechList + fun:build_mechSet + fun:gss_indicate_mechs +} +{ + mit-kadmin-ovku-error + Memcheck:Leak + fun:*alloc* + fun:initialize_ovku_error_table_r +} +{ + mit-krb5-changepw + Memcheck:Leak + fun:*alloc + fun:change_set_password + fun:krb5_change_password + fun:krb5_get_init_creds_password +} +{ + mit-krb5-pkinit-openssl-init + Memcheck:Leak + fun:*alloc + ... + fun:krb5_init_preauth_context +} +{ + mit-krb5-pkinit-openssl-request + Memcheck:Leak + fun:*alloc + ... + fun:krb5_preauth_request_context_init +} +{ + mit-krb5-pkinit-openssl-request-2 + Memcheck:Leak + fun:*alloc + ... + fun:k5_preauth_request_context_init +} +{ + mit-krb5-plugin-dirs + Memcheck:Leak + fun:calloc + fun:krb5int_open_plugin_dirs +} +{ + mit-krb5-plugin-dlerror + Memcheck:Leak + fun:calloc + fun:_dlerror_run + ... + fun:krb5int_open_plugin +} +{ + mit-krb5-plugin-register + Memcheck:Leak + fun:malloc + fun:strdup + fun:register_module.isra.1 +} +{ + mit-krb5-preauth-init + Memcheck:Leak + fun:*alloc + ... + fun:k5_init_preauth_context +} +{ + mit-krb5-preauth-init + Memcheck:Leak + fun:strdup + fun:add_to_list + fun:profile_get_values + ... + fun:clpreauth_prep_questions +} +{ + mit-krb5-preauth-init-2 + Memcheck:Leak + fun:*alloc + fun:init_list + fun:profile_get_values + ... + fun:clpreauth_prep_questions +} +{ + mit-krb5-profile + Memcheck:Leak + fun:*alloc + ... + fun:profile_open_file +} +{ + portable-setenv + Memcheck:Leak + fun:malloc + fun:test_setenv +} diff --git a/tests/docs/pod-spelling-t b/tests/docs/pod-spelling-t new file mode 100755 index 000000000000..2ea5bf3ba6ee --- /dev/null +++ b/tests/docs/pod-spelling-t @@ -0,0 +1,55 @@ +#!/usr/bin/perl +# +# Checks all POD files in the tree for spelling errors using Test::Spelling. +# +# The canonical version of this file is maintained in the rra-c-util package, +# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2016, 2019, 2021 Russ Allbery <eagle@eyrie.org> +# Copyright 2012-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 + +use 5.010; +use strict; +use warnings; + +use lib "$ENV{C_TAP_SOURCE}/tap/perl"; + +use Test::RRA qw(skip_unless_author use_prereq); +use Test::RRA::Automake qw(automake_setup perl_dirs); + +use Test::More; + +# Only run this test for the module author since the required stopwords are +# too sensitive to the exact spell-checking program and dictionary. +skip_unless_author('Spelling tests'); + +# Load prerequisite modules. +use_prereq('Test::Spelling'); + +# Set up Automake testing. +automake_setup(); + +# Run the tests. +all_pod_files_spelling_ok(perl_dirs()); diff --git a/tests/docs/pod-t b/tests/docs/pod-t new file mode 100755 index 000000000000..be21ddf01cea --- /dev/null +++ b/tests/docs/pod-t @@ -0,0 +1,56 @@ +#!/usr/bin/perl +# +# Check all POD documents in the tree, except for any embedded Perl module +# distribution, for POD formatting errors. +# +# The canonical version of this file is maintained in the rra-c-util package, +# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2016, 2019, 2021 Russ Allbery <eagle@eyrie.org> +# Copyright 2012-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 + +use 5.010; +use strict; +use warnings; + +use lib "$ENV{C_TAP_SOURCE}/tap/perl"; + +use Test::RRA qw(skip_unless_automated use_prereq); +use Test::RRA::Automake qw(automake_setup perl_dirs); + +use Test::More; + +# Skip this test for normal user installs, since we normally pre-generate all +# of the documentation and the end user doesn't care. +skip_unless_automated('POD syntax tests'); + +# Load prerequisite modules. +use_prereq('Test::Pod'); + +# Set up Automake testing. +automake_setup(); + +# Run the tests. +all_pod_files_ok(perl_dirs()); diff --git a/tests/docs/spdx-license-t b/tests/docs/spdx-license-t new file mode 100755 index 000000000000..2841835fb69a --- /dev/null +++ b/tests/docs/spdx-license-t @@ -0,0 +1,149 @@ +#!/usr/bin/perl +# +# Check source files for SPDX-License-Identifier fields. +# +# Examine all source files in a distribution to check that they contain an +# SPDX-License-Identifier field. This does not check the syntax or whether +# the identifiers are valid. +# +# The canonical version of this file is maintained in the rra-c-util package, +# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. +# +# Copyright 2018-2021 Russ Allbery <eagle@eyrie.org> +# +# 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 + +use 5.010; +use strict; +use warnings; + +use lib "$ENV{C_TAP_SOURCE}/tap/perl"; + +use Test::RRA qw(skip_unless_automated); +use Test::RRA::Automake qw(all_files automake_setup); + +use File::Basename qw(basename); +use Test::More; + +# File name (the file without any directory component) and path patterns to +# skip for this check. +## no critic (RegularExpressions::ProhibitFixedStringMatches) +my @IGNORE = ( + qr{ \A LICENSE \z }xms, # Generated file with no license itself + qr{ \A (NEWS|THANKS|TODO) \z }xms, # Package license should be fine + qr{ \A README ( [.] .* )? \z }xms, # Package license should be fine + qr{ \A (Makefile|libtool) \z }xms, # Generated file + qr{ ~ \z }xms, # Backup files + qr{ [.] l?a \z }xms, # Created by libtool + qr{ [.] o \z }xms, # Compiler objects + qr{ [.] output \z }xms, # Test data +); +my @IGNORE_PATHS = ( + qr{ \A debian/ }xms, # Found in debian/* branches + qr{ \A docs/metadata/ }xms, # Package license should be fine + qr{ \A docs/protocol[.](html|txt) \z }xms, # Generated by xml2rfc + qr{ \A m4/ (libtool|lt.*) [.] m4 \z }xms, # Files from Libtool + qr{ \A perl/Build \z }xms, # Perl build files + qr{ \A perl/MANIFEST \z }xms, # Perl build files + qr{ \A perl/MYMETA [.] }xms, # Perl build files + qr{ \A perl/blib/ }xms, # Perl build files + qr{ \A perl/cover_db/ }xms, # Perl test files + qr{ \A perl/_build }xms, # Perl build files + qr{ \A php/Makefile [.] global \z }xms, # Created by phpize + qr{ \A php/autom4te [.] cache/ }xms, # Created by phpize + qr{ \A php/acinclude [.] m4 \z }xms, # Created by phpize + qr{ \A php/build/ }xms, # Created by phpize + qr{ \A php/config [.] (guess|sub) \z }xms, # Created by phpize + qr{ \A php/configure [.] (ac|in) \z }xms, # Created by phpize + qr{ \A php/ltmain [.] sh \z }xms, # Created by phpize + qr{ \A php/run-tests [.] php \z }xms, # Created by phpize + qr{ \A python/ .* [.] egg-info/ }xms, # Python build files + qr{ \A tests/config/ (?!README) }xms, # Test configuration + qr{ \A tests/tmp/ }xms, # Temporary test files +); +## use critic + +# Only run this test during automated testing, since failure doesn't indicate +# any user-noticable flaw in the package itself. +skip_unless_automated('SPDX identifier tests'); + +# Set up Automake testing. +automake_setup(); + +# Check a single file for an occurrence of the string. +# +# $path - Path to the file +# +# Returns: undef +sub check_file { + my ($path) = @_; + my $filename = basename($path); + + # Ignore files in the whitelist and binary files. + for my $pattern (@IGNORE) { + return if $filename =~ $pattern; + } + for my $pattern (@IGNORE_PATHS) { + return if $path =~ $pattern; + } + return if !-T $path; + + # Scan the file. + my ($saw_legacy_notice, $saw_spdx, $skip_spdx); + open(my $file, '<', $path) or BAIL_OUT("Cannot open $path: $!"); + while (defined(my $line = <$file>)) { + if ($line =~ m{ Generated [ ] by [ ] libtool [ ] }xms) { + close($file) or BAIL_OUT("Cannot close $path: $!"); + return; + } + if ($line =~ m{ \b See \s+ LICENSE \s+ for \s+ licensing }xms) { + $saw_legacy_notice = 1; + } + if ($line =~ m{ \b SPDX-License-Identifier: \s+ \S+ }xms) { + $saw_spdx = 1; + last; + } + if ($line =~ m{ no \s SPDX-License-Identifier \s registered }xms) { + $skip_spdx = 1; + last; + } + } + close($file) or BAIL_OUT("Cannot close $path: $!"); + + # If there is a legacy license notice, report a failure regardless of file + # size. Otherwise, skip files under 1KB. They can be rolled up into the + # overall project license and the license notice may be a substantial + # portion of the file size. + if ($saw_legacy_notice) { + ok(!$saw_legacy_notice, "$path has legacy license notice"); + } else { + ok($saw_spdx || $skip_spdx || -s $path < 1024, $path); + } + return; +} + +# Scan every file. We don't declare a plan since we skip a lot of files and +# don't want to precalculate the file list. +my @paths = all_files(); +for my $path (@paths) { + check_file($path); +} +done_testing(); diff --git a/tests/fakepam/README b/tests/fakepam/README new file mode 100644 index 000000000000..f7522cc1d66e --- /dev/null +++ b/tests/fakepam/README @@ -0,0 +1,276 @@ + PAM Testing Framework + +Overview + + The files in this directory provide a shim PAM library that's used for + testing and a test framework used to exercise a PAM module. + + This library and its include files define the minimum amount + of the PAM module interface so that PAM modules can be tested without + such problems as needing configuration files in /etc/pam.d or needing + changes to the system configuration to run a testing PAM module + instead of the normal system PAM modules. + + The goal of this library is that all PAM code should be able to be + left unchanged and the code just linked with the fakepam library + rather than the regular PAM library. The testing code can then call + pam_start and pam_end as defined in the fakepam/pam.h header file and + inspect internal PAM state as needed. + + The library also provides an interface to exercise a PAM module via an + interaction script, so that as much of the testing process as possible + is moved into simple text files instead of C code. That test script + format supports specifying the PAM configuration, the PAM interfaces + to run, the expected prompts and replies, and the expected log + messages. That interface is defined in fakepam/script.h. + +Fake PAM Library + + Unfortunately, the standard PAM library for most operating systems + does not provide a reasonable testing framework. The primary problem + is configuration: the PAM library usually hard-codes a configuration + location such as /etc/pam.conf or /etc/pam.d/<application>. But there + are other problems as well, such as capturing logging rather than + having it go to syslog and inspecting PAM internal state to make sure + that it's updated properly by the module. + + This library implements some of the same API as the system PAM library + and uses the system PAM library headers, but the underlying + implementation does not call the system PAM library or dynamically + load modules. Instead, it's meant to be linked into a single + executable along with the implementation of a PAM module. It does not + provide most of the application-level PAM interfaces (so one cannot + link a PAM-using application against it), just the interfaces called + by a module. The caller of the library can then call the module API + (such as pam_sm_authenticate) directly. + + All of the internal state maintained by the PAM library is made + available to the test program linked with this library. See + fakepam/pam.h for the data structures. This allows verification that + the PAM module is setting the internal PAM state properly. + + User Handling + + In order to write good test suites, one often has to be able to + authenticate as a variety of users, but PAM modules may expect the + authenticating user to exist on the system. The fakepam library + provides a pam_modutil_getpwnam (if available) or a getpwnam + implementation that returns information for a single user (and user + unknown for everyone else). To set the information for the one valid + user, call the pam_set_pwd function and provide a struct passwd that + will be returned by pam_modutil_getpwnam. + + The fakepam library also provides a replacement krb5_kuserok function + for testing PAM modules that use Kerberos. This source file should + only be included in packages that are building with Kerberos. It + implements the same functionality as the default krb5_kuserok + function, but looks for .k5login in the home directory configured by + the test framework instead of using getpwnam. + + Only those two functions are intercepted, so if the module looks up + users in other ways, it may still bypass the fakepam library and look + at system users. + + Output Handling + + The fakepam library intercepts the PAM functions that would normally + log to syslog and instead accumulates the output in a static string + variable. To retrieve the logging output so far, call pam_output, + which returns a struct of all the output strings up to that point and + resets the accumulated output. + +Scripted PAM Testing + + Also provided as part of the fakepam library is a test framework for + testing PAM modules. This test framework allows most of the testing + process to be encapsulated in a text configuration file per test, + rather than in a tedious set of checks and calls written in C. + + Test API + + The basic test API is to call either run_script (to run a single test + script) or run_script_dir (to run all scripts in a particular + directory). Both take a configuration struct that controls how the + PAM library is set up and called. + + That configuration struct takes the following elements: + + user + The user as which to authenticate, passed into pam_start and also + substituted for the %u escape. This should match the user whose + home directory information is configured using pam_set_pwd if that + function is in use. + + password + Only used for the %p escape. This is not used to set the + authentication token in the PAM library (see authtok below). + + newpass + Only used for the %n escape. + + extra + An array of up to 10 additional strings used by the %0 through %9 + escapes when parsing the configuration file, as discussed below. + + authtok + Sets the default value of the PAM_AUTHTOK data item. This will be + set immediately after initializing the PAM library and before + calling any PAM module functions. + + authtok + Like authtok, but for the PAM_OLDAUTHTOK data item. + + callback + This, and the associated data element, specifies a callback that's + called at the end of processing of the script before calling + pam_end. This can be used to inspect and verify the internal + state of PAM. The data element is an opaque pointer passed into + the callback. + + Test Script Basic Format + + Test scripts are composed of one or more sections. Each section + begins with: + + [<section>] + + starting in column 1, where <section> is the name of the section. The + valid section types and the format of their contents are described + below. + + Blank lines and lines starting with # are ignored. + + Several strings undergo %-escape expansion as mentioned below. For + any such string, the following escapes are supported: + + %i Current UID (not the UID of the target user) + %n New password + %p Password + %u Username + %0 extra[0] + ... + %9 extra[9] + + All of these are set in the script_config struct. + + Regular expression matching is supported for output lines and for + prompts. To mark an expected prompt or output line as a regular + expression, it must begin and end with a slash (/). Slashes inside + the regular expression do not need to be escaped. If regular + expression support is not available in the C library, those matching + tests will be skipped. + + The [options] Section + + The [options] section contains the PAM configuration that will be + passed to the module. These are the options that are normally listed + in the PAM configuration file after the name of the module. The + syntax of this section is one or more lines of the form: + + <group> = <options> + + where <group> is one of "account", "auth", "password", or "session". + The options are space-delimited and may be either option names or + option=value pairs. + + The [run] Section + + The [run] section specifies what PAM interfaces to call. It consists + of one or more lines in the format: + + <call> = <status> + + where <call> is the PAM call to make and <status> is the status code + that it should return. <call> is one of the PAM module interface + functions without the leading "pam_sm_", so one of "acct_mgmt", + "authenticate", "setcred", "chauthtok", "open_session", or + "close_session". The return status is one of the PAM constants + defined for return status, such as PAM_IGNORE or PAM_SUCCESS. The + test framework will ensure that the PAM call returns the appropriate + status. + + The <call> may be optionally followed by an open parentheses and then + a list of flags separated by |, or syntactically: + + <call>(<flag>|<flag>|...) = <status> + + In this form, rather than passing a flags value of 0 to the PAM call, + the test framework will pass the combination of the provided flags. + The flags are PAM constants without the leading PAM_, so (for example) + DELETE_CRED, ESTABLISH_CRED, REFRESH_CRED, or REINITIALIZE_CRED for + the "setcred" call. + + As a special case, <call> may be "end" to specify flags to pass to the + pam_end call (such as PAM_DATA_SILENT). + + The [end] Section + + The [end] section defines how to call pam_end. It currently takes + only one setting, flags, the syntax of which is: + + flags = <flag>|<flag> + + This allows PAM_DATA_SILENT or other flags to be passed to pam_end + when running the test script. + + The [output] Section + + The [output] section defines the logging output expected from the + module. It consists of zero or more lines in the format: + + <priority> <output> + <priority> /<regex>/ + + where <priority> is a syslog priority and <output> is the remaining + output or a regular expression to match against the output. Valid + values for <priority> are DEBUG, INFO, NOTICE, ERR, and CRIT. + <output> and <regex> may contain spaces and undergoes %-escape + expansion. + + The replacement values are taken from the script_config struct passed + as a parameter to run_script or run_script_dir. + + If the [output] section is missing entirely, the test framework will + expect there to be no logging output from the PAM module. + + This defines the logging output, not the prompts returned through the + conversation function. For that, see the next section. + + The [prompts] Section + + The [prompts] section defines the prompts that the PAM module is + expected to send via the conversation function, and the responses that + the test harness will send back (if any). This consists of zero or + more lines in one of the following formats: + + <type> = <prompt> + <type> = /<prompt>/ + <type> = <prompt>|<response> + <type> = /<prompt>/|<response> + + The <type> is the style of prompt, chosen from "echo_off", "echo_on", + "error_msg", and "info". The <prompt> is the actual prompt sent and + undergoes %-escape expansion. It may be enclosed in slashes (/) to + indicate that it's a regular expression instead of literal text. The + <response> if present (and its presence is signaled by the | + character) contains the response sent back by the test framework and + also undergoes %-escape expansion. The response starts with the final + | character on the line, so <prompt> regular expressions may freely + use | inside the regular expression. + + If the [prompts] section is present and empty, the test harness will + check that the PAM module does not send any prompts. If the [prompts] + section is absent entirely, the conversation function passed to the + PAM module will be NULL. + +License + + This file is part of the documentation of rra-c-util, which can be + found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + + Copyright 2011-2012, 2020-2021 Russ Allbery <eagle@eyrie.org> + + Copying and distribution of this file, with or without modification, + are permitted in any medium without royalty provided the copyright + notice and this notice are preserved. This file is offered as-is, + without any warranty. diff --git a/tests/fakepam/config.c b/tests/fakepam/config.c new file mode 100644 index 000000000000..8e0685604d55 --- /dev/null +++ b/tests/fakepam/config.c @@ -0,0 +1,766 @@ +/* + * Run a PAM interaction script for testing. + * + * Provides an interface that loads a PAM interaction script from a file and + * runs through that script, calling the internal PAM module functions and + * checking their results. This allows automation of PAM testing through + * external data files instead of coding everything in C. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2017-2018, 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012, 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <assert.h> +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <syslog.h> + +#include <tests/fakepam/internal.h> +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/string.h> + +/* Used for enumerating arrays. */ +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +/* Mapping of strings to PAM function pointers and group numbers. */ +static const struct { + const char *name; + pam_call call; + enum group_type group; +} CALLS[] = { + /* clang-format off */ + {"acct_mgmt", pam_sm_acct_mgmt, GROUP_ACCOUNT }, + {"authenticate", pam_sm_authenticate, GROUP_AUTH }, + {"setcred", pam_sm_setcred, GROUP_AUTH }, + {"chauthtok", pam_sm_chauthtok, GROUP_PASSWORD}, + {"open_session", pam_sm_open_session, GROUP_SESSION }, + {"close_session", pam_sm_close_session, GROUP_SESSION }, + /* clang-format on */ +}; + +/* Mapping of PAM flag names without the leading PAM_ to values. */ +static const struct { + const char *name; + int value; +} FLAGS[] = { + /* clang-format off */ + {"CHANGE_EXPIRED_AUTHTOK", PAM_CHANGE_EXPIRED_AUTHTOK}, + {"DELETE_CRED", PAM_DELETE_CRED }, + {"DISALLOW_NULL_AUTHTOK", PAM_DISALLOW_NULL_AUTHTOK }, + {"ESTABLISH_CRED", PAM_ESTABLISH_CRED }, + {"PRELIM_CHECK", PAM_PRELIM_CHECK }, + {"REFRESH_CRED", PAM_REFRESH_CRED }, + {"REINITIALIZE_CRED", PAM_REINITIALIZE_CRED }, + {"SILENT", PAM_SILENT }, + {"UPDATE_AUTHTOK", PAM_UPDATE_AUTHTOK }, + /* clang-format on */ +}; + +/* Mapping of strings to PAM groups. */ +static const struct { + const char *name; + enum group_type group; +} GROUPS[] = { + /* clang-format off */ + {"account", GROUP_ACCOUNT }, + {"auth", GROUP_AUTH }, + {"password", GROUP_PASSWORD}, + {"session", GROUP_SESSION }, + /* clang-format on */ +}; + +/* Mapping of strings to PAM return values. */ +static const struct { + const char *name; + int status; +} RETURNS[] = { + /* clang-format off */ + {"PAM_AUTH_ERR", PAM_AUTH_ERR }, + {"PAM_AUTHINFO_UNAVAIL", PAM_AUTHINFO_UNAVAIL}, + {"PAM_AUTHTOK_ERR", PAM_AUTHTOK_ERR }, + {"PAM_DATA_SILENT", PAM_DATA_SILENT }, + {"PAM_IGNORE", PAM_IGNORE }, + {"PAM_NEW_AUTHTOK_REQD", PAM_NEW_AUTHTOK_REQD}, + {"PAM_SESSION_ERR", PAM_SESSION_ERR }, + {"PAM_SUCCESS", PAM_SUCCESS }, + {"PAM_USER_UNKNOWN", PAM_USER_UNKNOWN }, + /* clang-format on */ +}; + +/* Mapping of PAM prompt styles to their values. */ +static const struct { + const char *name; + int style; +} STYLES[] = { + /* clang-format off */ + {"echo_off", PAM_PROMPT_ECHO_OFF}, + {"echo_on", PAM_PROMPT_ECHO_ON }, + {"error_msg", PAM_ERROR_MSG }, + {"info", PAM_TEXT_INFO }, + /* clang-format on */ +}; + +/* Mappings of strings to syslog priorities. */ +static const struct { + const char *name; + int priority; +} PRIORITIES[] = { + /* clang-format off */ + {"DEBUG", LOG_DEBUG }, + {"INFO", LOG_INFO }, + {"NOTICE", LOG_NOTICE}, + {"ERR", LOG_ERR }, + {"CRIT", LOG_CRIT }, + /* clang-format on */ +}; + + +/* + * Given a pointer to a string, skip any leading whitespace and return a + * pointer to the first non-whitespace character. + */ +static char * +skip_whitespace(char *p) +{ + while (isspace((unsigned char) (*p))) + p++; + return p; +} + + +/* + * Read a line from a file into a BUFSIZ buffer, failing if the line was too + * long to fit into the buffer, and returns a copy of that line in newly + * allocated memory. Ignores blank lines and comments. Caller is responsible + * for freeing. Returns NULL on end of file and fails on read errors. + */ +static char * +readline(FILE *file) +{ + char buffer[BUFSIZ]; + char *line, *first; + + do { + line = fgets(buffer, sizeof(buffer), file); + if (line == NULL) { + if (feof(file)) + return NULL; + sysbail("cannot read line from script"); + } + if (buffer[strlen(buffer) - 1] != '\n') + bail("script line too long"); + buffer[strlen(buffer) - 1] = '\0'; + first = skip_whitespace(buffer); + } while (first[0] == '#' || first[0] == '\0'); + line = bstrdup(buffer); + return line; +} + + +/* + * Given the name of a PAM call, map it to a call enum. This is used later in + * switch statements to determine which function to call. Fails on any + * unrecognized string. If the optional second argument is not NULL, also + * store the group number in that argument. + */ +static pam_call +string_to_call(const char *name, enum group_type *group) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(CALLS); i++) + if (strcmp(name, CALLS[i].name) == 0) { + if (group != NULL) + *group = CALLS[i].group; + return CALLS[i].call; + } + bail("unrecognized PAM call %s", name); +} + + +/* + * Given a PAM flag value without the leading PAM_, map it to the numeric + * value of that flag. Fails on any unrecognized string. + */ +static int +string_to_flag(const char *name) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(FLAGS); i++) + if (strcmp(name, FLAGS[i].name) == 0) + return FLAGS[i].value; + bail("unrecognized PAM flag %s", name); +} + + +/* + * Given a PAM group name, map it to the array index for the options array for + * that group. Fails on any unrecognized string. + */ +static enum group_type +string_to_group(const char *name) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(GROUPS); i++) + if (strcmp(name, GROUPS[i].name) == 0) + return GROUPS[i].group; + bail("unrecognized PAM group %s", name); +} + + +/* + * Given a syslog priority name, map it to the numeric value of that priority. + * Fails on any unrecognized string. + */ +static int +string_to_priority(const char *name) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(PRIORITIES); i++) + if (strcmp(name, PRIORITIES[i].name) == 0) + return PRIORITIES[i].priority; + bail("unrecognized syslog priority %s", name); +} + + +/* + * Given a PAM return status, map it to the actual expected value. Fails on + * any unrecognized string. + */ +static int +string_to_status(const char *name) +{ + size_t i; + + if (name == NULL) + bail("no PAM status on line"); + for (i = 0; i < ARRAY_SIZE(RETURNS); i++) + if (strcmp(name, RETURNS[i].name) == 0) + return RETURNS[i].status; + bail("unrecognized PAM status %s", name); +} + + +/* + * Given a PAM prompt style value without the leading PAM_PROMPT_, map it to + * the numeric value of that flag. Fails on any unrecognized string. + */ +static int +string_to_style(const char *name) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(STYLES); i++) + if (strcmp(name, STYLES[i].name) == 0) + return STYLES[i].style; + bail("unrecognized PAM prompt style %s", name); +} + + +/* + * We found a section delimiter while parsing another section. Rewind our + * input file back before the section delimiter so that we'll read it again. + * Takes the length of the line we read, which is used to determine how far to + * rewind. + */ +static void +rewind_section(FILE *script, size_t length) +{ + if (fseek(script, -length - 1, SEEK_CUR) != 0) + sysbail("cannot rewind file"); +} + + +/* + * Given a string that may contain %-escapes, expand it into the resulting + * value. The following escapes are supported: + * + * %i current UID (not target user UID) + * %n new password + * %p password + * %u username + * %0 user-supplied string + * ... + * %9 user-supplied string + * + * The %* escape is preserved as-is, as it has to be interpreted at the time + * of checking output. Returns the expanded string in newly-allocated memory. + */ +static char * +expand_string(const char *template, const struct script_config *config) +{ + size_t length = 0; + const char *p, *extra; + char *output, *out; + char *uid = NULL; + + length = 0; + for (p = template; *p != '\0'; p++) { + if (*p != '%') + length++; + else { + p++; + switch (*p) { + case 'i': + if (uid == NULL) + basprintf(&uid, "%lu", (unsigned long) getuid()); + length += strlen(uid); + break; + case 'n': + if (config->newpass == NULL) + bail("new password not set"); + length += strlen(config->newpass); + break; + case 'p': + if (config->password == NULL) + bail("password not set"); + length += strlen(config->password); + break; + case 'u': + length += strlen(config->user); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (config->extra[*p - '0'] == NULL) + bail("extra script parameter %%%c not set", *p); + length += strlen(config->extra[*p - '0']); + break; + case '*': + length += 2; + break; + default: + length++; + break; + } + } + } + output = bmalloc(length + 1); + for (p = template, out = output; *p != '\0'; p++) { + if (*p != '%') + *out++ = *p; + else { + p++; + switch (*p) { + case 'i': + assert(uid != NULL); + memcpy(out, uid, strlen(uid)); + out += strlen(uid); + break; + case 'n': + memcpy(out, config->newpass, strlen(config->newpass)); + out += strlen(config->newpass); + break; + case 'p': + memcpy(out, config->password, strlen(config->password)); + out += strlen(config->password); + break; + case 'u': + memcpy(out, config->user, strlen(config->user)); + out += strlen(config->user); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + extra = config->extra[*p - '0']; + memcpy(out, extra, strlen(extra)); + out += strlen(extra); + break; + case '*': + *out++ = '%'; + *out++ = '*'; + break; + default: + *out++ = *p; + break; + } + } + } + *out = '\0'; + free(uid); + return output; +} + + +/* + * Given a whitespace-delimited string of PAM options, split it into an argv + * array and argc count and store it in the provided option struct. + */ +static void +split_options(char *string, struct options *options, + const struct script_config *config) +{ + char *opt; + size_t size, count; + + for (opt = strtok(string, " "); opt != NULL; opt = strtok(NULL, " ")) { + if (options->argv == NULL) { + options->argv = bcalloc(2, sizeof(const char *)); + options->argv[0] = expand_string(opt, config); + options->argc = 1; + } else { + count = (options->argc + 2); + size = sizeof(const char *); + options->argv = breallocarray(options->argv, count, size); + options->argv[options->argc] = expand_string(opt, config); + options->argv[options->argc + 1] = NULL; + options->argc++; + } + } +} + + +/* + * Parse the options section of a PAM script. This consists of one or more + * lines in the format: + * + * <group> = <options> + * + * where options are either option names or option=value pairs, where the + * value may not contain whitespace. Returns an options struct, which stores + * argc and argv values for each group. + * + * Takes the work struct as an argument and puts values into its array. + */ +static void +parse_options(FILE *script, struct work *work, + const struct script_config *config) +{ + char *line, *group, *token; + size_t length = 0; + enum group_type type; + + for (line = readline(script); line != NULL; line = readline(script)) { + length = strlen(line); + group = strtok(line, " "); + if (group == NULL) + bail("malformed script line"); + if (group[0] == '[') + break; + type = string_to_group(group); + token = strtok(NULL, " "); + if (token == NULL) + bail("malformed action line"); + if (strcmp(token, "=") != 0) + bail("malformed action line near %s", token); + token = strtok(NULL, ""); + split_options(token, &work->options[type], config); + free(line); + } + if (line != NULL) { + free(line); + rewind_section(script, length); + } +} + + +/* + * Parse the call portion of a PAM call in the run section of a PAM script. + * This handles parsing the PAM flags that optionally may be given as part of + * the call. Takes the token representing the call and a pointer to the + * action struct to fill in with the call and the option flags. + */ +static void +parse_call(char *token, struct action *action) +{ + char *flags, *flag; + + action->flags = 0; + flags = strchr(token, '('); + if (flags != NULL) { + *flags = '\0'; + flags++; + for (flag = strtok(flags, "|,)"); flag != NULL; + flag = strtok(NULL, "|,)")) { + action->flags |= string_to_flag(flag); + } + } + action->call = string_to_call(token, &action->group); +} + + +/* + * Parse the run section of a PAM script. This consists of one or more lines + * in the format: + * + * <call> = <status> + * + * where <call> is a PAM call and <status> is what it should return. Returns + * a linked list of actions. Fails on any error in parsing. + */ +static struct action * +parse_run(FILE *script) +{ + struct action *head = NULL, *current = NULL, *next; + char *line, *token, *call; + size_t length = 0; + + for (line = readline(script); line != NULL; line = readline(script)) { + length = strlen(line); + token = strtok(line, " "); + if (token[0] == '[') + break; + next = bmalloc(sizeof(struct action)); + next->next = NULL; + if (head == NULL) + head = next; + else + current->next = next; + next->name = bstrdup(token); + call = token; + token = strtok(NULL, " "); + if (token == NULL) + bail("malformed action line"); + if (strcmp(token, "=") != 0) + bail("malformed action line near %s", token); + token = strtok(NULL, " "); + next->status = string_to_status(token); + parse_call(call, next); + free(line); + current = next; + } + if (head == NULL) + bail("empty run section in script"); + if (line != NULL) { + free(line); + rewind_section(script, length); + } + return head; +} + + +/* + * Parse the end section of a PAM script. There is one supported line in the + * format: + * + * flags = <flag>|<flag> + * + * where <flag> is a flag to pass to pam_end. Returns the flags. + */ +static int +parse_end(FILE *script) +{ + char *line, *token, *flag; + size_t length = 0; + int flags = PAM_SUCCESS; + + for (line = readline(script); line != NULL; line = readline(script)) { + length = strlen(line); + token = strtok(line, " "); + if (token[0] == '[') + break; + if (strcmp(token, "flags") != 0) + bail("unknown end setting %s", token); + token = strtok(NULL, " "); + if (token == NULL) + bail("malformed end line"); + if (strcmp(token, "=") != 0) + bail("malformed end line near %s", token); + token = strtok(NULL, " "); + flag = strtok(token, "|"); + while (flag != NULL) { + flags |= string_to_status(flag); + flag = strtok(NULL, "|"); + } + free(line); + } + if (line != NULL) { + free(line); + rewind_section(script, length); + } + return flags; +} + + +/* + * Parse the output section of a PAM script. This consists of zero or more + * lines in the format: + * + * PRIORITY some output information + * PRIORITY /output regex/ + * + * where PRIORITY is replaced by the numeric syslog priority corresponding to + * that priority and the rest of the output undergoes %-esacape expansion. + * Returns the accumulated output as a vector. + */ +static struct output * +parse_output(FILE *script, const struct script_config *config) +{ + char *line, *token, *message; + struct output *output; + int priority; + + output = output_new(); + if (output == NULL) + sysbail("cannot allocate vector"); + for (line = readline(script); line != NULL; line = readline(script)) { + token = strtok(line, " "); + priority = string_to_priority(token); + token = strtok(NULL, ""); + if (token == NULL) + bail("malformed line %s", line); + message = expand_string(token, config); + output_add(output, priority, message); + free(message); + free(line); + } + return output; +} + + +/* + * Parse the prompts section of a PAM script. This consists of zero or more + * lines in one of the formats: + * + * type = prompt + * type = /prompt/ + * type = prompt|response + * type = /prompt/|response + * + * If the type is error_msg or info, there is no response. Otherwise, + * everything after the last | is taken to be the response that should be + * provided to that prompt. The response undergoes %-escape expansion. + */ +static struct prompts * +parse_prompts(FILE *script, const struct script_config *config) +{ + struct prompts *prompts = NULL; + struct prompt *prompt; + char *line, *token, *style, *end; + size_t size, count, i; + size_t length = 0; + + for (line = readline(script); line != NULL; line = readline(script)) { + length = strlen(line); + token = strtok(line, " "); + if (token[0] == '[') + break; + if (prompts == NULL) { + prompts = bcalloc(1, sizeof(struct prompts)); + prompts->prompts = bcalloc(1, sizeof(struct prompt)); + prompts->allocated = 1; + } else if (prompts->allocated == prompts->size) { + count = prompts->allocated * 2; + size = sizeof(struct prompt); + prompts->prompts = breallocarray(prompts->prompts, count, size); + prompts->allocated = count; + for (i = prompts->size; i < prompts->allocated; i++) { + prompts->prompts[i].prompt = NULL; + prompts->prompts[i].response = NULL; + } + } + prompt = &prompts->prompts[prompts->size]; + style = token; + token = strtok(NULL, " "); + if (token == NULL) + bail("malformed prompt line"); + if (strcmp(token, "=") != 0) + bail("malformed prompt line near %s", token); + prompt->style = string_to_style(style); + token = strtok(NULL, ""); + if (prompt->style == PAM_ERROR_MSG || prompt->style == PAM_TEXT_INFO) + prompt->prompt = expand_string(token, config); + else { + end = strrchr(token, '|'); + if (end == NULL) + bail("malformed prompt line near %s", token); + *end = '\0'; + prompt->prompt = expand_string(token, config); + token = end + 1; + prompt->response = expand_string(token, config); + } + prompts->size++; + free(line); + } + if (line != NULL) { + free(line); + rewind_section(script, length); + } + return prompts; +} + + +/* + * Parse a PAM interaction script. This handles parsing of the top-level + * section markers and dispatches the parsing to other functions. Returns the + * total work to do as a work struct. + */ +struct work * +parse_script(FILE *script, const struct script_config *config) +{ + struct work *work; + char *line, *token; + + work = bmalloc(sizeof(struct work)); + memset(work, 0, sizeof(struct work)); + work->end_flags = PAM_SUCCESS; + for (line = readline(script); line != NULL; line = readline(script)) { + token = strtok(line, " "); + if (token[0] != '[') + bail("line outside of section: %s", line); + if (strcmp(token, "[options]") == 0) + parse_options(script, work, config); + else if (strcmp(token, "[run]") == 0) + work->actions = parse_run(script); + else if (strcmp(token, "[end]") == 0) + work->end_flags = parse_end(script); + else if (strcmp(token, "[output]") == 0) + work->output = parse_output(script, config); + else if (strcmp(token, "[prompts]") == 0) + work->prompts = parse_prompts(script, config); + else + bail("unknown section: %s", token); + free(line); + } + if (work->actions == NULL) + bail("no run section defined"); + return work; +} diff --git a/tests/fakepam/data.c b/tests/fakepam/data.c new file mode 100644 index 000000000000..0650d59e9b75 --- /dev/null +++ b/tests/fakepam/data.c @@ -0,0 +1,356 @@ +/* + * Data manipulation functions for the fake PAM library, used for testing. + * + * This file contains the implementation of pam_get_* and pam_set_* for the + * various data items supported by the PAM library, plus the PAM environment + * manipulation functions. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2010-2011, 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <tests/fakepam/pam.h> + +/* Used for unused parameters to silence gcc warnings. */ +#define UNUSED __attribute__((__unused__)) + + +/* + * Return a stored PAM data element in the provided data variable. As a + * special case, if the data is NULL, pretend it doesn't exist. + */ +int +pam_get_data(const pam_handle_t *pamh, const char *name, const void **data) +{ + struct fakepam_data *item; + + for (item = pamh->data; item != NULL; item = item->next) + if (strcmp(item->name, name) == 0) { + if (item->data == NULL) + return PAM_NO_MODULE_DATA; + *data = item->data; + return PAM_SUCCESS; + } + return PAM_NO_MODULE_DATA; +} + + +/* + * Store a data item. Replaces the existing data item (calling its cleanup) + * if it is already set; otherwise, add a new data item. + */ +int +pam_set_data(pam_handle_t *pamh, const char *item, void *data, + void (*cleanup)(pam_handle_t *, void *, int)) +{ + struct fakepam_data *p; + + for (p = pamh->data; p != NULL; p = p->next) + if (strcmp(p->name, item) == 0) { + if (p->cleanup != NULL) + p->cleanup(pamh, p->data, PAM_DATA_REPLACE); + p->data = data; + p->cleanup = cleanup; + return PAM_SUCCESS; + } + p = malloc(sizeof(struct fakepam_data)); + if (p == NULL) + return PAM_BUF_ERR; + p->name = strdup(item); + if (p->name == NULL) { + free(p); + return PAM_BUF_ERR; + } + p->data = data; + p->cleanup = cleanup; + p->next = pamh->data; + pamh->data = p; + return PAM_SUCCESS; +} + + +/* + * Retrieve a PAM item. Currently, this only supports a limited subset of the + * possible items. + */ +int +pam_get_item(const pam_handle_t *pamh, int item, PAM_CONST void **data) +{ + switch (item) { + case PAM_AUTHTOK: + *data = pamh->authtok; + return PAM_SUCCESS; + case PAM_CONV: + if (pamh->conversation) { + *data = pamh->conversation; + return PAM_SUCCESS; + } else { + return PAM_BAD_ITEM; + } + case PAM_OLDAUTHTOK: + *data = pamh->oldauthtok; + return PAM_SUCCESS; + case PAM_RHOST: + *data = (PAM_CONST char *) pamh->rhost; + return PAM_SUCCESS; + case PAM_RUSER: + *data = (PAM_CONST char *) pamh->ruser; + return PAM_SUCCESS; + case PAM_SERVICE: + *data = (PAM_CONST char *) pamh->service; + return PAM_SUCCESS; + case PAM_TTY: + *data = (PAM_CONST char *) pamh->tty; + return PAM_SUCCESS; + case PAM_USER: + *data = (PAM_CONST char *) pamh->user; + return PAM_SUCCESS; + case PAM_USER_PROMPT: + *data = "login: "; + return PAM_SUCCESS; + default: + return PAM_BAD_ITEM; + } +} + + +/* + * Set a PAM item. Currently only PAM_USER is supported. + */ +int +pam_set_item(pam_handle_t *pamh, int item, PAM_CONST void *data) +{ + switch (item) { + case PAM_AUTHTOK: + free(pamh->authtok); + pamh->authtok = strdup(data); + if (pamh->authtok == NULL) + return PAM_BUF_ERR; + return PAM_SUCCESS; + case PAM_OLDAUTHTOK: + free(pamh->oldauthtok); + pamh->oldauthtok = strdup(data); + if (pamh->oldauthtok == NULL) + return PAM_BUF_ERR; + return PAM_SUCCESS; + case PAM_RHOST: + free(pamh->rhost); + pamh->rhost = strdup(data); + if (pamh->rhost == NULL) + return PAM_BUF_ERR; + return PAM_SUCCESS; + case PAM_RUSER: + free(pamh->ruser); + pamh->ruser = strdup(data); + if (pamh->ruser == NULL) + return PAM_BUF_ERR; + return PAM_SUCCESS; + case PAM_TTY: + free(pamh->tty); + pamh->tty = strdup(data); + if (pamh->tty == NULL) + return PAM_BUF_ERR; + return PAM_SUCCESS; + case PAM_USER: + pamh->user = (const char *) data; + return PAM_SUCCESS; + default: + return PAM_BAD_ITEM; + } +} + + +/* + * Return the user for the PAM context. + */ +int +pam_get_user(pam_handle_t *pamh, PAM_CONST char **user, + const char *prompt UNUSED) +{ + if (pamh->user == NULL) + return PAM_CONV_ERR; + else { + *user = (PAM_CONST char *) pamh->user; + return PAM_SUCCESS; + } +} + + +/* + * Return a setting in the PAM environment. + */ +PAM_CONST char * +pam_getenv(pam_handle_t *pamh, const char *name) +{ + size_t i; + + if (pamh->environ == NULL) + return NULL; + for (i = 0; pamh->environ[i] != NULL; i++) + if (strncmp(name, pamh->environ[i], strlen(name)) == 0 + && pamh->environ[i][strlen(name)] == '=') + return pamh->environ[i] + strlen(name) + 1; + return NULL; +} + + +/* + * Return a newly malloc'd copy of the complete PAM environment. This must be + * freed by the caller. + */ +char ** +pam_getenvlist(pam_handle_t *pamh) +{ + char **env; + size_t i; + + if (pamh->environ == NULL) { + pamh->environ = malloc(sizeof(char *)); + if (pamh->environ == NULL) + return NULL; + pamh->environ[0] = NULL; + } + for (i = 0; pamh->environ[i] != NULL; i++) + ; + env = calloc(i + 1, sizeof(char *)); + if (env == NULL) + return NULL; + for (i = 0; pamh->environ[i] != NULL; i++) { + env[i] = strdup(pamh->environ[i]); + if (env[i] == NULL) + goto fail; + } + env[i] = NULL; + return env; + +fail: + for (i = 0; env[i] != NULL; i++) + free(env[i]); + free(env); + return NULL; +} + + +/* + * Add a setting to the PAM environment. If there is another existing + * variable with the same value, the value is replaced, unless the setting + * doesn't end in an equal sign. If it doesn't end in an equal sign, any + * existing environment variable of that name is removed. This follows the + * Linux PAM semantics. + * + * On HP-UX, there is no separate PAM environment, so the module just uses the + * main environment. For our tests to work on that platform, we therefore + * have to do the same thing. + */ +#ifdef HAVE_PAM_GETENV +int +pam_putenv(pam_handle_t *pamh, const char *setting) +{ + char *copy = NULL; + const char *equals; + size_t namelen; + bool delete = false; + bool found = false; + size_t i, j; + char **env; + + equals = strchr(setting, '='); + if (equals != NULL) + namelen = equals - setting; + else { + delete = true; + namelen = strlen(setting); + } + if (!delete) { + copy = strdup(setting); + if (copy == NULL) + return PAM_BUF_ERR; + } + + /* Handle the first call to pam_putenv. */ + if (pamh->environ == NULL) { + if (delete) + return PAM_BAD_ITEM; + pamh->environ = calloc(2, sizeof(char *)); + if (pamh->environ == NULL) { + free(copy); + return PAM_BUF_ERR; + } + pamh->environ[0] = copy; + pamh->environ[1] = NULL; + return PAM_SUCCESS; + } + + /* + * We have an existing array. See if we're replacing a value, deleting a + * value, or adding a new one. When deleting, waste a bit of memory but + * save some time by not bothering to reduce the size of the array. + */ + for (i = 0; pamh->environ[i] != NULL; i++) + if (strncmp(setting, pamh->environ[i], namelen) == 0 + && pamh->environ[i][namelen] == '=') { + if (delete) { + free(pamh->environ[i]); + for (j = i + 1; pamh->environ[j] != NULL; i++, j++) + pamh->environ[i] = pamh->environ[j]; + pamh->environ[i] = NULL; + } else { + free(pamh->environ[i]); + pamh->environ[i] = copy; + } + found = true; + break; + } + if (!found) { + if (delete) + return PAM_BAD_ITEM; + env = reallocarray(pamh->environ, (i + 2), sizeof(char *)); + if (env == NULL) { + free(copy); + return PAM_BUF_ERR; + } + pamh->environ = env; + pamh->environ[i] = copy; + pamh->environ[i + 1] = NULL; + } + return PAM_SUCCESS; +} + +#else /* !HAVE_PAM_GETENV */ + +int +pam_putenv(pam_handle_t *pamh UNUSED, const char *setting) +{ + return putenv((char *) setting); +} + +#endif /* !HAVE_PAM_GETENV */ diff --git a/tests/fakepam/general.c b/tests/fakepam/general.c new file mode 100644 index 000000000000..0f11bb2f7995 --- /dev/null +++ b/tests/fakepam/general.c @@ -0,0 +1,151 @@ +/* + * Interface for fake PAM library, used for testing. + * + * This contains the basic public interfaces for the fake PAM library, used + * for testing, and some general utility functions. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2010-2011, 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <errno.h> +#include <pwd.h> + +#include <tests/fakepam/pam.h> + +/* Stores the static struct passwd returned by getpwnam if the name matches. */ +static struct passwd *pwd_info = NULL; + +/* Used for unused parameters to silence gcc warnings. */ +#define UNUSED __attribute__((__unused__)) + + +/* + * Initializes the pam_handle_t data structure. This function is only called + * from test programs, not from any of the module code. We can put anything + * we want in this structure, since it's opaque to the regular code. + */ +int +pam_start(const char *service_name, const char *user, + const struct pam_conv *pam_conversation, pam_handle_t **pamh) +{ + struct pam_handle *handle; + + handle = calloc(1, sizeof(struct pam_handle)); + if (handle == NULL) + return PAM_BUF_ERR; + handle->service = service_name; + handle->user = user; + handle->conversation = pam_conversation; + *pamh = handle; + return PAM_SUCCESS; +} + + +/* + * Free the pam_handle_t data structure and related resources. This is + * important to test the data cleanups. Freeing the memory is not strictly + * required since it's only used for testing, but it helps keep our memory + * usage clean so that we can run the test suite under valgrind. + */ +int +pam_end(pam_handle_t *pamh, int status) +{ + struct fakepam_data *item, *next; + size_t i; + + if (pamh->environ != NULL) { + for (i = 0; pamh->environ[i] != NULL; i++) + free(pamh->environ[i]); + free(pamh->environ); + } + free(pamh->authtok); + free(pamh->oldauthtok); + free(pamh->rhost); + free(pamh->ruser); + free(pamh->tty); + for (item = pamh->data; item != NULL;) { + if (item->cleanup != NULL) + item->cleanup(pamh, item->data, status); + free(item->name); + next = item->next; + free(item); + item = next; + } + free(pamh); + return PAM_SUCCESS; +} + + +/* + * Interface specific to this fake PAM library to set the struct passwd that's + * returned by getpwnam queries if the name matches. + */ +void +pam_set_pwd(struct passwd *pwd) +{ + pwd_info = pwd; +} + + +/* + * For testing purposes, we want to be able to intercept getpwnam. This is + * fairly easy on platforms that have pam_modutil_getpwnam, since then our + * code will always call that function and we can provide an implementation + * that does whatever we want. For platforms that don't have that function, + * we'll try to intercept the C library getpwnam function. + * + * We store only one struct passwd data structure statically. If the user + * we're looking up matches that, we return it; otherwise, we return NULL. + */ +#ifdef HAVE_PAM_MODUTIL_GETPWNAM +struct passwd * +pam_modutil_getpwnam(pam_handle_t *pamh UNUSED, const char *name) +{ + if (pwd_info != NULL && strcmp(pwd_info->pw_name, name) == 0) + return pwd_info; + else { + errno = 0; + return NULL; + } +} +#else +struct passwd * +getpwnam(const char *name) +{ + if (pwd_info != NULL && strcmp(pwd_info->pw_name, name) == 0) + return pwd_info; + else { + errno = 0; + return NULL; + } +} +#endif diff --git a/tests/fakepam/internal.h b/tests/fakepam/internal.h new file mode 100644 index 000000000000..3c6fedacd45e --- /dev/null +++ b/tests/fakepam/internal.h @@ -0,0 +1,119 @@ +/* + * Internal data types and prototypes for the fake PAM test framework. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2021 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012 + * 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 + */ + +#ifndef FAKEPAM_INTERNAL_H +#define FAKEPAM_INTERNAL_H 1 + +#include <portable/pam.h> +#include <sys/types.h> + +/* Forward declarations to avoid unnecessary includes. */ +struct output; +struct script_config; + +/* The type of a PAM module call. */ +typedef int (*pam_call)(pam_handle_t *, int, int, const char **); + +/* The possible PAM groups as element numbers in an array of options. */ +enum group_type +{ + GROUP_ACCOUNT = 0, + GROUP_AUTH = 1, + GROUP_PASSWORD = 2, + GROUP_SESSION = 3, +}; + +/* Holds a PAM argc and argv. */ +struct options { + char **argv; + int argc; +}; + +/* + * Holds a linked list of actions: a PAM call that should return some + * status. + */ +struct action { + char *name; + pam_call call; + int flags; + enum group_type group; + int status; + struct action *next; +}; + +/* Holds an expected PAM prompt style, the prompt, and the response. */ +struct prompt { + int style; + char *prompt; + char *response; +}; + +/* Holds an array of PAM prompts and the current index into that array. */ +struct prompts { + struct prompt *prompts; + size_t size; + size_t allocated; + size_t current; +}; + +/* + * Holds the complete set of things that we should do, configuration for them, + * and expected output and return values. + */ +struct work { + struct options options[4]; + struct action *actions; + struct prompts *prompts; + struct output *output; + int end_flags; +}; + +BEGIN_DECLS + + +/* Create a new output struct. */ +struct output *output_new(void); + +/* Add a new output line (with numeric priority) to an output struct. */ +void output_add(struct output *, int, const char *); + + +/* + * Parse a PAM interaction script. Returns the total work to do as a work + * struct. + */ +struct work *parse_script(FILE *, const struct script_config *); + +END_DECLS + +#endif /* !FAKEPAM_API_H */ diff --git a/tests/fakepam/kuserok.c b/tests/fakepam/kuserok.c new file mode 100644 index 000000000000..d66bc1d03acc --- /dev/null +++ b/tests/fakepam/kuserok.c @@ -0,0 +1,119 @@ +/* + * Replacement for krb5_kuserok for testing. + * + * This is a reimplementation of krb5_kuserok that uses the replacement + * getpwnam function and the special passwd struct internal to the fake PAM + * module to locate .k5login. The default Kerberos krb5_kuserok always calls + * the system getpwnam, which we may not be able to intercept, and will + * therefore fail because it can't locate the .k5login file for the test user + * (or succeed oddly because it finds some random file on the testing system). + * + * This implementation is drastically simplified from the Kerberos library + * version, and much less secure (which shouldn't matter since it's only + * acting on test data). + * + * This is an optional part of the fake PAM library and can be omitted when + * testing modules that don't use Kerberos. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2011 + * 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 <config.h> +#include <portable/krb5.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <pwd.h> + +#include <tests/fakepam/pam.h> +#include <tests/tap/string.h> + + +/* + * Given a Kerberos principal representing the authenticated identity and the + * username of the local account, return true if that principal is authorized + * to log on to that account. The principal is authorized if the .k5login + * file does not exist and the user matches the localname form of the + * principal, or if the file does exist and the principal is listed in it. + * + * This version retrieves the home directory from the internal fake PAM + * library path. + */ +krb5_boolean +krb5_kuserok(krb5_context ctx, krb5_principal princ, const char *user) +{ + char *principal, *path; + struct passwd *pwd; + FILE *file; + krb5_error_code code; + char buffer[BUFSIZ]; + bool found = false; +#ifdef HAVE_PAM_MODUTIL_GETPWNAM + struct pam_handle pamh; +#endif + + /* + * Find .k5login and confirm if it exists. If it doesn't, fall back on + * krb5_aname_to_localname. + */ +#ifdef HAVE_PAM_MODUTIL_GETPWNAM + memset(&pamh, 0, sizeof(pamh)); + pwd = pam_modutil_getpwnam(&pamh, user); +#else + pwd = getpwnam(user); +#endif + if (pwd == NULL) + return false; + basprintf(&path, "%s/.k5login", pwd->pw_dir); + if (access(path, R_OK) < 0) { + free(path); + code = krb5_aname_to_localname(ctx, princ, sizeof(buffer), buffer); + return (code == 0 && strcmp(buffer, user) == 0); + } + file = fopen(path, "r"); + if (file == NULL) { + free(path); + return false; + } + free(path); + + /* .k5login exists. Scan it for the principal. */ + if (krb5_unparse_name(ctx, princ, &principal) != 0) { + fclose(file); + return false; + } + while (!found && (fgets(buffer, sizeof(buffer), file) != NULL)) { + if (buffer[strlen(buffer) - 1] == '\n') + buffer[strlen(buffer) - 1] = '\0'; + if (strcmp(buffer, principal) == 0) + found = true; + } + fclose(file); + krb5_free_unparsed_name(ctx, principal); + return found; +} diff --git a/tests/fakepam/logging.c b/tests/fakepam/logging.c new file mode 100644 index 000000000000..c3a3fa044576 --- /dev/null +++ b/tests/fakepam/logging.c @@ -0,0 +1,183 @@ +/* + * Logging functions for the fake PAM library, used for testing. + * + * This file contains the implementation of pam_syslog and pam_vsyslog, which + * log to an internal buffer rather than to syslog, and the testing function + * used to recover that buffer. It also includes the pam_strerror + * implementation. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2010-2012, 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <tests/fakepam/internal.h> +#include <tests/fakepam/pam.h> +#include <tests/tap/basic.h> +#include <tests/tap/string.h> + +/* Used for unused parameters to silence gcc warnings. */ +#define UNUSED __attribute__((__unused__)) + +/* The struct used to accumulate log messages. */ +static struct output *messages = NULL; + + +/* + * Allocate a new, empty output struct and call bail if memory allocation + * fails. + */ +struct output * +output_new(void) +{ + struct output *output; + + output = bmalloc(sizeof(struct output)); + output->count = 0; + output->allocated = 1; + output->lines = bmalloc(sizeof(output->lines[0])); + output->lines[0].line = NULL; + return output; +} + + +/* + * Add a new output line to the output struct, resizing the array as + * necessary. Calls bail if memory allocation fails. + */ +void +output_add(struct output *output, int priority, const char *string) +{ + size_t next = output->count; + size_t size, n; + + if (output->count == output->allocated) { + n = output->allocated + 1; + size = sizeof(output->lines[0]); + output->lines = breallocarray(output->lines, n, size); + output->allocated = n; + } + output->lines[next].priority = priority; + output->lines[next].line = bstrdup(string); + output->count++; +} + + +/* + * Return the error string associated with the PAM error code. We do this as + * a giant case statement so that we don't assume anything about the error + * codes used by the system PAM library. + */ +const char * +pam_strerror(PAM_STRERROR_CONST pam_handle_t *pamh UNUSED, int code) +{ + /* clang-format off */ + switch (code) { + case PAM_SUCCESS: return "No error"; + case PAM_OPEN_ERR: return "Failure loading service module"; + case PAM_SYMBOL_ERR: return "Symbol not found"; + case PAM_SERVICE_ERR: return "Error in service module"; + case PAM_SYSTEM_ERR: return "System error"; + case PAM_BUF_ERR: return "Memory buffer error"; + default: return "Unknown error"; + } + /* clang-format on */ +} + + +/* + * Log a message using variadic arguments. Just a wrapper around + * pam_vsyslog. + */ +void +pam_syslog(const pam_handle_t *pamh, int priority, const char *format, ...) +{ + va_list args; + + va_start(args, format); + pam_vsyslog(pamh, priority, format, args); + va_end(args); +} + + +/* + * Log a PAM error message with a given priority. Just appends the priority, + * a space, and the error message, followed by a newline, to the internal + * buffer, allocating new space if needed. Ignore memory allocation failures; + * we have no way of reporting them, but the tests will fail due to missing + * output. + */ +void +pam_vsyslog(const pam_handle_t *pamh UNUSED, int priority, const char *format, + va_list args) +{ + char *message = NULL; + + bvasprintf(&message, format, args); + if (messages == NULL) + messages = output_new(); + output_add(messages, priority, message); + free(message); +} + + +/* + * Used by test code. Returns the accumulated messages in an output struct + * and starts a new one. Caller is responsible for freeing with + * pam_output_free. + */ +struct output * +pam_output(void) +{ + struct output *output; + + output = messages; + messages = NULL; + return output; +} + + +/* + * Free an output struct. + */ +void +pam_output_free(struct output *output) +{ + size_t i; + + if (output == NULL) + return; + for (i = 0; i < output->count; i++) + if (output->lines[i].line != NULL) + free(output->lines[i].line); + free(output->lines); + free(output); +} diff --git a/tests/fakepam/pam.h b/tests/fakepam/pam.h new file mode 100644 index 000000000000..41f508e0f31f --- /dev/null +++ b/tests/fakepam/pam.h @@ -0,0 +1,101 @@ +/* + * Testing interfaces to the fake PAM library. + * + * This header defines the interfaces to the fake PAM library that are used by + * test code to initialize the library and recover test data from it. We + * don't define any interface that we're going to duplicate from the main PAM + * API. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2010-2012 + * 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 + */ + +#ifndef FAKEPAM_PAM_H +#define FAKEPAM_PAM_H 1 + +#include <config.h> +#include <portable/macros.h> +#include <portable/pam.h> + +/* Used inside the fake PAM library to hold data items. */ +struct fakepam_data { + char *name; + void *data; + void (*cleanup)(pam_handle_t *, void *, int); + struct fakepam_data *next; +}; + +/* This is an opaque data structure, so we can put whatever we want in it. */ +struct pam_handle { + const char *service; + const char *user; + char *authtok; + char *oldauthtok; + char *rhost; + char *ruser; + char *tty; + const struct pam_conv *conversation; + char **environ; + struct fakepam_data *data; + struct passwd *pwd; +}; + +/* + * Used to accumulate output from the PAM module. Each call to a logging + * function will result in an additional line added to the array, and count + * will hold the total. + */ +struct output { + size_t count; + size_t allocated; + struct { + int priority; + char *line; + } * lines; +}; + +BEGIN_DECLS + +/* + * Sets the struct passwd returned by getpwnam calls. The last struct passed + * to this function will be returned provided the pw_name matches. + */ +void pam_set_pwd(struct passwd *pwd); + +/* + * Returns the accumulated messages logged with pam_syslog or pam_vsyslog + * since the last call to pam_output and then clears the output. Returns + * newly allocated memory that the caller is responsible for freeing with + * pam_output_free, or NULL if no output has been logged since the last call + * or since startup. + */ +struct output *pam_output(void); +void pam_output_free(struct output *); + +END_DECLS + +#endif /* !FAKEPAM_API_H */ diff --git a/tests/fakepam/script.c b/tests/fakepam/script.c new file mode 100644 index 000000000000..6f3812577960 --- /dev/null +++ b/tests/fakepam/script.c @@ -0,0 +1,411 @@ +/* + * Run a PAM interaction script for testing. + * + * Provides an interface that loads a PAM interaction script from a file and + * runs through that script, calling the internal PAM module functions and + * checking their results. This allows automation of PAM testing through + * external data files instead of coding everything in C. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2016, 2018, 2020-2021 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012, 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <ctype.h> +#include <dirent.h> +#include <errno.h> +#ifdef HAVE_REGCOMP +# include <regex.h> +#endif +#include <syslog.h> + +#include <tests/fakepam/internal.h> +#include <tests/fakepam/pam.h> +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/macros.h> +#include <tests/tap/string.h> + + +/* + * Compare a regex to a string. If regular expression support isn't + * available, we skip this test. + */ +#ifdef HAVE_REGCOMP +static void __attribute__((__format__(printf, 3, 4))) +like(const char *wanted, const char *seen, const char *format, ...) +{ + va_list args; + regex_t regex; + char err[BUFSIZ]; + int status; + + if (seen == NULL) { + fflush(stderr); + printf("# wanted: /%s/\n# seen: (null)\n", wanted); + va_start(args, format); + okv(0, format, args); + va_end(args); + return; + } + memset(®ex, 0, sizeof(regex)); + status = regcomp(®ex, wanted, REG_EXTENDED | REG_NOSUB); + if (status != 0) { + regerror(status, ®ex, err, sizeof(err)); + bail("invalid regex /%s/: %s", wanted, err); + } + status = regexec(®ex, seen, 0, NULL, 0); + switch (status) { + case 0: + va_start(args, format); + okv(1, format, args); + va_end(args); + break; + case REG_NOMATCH: + printf("# wanted: /%s/\n# seen: %s\n", wanted, seen); + va_start(args, format); + okv(0, format, args); + va_end(args); + break; + default: + regerror(status, ®ex, err, sizeof(err)); + bail("regexec failed for regex /%s/: %s", wanted, err); + } + regfree(®ex); +} +#else /* !HAVE_REGCOMP */ +static void +like(const char *wanted, const char *seen, const char *format UNUSED, ...) +{ + diag("wanted /%s/", wanted); + diag(" seen %s", seen); + skip("regex support not available"); +} +#endif /* !HAVE_REGCOMP */ + + +/* + * Compare an expected string with a seen string, used by both output checking + * and prompt checking. This is a separate function because the expected + * string may be a regex, determined by seeing if it starts and ends with a + * slash (/), which may require a regex comparison. + * + * Eventually calls either is_string or ok to report results via TAP. + */ +static void __attribute__((__format__(printf, 3, 4))) +compare_string(char *wanted, char *seen, const char *format, ...) +{ + va_list args; + char *comment, *regex; + size_t length; + + /* Format the comment since we need it regardless. */ + va_start(args, format); + bvasprintf(&comment, format, args); + va_end(args); + + /* Check whether the wanted string is a regex. */ + length = strlen(wanted); + if (wanted[0] == '/' && wanted[length - 1] == '/') { + regex = bstrndup(wanted + 1, length - 2); + like(regex, seen, "%s", comment); + free(regex); + } else { + is_string(wanted, seen, "%s", comment); + } + free(comment); +} + + +/* + * The PAM conversation function. Takes the prompts struct from the + * configuration and interacts appropriately. If a prompt is of the expected + * type but not the expected string, it still responds; if it's not of the + * expected type, it returns PAM_CONV_ERR. + * + * Currently only handles a single prompt at a time. + */ +static int +converse(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) +{ + struct prompts *prompts = appdata_ptr; + struct prompt *prompt; + char *message; + size_t length; + int i; + + *resp = bcalloc(num_msg, sizeof(struct pam_response)); + for (i = 0; i < num_msg; i++) { + message = bstrdup(msg[i]->msg); + + /* Remove newlines for comparison purposes. */ + length = strlen(message); + while (length > 0 && message[length - 1] == '\n') + message[length-- - 1] = '\0'; + + /* Check if we've gotten too many prompts but quietly ignore them. */ + if (prompts->current >= prompts->size) { + diag("unexpected prompt: %s", message); + free(message); + ok(0, "more prompts than expected"); + continue; + } + + /* Be sure everything matches and return the response, if any. */ + prompt = &prompts->prompts[prompts->current]; + is_int(prompt->style, msg[i]->msg_style, "style of prompt %lu", + (unsigned long) prompts->current + 1); + compare_string(prompt->prompt, message, "value of prompt %lu", + (unsigned long) prompts->current + 1); + free(message); + prompts->current++; + if (prompt->style == msg[i]->msg_style && prompt->response != NULL) { + (*resp)[i].resp = bstrdup(prompt->response); + (*resp)[i].resp_retcode = 0; + } + } + + /* + * Always return success even if the prompts don't match. Otherwise, + * we're likely to abort the conversation in the middle and possibly + * leave passwords set incorrectly. + */ + return PAM_SUCCESS; +} + + +/* + * Check the actual PAM output against the expected output. We divide the + * expected and seen output into separate lines and compare each one so that + * we can handle regular expressions and the output priority. + */ +static void +check_output(const struct output *wanted, const struct output *seen) +{ + size_t i; + + if (wanted == NULL && seen == NULL) + ok(1, "no output"); + else if (wanted == NULL) { + for (i = 0; i < seen->count; i++) + diag("unexpected: (%d) %s", seen->lines[i].priority, + seen->lines[i].line); + ok(0, "no output"); + } else if (seen == NULL) { + for (i = 0; i < wanted->count; i++) { + is_int(wanted->lines[i].priority, 0, "output priority %lu", + (unsigned long) i + 1); + is_string(wanted->lines[i].line, NULL, "output line %lu", + (unsigned long) i + 1); + } + } else { + for (i = 0; i < wanted->count && i < seen->count; i++) { + is_int(wanted->lines[i].priority, seen->lines[i].priority, + "output priority %lu", (unsigned long) i + 1); + compare_string(wanted->lines[i].line, seen->lines[i].line, + "output line %lu", (unsigned long) i + 1); + } + if (wanted->count > seen->count) + for (i = seen->count; i < wanted->count; i++) { + is_int(wanted->lines[i].priority, 0, "output priority %lu", + (unsigned long) i + 1); + is_string(wanted->lines[i].line, NULL, "output line %lu", + (unsigned long) i + 1); + } + if (seen->count > wanted->count) { + for (i = wanted->count; i < seen->count; i++) + diag("unexpected: (%d) %s", seen->lines[i].priority, + seen->lines[i].line); + ok(0, "unexpected output lines"); + } else { + ok(1, "no excess output"); + } + } +} + + +/* + * The core of the work. Given the path to a PAM interaction script, which + * may be relative to C_TAP_SOURCE or C_TAP_BUILD, the user (may be NULL), and + * the stored password (may be NULL), run that script, outputting the results + * in TAP format. + */ +void +run_script(const char *file, const struct script_config *config) +{ + char *path; + struct output *output; + FILE *script; + struct work *work; + struct options *opts; + struct action *action, *oaction; + struct pam_conv conv = {NULL, NULL}; + pam_handle_t *pamh; + int status; + size_t i, j; + const char *argv_empty[] = {NULL}; + + /* Open and parse the script. */ + if (access(file, R_OK) == 0) + path = bstrdup(file); + else { + path = test_file_path(file); + if (path == NULL) + bail("cannot find PAM script %s", file); + } + script = fopen(path, "r"); + if (script == NULL) + sysbail("cannot open %s", path); + work = parse_script(script, config); + fclose(script); + diag("Starting %s", file); + if (work->prompts != NULL) { + conv.conv = converse; + conv.appdata_ptr = work->prompts; + } + + /* Initialize PAM. */ + status = pam_start("test", config->user, &conv, &pamh); + if (status != PAM_SUCCESS) + sysbail("cannot create PAM handle"); + if (config->authtok != NULL) + pamh->authtok = bstrdup(config->authtok); + if (config->oldauthtok != NULL) + pamh->oldauthtok = bstrdup(config->oldauthtok); + + /* Run the actions and check their return status. */ + for (action = work->actions; action != NULL; action = action->next) { + if (work->options[action->group].argv == NULL) + status = (*action->call)(pamh, action->flags, 0, argv_empty); + else { + opts = &work->options[action->group]; + status = (*action->call)(pamh, action->flags, opts->argc, + (const char **) opts->argv); + } + is_int(action->status, status, "status for %s", action->name); + } + output = pam_output(); + check_output(work->output, output); + pam_output_free(output); + + /* If we have a test callback, call it now. */ + if (config->callback != NULL) + config->callback(pamh, config, config->data); + + /* Free memory and return. */ + pam_end(pamh, work->end_flags); + action = work->actions; + while (action != NULL) { + free(action->name); + oaction = action; + action = action->next; + free(oaction); + } + for (i = 0; i < ARRAY_SIZE(work->options); i++) + if (work->options[i].argv != NULL) { + for (j = 0; work->options[i].argv[j] != NULL; j++) + free(work->options[i].argv[j]); + free(work->options[i].argv); + } + if (work->output) + pam_output_free(work->output); + if (work->prompts != NULL) { + for (i = 0; i < work->prompts->size; i++) { + free(work->prompts->prompts[i].prompt); + free(work->prompts->prompts[i].response); + } + free(work->prompts->prompts); + free(work->prompts); + } + free(work); + free(path); +} + + +/* + * Check a filename for acceptable characters. Returns true if the file + * consists solely of [a-zA-Z0-9-] and false otherwise. + */ +static bool +valid_filename(const char *filename) +{ + const char *p; + + for (p = filename; *p != '\0'; p++) { + if (*p >= 'A' && *p <= 'Z') + continue; + if (*p >= 'a' && *p <= 'z') + continue; + if (*p >= '0' && *p <= '9') + continue; + if (*p == '-') + continue; + return false; + } + return true; +} + + +/* + * The same as run_script, but run every script found in the given directory, + * skipping file names that contain characters other than alphanumerics and -. + */ +void +run_script_dir(const char *dir, const struct script_config *config) +{ + DIR *handle; + struct dirent *entry; + const char *path; + char *file; + + if (access(dir, R_OK) == 0) + path = dir; + else + path = test_file_path(dir); + handle = opendir(path); + if (handle == NULL) + sysbail("cannot open directory %s", dir); + errno = 0; + while ((entry = readdir(handle)) != NULL) { + if (!valid_filename(entry->d_name)) + continue; + basprintf(&file, "%s/%s", path, entry->d_name); + run_script(file, config); + free(file); + errno = 0; + } + if (errno != 0) + sysbail("cannot read directory %s", dir); + closedir(handle); + if (path != dir) + test_file_path_free((char *) path); +} diff --git a/tests/fakepam/script.h b/tests/fakepam/script.h new file mode 100644 index 000000000000..c99fc12f55d2 --- /dev/null +++ b/tests/fakepam/script.h @@ -0,0 +1,82 @@ +/* + * PAM interaction script API. + * + * Provides an interface that loads a PAM interaction script from a file and + * runs through that script, calling the internal PAM module functions and + * checking their results. This allows automation of PAM testing through + * external data files instead of coding everything in C. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2016 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012 + * 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 + */ + +#ifndef TESTS_MODULE_SCRIPT_H +#define TESTS_MODULE_SCRIPT_H 1 + +#include <portable/pam.h> + +#include <tests/tap/basic.h> + +/* A test callback called after PAM functions are run but before pam_end. */ +struct script_config; +typedef void (*script_callback)(pam_handle_t *, const struct script_config *, + void *); + +/* Configuration for the PAM interaction script API. */ +struct script_config { + const char *user; /* Username to pass into pam_start (%u). */ + const char *password; /* Substituted for %p in prompts. */ + const char *newpass; /* Substituted for %n in prompts. */ + const char *extra[10]; /* Substituted for %0-%9 in logging. */ + const char *authtok; /* Stored as AUTHTOK before PAM. */ + const char *oldauthtok; /* Stored as OLDAUTHTOK before PAM. */ + script_callback callback; /* Called after PAM, before pam_end. */ + void *data; /* Passed to the callback function. */ +}; + +BEGIN_DECLS + +/* + * Given the file name of an interaction script (which may be a full path or + * relative to C_TAP_SOURCE or C_TAP_BUILD) and configuration containing other + * parameters such as the user, run that script, reporting the results via the + * TAP format. + */ +void run_script(const char *file, const struct script_config *) + __attribute__((__nonnull__)); + +/* + * The same as run_script, but run every script found in the given directory, + * skipping file names that contain characters other than alphanumerics and -. + */ +void run_script_dir(const char *dir, const struct script_config *) + __attribute__((__nonnull__)); + +END_DECLS + +#endif /* !TESTS_MODULE_SCRIPT_H */ diff --git a/tests/module/alt-auth-t.c b/tests/module/alt-auth-t.c new file mode 100644 index 000000000000..df32ff941001 --- /dev/null +++ b/tests/module/alt-auth-t.c @@ -0,0 +1,117 @@ +/* + * Tests for the alt_auth_map functionality in libpam-krb5. + * + * This test case tests the variations of the alt_auth_map functionality for + * both authentication and account management. It requires a Kerberos + * configuration, but does not attempt to save a session ticket cache (to + * avoid requiring user configuration). + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + char *user; + + /* + * Load the Kerberos principal and password from a file, but set the + * principal as extra[0] and use something else bogus as the user. We + * want to test that alt_auth_map works when there's no relationship + * between the mapped principal and the user. + */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = "bogus-nonexistent-account"; + config.authtok = krbconf->password; + config.extra[0] = krbconf->username; + config.extra[1] = krbconf->userprinc; + + /* + * Generate a testing krb5.conf file with a nonexistent default realm so + * that we can be sure that our principals will stay fully-qualified in + * the logs. + */ + kerberos_generate_conf("bogus.example.com"); + config.extra[2] = "bogus.example.com"; + + /* Test without password prompting. */ + plan_lazy(); + run_script("data/scripts/alt-auth/basic", &config); + run_script("data/scripts/alt-auth/basic-debug", &config); + run_script("data/scripts/alt-auth/fail", &config); + run_script("data/scripts/alt-auth/fail-debug", &config); + run_script("data/scripts/alt-auth/force", &config); + run_script("data/scripts/alt-auth/only", &config); + + /* + * If the alternate account exists but the password is incorrect, we + * should not fall back to the regular account. Test with debug so that + * we don't need two principals configured. + */ + config.authtok = "bogus incorrect password"; + run_script("data/scripts/alt-auth/force-fail-debug", &config); + + /* + * Switch to our correct user (but wrong realm) realm to test username + * mapping to a different realm. + */ + config.authtok = krbconf->password; + config.user = krbconf->username; + config.extra[2] = krbconf->realm; + run_script("data/scripts/alt-auth/username-map", &config); + + /* + * Split the username into two parts, one in the PAM configuration and one + * in the real username, so that we can test interpolation of the username + * when %s isn't the first token. + */ + config.user = &krbconf->username[1]; + user = bstrndup(krbconf->username, 1); + config.extra[3] = user; + run_script("data/scripts/alt-auth/username-map-prefix", &config); + free(user); + config.extra[3] = NULL; + + /* + * Ensure that we don't add the realm of the authentication username when + * the alt_auth_map already includes a realm. + */ + basprintf(&user, "%s@foo.example.com", krbconf->username); + config.user = user; + diag("re-running username-map with fully-qualified PAM user"); + run_script("data/scripts/alt-auth/username-map", &config); + free(user); + + /* + * Add the password and make the user match our authentication principal, + * and then test fallback to normal authentication when alternative + * authentication fails. + */ + config.user = krbconf->userprinc; + config.password = krbconf->password; + config.extra[2] = krbconf->realm; + run_script("data/scripts/alt-auth/fallback", &config); + run_script("data/scripts/alt-auth/fallback-debug", &config); + run_script("data/scripts/alt-auth/fallback-realm", &config); + run_script("data/scripts/alt-auth/force-fallback", &config); + run_script("data/scripts/alt-auth/only-fail", &config); + + return 0; +} diff --git a/tests/module/bad-authtok-t.c b/tests/module/bad-authtok-t.c new file mode 100644 index 000000000000..385dd5946849 --- /dev/null +++ b/tests/module/bad-authtok-t.c @@ -0,0 +1,53 @@ +/* + * Authentication tests for the pam-krb5 module with an incorrect AUTHTOK. + * + * This test case includes tests that require Kerberos to be configured and a + * username and password available and that run with an incorrect AUTHTOK + * already set. They test various prompting fallback cases. They don't write + * a ticket cache (which requires additional work to test the cache + * ownership). + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->userprinc; + config.password = krbconf->password; + + /* Set the authtok to something bogus. */ + config.authtok = "BAD PASSWORD THAT WILL NOT WORK"; + + /* + * Generate a testing krb5.conf file with a nonexistent default realm so + * that we can be sure that our principals will stay fully-qualified in + * the logs. + */ + kerberos_generate_conf("bogus.example.com"); + + plan_lazy(); + run_script_dir("data/scripts/bad-authtok", &config); + + return 0; +} diff --git a/tests/module/basic-t.c b/tests/module/basic-t.c new file mode 100644 index 000000000000..cacad5906ffb --- /dev/null +++ b/tests/module/basic-t.c @@ -0,0 +1,67 @@ +/* + * Basic tests for the pam-krb5 module. + * + * This test case includes all tests that can be done without having Kerberos + * configured and a username and password available, and without any special + * configuration. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2011 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <pwd.h> + +#include <tests/fakepam/pam.h> +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + struct passwd pwd; + char *uid; + char *uidplus; + + plan_lazy(); + + /* + * Generate a testing krb5.conf file with a nonexistent default realm so + * that this test will run on any system. + */ + kerberos_generate_conf("bogus.example.com"); + + /* Create a fake passwd struct for our user. */ + memset(&pwd, 0, sizeof(pwd)); + pwd.pw_name = (char *) "root"; + pwd.pw_uid = getuid(); + pwd.pw_gid = getgid(); + pam_set_pwd(&pwd); + + /* + * Attempt login as the root user to test ignore_root. Set our current + * UID and a UID one larger for testing minimum_uid. + */ + basprintf(&uid, "%lu", (unsigned long) pwd.pw_uid); + basprintf(&uidplus, "%lu", (unsigned long) pwd.pw_uid + 1); + memset(&config, 0, sizeof(config)); + config.user = "root"; + config.extra[0] = uid; + config.extra[1] = uidplus; + + run_script_dir("data/scripts/basic", &config); + + free(uid); + free(uidplus); + return 0; +} diff --git a/tests/module/cache-cleanup-t.c b/tests/module/cache-cleanup-t.c new file mode 100644 index 000000000000..8b5012fc3507 --- /dev/null +++ b/tests/module/cache-cleanup-t.c @@ -0,0 +1,104 @@ +/* + * Test for properly cleaning up ticket caches. + * + * Verify that the temporary Kerberos ticket cache generated during + * authentication is cleaned up on pam_end, even if no session was opened. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <dirent.h> + +#include <tests/fakepam/pam.h> +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + DIR *tmpdir; + struct dirent *file; + char *tmppath, *path; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->username; + config.authtok = krbconf->password; + config.extra[0] = krbconf->userprinc; + + /* Generate a testing krb5.conf file. */ + kerberos_generate_conf(krbconf->realm); + + /* Get the temporary directory and store that as the %1 substitution. */ + tmppath = test_tmpdir(); + config.extra[1] = tmppath; + + plan_lazy(); + + /* + * We need to ensure that the only thing in the test temporary directory + * is the krb5.conf file that we generated and any valgrind logs, since + * we're going to check for cleanup by looking for any out-of-place files. + */ + tmpdir = opendir(tmppath); + if (tmpdir == NULL) + sysbail("cannot open directory %s", tmppath); + while ((file = readdir(tmpdir)) != NULL) { + if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0) + continue; + if (strcmp(file->d_name, "krb5.conf") == 0) + continue; + if (strcmp(file->d_name, "valgrind") == 0) + continue; + basprintf(&path, "%s/%s", tmppath, file->d_name); + if (unlink(path) < 0) + sysbail("cannot delete temporary file %s", path); + free(path); + } + closedir(tmpdir); + + /* + * Authenticate only, call pam_end, and be sure the ticket cache is + * gone. The auth-only script sets ccache_dir to the temporary directory, + * so the module will create a temporary ticket cache there and then + * should clean it up. + */ + run_script("data/scripts/cache-cleanup/auth-only", &config); + path = NULL; + tmpdir = opendir(tmppath); + if (tmpdir == NULL) + sysbail("cannot open directory %s", tmppath); + while ((file = readdir(tmpdir)) != NULL) { + if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0) + continue; + if (strcmp(file->d_name, "krb5.conf") == 0) + continue; + if (strcmp(file->d_name, "valgrind") == 0) + continue; + if (path == NULL) + basprintf(&path, "%s/%s", tmppath, file->d_name); + } + closedir(tmpdir); + if (path != NULL) + diag("found stray temporary file %s", path); + ok(path == NULL, "ticket cache cleaned up"); + if (path != NULL) + free(path); + + test_tmpdir_free(tmppath); + return 0; +} diff --git a/tests/module/cache-t.c b/tests/module/cache-t.c new file mode 100644 index 000000000000..8ec82df7c460 --- /dev/null +++ b/tests/module/cache-t.c @@ -0,0 +1,210 @@ +/* + * Authentication tests for the pam-krb5 module with ticket cache. + * + * This test case includes all tests that require Kerberos to be configured, a + * username and password available, and a ticket cache created, but with the + * PAM module running as the same user for which the ticket cache will be + * created (so without setuid and with chown doing nothing). + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2017, 2020-2021 Russ Allbery <eagle@eyrie.org> + * Copyright 2011, 2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/krb5.h> +#include <portable/system.h> + +#include <pwd.h> +#include <sys/stat.h> +#include <time.h> + +#include <tests/fakepam/pam.h> +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + +/* Additional data used by the cache check callback. */ +struct extra { + char *realm; + char *cache_path; +}; + + +/* + * PAM test callback to check whether we created a ticket cache and the ticket + * cache is for the correct user. + */ +static void +check_cache(const char *file, const struct script_config *config, + const struct extra *extra) +{ + struct stat st; + krb5_error_code code; + krb5_context ctx = NULL; + krb5_ccache ccache = NULL; + krb5_principal princ = NULL; + krb5_principal tgtprinc = NULL; + krb5_creds in, out; + char *principal = NULL; + + /* Check ownership and permissions. */ + is_int(0, stat(file, &st), "cache exists"); + is_int(getuid(), st.st_uid, "...with correct UID"); + is_int(getgid(), st.st_gid, "...with correct GID"); + is_int(0600, (st.st_mode & 0777), "...with correct permissions"); + + /* Check the existence of the ticket cache and its principal. */ + code = krb5_init_context(&ctx); + if (code != 0) + bail("cannot create Kerberos context"); + code = krb5_cc_resolve(ctx, file, &ccache); + is_int(0, code, "able to resolve Kerberos ticket cache"); + code = krb5_cc_get_principal(ctx, ccache, &princ); + is_int(0, code, "able to get principal"); + code = krb5_unparse_name(ctx, princ, &principal); + is_int(0, code, "...and principal is valid"); + is_string(config->extra[0], principal, "...and matches our principal"); + + /* Retrieve the krbtgt for the realm and check properties. */ + code = krb5_build_principal_ext( + ctx, &tgtprinc, (unsigned int) strlen(extra->realm), extra->realm, + KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME, strlen(extra->realm), extra->realm, + NULL); + if (code != 0) + bail("cannot create krbtgt principal name"); + memset(&in, 0, sizeof(in)); + memset(&out, 0, sizeof(out)); + in.server = tgtprinc; + in.client = princ; + code = krb5_cc_retrieve_cred(ctx, ccache, KRB5_TC_MATCH_SRV_NAMEONLY, &in, + &out); + is_int(0, code, "able to get krbtgt credentials"); + ok(out.times.endtime > time(NULL) + 30 * 60, "...good for 30 minutes"); + krb5_free_cred_contents(ctx, &out); + + /* Close things and release memory. */ + krb5_free_principal(ctx, tgtprinc); + krb5_free_unparsed_name(ctx, principal); + krb5_free_principal(ctx, princ); + krb5_cc_close(ctx, ccache); + krb5_free_context(ctx); +} + + +/* + * Same as check_cache except unlink the ticket cache afterwards. Used to + * check the ticket cache in cases where the PAM module will not clean it up + * afterwards, such as calling pam_end with PAM_DATA_SILENT. + */ +static void +check_cache_callback(pam_handle_t *pamh, const struct script_config *config, + void *data) +{ + struct extra *extra = data; + const char *cache, *file; + char *prefix; + + cache = pam_getenv(pamh, "KRB5CCNAME"); + ok(cache != NULL, "KRB5CCNAME is set in PAM environment"); + if (cache == NULL) + return; + basprintf(&prefix, "FILE:/tmp/krb5cc_%lu_", (unsigned long) getuid()); + diag("KRB5CCNAME = %s", cache); + ok(strncmp(prefix, cache, strlen(prefix)) == 0, + "cache file name prefix is correct"); + free(prefix); + file = cache + strlen("FILE:"); + extra->cache_path = bstrdup(file); + check_cache(file, config, extra); +} + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + char *k5login; + struct extra extra; + struct passwd pwd; + FILE *file; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->username; + extra.realm = krbconf->realm; + extra.cache_path = NULL; + config.authtok = krbconf->password; + config.extra[0] = krbconf->userprinc; + + /* Generate a testing krb5.conf file. */ + kerberos_generate_conf(krbconf->realm); + + /* Create a fake passwd struct for our user. */ + memset(&pwd, 0, sizeof(pwd)); + pwd.pw_name = krbconf->username; + pwd.pw_uid = getuid(); + pwd.pw_gid = getgid(); + basprintf(&pwd.pw_dir, "%s/tmp", getenv("BUILD")); + pam_set_pwd(&pwd); + + plan_lazy(); + + /* Basic test. */ + run_script("data/scripts/cache/basic", &config); + + /* Check the cache status before the session is closed. */ + config.callback = check_cache_callback; + config.data = &extra; + run_script("data/scripts/cache/open-session", &config); + free(extra.cache_path); + extra.cache_path = NULL; + + /* + * Try again but passing PAM_DATA_SILENT to pam_end. This should leave + * the ticket cache intact. + */ + run_script("data/scripts/cache/end-data-silent", &config); + check_cache(extra.cache_path, &config, &extra); + if (unlink(extra.cache_path) < 0) + sysdiag("unable to unlink temporary cache %s", extra.cache_path); + free(extra.cache_path); + extra.cache_path = NULL; + + /* Change the authenticating user and test search_k5login. */ + pwd.pw_name = (char *) "testuser"; + pam_set_pwd(&pwd); + config.user = "testuser"; + basprintf(&k5login, "%s/.k5login", pwd.pw_dir); + file = fopen(k5login, "w"); + if (file == NULL) + sysbail("cannot create %s", k5login); + if (fprintf(file, "%s\n", krbconf->userprinc) < 0) + sysbail("cannot write to %s", k5login); + if (fclose(file) < 0) + sysbail("cannot flush %s", k5login); + run_script("data/scripts/cache/search-k5login", &config); + free(extra.cache_path); + extra.cache_path = NULL; + config.callback = NULL; + run_script("data/scripts/cache/search-k5login-debug", &config); + unlink(k5login); + free(k5login); + + /* Test search_k5login when no .k5login file exists. */ + pwd.pw_name = krbconf->username; + pam_set_pwd(&pwd); + config.user = krbconf->username; + diag("testing search_k5login with no .k5login file"); + run_script("data/scripts/cache/search-k5login", &config); + + free(pwd.pw_dir); + return 0; +} diff --git a/tests/module/expired-t.c b/tests/module/expired-t.c new file mode 100644 index 000000000000..01a1892a0d04 --- /dev/null +++ b/tests/module/expired-t.c @@ -0,0 +1,175 @@ +/* + * Tests for the pam-krb5 module with an expired password. + * + * This test case checks correct handling of an account whose password has + * expired and the multiple different paths the module can take for handling + * that case. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <pwd.h> +#include <time.h> + +#include <tests/fakepam/pam.h> +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/kadmin.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + char *newpass, *date; + struct passwd pwd; + time_t now; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->username; + config.password = krbconf->password; + config.extra[0] = krbconf->userprinc; + + /* + * Ensure we can expire the password. Heimdal has a prompt for the + * expiration time, so save that to use as a substitution in the script. + */ + now = time(NULL) - 1; + if (!kerberos_expire_password(krbconf->userprinc, now)) + skip_all("kadmin not configured or kadmin mismatch"); + date = bstrdup(ctime(&now)); + date[strlen(date) - 1] = '\0'; + config.extra[1] = date; + + /* Generate a testing krb5.conf file. */ + kerberos_generate_conf(krbconf->realm); + + /* Create a fake passwd struct for our user. */ + memset(&pwd, 0, sizeof(pwd)); + pwd.pw_name = krbconf->username; + pwd.pw_uid = getuid(); + pwd.pw_gid = getgid(); + basprintf(&pwd.pw_dir, "%s/tmp", getenv("BUILD")); + pam_set_pwd(&pwd); + + /* + * We'll be changing the password to something new. This needs to be + * sufficiently random that it's unlikely to fall afoul of password + * strength checking. + */ + basprintf(&newpass, "ngh1,a%lu nn9af6", (unsigned long) getpid()); + config.newpass = newpass; + + plan_lazy(); + + /* + * Default behavior. We have to distinguish between two versions of + * Heimdal for testing because the prompts changed substantially. Use the + * existence of krb5_principal_set_comp_string to distinguish because it + * was introduced at the same time. + */ +#ifdef HAVE_KRB5_HEIMDAL +# ifdef HAVE_KRB5_PRINCIPAL_SET_COMP_STRING + run_script("data/scripts/expired/basic-heimdal", &config); + config.newpass = krbconf->password; + config.password = newpass; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/basic-heimdal-debug", &config); +# else + run_script("data/scripts/expired/basic-heimdal-old", &config); + config.newpass = krbconf->password; + config.password = newpass; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/basic-heimdal-old-debug", &config); +# endif +#else + run_script("data/scripts/expired/basic-mit", &config); + config.newpass = krbconf->password; + config.password = newpass; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/basic-mit-debug", &config); +#endif + + /* Test again with PAM_SILENT, specified two ways. */ +#ifdef HAVE_KRB5_HEIMDAL + config.newpass = newpass; + config.password = krbconf->password; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/basic-heimdal-silent", &config); + config.newpass = krbconf->password; + config.password = newpass; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/basic-heimdal-flag-silent", &config); +#else + config.newpass = newpass; + config.password = krbconf->password; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/basic-mit-silent", &config); + config.newpass = krbconf->password; + config.password = newpass; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/basic-mit-flag-silent", &config); +#endif + + /* + * We can only run the remaining checks if we can suppress the Kerberos + * library behavior of prompting for a new password when the password has + * expired. + */ +#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT + + /* Check the forced failure behavior. */ + run_script("data/scripts/expired/fail", &config); + run_script("data/scripts/expired/fail-debug", &config); + + /* + * Defer the error to the account management check. + * + * Skip this check on Heimdal currently (Heimdal 7.4.0) because its + * implementation of krb5_get_init_creds_opt_set_change_password_prompt is + * incomplete. See <https://github.com/heimdal/heimdal/issues/322>. + */ +# ifdef HAVE_KRB5_HEIMDAL + skip_block(2, "deferring password changes broken in Heimdal"); +# else + config.newpass = newpass; + config.password = krbconf->password; + config.authtok = krbconf->password; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/defer-mit", &config); + config.newpass = krbconf->password; + config.password = newpass; + config.authtok = newpass; + kerberos_expire_password(krbconf->userprinc, now); + run_script("data/scripts/expired/defer-mit-debug", &config); +# endif + +#else /* !HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT */ + + /* Mention that we skipped something for the record. */ + skip_block(4, "cannot disable library password prompting"); + +#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT */ + + /* In case we ran into some error, try to unexpire the password. */ + kerberos_expire_password(krbconf->userprinc, 0); + + free(date); + free(newpass); + free(pwd.pw_dir); + return 0; +} diff --git a/tests/module/fast-anon-t.c b/tests/module/fast-anon-t.c new file mode 100644 index 000000000000..6355a5154f69 --- /dev/null +++ b/tests/module/fast-anon-t.c @@ -0,0 +1,108 @@ +/* + * Tests for anonymous FAST support in pam-krb5. + * + * Tests for anonymous Flexible Authentication Secure Tunneling, a mechanism + * for improving the preauthentication part of the Kerberos protocol and + * protecting it against various attacks. + * + * This is broken out from the other FAST tests because it uses PKINIT, and + * PKINIT code cannot be tested under valgrind with MIT Kerberos due to some + * bug in valgrind. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/krb5.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/kerberos.h> + + +/* + * Test whether anonymous authentication works. If this doesn't, we need to + * skip the tests of anonymous FAST. + */ +static bool +anon_fast_works(void) +{ + krb5_context ctx; + krb5_error_code retval; + krb5_principal princ = NULL; + char *realm; + krb5_creds creds; + krb5_get_init_creds_opt *opts = NULL; + + /* Construct the anonymous principal name. */ + retval = krb5_init_context(&ctx); + if (retval != 0) + bail("cannot initialize Kerberos"); + retval = krb5_get_default_realm(ctx, &realm); + if (retval != 0) + bail("cannot get default realm"); + retval = krb5_build_principal_ext( + ctx, &princ, (unsigned int) strlen(realm), realm, + strlen(KRB5_WELLKNOWN_NAME), KRB5_WELLKNOWN_NAME, + strlen(KRB5_ANON_NAME), KRB5_ANON_NAME, NULL); + if (retval != 0) + bail("cannot construct anonymous principal"); + krb5_free_default_realm(ctx, realm); + + /* Obtain the credentials. */ + memset(&creds, 0, sizeof(creds)); + retval = krb5_get_init_creds_opt_alloc(ctx, &opts); + if (retval != 0) + bail("cannot create credential options"); + krb5_get_init_creds_opt_set_anonymous(opts, 1); + krb5_get_init_creds_opt_set_tkt_life(opts, 60); + retval = krb5_get_init_creds_password(ctx, &creds, princ, NULL, NULL, NULL, + 0, NULL, opts); + + /* Clean up. */ + if (princ != NULL) + krb5_free_principal(ctx, princ); + if (opts != NULL) + krb5_get_init_creds_opt_free(ctx, opts); + krb5_free_cred_contents(ctx, &creds); + + /* Return whether authentication succeeded. */ + return (retval == 0); +} + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + + /* Skip the test if FAST is not available. */ +#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME + skip_all("FAST support not available"); +#endif + + /* Initialize Kerberos configuration. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->username; + config.authtok = krbconf->password; + config.extra[0] = krbconf->userprinc; + kerberos_generate_conf(krbconf->realm); + + /* Skip the test if anonymous PKINIT doesn't work. */ + if (!anon_fast_works()) + skip_all("anonymous PKINIT failed"); + + /* Test anonymous FAST. */ + plan_lazy(); + run_script("data/scripts/fast/anonymous", &config); + run_script("data/scripts/fast/anonymous-debug", &config); + + return 0; +} diff --git a/tests/module/fast-t.c b/tests/module/fast-t.c new file mode 100644 index 000000000000..51fee27098c8 --- /dev/null +++ b/tests/module/fast-t.c @@ -0,0 +1,57 @@ +/* + * Tests for authenticated FAST support in pam-krb5. + * + * Tests for Flexible Authentication Secure Tunneling, a mechanism for + * improving the preauthentication part of the Kerberos protocol and + * protecting it against various attacks. This tests authenticated FAST; + * anonymous FAST is tested separately. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/kerberos.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + + /* Skip the test if FAST is not available. */ +#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME + skip_all("FAST support not available"); +#endif + + /* Initialize Kerberos configuration. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_BOTH); + memset(&config, 0, sizeof(config)); + config.user = krbconf->userprinc; + config.authtok = krbconf->password; + config.extra[0] = krbconf->cache; + + /* + * Generate a testing krb5.conf file with a nonexistent default realm so + * that we can be sure that our principals will stay fully-qualified in + * the logs. + */ + kerberos_generate_conf("bogus.example.com"); + + /* Test fast_ccache */ + plan_lazy(); + run_script("data/scripts/fast/ccache", &config); + run_script("data/scripts/fast/ccache-debug", &config); + run_script("data/scripts/fast/no-ccache", &config); + run_script("data/scripts/fast/no-ccache-debug", &config); + + return 0; +} diff --git a/tests/module/long-t.c b/tests/module/long-t.c new file mode 100644 index 000000000000..73614b0f6ec9 --- /dev/null +++ b/tests/module/long-t.c @@ -0,0 +1,46 @@ +/* + * Excessively long password tests for the pam-krb5 module. + * + * This test case includes all tests for excessively long passwords that can + * be done without having Kerberos configured and a username and password + * available. + * + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> + + +int +main(void) +{ + struct script_config config; + char *password; + + plan_lazy(); + + memset(&config, 0, sizeof(config)); + config.user = "test"; + + /* Test a password that is too long. */ + password = bcalloc_type(PAM_MAX_RESP_SIZE + 1, char); + memset(password, 'a', PAM_MAX_RESP_SIZE); + config.password = password; + run_script("data/scripts/long/password", &config); + run_script("data/scripts/long/password-debug", &config); + + /* Test a stored authtok that's too long. */ + config.authtok = password; + config.password = "testing"; + run_script("data/scripts/long/use-first", &config); + run_script("data/scripts/long/use-first-debug", &config); + + free(password); + return 0; +} diff --git a/tests/module/no-cache-t.c b/tests/module/no-cache-t.c new file mode 100644 index 000000000000..8b282d1de397 --- /dev/null +++ b/tests/module/no-cache-t.c @@ -0,0 +1,47 @@ +/* + * Authentication tests for the pam-krb5 module without a ticket cache. + * + * This test case includes tests that require Kerberos to be configured and a + * username and password available, but which don't write a ticket cache + * (which requires additional work to test the cache ownership). This test + * does not set AUTHTOK. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2011, 2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/kerberos.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->userprinc; + config.password = krbconf->password; + + /* + * Generate a testing krb5.conf file with a nonexistent default realm so + * that we can be sure that our principals will stay fully-qualified in + * the logs. + */ + kerberos_generate_conf("bogus.example.com"); + + plan_lazy(); + run_script_dir("data/scripts/no-cache", &config); + + return 0; +} diff --git a/tests/module/pam-user-t.c b/tests/module/pam-user-t.c new file mode 100644 index 000000000000..72cc21eebae3 --- /dev/null +++ b/tests/module/pam-user-t.c @@ -0,0 +1,80 @@ +/* + * Tests for PAM_USER handling. + * + * This test case includes tests that require Kerberos to be configured and a + * username and password available, but which don't write a ticket cache + * (which requires additional work to test the cache ownership). + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org> + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/macros.h> + + +/* + * Callback to check that PAM_USER matches the desired value, passed in as the + * data parameter. + */ +static void +check_pam_user(pam_handle_t *pamh, const struct script_config *config UNUSED, + void *data) +{ + int retval; + const char *name = NULL; + const char *expected = data; + + retval = pam_get_item(pamh, PAM_USER, (PAM_CONST void **) &name); + is_int(PAM_SUCCESS, retval, "Found PAM_USER"); + is_string(expected, name, "...matching %s", expected); +} + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.password = krbconf->password; + config.callback = check_pam_user; + config.extra[0] = krbconf->username; + config.extra[1] = krbconf->userprinc; + + /* + * Generate a testing krb5.conf file matching the realm of the Kerberos + * configuration so that canonicalization will work. + */ + kerberos_generate_conf(krbconf->realm); + + /* Declare our plan. */ + plan_lazy(); + + /* Authentication without a realm. No canonicalization. */ + config.user = krbconf->username; + config.data = krbconf->username; + run_script("data/scripts/pam-user/update", &config); + + /* Authentication with the local realm. Should be canonicalized. */ + config.user = krbconf->userprinc; + run_script("data/scripts/pam-user/update", &config); + + /* + * Now, test again with user updates disabled. The PAM_USER value should + * now not be canonicalized. + */ + config.data = krbconf->userprinc; + run_script("data/scripts/pam-user/no-update", &config); + + return 0; +} diff --git a/tests/module/password-t.c b/tests/module/password-t.c new file mode 100644 index 000000000000..bdf9762bc6cb --- /dev/null +++ b/tests/module/password-t.c @@ -0,0 +1,152 @@ +/* + * Authentication tests for the pam-krb5 module with ticket cache. + * + * This test case includes all tests that require Kerberos to be configured, a + * username and password available, and a ticket cache created, but with the + * PAM module running as the same user for which the ticket cache will be + * created (so without setuid and with chown doing nothing). + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012, 2014 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/krb5.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <pwd.h> +#include <sys/stat.h> +#include <time.h> + +#include <tests/fakepam/pam.h> +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/macros.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + + +static void +check_authtok(pam_handle_t *pamh, const struct script_config *config, + void *data UNUSED) +{ + int retval; + const char *authtok; + + retval = pam_get_item(pamh, PAM_AUTHTOK, (PAM_CONST void **) &authtok); + is_int(PAM_SUCCESS, retval, "Found PAM_AUTHTOK"); + is_string(config->newpass, authtok, "...and it is correct"); +} + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + char *newpass; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->username; + config.password = krbconf->password; + config.extra[0] = krbconf->userprinc; + + /* Generate a testing krb5.conf file. */ + kerberos_generate_conf(krbconf->realm); + + plan_lazy(); + + /* + * First test trying to change the password to something that's + * excessively long. + */ + newpass = bcalloc_type(PAM_MAX_RESP_SIZE + 1, char); + memset(newpass, 'a', PAM_MAX_RESP_SIZE); + config.newpass = newpass; + run_script("data/scripts/password/too-long", &config); + run_script("data/scripts/password/too-long-debug", &config); + + /* Test use_authtok with an excessively long password. */ + config.newpass = NULL; + config.authtok = newpass; + run_script("data/scripts/password/authtok-too-long", &config); + run_script("data/scripts/password/authtok-too-long-debug", &config); + + /* + * Change the password to something new. This needs to be sufficiently + * random that it's unlikely to fall afoul of password strength checking. + */ + free(newpass); + config.authtok = NULL; + basprintf(&newpass, "ngh1,a%lu nn9af6%lu", (unsigned long) getpid(), + (unsigned long) time(NULL)); + config.newpass = newpass; + run_script("data/scripts/password/basic", &config); + config.password = newpass; + config.newpass = krbconf->password; + run_script("data/scripts/password/basic-debug", &config); + + /* Test prompt_principal with password change. */ + config.password = krbconf->password; + config.newpass = newpass; + run_script("data/scripts/password/prompt-principal", &config); + + /* Change the password back and test expose-account. */ + config.password = newpass; + config.newpass = krbconf->password; + run_script("data/scripts/password/expose", &config); + + /* + * Test two banner settings by changing the password and then changing it + * back again. + */ + config.password = krbconf->password; + config.newpass = newpass; + run_script("data/scripts/password/banner", &config); + config.password = newpass; + config.newpass = krbconf->password; + run_script("data/scripts/password/no-banner", &config); + + /* Do the same, but with expose_account set as well. */ + config.password = krbconf->password; + config.newpass = newpass; + run_script("data/scripts/password/banner-expose", &config); + config.password = newpass; + config.newpass = krbconf->password; + run_script("data/scripts/password/no-banner-expose", &config); + + /* Test use_authtok. */ + config.password = krbconf->password; + config.newpass = NULL; + config.authtok = newpass; + run_script("data/scripts/password/authtok", &config); + + /* Test use_authtok with force_first_pass. */ + config.password = NULL; + config.authtok = krbconf->password; + config.oldauthtok = newpass; + run_script("data/scripts/password/authtok-force", &config); + + /* + * Ensure PAM_AUTHTOK and PAM_OLDAUTHTOK are set even if the user is + * ignored. + */ + config.user = "root"; + config.authtok = NULL; + config.oldauthtok = NULL; + config.password = "old-password"; + config.newpass = "new-password"; + config.callback = check_authtok; + run_script("data/scripts/password/ignore", &config); + + free(newpass); + return 0; +} diff --git a/tests/module/pkinit-t.c b/tests/module/pkinit-t.c new file mode 100644 index 000000000000..6bbb6993b2af --- /dev/null +++ b/tests/module/pkinit-t.c @@ -0,0 +1,98 @@ +/* + * PKINIT authentication tests for the pam-krb5 module. + * + * This test case includes tests that require a PKINIT certificate, but which + * don't write a Kerberos ticket cache. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; +#if defined(HAVE_KRB5_MIT) && defined(PATH_OPENSSL) + const char **generate_pkcs12; + char *tmpdir, *pkcs12_path; +#endif + + /* Load the Kerberos principal and certificate path. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PKINIT); + memset(&config, 0, sizeof(config)); + config.user = krbconf->pkinit_principal; + config.extra[0] = krbconf->pkinit_cert; + + /* + * Generate a testing krb5.conf file with a nonexistent default realm so + * that we can be sure that our principals will stay fully-qualified in + * the logs. + */ + kerberos_generate_conf("bogus.example.com"); + + /* Check things that are the same with both Kerberos implementations. */ + plan_lazy(); + run_script("data/scripts/pkinit/basic", &config); + run_script("data/scripts/pkinit/basic-debug", &config); + run_script("data/scripts/pkinit/prompt-use", &config); + run_script("data/scripts/pkinit/prompt-try", &config); + run_script("data/scripts/pkinit/try-pkinit", &config); + + /* Debugging output is a little different between the implementations. */ +#ifdef HAVE_KRB5_HEIMDAL + run_script("data/scripts/pkinit/try-pkinit-debug", &config); +#else + run_script("data/scripts/pkinit/try-pkinit-debug-mit", &config); +#endif + + /* Only MIT Kerberos supports setting preauth options. */ +#ifdef HAVE_KRB5_MIT + run_script("data/scripts/pkinit/preauth-opt-mit", &config); +#endif + + /* + * If OpenSSL is available, test prompting with MIT Kerberos since we have + * to implement the prompting for the use_pkinit case ourselves. To do + * this, convert the input PKINIT certificate to a PKCS12 file with a + * password. + */ +#if defined(HAVE_KRB5_MIT) && defined(PATH_OPENSSL) + tmpdir = test_tmpdir(); + basprintf(&pkcs12_path, "%s/%s", tmpdir, "pkinit-pkcs12"); + generate_pkcs12 = bcalloc_type(10, const char *); + generate_pkcs12[0] = PATH_OPENSSL; + generate_pkcs12[1] = "pkcs12"; + generate_pkcs12[2] = "-export"; + generate_pkcs12[3] = "-in"; + generate_pkcs12[4] = krbconf->pkinit_cert; + generate_pkcs12[5] = "-password"; + generate_pkcs12[6] = "pass:some-password"; + generate_pkcs12[7] = "-out"; + generate_pkcs12[8] = pkcs12_path; + generate_pkcs12[9] = NULL; + run_setup(generate_pkcs12); + free(generate_pkcs12); + config.extra[0] = pkcs12_path; + config.extra[1] = "some-password"; + run_script("data/scripts/pkinit/pin-mit", &config); + unlink(pkcs12_path); + free(pkcs12_path); + test_tmpdir_free(tmpdir); +#endif /* HAVE_KRB5_MIT && PATH_OPENSSL */ + + return 0; +} diff --git a/tests/module/realm-t.c b/tests/module/realm-t.c new file mode 100644 index 000000000000..d5643ca1f3e5 --- /dev/null +++ b/tests/module/realm-t.c @@ -0,0 +1,87 @@ +/* + * Authentication tests for realm support in pam-krb5. + * + * Test the realm and user_realm option in the PAM configuration, which is + * special in several ways since it influences krb5.conf parsing and is read + * out of order in the initial configuration. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/krb5.h> +#include <portable/system.h> + +#include <pwd.h> + +#include <tests/fakepam/pam.h> +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + struct passwd pwd; + FILE *file; + char *k5login; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->username; + config.authtok = krbconf->password; + + /* Don't keep track of the tests in each script. */ + plan_lazy(); + + /* Start with a nonexistent default realm for authentication failure. */ + kerberos_generate_conf("bogus.example.com"); + config.extra[0] = "bogus.example.com"; + run_script("data/scripts/realm/fail-no-realm", &config); + run_script("data/scripts/realm/fail-no-realm-debug", &config); + + /* Running a script that sets realm properly should pass. */ + config.extra[0] = krbconf->realm; + run_script("data/scripts/realm/pass-realm", &config); + + /* Setting user_realm should continue to fail due to no .k5login file. */ + run_script("data/scripts/realm/fail-user-realm", &config); + + /* If we add a .k5login file for the user, user_realm should work. */ + pwd.pw_name = krbconf->username; + pwd.pw_uid = getuid(); + pwd.pw_gid = getgid(); + pwd.pw_dir = test_tmpdir(); + pam_set_pwd(&pwd); + basprintf(&k5login, "%s/.k5login", pwd.pw_dir); + file = fopen(k5login, "w"); + if (file == NULL) + sysbail("cannot create %s", k5login); + if (fprintf(file, "%s\n", krbconf->userprinc) < 0) + sysbail("cannot write to %s", k5login); + if (fclose(file) < 0) + sysbail("cannot flush %s", k5login); + run_script("data/scripts/realm/pass-user-realm", &config); + pam_set_pwd(NULL); + unlink(k5login); + free(k5login); + test_tmpdir_free(pwd.pw_dir); + + /* Switch to the correct realm, but set the wrong realm in PAM. */ + kerberos_generate_conf(krbconf->realm); + config.extra[0] = "bogus.example.com"; + run_script("data/scripts/realm/fail-realm", &config); + run_script("data/scripts/realm/fail-bad-user-realm", &config); + + return 0; +} diff --git a/tests/module/stacked-t.c b/tests/module/stacked-t.c new file mode 100644 index 000000000000..ef8e70885ecb --- /dev/null +++ b/tests/module/stacked-t.c @@ -0,0 +1,50 @@ +/* + * Authentication tests for the pam-krb5 module with an existing AUTHTOK. + * + * This test case includes tests that require Kerberos to be configured and a + * username and password available and that run with AUTHTOK already set, but + * which don't write a ticket cache (which requires additional work to test + * the cache ownership). + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + struct kerberos_config *krbconf; + + /* Load the Kerberos principal and password from a file. */ + krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD); + memset(&config, 0, sizeof(config)); + config.user = krbconf->userprinc; + config.password = krbconf->password; + config.authtok = krbconf->password; + + /* + * Generate a testing krb5.conf file with a nonexistent default realm so + * that we can be sure that our principals will stay fully-qualified in + * the logs. + */ + kerberos_generate_conf("bogus.example.com"); + + plan_lazy(); + run_script_dir("data/scripts/stacked", &config); + + return 0; +} diff --git a/tests/module/trace-t.c b/tests/module/trace-t.c new file mode 100644 index 000000000000..db3aa67f9e24 --- /dev/null +++ b/tests/module/trace-t.c @@ -0,0 +1,48 @@ +/* + * Tests for trace logging in the pam-krb5 module. + * + * Checks that trace logging is handled properly. This is currently very + * simple and just checks that the file is created. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * SPDX-License-Identifier: BSD-3-clause or GPL-1+ + */ + +#include <config.h> +#include <portable/system.h> + +#include <tests/fakepam/script.h> +#include <tests/tap/basic.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct script_config config; + char *tmpdir, *trace; + + plan_lazy(); + + memset(&config, 0, sizeof(config)); + config.user = "testuser"; + tmpdir = test_tmpdir(); + basprintf(&trace, "%s/trace", tmpdir); + config.extra[0] = trace; +#ifdef HAVE_KRB5_SET_TRACE_FILENAME + run_script("data/scripts/trace/supported", &config); + is_int(0, access(trace, F_OK), "Trace file was created"); + unlink(trace); +#else + run_script("data/scripts/trace/unsupported", &config); + is_int(-1, access(trace, F_OK), "Trace file does not exist"); +#endif + + free(trace); + test_tmpdir_free(tmpdir); + return 0; +} diff --git a/tests/pam-util/args-t.c b/tests/pam-util/args-t.c new file mode 100644 index 000000000000..4ec102e511ed --- /dev/null +++ b/tests/pam-util/args-t.c @@ -0,0 +1,86 @@ +/* + * PAM utility argument initialization test suite. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2010, 2012-2013 + * 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <pam-util/args.h> +#include <tests/fakepam/pam.h> +#include <tests/tap/basic.h> + + +int +main(void) +{ + pam_handle_t *pamh; + struct pam_conv conv = {NULL, NULL}; + struct pam_args *args; + + plan(12); + + if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS) + sysbail("Fake PAM initialization failed"); + args = putil_args_new(pamh, 0); + ok(args != NULL, "New args struct is not NULL"); + if (args == NULL) + ok_block(7, 0, "...args struct is NULL"); + else { + ok(args->pamh == pamh, "...and pamh is correct"); + ok(args->config == NULL, "...and config is NULL"); + ok(args->user == NULL, "...and user is NULL"); + is_int(args->debug, false, "...and debug is false"); + is_int(args->silent, false, "...and silent is false"); +#ifdef HAVE_KRB5 + ok(args->ctx != NULL, "...and the Kerberos context is initialized"); + ok(args->realm == NULL, "...and realm is NULL"); +#else + skip_block(2, "Kerberos support not configured"); +#endif + } + putil_args_free(args); + ok(1, "Freeing the args struct works"); + + args = putil_args_new(pamh, PAM_SILENT); + ok(args != NULL, "New args struct with PAM_SILENT is not NULL"); + if (args == NULL) + ok(0, "...args is NULL"); + else + is_int(args->silent, true, "...and silent is true"); + putil_args_free(args); + + putil_args_free(NULL); + ok(1, "Freeing a NULL args struct works"); + + pam_end(pamh, 0); + + return 0; +} diff --git a/tests/pam-util/fakepam-t.c b/tests/pam-util/fakepam-t.c new file mode 100644 index 000000000000..1e09c5fdde75 --- /dev/null +++ b/tests/pam-util/fakepam-t.c @@ -0,0 +1,121 @@ +/* + * Fake PAM library test suite. + * + * This is not actually a test for the pam-util layer, but rather is a test + * for the trickier components of the fake PAM library that in turn is used to + * test the pam-util layer and PAM modules. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2010, 2013 + * 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <tests/fakepam/pam.h> +#include <tests/tap/basic.h> + + +int +main(void) +{ + pam_handle_t *pamh; + struct pam_conv conv = {NULL, NULL}; + char **env; + size_t i; + + /* + * Skip this test if the native PAM library doesn't support a PAM + * environment, since we "break" pam_putenv to mirror the native behavior + * in that case. + */ +#ifndef HAVE_PAM_GETENV + skip_all("system doesn't support PAM environment"); +#endif + + plan(33); + + /* Basic environment manipulation. */ + if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS) + sysbail("Fake PAM initialization failed"); + is_int(PAM_BAD_ITEM, pam_putenv(pamh, "TEST"), "delete when NULL"); + ok(pam_getenv(pamh, "TEST") == NULL, "getenv when NULL"); + env = pam_getenvlist(pamh); + ok(env != NULL, "getenvlist when NULL returns non-NULL"); + if (env == NULL) + bail("pam_getenvlist returned NULL"); + is_string(NULL, env[0], "...but first element is NULL"); + for (i = 0; env[i] != NULL; i++) + free(env[i]); + free(env); + + /* putenv and getenv. */ + is_int(PAM_SUCCESS, pam_putenv(pamh, "TEST=foo"), "putenv TEST"); + is_string("foo", pam_getenv(pamh, "TEST"), "getenv TEST"); + is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=bar"), "putenv FOO"); + is_int(PAM_SUCCESS, pam_putenv(pamh, "BAR=baz"), "putenv BAR"); + is_string("foo", pam_getenv(pamh, "TEST"), "getenv TEST"); + is_string("bar", pam_getenv(pamh, "FOO"), "getenv FOO"); + is_string("baz", pam_getenv(pamh, "BAR"), "getenv BAR"); + ok(pam_getenv(pamh, "BAZ") == NULL, "getenv BAZ is NULL"); + + /* Replacing and deleting environment variables. */ + is_int(PAM_BAD_ITEM, pam_putenv(pamh, "BAZ"), "putenv nonexistent delete"); + is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=foo"), "putenv replace"); + is_int(PAM_SUCCESS, pam_putenv(pamh, "FOON=bar=n"), "putenv prefix"); + is_string("foo", pam_getenv(pamh, "FOO"), "getenv FOO"); + is_string("bar=n", pam_getenv(pamh, "FOON"), "getenv FOON"); + is_int(PAM_BAD_ITEM, pam_putenv(pamh, "FO"), "putenv delete FO"); + is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO"), "putenv delete FOO"); + ok(pam_getenv(pamh, "FOO") == NULL, "getenv FOO is NULL"); + is_string("bar=n", pam_getenv(pamh, "FOON"), "getenv FOON"); + is_string("baz", pam_getenv(pamh, "BAR"), "getenv BAR"); + + /* pam_getenvlist. */ + env = pam_getenvlist(pamh); + ok(env != NULL, "getenvlist not NULL"); + if (env == NULL) + bail("pam_getenvlist returned NULL"); + is_string("TEST=foo", env[0], "getenvlist TEST"); + is_string("BAR=baz", env[1], "getenvlist BAR"); + is_string("FOON=bar=n", env[2], "getenvlist FOON"); + ok(env[3] == NULL, "getenvlist length"); + for (i = 0; env[i] != NULL; i++) + free(env[i]); + free(env); + is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=foo"), "putenv FOO"); + is_string("TEST=foo", pamh->environ[0], "pamh environ TEST"); + is_string("BAR=baz", pamh->environ[1], "pamh environ BAR"); + is_string("FOON=bar=n", pamh->environ[2], "pamh environ FOON"); + is_string("FOO=foo", pamh->environ[3], "pamh environ FOO"); + ok(pamh->environ[4] == NULL, "pamh environ length"); + + pam_end(pamh, 0); + + return 0; +} diff --git a/tests/pam-util/logging-t.c b/tests/pam-util/logging-t.c new file mode 100644 index 000000000000..84072bd6b91a --- /dev/null +++ b/tests/pam-util/logging-t.c @@ -0,0 +1,146 @@ +/* + * PAM logging test suite. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2010-2013 + * 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <syslog.h> + +#include <pam-util/args.h> +#include <pam-util/logging.h> +#include <tests/fakepam/pam.h> +#include <tests/tap/basic.h> +#include <tests/tap/string.h> + +/* Test a normal PAM logging function. */ +#define TEST(func, p, n) \ + do { \ + (func)(args, "%s", "foo"); \ + seen = pam_output(); \ + is_int((p), seen->lines[0].priority, "priority %d", (p)); \ + is_string("foo", seen->lines[0].line, "line %s", (n)); \ + pam_output_free(seen); \ + } while (0) + +/* Test a PAM error logging function. */ +#define TEST_PAM(func, c, p, n) \ + do { \ + (func)(args, (c), "%s", "bar"); \ + if ((c) == PAM_SUCCESS) \ + expected = strdup("bar"); \ + else \ + basprintf(&expected, "%s: %s", "bar", \ + pam_strerror(args->pamh, c)); \ + seen = pam_output(); \ + is_int((p), seen->lines[0].priority, "priority %s", (n)); \ + is_string(expected, seen->lines[0].line, "line %s", (n)); \ + pam_output_free(seen); \ + free(expected); \ + } while (0) + +/* Test a PAM Kerberos error logging function .*/ +#define TEST_KRB5(func, p, n) \ + do { \ + const char *msg; \ + \ + code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ); \ + (func)(args, code, "%s", "krb"); \ + code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ); \ + msg = krb5_get_error_message(args->ctx, code); \ + basprintf(&expected, "%s: %s", "krb", msg); \ + seen = pam_output(); \ + is_int((p), seen->lines[0].priority, "priority %s", (n)); \ + is_string(expected, seen->lines[0].line, "line %s", (n)); \ + pam_output_free(seen); \ + free(expected); \ + krb5_free_error_message(args->ctx, msg); \ + } while (0) + + +int +main(void) +{ + pam_handle_t *pamh; + struct pam_args *args; + struct pam_conv conv = {NULL, NULL}; + char *expected; + struct output *seen; +#ifdef HAVE_KRB5 + krb5_error_code code; + krb5_principal princ; +#endif + + plan(27); + + if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS) + sysbail("Fake PAM initialization failed"); + args = putil_args_new(pamh, 0); + if (args == NULL) + bail("cannot create PAM argument struct"); + TEST(putil_crit, LOG_CRIT, "putil_crit"); + TEST(putil_err, LOG_ERR, "putil_err"); + putil_debug(args, "%s", "foo"); + ok(pam_output() == NULL, "putil_debug without debug on"); + args->debug = true; + TEST(putil_debug, LOG_DEBUG, "putil_debug"); + args->debug = false; + + TEST_PAM(putil_crit_pam, PAM_SYSTEM_ERR, LOG_CRIT, "putil_crit_pam S"); + TEST_PAM(putil_crit_pam, PAM_BUF_ERR, LOG_CRIT, "putil_crit_pam B"); + TEST_PAM(putil_crit_pam, PAM_SUCCESS, LOG_CRIT, "putil_crit_pam ok"); + TEST_PAM(putil_err_pam, PAM_SYSTEM_ERR, LOG_ERR, "putil_err_pam"); + putil_debug_pam(args, PAM_SYSTEM_ERR, "%s", "bar"); + ok(pam_output() == NULL, "putil_debug_pam without debug on"); + args->debug = true; + TEST_PAM(putil_debug_pam, PAM_SYSTEM_ERR, LOG_DEBUG, "putil_debug_pam"); + TEST_PAM(putil_debug_pam, PAM_SUCCESS, LOG_DEBUG, "putil_debug_pam ok"); + args->debug = false; + +#ifdef HAVE_KRB5 + TEST_KRB5(putil_crit_krb5, LOG_CRIT, "putil_crit_krb5"); + TEST_KRB5(putil_err_krb5, LOG_ERR, "putil_err_krb5"); + code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ); + putil_debug_krb5(args, code, "%s", "krb"); + ok(pam_output() == NULL, "putil_debug_krb5 without debug on"); + args->debug = true; + TEST_KRB5(putil_debug_krb5, LOG_DEBUG, "putil_debug_krb5"); + args->debug = false; +#else + skip_block(4, "not built with Kerberos support"); +#endif + + putil_args_free(args); + pam_end(pamh, 0); + + return 0; +} diff --git a/tests/pam-util/options-t.c b/tests/pam-util/options-t.c new file mode 100644 index 000000000000..f8b76730fb2d --- /dev/null +++ b/tests/pam-util/options-t.c @@ -0,0 +1,458 @@ +/* + * PAM option parsing test suite. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2020 Russ Allbery <eagle@eyrie.org> + * 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 <config.h> +#include <portable/pam.h> +#include <portable/system.h> + +#include <syslog.h> + +#include <pam-util/args.h> +#include <pam-util/options.h> +#include <pam-util/vector.h> +#include <tests/fakepam/pam.h> +#include <tests/tap/basic.h> +#include <tests/tap/string.h> + +/* 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; +} diff --git a/tests/pam-util/vector-t.c b/tests/pam-util/vector-t.c new file mode 100644 index 000000000000..d7b87e36d8f4 --- /dev/null +++ b/tests/pam-util/vector-t.c @@ -0,0 +1,149 @@ +/* + * PAM utility vector library test suite. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2014, 2016, 2018-2019 Russ Allbery <eagle@eyrie.org> + * Copyright 2010-2011, 2013 + * The Board of Trustees of the Leland Stanford Junior University + * + * Copying and distribution of this file, with or without modification, are + * permitted in any medium without royalty provided the copyright notice and + * this notice are preserved. This file is offered as-is, without any + * warranty. + * + * SPDX-License-Identifier: FSFAP + */ + +#include <config.h> +#include <portable/system.h> + +#include <sys/wait.h> + +#include <pam-util/vector.h> +#include <tests/tap/basic.h> +#include <tests/tap/string.h> + + +int +main(void) +{ + struct vector *vector, *ovector, *copy; + char *command, *string; + const char *env[2]; + pid_t child; + size_t i; + const char cstring[] = "This is a\ttest. "; + + plan(60); + + vector = vector_new(); + ok(vector != NULL, "vector_new returns non-NULL"); + if (vector == NULL) + bail("vector_new returned NULL"); + ok(vector_add(vector, cstring), "vector_add succeeds"); + is_int(1, vector->count, "vector_add increases count"); + ok(vector->strings[0] != cstring, "...and allocated new memory"); + ok(vector_resize(vector, 4), "vector_resize succeeds"); + is_int(4, vector->allocated, "vector_resize works"); + ok(vector_add(vector, cstring), "vector_add #2"); + ok(vector_add(vector, cstring), "vector_add #3"); + ok(vector_add(vector, cstring), "vector_add #4"); + is_int(4, vector->allocated, "...and no reallocation when adding strings"); + is_int(4, vector->count, "...and the count matches"); + is_string(cstring, vector->strings[0], "added the right string"); + is_string(cstring, vector->strings[1], "added the right string"); + is_string(cstring, vector->strings[2], "added the right string"); + is_string(cstring, vector->strings[3], "added the right string"); + ok(vector->strings[1] != vector->strings[2], "each pointer is different"); + ok(vector->strings[2] != vector->strings[3], "each pointer is different"); + ok(vector->strings[3] != vector->strings[0], "each pointer is different"); + ok(vector->strings[0] != cstring, "each pointer is different"); + copy = vector_copy(vector); + ok(copy != NULL, "vector_copy returns non-NULL"); + if (copy == NULL) + bail("vector_copy returned NULL"); + is_int(4, copy->count, "...and has right count"); + is_int(4, copy->allocated, "...and has right allocated count"); + for (i = 0; i < 4; i++) { + is_string(cstring, copy->strings[i], "...and string %lu is right", + (unsigned long) i); + ok(copy->strings[i] != vector->strings[i], + "...and pointer %lu is different", (unsigned long) i); + } + vector_free(copy); + vector_clear(vector); + is_int(0, vector->count, "vector_clear works"); + is_int(4, vector->allocated, "...but doesn't free the allocation"); + string = strdup(cstring); + if (string == NULL) + sysbail("cannot allocate memory"); + ok(vector_add(vector, cstring), "vector_add succeeds"); + ok(vector_add(vector, string), "vector_add succeeds"); + is_int(2, vector->count, "added two strings to the vector"); + ok(vector->strings[1] != string, "...and the pointers are different"); + ok(vector_resize(vector, 1), "vector_resize succeeds"); + is_int(1, vector->count, "vector_resize shrinks the vector"); + ok(vector->strings[0] != cstring, "...and the pointer is different"); + vector_free(vector); + free(string); + + vector = vector_split_multi("foo, bar, baz", ", ", NULL); + ok(vector != NULL, "vector_split_multi returns non-NULL"); + if (vector == NULL) + bail("vector_split_multi returned NULL"); + is_int(3, vector->count, "vector_split_multi returns right count"); + is_string("foo", vector->strings[0], "...first string"); + is_string("bar", vector->strings[1], "...second string"); + is_string("baz", vector->strings[2], "...third string"); + ovector = vector; + vector = vector_split_multi("", ", ", vector); + ok(vector != NULL, "reuse of vector doesn't return NULL"); + ok(vector == ovector, "...and reuses the same vector pointer"); + is_int(0, vector->count, "vector_split_multi reuse with empty string"); + is_int(3, vector->allocated, "...and doesn't free allocation"); + vector = vector_split_multi(",,, foo, ", ", ", vector); + ok(vector != NULL, "reuse of vector doesn't return NULL"); + is_int(1, vector->count, "vector_split_multi with extra separators"); + is_string("foo", vector->strings[0], "...first string"); + vector = vector_split_multi(", , ", ", ", vector); + is_int(0, vector->count, "vector_split_multi with only separators"); + vector_free(vector); + + vector = vector_new(); + ok(vector_add(vector, "/bin/sh"), "vector_add succeeds"); + ok(vector_add(vector, "-c"), "vector_add succeeds"); + basprintf(&command, "echo ok %lu - vector_exec", testnum++); + ok(vector_add(vector, command), "vector_add succeeds"); + child = fork(); + if (child < 0) + sysbail("unable to fork"); + else if (child == 0) + if (vector_exec("/bin/sh", vector) < 0) + sysdiag("unable to exec /bin/sh"); + waitpid(child, NULL, 0); + vector_free(vector); + free(command); + + vector = vector_new(); + ok(vector_add(vector, "/bin/sh"), "vector_add succeeds"); + ok(vector_add(vector, "-c"), "vector_add succeeds"); + ok(vector_add(vector, "echo ok $NUMBER - vector_exec_env"), + "vector_add succeeds"); + basprintf(&string, "NUMBER=%lu", testnum++); + env[0] = string; + env[1] = NULL; + child = fork(); + if (child < 0) + sysbail("unable to fork"); + else if (child == 0) + if (vector_exec_env("/bin/sh", vector, env) < 0) + sysdiag("unable to exec /bin/sh"); + waitpid(child, NULL, 0); + vector_free(vector); + free(string); + + return 0; +} diff --git a/tests/portable/asprintf-t.c b/tests/portable/asprintf-t.c new file mode 100644 index 000000000000..3b10a6622c31 --- /dev/null +++ b/tests/portable/asprintf-t.c @@ -0,0 +1,69 @@ +/* + * asprintf and vasprintf test suite. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2014, 2018 Russ Allbery <eagle@eyrie.org> + * Copyright 2006-2009, 2011 + * The Board of Trustees of the Leland Stanford Junior University + * + * Copying and distribution of this file, with or without modification, are + * permitted in any medium without royalty provided the copyright notice and + * this notice are preserved. This file is offered as-is, without any + * warranty. + * + * SPDX-License-Identifier: FSFAP + */ + +#include <config.h> +#include <portable/macros.h> +#include <portable/system.h> + +#include <tests/tap/basic.h> + +int test_asprintf(char **, const char *, ...) + __attribute__((__format__(printf, 2, 3))); +int test_vasprintf(char **, const char *, va_list) + __attribute__((__format__(printf, 2, 0))); + +static int __attribute__((__format__(printf, 2, 3))) +vatest(char **result, const char *format, ...) +{ + va_list args; + int status; + + va_start(args, format); + status = test_vasprintf(result, format, args); + va_end(args); + return status; +} + +int +main(void) +{ + char *result = NULL; + + plan(12); + + is_int(7, test_asprintf(&result, "%s", "testing"), "asprintf length"); + is_string("testing", result, "asprintf result"); + free(result); + ok(3, "free asprintf"); + is_int(0, test_asprintf(&result, "%s", ""), "asprintf empty length"); + is_string("", result, "asprintf empty string"); + free(result); + ok(6, "free asprintf of empty string"); + + is_int(6, vatest(&result, "%d %s", 2, "test"), "vasprintf length"); + is_string("2 test", result, "vasprintf result"); + free(result); + ok(9, "free vasprintf"); + is_int(0, vatest(&result, "%s", ""), "vasprintf empty length"); + is_string("", result, "vasprintf empty string"); + free(result); + ok(12, "free vasprintf of empty string"); + + return 0; +} diff --git a/tests/portable/asprintf.c b/tests/portable/asprintf.c new file mode 100644 index 000000000000..221c9932c5cd --- /dev/null +++ b/tests/portable/asprintf.c @@ -0,0 +1,2 @@ +#define TESTING 1 +#include <portable/asprintf.c> diff --git a/tests/portable/mkstemp-t.c b/tests/portable/mkstemp-t.c new file mode 100644 index 000000000000..dc268210f063 --- /dev/null +++ b/tests/portable/mkstemp-t.c @@ -0,0 +1,81 @@ +/* + * mkstemp test suite. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2009, 2011 + * The Board of Trustees of the Leland Stanford Junior University + * + * Copying and distribution of this file, with or without modification, are + * permitted in any medium without royalty provided the copyright notice and + * this notice are preserved. This file is offered as-is, without any + * warranty. + * + * SPDX-License-Identifier: FSFAP + */ + +#include <config.h> +#include <portable/system.h> + +#include <errno.h> +#include <sys/stat.h> + +#include <tests/tap/basic.h> + +int test_mkstemp(char *template); + +int +main(void) +{ + int fd; + char template[] = "tsXXXXXXX"; + char tooshort[] = "XXXXX"; + char bad1[] = "/foo/barXXXXX"; + char bad2[] = "/foo/barXXXXXX.out"; + char buffer[256]; + struct stat st1, st2; + ssize_t length; + + plan(20); + + /* First, test a few error messages. */ + errno = 0; + is_int(-1, test_mkstemp(tooshort), "too short of template"); + is_int(EINVAL, errno, "...with correct errno"); + is_string("XXXXX", tooshort, "...and template didn't change"); + errno = 0; + is_int(-1, test_mkstemp(bad1), "bad template"); + is_int(EINVAL, errno, "...with correct errno"); + is_string("/foo/barXXXXX", bad1, "...and template didn't change"); + errno = 0; + is_int(-1, test_mkstemp(bad2), "template doesn't end in XXXXXX"); + is_int(EINVAL, errno, "...with correct errno"); + is_string("/foo/barXXXXXX.out", bad2, "...and template didn't change"); + errno = 0; + + /* Now try creating a real file. */ + fd = test_mkstemp(template); + ok(fd >= 0, "mkstemp works with valid template"); + ok(strcmp(template, "tsXXXXXXX") != 0, "...and template changed"); + ok(strncmp(template, "tsX", 3) == 0, "...and didn't touch first X"); + ok(access(template, F_OK) == 0, "...and the file exists"); + + /* Make sure that it's the same file as template refers to now. */ + ok(stat(template, &st1) == 0, "...and stat of template works"); + ok(fstat(fd, &st2) == 0, "...and stat of open file descriptor works"); + ok(st1.st_ino == st2.st_ino, "...and they're the same file"); + unlink(template); + + /* Make sure the open mode is correct. */ + length = strlen(template); + is_int(length, write(fd, template, length), "write to open file works"); + ok(lseek(fd, 0, SEEK_SET) == 0, "...and rewind works"); + is_int(length, read(fd, buffer, length), "...and the data is there"); + buffer[length] = '\0'; + is_string(template, buffer, "...and matches what we wrote"); + close(fd); + + return 0; +} diff --git a/tests/portable/mkstemp.c b/tests/portable/mkstemp.c new file mode 100644 index 000000000000..4632d3de86ed --- /dev/null +++ b/tests/portable/mkstemp.c @@ -0,0 +1,2 @@ +#define TESTING 1 +#include <portable/mkstemp.c> diff --git a/tests/portable/strndup-t.c b/tests/portable/strndup-t.c new file mode 100644 index 000000000000..9bf28a31beec --- /dev/null +++ b/tests/portable/strndup-t.c @@ -0,0 +1,60 @@ +/* + * strndup test suite. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2018 Russ Allbery <eagle@eyrie.org> + * Copyright 2011-2012 + * The Board of Trustees of the Leland Stanford Junior University + * + * Copying and distribution of this file, with or without modification, are + * permitted in any medium without royalty provided the copyright notice and + * this notice are preserved. This file is offered as-is, without any + * warranty. + * + * SPDX-License-Identifier: FSFAP + */ + +#include <config.h> +#include <portable/system.h> + +#include <errno.h> + +#include <tests/tap/basic.h> + +char *test_strndup(const char *, size_t); + + +int +main(void) +{ + char buffer[3]; + char *result; + + plan(7); + + result = test_strndup("foo", 8); + is_string("foo", result, "strndup longer than string"); + free(result); + result = test_strndup("foo", 2); + is_string("fo", result, "strndup shorter than string"); + free(result); + result = test_strndup("foo", 3); + is_string("foo", result, "strndup same size as string"); + free(result); + result = test_strndup("foo", 0); + is_string("", result, "strndup of size 0"); + free(result); + memcpy(buffer, "foo", 3); + result = test_strndup(buffer, 3); + is_string("foo", result, "strndup of non-nul-terminated string"); + free(result); + errno = 0; + result = test_strndup(NULL, 0); + is_string(NULL, result, "strndup of NULL"); + is_int(errno, EINVAL, "...and returns EINVAL"); + + return 0; +} diff --git a/tests/portable/strndup.c b/tests/portable/strndup.c new file mode 100644 index 000000000000..99c3bc13a744 --- /dev/null +++ b/tests/portable/strndup.c @@ -0,0 +1,2 @@ +#define TESTING 1 +#include <portable/strndup.c> diff --git a/tests/runtests.c b/tests/runtests.c new file mode 100644 index 000000000000..54ec1c93d08b --- /dev/null +++ b/tests/runtests.c @@ -0,0 +1,1782 @@ +/* + * Run a set of tests, reporting results. + * + * Test suite driver that runs a set of tests implementing a subset of the + * Test Anything Protocol (TAP) and reports the results. + * + * Any bug reports, bug fixes, and improvements are very much welcome and + * should be sent to the e-mail address below. This program is part of C TAP + * Harness <https://www.eyrie.org/~eagle/software/c-tap-harness/>. + * + * Copyright 2000-2001, 2004, 2006-2019 Russ Allbery <eagle@eyrie.org> + * + * 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 + */ + +/* + * Usage: + * + * runtests [-hv] [-b <build-dir>] [-s <source-dir>] -l <test-list> + * runtests [-hv] [-b <build-dir>] [-s <source-dir>] <test> [<test> ...] + * runtests -o [-h] [-b <build-dir>] [-s <source-dir>] <test> + * + * In the first case, expects a list of executables located in the given file, + * one line per executable, possibly followed by a space-separated list of + * options. For each one, runs it as part of a test suite, reporting results. + * In the second case, use the same infrastructure, but run only the tests + * listed on the command line. + * + * Test output should start with a line containing the number of tests + * (numbered from 1 to this number), optionally preceded by "1..", although + * that line may be given anywhere in the output. Each additional line should + * be in the following format: + * + * ok <number> + * not ok <number> + * ok <number> # skip + * not ok <number> # todo + * + * where <number> is the number of the test. An optional comment is permitted + * after the number if preceded by whitespace. ok indicates success, not ok + * indicates failure. "# skip" and "# todo" are a special cases of a comment, + * and must start with exactly that formatting. They indicate the test was + * skipped for some reason (maybe because it doesn't apply to this platform) + * or is testing something known to currently fail. The text following either + * "# skip" or "# todo" and whitespace is the reason. + * + * As a special case, the first line of the output may be in the form: + * + * 1..0 # skip some reason + * + * which indicates that this entire test case should be skipped and gives a + * reason. + * + * Any other lines are ignored, although for compliance with the TAP protocol + * all lines other than the ones in the above format should be sent to + * standard error rather than standard output and start with #. + * + * This is a subset of TAP as documented in Test::Harness::TAP or + * TAP::Parser::Grammar, which comes with Perl. + * + * If the -o option is given, instead run a single test and display all of its + * output. This is intended for use with failing tests so that the person + * running the test suite can get more details about what failed. + * + * If built with the C preprocessor symbols C_TAP_SOURCE and C_TAP_BUILD + * defined, C TAP Harness will export those values in the environment so that + * tests can find the source and build directory and will look for tests under + * both directories. These paths can also be set with the -b and -s + * command-line options, which will override anything set at build time. + * + * If the -v option is given, or the C_TAP_VERBOSE environment variable is set, + * display the full output of each test as it runs rather than showing a + * summary of the results of each test. + */ + +/* Required for fdopen(), getopt(), and putenv(). */ +#if defined(__STRICT_ANSI__) || defined(PEDANTIC) +# ifndef _XOPEN_SOURCE +# define _XOPEN_SOURCE 500 +# endif +#endif + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdarg.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +/* sys/time.h must be included before sys/resource.h on some platforms. */ +#include <sys/resource.h> + +/* AIX 6.1 (and possibly later) doesn't have WCOREDUMP. */ +#ifndef WCOREDUMP +# define WCOREDUMP(status) ((unsigned) (status) &0x80) +#endif + +/* + * POSIX requires that these be defined in <unistd.h>, but they're not always + * available. If one of them has been defined, all the rest almost certainly + * have. + */ +#ifndef STDIN_FILENO +# define STDIN_FILENO 0 +# define STDOUT_FILENO 1 +# define STDERR_FILENO 2 +#endif + +/* + * Used for iterating through arrays. Returns the number of elements in the + * array (useful for a < upper bound in a for loop). + */ +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +/* + * The source and build versions of the tests directory. This is used to set + * the C_TAP_SOURCE and C_TAP_BUILD environment variables (and the SOURCE and + * BUILD environment variables set for backward compatibility) and find test + * programs, if set. Normally, this should be set as part of the build + * process to the test subdirectories of $(abs_top_srcdir) and + * $(abs_top_builddir) respectively. + */ +#ifndef C_TAP_SOURCE +# define C_TAP_SOURCE NULL +#endif +#ifndef C_TAP_BUILD +# define C_TAP_BUILD NULL +#endif + +/* Test status codes. */ +enum test_status +{ + TEST_FAIL, + TEST_PASS, + TEST_SKIP, + TEST_INVALID +}; + +/* Really, just a boolean, but this is more self-documenting. */ +enum test_verbose +{ + CONCISE = 0, + VERBOSE = 1 +}; + +/* Indicates the state of our plan. */ +enum plan_status +{ + PLAN_INIT, /* Nothing seen yet. */ + PLAN_FIRST, /* Plan seen before any tests. */ + PLAN_PENDING, /* Test seen and no plan yet. */ + PLAN_FINAL /* Plan seen after some tests. */ +}; + +/* Error exit statuses for test processes. */ +#define CHILDERR_DUP 100 /* Couldn't redirect stderr or stdout. */ +#define CHILDERR_EXEC 101 /* Couldn't exec child process. */ +#define CHILDERR_STDIN 102 /* Couldn't open stdin file. */ +#define CHILDERR_STDERR 103 /* Couldn't open stderr file. */ + +/* Structure to hold data for a set of tests. */ +struct testset { + char *file; /* The file name of the test. */ + char **command; /* The argv vector to run the command. */ + enum plan_status plan; /* The status of our plan. */ + unsigned long count; /* Expected count of tests. */ + unsigned long current; /* The last seen test number. */ + unsigned int length; /* The length of the last status message. */ + unsigned long passed; /* Count of passing tests. */ + unsigned long failed; /* Count of failing lists. */ + unsigned long skipped; /* Count of skipped tests (passed). */ + unsigned long allocated; /* The size of the results table. */ + enum test_status *results; /* Table of results by test number. */ + unsigned int aborted; /* Whether the set was aborted. */ + unsigned int reported; /* Whether the results were reported. */ + int status; /* The exit status of the test. */ + unsigned int all_skipped; /* Whether all tests were skipped. */ + char *reason; /* Why all tests were skipped. */ +}; + +/* Structure to hold a linked list of test sets. */ +struct testlist { + struct testset *ts; + struct testlist *next; +}; + +/* + * Usage message. Should be used as a printf format with four arguments: the + * path to runtests, given three times, and the usage_description. This is + * split into variables to satisfy the pedantic ISO C90 limit on strings. + */ +static const char usage_message[] = "\ +Usage: %s [-hv] [-b <build-dir>] [-s <source-dir>] <test> ...\n\ + %s [-hv] [-b <build-dir>] [-s <source-dir>] -l <test-list>\n\ + %s -o [-h] [-b <build-dir>] [-s <source-dir>] <test>\n\ +\n\ +Options:\n\ + -b <build-dir> Set the build directory to <build-dir>\n\ +%s"; +static const char usage_extra[] = "\ + -l <list> Take the list of tests to run from <test-list>\n\ + -o Run a single test rather than a list of tests\n\ + -s <source-dir> Set the source directory to <source-dir>\n\ + -v Show the full output of each test\n\ +\n\ +runtests normally runs each test listed on the command line. With the -l\n\ +option, it instead runs every test listed in a file. With the -o option,\n\ +it instead runs a single test and shows its complete output.\n"; + +/* + * Header used for test output. %s is replaced by the file name of the list + * of tests. + */ +static const char banner[] = "\n\ +Running all tests listed in %s. If any tests fail, run the failing\n\ +test program with runtests -o to see more details.\n\n"; + +/* Header for reports of failed tests. */ +static const char header[] = "\n\ +Failed Set Fail/Total (%) Skip Stat Failing Tests\n\ +-------------------------- -------------- ---- ---- ------------------------"; + +/* Include the file name and line number in malloc failures. */ +#define xcalloc(n, type) \ + ((type *) x_calloc((n), sizeof(type), __FILE__, __LINE__)) +#define xmalloc(size) ((char *) x_malloc((size), __FILE__, __LINE__)) +#define xstrdup(p) x_strdup((p), __FILE__, __LINE__) +#define xstrndup(p, size) x_strndup((p), (size), __FILE__, __LINE__) +#define xreallocarray(p, n, type) \ + ((type *) x_reallocarray((p), (n), sizeof(type), __FILE__, __LINE__)) + +/* + * __attribute__ is available in gcc 2.5 and later, but only with gcc 2.7 + * could you use the __format__ form of the attributes, which is what we use + * (to avoid confusion with other macros). + */ +#ifndef __attribute__ +# if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 7) +# define __attribute__(spec) /* empty */ +# endif +#endif + +/* + * We use __alloc_size__, but it was only available in fairly recent versions + * of GCC. Suppress warnings about the unknown attribute if GCC is too old. + * We know that we're GCC at this point, so we can use the GCC variadic macro + * extension, which will still work with versions of GCC too old to have C99 + * variadic macro support. + */ +#if !defined(__attribute__) && !defined(__alloc_size__) +# if defined(__GNUC__) && !defined(__clang__) +# if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 3) +# define __alloc_size__(spec, args...) /* empty */ +# endif +# endif +#endif + +/* + * LLVM and Clang pretend to be GCC but don't support all of the __attribute__ + * settings that GCC does. For them, suppress warnings about unknown + * attributes on declarations. This unfortunately will affect the entire + * compilation context, but there's no push and pop available. + */ +#if !defined(__attribute__) && (defined(__llvm__) || defined(__clang__)) +# pragma GCC diagnostic ignored "-Wattributes" +#endif + +/* Declare internal functions that benefit from compiler attributes. */ +static void die(const char *, ...) + __attribute__((__nonnull__, __noreturn__, __format__(printf, 1, 2))); +static void sysdie(const char *, ...) + __attribute__((__nonnull__, __noreturn__, __format__(printf, 1, 2))); +static void *x_calloc(size_t, size_t, const char *, int) + __attribute__((__alloc_size__(1, 2), __malloc__, __nonnull__)); +static void *x_malloc(size_t, const char *, int) + __attribute__((__alloc_size__(1), __malloc__, __nonnull__)); +static void *x_reallocarray(void *, size_t, size_t, const char *, int) + __attribute__((__alloc_size__(2, 3), __malloc__, __nonnull__(4))); +static char *x_strdup(const char *, const char *, int) + __attribute__((__malloc__, __nonnull__)); +static char *x_strndup(const char *, size_t, const char *, int) + __attribute__((__malloc__, __nonnull__)); + + +/* + * Report a fatal error and exit. + */ +static void +die(const char *format, ...) +{ + va_list args; + + fflush(stdout); + fprintf(stderr, "runtests: "); + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + fprintf(stderr, "\n"); + exit(1); +} + + +/* + * Report a fatal error, including the results of strerror, and exit. + */ +static void +sysdie(const char *format, ...) +{ + int oerrno; + va_list args; + + oerrno = errno; + fflush(stdout); + fprintf(stderr, "runtests: "); + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + fprintf(stderr, ": %s\n", strerror(oerrno)); + exit(1); +} + + +/* + * Allocate zeroed memory, reporting a fatal error and exiting on failure. + */ +static void * +x_calloc(size_t n, size_t size, const char *file, int line) +{ + void *p; + + n = (n > 0) ? n : 1; + size = (size > 0) ? size : 1; + p = calloc(n, size); + if (p == NULL) + sysdie("failed to calloc %lu bytes at %s line %d", + (unsigned long) size, file, line); + return p; +} + + +/* + * Allocate memory, reporting a fatal error and exiting on failure. + */ +static void * +x_malloc(size_t size, const char *file, int line) +{ + void *p; + + p = malloc(size); + if (p == NULL) + sysdie("failed to malloc %lu bytes at %s line %d", + (unsigned long) size, file, line); + return p; +} + + +/* + * Reallocate memory, reporting a fatal error and exiting on failure. + * + * We should technically use SIZE_MAX here for the overflow check, but + * SIZE_MAX is C99 and we're only assuming C89 + SUSv3, which does not + * guarantee that it exists. They do guarantee that UINT_MAX exists, and we + * can assume that UINT_MAX <= SIZE_MAX. And we should not be allocating + * anything anywhere near that large. + * + * (In theory, C89 and C99 permit size_t to be smaller than unsigned int, but + * I disbelieve in the existence of such systems and they will have to cope + * without overflow checks.) + */ +static void * +x_reallocarray(void *p, size_t n, size_t size, const char *file, int line) +{ + n = (n > 0) ? n : 1; + size = (size > 0) ? size : 1; + + if (n > 0 && UINT_MAX / n <= size) + sysdie("realloc too large at %s line %d", file, line); + p = realloc(p, n * size); + if (p == NULL) + sysdie("failed to realloc %lu bytes at %s line %d", + (unsigned long) (n * size), file, line); + return p; +} + + +/* + * Copy a string, reporting a fatal error and exiting on failure. + */ +static char * +x_strdup(const char *s, const char *file, int line) +{ + char *p; + size_t len; + + len = strlen(s) + 1; + p = (char *) malloc(len); + if (p == NULL) + sysdie("failed to strdup %lu bytes at %s line %d", (unsigned long) len, + file, line); + memcpy(p, s, len); + return p; +} + + +/* + * Copy the first n characters of a string, reporting a fatal error and + * existing on failure. + * + * Avoid using the system strndup function since it may not exist (on Mac OS + * X, for example), and there's no need to introduce another portability + * requirement. + */ +char * +x_strndup(const char *s, size_t size, const char *file, int line) +{ + const char *p; + size_t len; + char *copy; + + /* Don't assume that the source string is nul-terminated. */ + for (p = s; (size_t)(p - s) < size && *p != '\0'; p++) + ; + len = (size_t)(p - s); + copy = (char *) malloc(len + 1); + if (copy == NULL) + sysdie("failed to strndup %lu bytes at %s line %d", + (unsigned long) len, file, line); + memcpy(copy, s, len); + copy[len] = '\0'; + return copy; +} + + +/* + * Form a new string by concatenating multiple strings. The arguments must be + * terminated by (const char *) 0. + * + * This function only exists because we can't assume asprintf. We can't + * simulate asprintf with snprintf because we're only assuming SUSv3, which + * does not require that snprintf with a NULL buffer return the required + * length. When those constraints are relaxed, this should be ripped out and + * replaced with asprintf or a more trivial replacement with snprintf. + */ +static char * +concat(const char *first, ...) +{ + va_list args; + char *result; + const char *string; + size_t offset; + size_t length = 0; + + /* + * Find the total memory required. Ensure we don't overflow length. We + * aren't guaranteed to have SIZE_MAX, so use UINT_MAX as an acceptable + * substitute (see the x_nrealloc comments). + */ + va_start(args, first); + for (string = first; string != NULL; string = va_arg(args, const char *)) { + if (length >= UINT_MAX - strlen(string)) { + errno = EINVAL; + sysdie("strings too long in concat"); + } + length += strlen(string); + } + va_end(args); + length++; + + /* Create the string. */ + result = xmalloc(length); + va_start(args, first); + offset = 0; + for (string = first; string != NULL; string = va_arg(args, const char *)) { + memcpy(result + offset, string, strlen(string)); + offset += strlen(string); + } + va_end(args); + result[offset] = '\0'; + return result; +} + + +/* + * Given a struct timeval, return the number of seconds it represents as a + * double. Use difftime() to convert a time_t to a double. + */ +static double +tv_seconds(const struct timeval *tv) +{ + return difftime(tv->tv_sec, 0) + (double) tv->tv_usec * 1e-6; +} + + +/* + * Given two struct timevals, return the difference in seconds. + */ +static double +tv_diff(const struct timeval *tv1, const struct timeval *tv0) +{ + return tv_seconds(tv1) - tv_seconds(tv0); +} + + +/* + * Given two struct timevals, return the sum in seconds as a double. + */ +static double +tv_sum(const struct timeval *tv1, const struct timeval *tv2) +{ + return tv_seconds(tv1) + tv_seconds(tv2); +} + + +/* + * Given a pointer to a string, skip any leading whitespace and return a + * pointer to the first non-whitespace character. + */ +static const char * +skip_whitespace(const char *p) +{ + while (isspace((unsigned char) (*p))) + p++; + return p; +} + + +/* + * Given a pointer to a string, skip any non-whitespace characters and return + * a pointer to the first whitespace character, or to the end of the string. + */ +static const char * +skip_non_whitespace(const char *p) +{ + while (*p != '\0' && !isspace((unsigned char) (*p))) + p++; + return p; +} + + +/* + * Start a program, connecting its stdout to a pipe on our end and its stderr + * to /dev/null, and storing the file descriptor to read from in the two + * argument. Returns the PID of the new process. Errors are fatal. + */ +static pid_t +test_start(char *const *command, int *fd) +{ + int fds[2], infd, errfd; + pid_t child; + + /* Create a pipe used to capture the output from the test program. */ + if (pipe(fds) == -1) { + puts("ABORTED"); + fflush(stdout); + sysdie("can't create pipe"); + } + + /* Fork a child process, massage the file descriptors, and exec. */ + child = fork(); + switch (child) { + case -1: + puts("ABORTED"); + fflush(stdout); + sysdie("can't fork"); + + /* In the child. Set up our standard output. */ + case 0: + close(fds[0]); + close(STDOUT_FILENO); + if (dup2(fds[1], STDOUT_FILENO) < 0) + _exit(CHILDERR_DUP); + close(fds[1]); + + /* Point standard input at /dev/null. */ + close(STDIN_FILENO); + infd = open("/dev/null", O_RDONLY); + if (infd < 0) + _exit(CHILDERR_STDIN); + if (infd != STDIN_FILENO) { + if (dup2(infd, STDIN_FILENO) < 0) + _exit(CHILDERR_DUP); + close(infd); + } + + /* Point standard error at /dev/null. */ + close(STDERR_FILENO); + errfd = open("/dev/null", O_WRONLY); + if (errfd < 0) + _exit(CHILDERR_STDERR); + if (errfd != STDERR_FILENO) { + if (dup2(errfd, STDERR_FILENO) < 0) + _exit(CHILDERR_DUP); + close(errfd); + } + + /* Now, exec our process. */ + if (execv(command[0], command) == -1) + _exit(CHILDERR_EXEC); + break; + + /* In parent. Close the extra file descriptor. */ + default: + close(fds[1]); + break; + } + *fd = fds[0]; + return child; +} + + +/* + * Back up over the output saying what test we were executing. + */ +static void +test_backspace(struct testset *ts) +{ + unsigned int i; + + if (!isatty(STDOUT_FILENO)) + return; + for (i = 0; i < ts->length; i++) + putchar('\b'); + for (i = 0; i < ts->length; i++) + putchar(' '); + for (i = 0; i < ts->length; i++) + putchar('\b'); + ts->length = 0; +} + + +/* + * Allocate or resize the array of test results to be large enough to contain + * the test number in. + */ +static void +resize_results(struct testset *ts, unsigned long n) +{ + unsigned long i; + size_t s; + + /* If there's already enough space, return quickly. */ + if (n <= ts->allocated) + return; + + /* + * If no space has been allocated, do the initial allocation. Otherwise, + * resize. Start with 32 test cases and then add 1024 with each resize to + * try to reduce the number of reallocations. + */ + if (ts->allocated == 0) { + s = (n > 32) ? n : 32; + ts->results = xcalloc(s, enum test_status); + } else { + s = (n > ts->allocated + 1024) ? n : ts->allocated + 1024; + ts->results = xreallocarray(ts->results, s, enum test_status); + } + + /* Set the results for the newly-allocated test array. */ + for (i = ts->allocated; i < s; i++) + ts->results[i] = TEST_INVALID; + ts->allocated = s; +} + + +/* + * Report an invalid test number and set the appropriate flags. Pulled into a + * separate function since we do this in several places. + */ +static void +invalid_test_number(struct testset *ts, long n, enum test_verbose verbose) +{ + if (!verbose) + test_backspace(ts); + printf("ABORTED (invalid test number %ld)\n", n); + ts->aborted = 1; + ts->reported = 1; +} + + +/* + * Read the plan line of test output, which should contain the range of test + * numbers. We may initialize the testset structure here if we haven't yet + * seen a test. Return true if initialization succeeded and the test should + * continue, false otherwise. + */ +static int +test_plan(const char *line, struct testset *ts, enum test_verbose verbose) +{ + long n; + + /* + * Accept a plan without the leading 1.. for compatibility with older + * versions of runtests. This will only be allowed if we've not yet seen + * a test result. + */ + line = skip_whitespace(line); + if (strncmp(line, "1..", 3) == 0) + line += 3; + + /* + * Get the count and check it for validity. + * + * If we have something of the form "1..0 # skip foo", the whole file was + * skipped; record that. If we do skip the whole file, zero out all of + * our statistics, since they're no longer relevant. + * + * strtol is called with a second argument to advance the line pointer + * past the count to make it simpler to detect the # skip case. + */ + n = strtol(line, (char **) &line, 10); + if (n == 0) { + line = skip_whitespace(line); + if (*line == '#') { + line = skip_whitespace(line + 1); + if (strncasecmp(line, "skip", 4) == 0) { + line = skip_whitespace(line + 4); + if (*line != '\0') { + ts->reason = xstrdup(line); + ts->reason[strlen(ts->reason) - 1] = '\0'; + } + ts->all_skipped = 1; + ts->aborted = 1; + ts->count = 0; + ts->passed = 0; + ts->skipped = 0; + ts->failed = 0; + return 0; + } + } + } + if (n <= 0) { + puts("ABORTED (invalid test count)"); + ts->aborted = 1; + ts->reported = 1; + return 0; + } + + /* + * If we are doing lazy planning, check the plan against the largest test + * number that we saw and fail now if we saw a check outside the plan + * range. + */ + if (ts->plan == PLAN_PENDING && (unsigned long) n < ts->count) { + invalid_test_number(ts, (long) ts->count, verbose); + return 0; + } + + /* + * Otherwise, allocated or resize the results if needed and update count, + * and then record that we've seen a plan. + */ + resize_results(ts, (unsigned long) n); + ts->count = (unsigned long) n; + if (ts->plan == PLAN_INIT) + ts->plan = PLAN_FIRST; + else if (ts->plan == PLAN_PENDING) + ts->plan = PLAN_FINAL; + return 1; +} + + +/* + * Given a single line of output from a test, parse it and return the success + * status of that test. Anything printed to stdout not matching the form + * /^(not )?ok \d+/ is ignored. Sets ts->current to the test number that just + * reported status. + */ +static void +test_checkline(const char *line, struct testset *ts, enum test_verbose verbose) +{ + enum test_status status = TEST_PASS; + const char *bail; + char *end; + long number; + unsigned long current; + int outlen; + + /* Before anything, check for a test abort. */ + bail = strstr(line, "Bail out!"); + if (bail != NULL) { + bail = skip_whitespace(bail + strlen("Bail out!")); + if (*bail != '\0') { + size_t length; + + length = strlen(bail); + if (bail[length - 1] == '\n') + length--; + if (!verbose) + test_backspace(ts); + printf("ABORTED (%.*s)\n", (int) length, bail); + ts->reported = 1; + } + ts->aborted = 1; + return; + } + + /* + * If the given line isn't newline-terminated, it was too big for an + * fgets(), which means ignore it. + */ + if (line[strlen(line) - 1] != '\n') + return; + + /* If the line begins with a hash mark, ignore it. */ + if (line[0] == '#') + return; + + /* If we haven't yet seen a plan, look for one. */ + if (ts->plan == PLAN_INIT && isdigit((unsigned char) (*line))) { + if (!test_plan(line, ts, verbose)) + return; + } else if (strncmp(line, "1..", 3) == 0) { + if (ts->plan == PLAN_PENDING) { + if (!test_plan(line, ts, verbose)) + return; + } else { + if (!verbose) + test_backspace(ts); + puts("ABORTED (multiple plans)"); + ts->aborted = 1; + ts->reported = 1; + return; + } + } + + /* Parse the line, ignoring something we can't parse. */ + if (strncmp(line, "not ", 4) == 0) { + status = TEST_FAIL; + line += 4; + } + if (strncmp(line, "ok", 2) != 0) + return; + line = skip_whitespace(line + 2); + errno = 0; + number = strtol(line, &end, 10); + if (errno != 0 || end == line) + current = ts->current + 1; + else if (number <= 0) { + invalid_test_number(ts, number, verbose); + return; + } else + current = (unsigned long) number; + if (current > ts->count && ts->plan == PLAN_FIRST) { + invalid_test_number(ts, (long) current, verbose); + return; + } + + /* We have a valid test result. Tweak the results array if needed. */ + if (ts->plan == PLAN_INIT || ts->plan == PLAN_PENDING) { + ts->plan = PLAN_PENDING; + resize_results(ts, current); + if (current > ts->count) + ts->count = current; + } + + /* + * Handle directives. We should probably do something more interesting + * with unexpected passes of todo tests. + */ + while (isdigit((unsigned char) (*line))) + line++; + line = skip_whitespace(line); + if (*line == '#') { + line = skip_whitespace(line + 1); + if (strncasecmp(line, "skip", 4) == 0) + status = TEST_SKIP; + if (strncasecmp(line, "todo", 4) == 0) + status = (status == TEST_FAIL) ? TEST_SKIP : TEST_FAIL; + } + + /* Make sure that the test number is in range and not a duplicate. */ + if (ts->results[current - 1] != TEST_INVALID) { + if (!verbose) + test_backspace(ts); + printf("ABORTED (duplicate test number %lu)\n", current); + ts->aborted = 1; + ts->reported = 1; + return; + } + + /* Good results. Increment our various counters. */ + switch (status) { + case TEST_PASS: + ts->passed++; + break; + case TEST_FAIL: + ts->failed++; + break; + case TEST_SKIP: + ts->skipped++; + break; + case TEST_INVALID: + break; + } + ts->current = current; + ts->results[current - 1] = status; + if (!verbose && isatty(STDOUT_FILENO)) { + test_backspace(ts); + if (ts->plan == PLAN_PENDING) + outlen = printf("%lu/?", current); + else + outlen = printf("%lu/%lu", current, ts->count); + ts->length = (outlen >= 0) ? (unsigned int) outlen : 0; + fflush(stdout); + } +} + + +/* + * Print out a range of test numbers, returning the number of characters it + * took up. Takes the first number, the last number, the number of characters + * already printed on the line, and the limit of number of characters the line + * can hold. Add a comma and a space before the range if chars indicates that + * something has already been printed on the line, and print ... instead if + * chars plus the space needed would go over the limit (use a limit of 0 to + * disable this). + */ +static unsigned int +test_print_range(unsigned long first, unsigned long last, unsigned long chars, + unsigned int limit) +{ + unsigned int needed = 0; + unsigned long n; + + for (n = first; n > 0; n /= 10) + needed++; + if (last > first) { + for (n = last; n > 0; n /= 10) + needed++; + needed++; + } + if (chars > 0) + needed += 2; + if (limit > 0 && chars + needed > limit) { + needed = 0; + if (chars <= limit) { + if (chars > 0) { + printf(", "); + needed += 2; + } + printf("..."); + needed += 3; + } + } else { + if (chars > 0) + printf(", "); + if (last > first) + printf("%lu-", first); + printf("%lu", last); + } + return needed; +} + + +/* + * Summarize a single test set. The second argument is 0 if the set exited + * cleanly, a positive integer representing the exit status if it exited + * with a non-zero status, and a negative integer representing the signal + * that terminated it if it was killed by a signal. + */ +static void +test_summarize(struct testset *ts, int status) +{ + unsigned long i; + unsigned long missing = 0; + unsigned long failed = 0; + unsigned long first = 0; + unsigned long last = 0; + + if (ts->aborted) { + fputs("ABORTED", stdout); + if (ts->count > 0) + printf(" (passed %lu/%lu)", ts->passed, ts->count - ts->skipped); + } else { + for (i = 0; i < ts->count; i++) { + if (ts->results[i] == TEST_INVALID) { + if (missing == 0) + fputs("MISSED ", stdout); + if (first && i == last) + last = i + 1; + else { + if (first) + test_print_range(first, last, missing - 1, 0); + missing++; + first = i + 1; + last = i + 1; + } + } + } + if (first) + test_print_range(first, last, missing - 1, 0); + first = 0; + last = 0; + for (i = 0; i < ts->count; i++) { + if (ts->results[i] == TEST_FAIL) { + if (missing && !failed) + fputs("; ", stdout); + if (failed == 0) + fputs("FAILED ", stdout); + if (first && i == last) + last = i + 1; + else { + if (first) + test_print_range(first, last, failed - 1, 0); + failed++; + first = i + 1; + last = i + 1; + } + } + } + if (first) + test_print_range(first, last, failed - 1, 0); + if (!missing && !failed) { + fputs(!status ? "ok" : "dubious", stdout); + if (ts->skipped > 0) { + if (ts->skipped == 1) + printf(" (skipped %lu test)", ts->skipped); + else + printf(" (skipped %lu tests)", ts->skipped); + } + } + } + if (status > 0) + printf(" (exit status %d)", status); + else if (status < 0) + printf(" (killed by signal %d%s)", -status, + WCOREDUMP(ts->status) ? ", core dumped" : ""); + putchar('\n'); +} + + +/* + * Given a test set, analyze the results, classify the exit status, handle a + * few special error messages, and then pass it along to test_summarize() for + * the regular output. Returns true if the test set ran successfully and all + * tests passed or were skipped, false otherwise. + */ +static int +test_analyze(struct testset *ts) +{ + if (ts->reported) + return 0; + if (ts->all_skipped) { + if (ts->reason == NULL) + puts("skipped"); + else + printf("skipped (%s)\n", ts->reason); + return 1; + } else if (WIFEXITED(ts->status) && WEXITSTATUS(ts->status) != 0) { + switch (WEXITSTATUS(ts->status)) { + case CHILDERR_DUP: + if (!ts->reported) + puts("ABORTED (can't dup file descriptors)"); + break; + case CHILDERR_EXEC: + if (!ts->reported) + puts("ABORTED (execution failed -- not found?)"); + break; + case CHILDERR_STDIN: + case CHILDERR_STDERR: + if (!ts->reported) + puts("ABORTED (can't open /dev/null)"); + break; + default: + test_summarize(ts, WEXITSTATUS(ts->status)); + break; + } + return 0; + } else if (WIFSIGNALED(ts->status)) { + test_summarize(ts, -WTERMSIG(ts->status)); + return 0; + } else if (ts->plan != PLAN_FIRST && ts->plan != PLAN_FINAL) { + puts("ABORTED (no valid test plan)"); + ts->aborted = 1; + return 0; + } else { + test_summarize(ts, 0); + return (ts->failed == 0); + } +} + + +/* + * Runs a single test set, accumulating and then reporting the results. + * Returns true if the test set was successfully run and all tests passed, + * false otherwise. + */ +static int +test_run(struct testset *ts, enum test_verbose verbose) +{ + pid_t testpid, child; + int outfd, status; + unsigned long i; + FILE *output; + char buffer[BUFSIZ]; + + /* Run the test program. */ + testpid = test_start(ts->command, &outfd); + output = fdopen(outfd, "r"); + if (!output) { + puts("ABORTED"); + fflush(stdout); + sysdie("fdopen failed"); + } + + /* + * Pass each line of output to test_checkline(), and print the line if + * verbosity is requested. + */ + while (!ts->aborted && fgets(buffer, sizeof(buffer), output)) { + if (verbose) + printf("%s", buffer); + test_checkline(buffer, ts, verbose); + } + if (ferror(output) || ts->plan == PLAN_INIT) + ts->aborted = 1; + if (!verbose) + test_backspace(ts); + + /* + * Consume the rest of the test output, close the output descriptor, + * retrieve the exit status, and pass that information to test_analyze() + * for eventual output. + */ + while (fgets(buffer, sizeof(buffer), output)) + if (verbose) + printf("%s", buffer); + fclose(output); + child = waitpid(testpid, &ts->status, 0); + if (child == (pid_t) -1) { + if (!ts->reported) { + puts("ABORTED"); + fflush(stdout); + } + sysdie("waitpid for %u failed", (unsigned int) testpid); + } + if (ts->all_skipped) + ts->aborted = 0; + status = test_analyze(ts); + + /* Convert missing tests to failed tests. */ + for (i = 0; i < ts->count; i++) { + if (ts->results[i] == TEST_INVALID) { + ts->failed++; + ts->results[i] = TEST_FAIL; + status = 0; + } + } + return status; +} + + +/* Summarize a list of test failures. */ +static void +test_fail_summary(const struct testlist *fails) +{ + struct testset *ts; + unsigned int chars; + unsigned long i, first, last, total; + double failed; + + puts(header); + + /* Failed Set Fail/Total (%) Skip Stat Failing (25) + -------------------------- -------------- ---- ---- -------------- */ + for (; fails; fails = fails->next) { + ts = fails->ts; + total = ts->count - ts->skipped; + failed = (double) ts->failed; + printf("%-26.26s %4lu/%-4lu %3.0f%% %4lu ", ts->file, ts->failed, + total, total ? (failed * 100.0) / (double) total : 0, + ts->skipped); + if (WIFEXITED(ts->status)) + printf("%4d ", WEXITSTATUS(ts->status)); + else + printf(" -- "); + if (ts->aborted) { + puts("aborted"); + continue; + } + chars = 0; + first = 0; + last = 0; + for (i = 0; i < ts->count; i++) { + if (ts->results[i] == TEST_FAIL) { + if (first != 0 && i == last) + last = i + 1; + else { + if (first != 0) + chars += test_print_range(first, last, chars, 19); + first = i + 1; + last = i + 1; + } + } + } + if (first != 0) + test_print_range(first, last, chars, 19); + putchar('\n'); + } +} + + +/* + * Check whether a given file path is a valid test. Currently, this checks + * whether it is executable and is a regular file. Returns true or false. + */ +static int +is_valid_test(const char *path) +{ + struct stat st; + + if (access(path, X_OK) < 0) + return 0; + if (stat(path, &st) < 0) + return 0; + if (!S_ISREG(st.st_mode)) + return 0; + return 1; +} + + +/* + * Given the name of a test, a pointer to the testset struct, and the source + * and build directories, find the test. We try first relative to the current + * directory, then in the build directory (if not NULL), then in the source + * directory. In each of those directories, we first try a "-t" extension and + * then a ".t" extension. When we find an executable program, we return the + * path to that program. If none of those paths are executable, just fill in + * the name of the test as is. + * + * The caller is responsible for freeing the path member of the testset + * struct. + */ +static char * +find_test(const char *name, const char *source, const char *build) +{ + char *path = NULL; + const char *bases[3], *suffix, *base; + unsigned int i, j; + const char *suffixes[3] = {"-t", ".t", ""}; + + /* Possible base directories. */ + bases[0] = "."; + bases[1] = build; + bases[2] = source; + + /* Try each suffix with each base. */ + for (i = 0; i < ARRAY_SIZE(suffixes); i++) { + suffix = suffixes[i]; + for (j = 0; j < ARRAY_SIZE(bases); j++) { + base = bases[j]; + if (base == NULL) + continue; + path = concat(base, "/", name, suffix, (const char *) 0); + if (is_valid_test(path)) + return path; + free(path); + path = NULL; + } + } + if (path == NULL) + path = xstrdup(name); + return path; +} + + +/* + * Parse a single line of a test list and store the test name and command to + * execute it in the given testset struct. + * + * Normally, each line is just the name of the test, which is located in the + * test directory and turned into a command to run. However, each line may + * have whitespace-separated options, which change the command that's run. + * Current supported options are: + * + * valgrind + * Run the test under valgrind if C_TAP_VALGRIND is set. The contents + * of that environment variable are taken as the valgrind command (with + * options) to run. The command is parsed with a simple split on + * whitespace and no quoting is supported. + * + * libtool + * If running under valgrind, use libtool to invoke valgrind. This avoids + * running valgrind on the wrapper shell script generated by libtool. If + * set, C_TAP_LIBTOOL must be set to the full path to the libtool program + * to use to run valgrind and thus the test. Ignored if the test isn't + * being run under valgrind. + */ +static void +parse_test_list_line(const char *line, struct testset *ts, const char *source, + const char *build) +{ + const char *p, *end, *option, *libtool; + const char *valgrind = NULL; + unsigned int use_libtool = 0; + unsigned int use_valgrind = 0; + size_t len, i; + + /* Determine the name of the test. */ + p = skip_non_whitespace(line); + ts->file = xstrndup(line, p - line); + + /* Check if any test options are set. */ + p = skip_whitespace(p); + while (*p != '\0') { + end = skip_non_whitespace(p); + if (strncmp(p, "libtool", end - p) == 0) { + use_libtool = 1; + } else if (strncmp(p, "valgrind", end - p) == 0) { + valgrind = getenv("C_TAP_VALGRIND"); + use_valgrind = (valgrind != NULL); + } else { + option = xstrndup(p, end - p); + die("unknown test list option %s", option); + } + p = skip_whitespace(end); + } + + /* Construct the argv to run the test. First, find the length. */ + len = 1; + if (use_valgrind && valgrind != NULL) { + p = skip_whitespace(valgrind); + while (*p != '\0') { + len++; + p = skip_whitespace(skip_non_whitespace(p)); + } + if (use_libtool) + len += 2; + } + + /* Now, build the command. */ + ts->command = xcalloc(len + 1, char *); + i = 0; + if (use_valgrind && valgrind != NULL) { + if (use_libtool) { + libtool = getenv("C_TAP_LIBTOOL"); + if (libtool == NULL) + die("valgrind with libtool requested, but C_TAP_LIBTOOL is not" + " set"); + ts->command[i++] = xstrdup(libtool); + ts->command[i++] = xstrdup("--mode=execute"); + } + p = skip_whitespace(valgrind); + while (*p != '\0') { + end = skip_non_whitespace(p); + ts->command[i++] = xstrndup(p, end - p); + p = skip_whitespace(end); + } + } + if (i != len - 1) + die("internal error while constructing command line"); + ts->command[i++] = find_test(ts->file, source, build); + ts->command[i] = NULL; +} + + +/* + * Read a list of tests from a file, returning the list of tests as a struct + * testlist, or NULL if there were no tests (such as a file containing only + * comments). Reports an error to standard error and exits if the list of + * tests cannot be read. + */ +static struct testlist * +read_test_list(const char *filename, const char *source, const char *build) +{ + FILE *file; + unsigned int line; + size_t length; + char buffer[BUFSIZ]; + const char *start; + struct testlist *listhead, *current; + + /* Create the initial container list that will hold our results. */ + listhead = xcalloc(1, struct testlist); + current = NULL; + + /* + * Open our file of tests to run and read it line by line, creating a new + * struct testlist and struct testset for each line. + */ + file = fopen(filename, "r"); + if (file == NULL) + sysdie("can't open %s", filename); + line = 0; + while (fgets(buffer, sizeof(buffer), file)) { + line++; + length = strlen(buffer) - 1; + if (buffer[length] != '\n') { + fprintf(stderr, "%s:%u: line too long\n", filename, line); + exit(1); + } + buffer[length] = '\0'; + + /* Skip comments, leading spaces, and blank lines. */ + start = skip_whitespace(buffer); + if (strlen(start) == 0) + continue; + if (start[0] == '#') + continue; + + /* Allocate the new testset structure. */ + if (current == NULL) + current = listhead; + else { + current->next = xcalloc(1, struct testlist); + current = current->next; + } + current->ts = xcalloc(1, struct testset); + current->ts->plan = PLAN_INIT; + + /* Parse the line and store the results in the testset struct. */ + parse_test_list_line(start, current->ts, source, build); + } + fclose(file); + + /* If there were no tests, current is still NULL. */ + if (current == NULL) { + free(listhead); + return NULL; + } + + /* Return the results. */ + return listhead; +} + + +/* + * Build a list of tests from command line arguments. Takes the argv and argc + * representing the command line arguments and returns a newly allocated test + * list, or NULL if there were no tests. The caller is responsible for + * freeing. + */ +static struct testlist * +build_test_list(char *argv[], int argc, const char *source, const char *build) +{ + int i; + struct testlist *listhead, *current; + + /* Create the initial container list that will hold our results. */ + listhead = xcalloc(1, struct testlist); + current = NULL; + + /* Walk the list of arguments and create test sets for them. */ + for (i = 0; i < argc; i++) { + if (current == NULL) + current = listhead; + else { + current->next = xcalloc(1, struct testlist); + current = current->next; + } + current->ts = xcalloc(1, struct testset); + current->ts->plan = PLAN_INIT; + current->ts->file = xstrdup(argv[i]); + current->ts->command = xcalloc(2, char *); + current->ts->command[0] = find_test(current->ts->file, source, build); + current->ts->command[1] = NULL; + } + + /* If there were no tests, current is still NULL. */ + if (current == NULL) { + free(listhead); + return NULL; + } + + /* Return the results. */ + return listhead; +} + + +/* Free a struct testset. */ +static void +free_testset(struct testset *ts) +{ + size_t i; + + free(ts->file); + for (i = 0; ts->command[i] != NULL; i++) + free(ts->command[i]); + free(ts->command); + free(ts->results); + free(ts->reason); + free(ts); +} + + +/* + * Run a batch of tests. Takes two additional parameters: the root of the + * source directory and the root of the build directory. Test programs will + * be first searched for in the current directory, then the build directory, + * then the source directory. Returns true iff all tests passed, and always + * frees the test list that's passed in. + */ +static int +test_batch(struct testlist *tests, enum test_verbose verbose) +{ + size_t length, i; + size_t longest = 0; + unsigned int count = 0; + struct testset *ts; + struct timeval start, end; + struct rusage stats; + struct testlist *failhead = NULL; + struct testlist *failtail = NULL; + struct testlist *current, *next; + int succeeded; + unsigned long total = 0; + unsigned long passed = 0; + unsigned long skipped = 0; + unsigned long failed = 0; + unsigned long aborted = 0; + + /* Walk the list of tests to find the longest name. */ + for (current = tests; current != NULL; current = current->next) { + length = strlen(current->ts->file); + if (length > longest) + longest = length; + } + + /* + * Add two to longest and round up to the nearest tab stop. This is how + * wide the column for printing the current test name will be. + */ + longest += 2; + if (longest % 8) + longest += 8 - (longest % 8); + + /* Start the wall clock timer. */ + gettimeofday(&start, NULL); + + /* Now, plow through our tests again, running each one. */ + for (current = tests; current != NULL; current = current->next) { + ts = current->ts; + + /* Print out the name of the test file. */ + fputs(ts->file, stdout); + if (verbose) + fputs("\n\n", stdout); + else + for (i = strlen(ts->file); i < longest; i++) + putchar('.'); + if (isatty(STDOUT_FILENO)) + fflush(stdout); + + /* Run the test. */ + succeeded = test_run(ts, verbose); + fflush(stdout); + if (verbose) + putchar('\n'); + + /* Record cumulative statistics. */ + aborted += ts->aborted; + total += ts->count + ts->all_skipped; + passed += ts->passed; + skipped += ts->skipped + ts->all_skipped; + failed += ts->failed; + count++; + + /* If the test fails, we shuffle it over to the fail list. */ + if (!succeeded) { + if (failhead == NULL) { + failhead = xcalloc(1, struct testlist); + failtail = failhead; + } else { + failtail->next = xcalloc(1, struct testlist); + failtail = failtail->next; + } + failtail->ts = ts; + failtail->next = NULL; + } + } + total -= skipped; + + /* Stop the timer and get our child resource statistics. */ + gettimeofday(&end, NULL); + getrusage(RUSAGE_CHILDREN, &stats); + + /* Summarize the failures and free the failure list. */ + if (failhead != NULL) { + test_fail_summary(failhead); + while (failhead != NULL) { + next = failhead->next; + free(failhead); + failhead = next; + } + } + + /* Free the memory used by the test lists. */ + while (tests != NULL) { + next = tests->next; + free_testset(tests->ts); + free(tests); + tests = next; + } + + /* Print out the final test summary. */ + putchar('\n'); + if (aborted != 0) { + if (aborted == 1) + printf("Aborted %lu test set", aborted); + else + printf("Aborted %lu test sets", aborted); + printf(", passed %lu/%lu tests", passed, total); + } else if (failed == 0) + fputs("All tests successful", stdout); + else + printf("Failed %lu/%lu tests, %.2f%% okay", failed, total, + (double) (total - failed) * 100.0 / (double) total); + if (skipped != 0) { + if (skipped == 1) + printf(", %lu test skipped", skipped); + else + printf(", %lu tests skipped", skipped); + } + puts("."); + printf("Files=%u, Tests=%lu", count, total); + printf(", %.2f seconds", tv_diff(&end, &start)); + printf(" (%.2f usr + %.2f sys = %.2f CPU)\n", tv_seconds(&stats.ru_utime), + tv_seconds(&stats.ru_stime), + tv_sum(&stats.ru_utime, &stats.ru_stime)); + return (failed == 0 && aborted == 0); +} + + +/* + * Run a single test case. This involves just running the test program after + * having done the environment setup and finding the test program. + */ +static void +test_single(const char *program, const char *source, const char *build) +{ + char *path; + + path = find_test(program, source, build); + if (execl(path, path, (char *) 0) == -1) + sysdie("cannot exec %s", path); +} + + +/* + * Main routine. Set the C_TAP_SOURCE, C_TAP_BUILD, SOURCE, and BUILD + * environment variables and then, given a file listing tests, run each test + * listed. + */ +int +main(int argc, char *argv[]) +{ + int option; + int status = 0; + int single = 0; + enum test_verbose verbose = CONCISE; + char *c_tap_source_env = NULL; + char *c_tap_build_env = NULL; + char *source_env = NULL; + char *build_env = NULL; + const char *program; + const char *shortlist; + const char *list = NULL; + const char *source = C_TAP_SOURCE; + const char *build = C_TAP_BUILD; + struct testlist *tests; + + program = argv[0]; + while ((option = getopt(argc, argv, "b:hl:os:v")) != EOF) { + switch (option) { + case 'b': + build = optarg; + break; + case 'h': + printf(usage_message, program, program, program, usage_extra); + exit(0); + case 'l': + list = optarg; + break; + case 'o': + single = 1; + break; + case 's': + source = optarg; + break; + case 'v': + verbose = VERBOSE; + break; + default: + exit(1); + } + } + argv += optind; + argc -= optind; + if ((list == NULL && argc < 1) || (list != NULL && argc > 0)) { + fprintf(stderr, usage_message, program, program, program, usage_extra); + exit(1); + } + + /* + * If C_TAP_VERBOSE is set in the environment, that also turns on verbose + * mode. + */ + if (getenv("C_TAP_VERBOSE") != NULL) + verbose = VERBOSE; + + /* + * Set C_TAP_SOURCE and C_TAP_BUILD environment variables. Also set + * SOURCE and BUILD for backward compatibility, although we're trying to + * migrate to the ones with a C_TAP_* prefix. + */ + if (source != NULL) { + c_tap_source_env = concat("C_TAP_SOURCE=", source, (const char *) 0); + if (putenv(c_tap_source_env) != 0) + sysdie("cannot set C_TAP_SOURCE in the environment"); + source_env = concat("SOURCE=", source, (const char *) 0); + if (putenv(source_env) != 0) + sysdie("cannot set SOURCE in the environment"); + } + if (build != NULL) { + c_tap_build_env = concat("C_TAP_BUILD=", build, (const char *) 0); + if (putenv(c_tap_build_env) != 0) + sysdie("cannot set C_TAP_BUILD in the environment"); + build_env = concat("BUILD=", build, (const char *) 0); + if (putenv(build_env) != 0) + sysdie("cannot set BUILD in the environment"); + } + + /* Run the tests as instructed. */ + if (single) + test_single(argv[0], source, build); + else if (list != NULL) { + shortlist = strrchr(list, '/'); + if (shortlist == NULL) + shortlist = list; + else + shortlist++; + printf(banner, shortlist); + tests = read_test_list(list, source, build); + status = test_batch(tests, verbose) ? 0 : 1; + } else { + tests = build_test_list(argv, argc, source, build); + status = test_batch(tests, verbose) ? 0 : 1; + } + + /* For valgrind cleanliness, free all our memory. */ + if (source_env != NULL) { + putenv((char *) "C_TAP_SOURCE="); + putenv((char *) "SOURCE="); + free(c_tap_source_env); + free(source_env); + } + if (build_env != NULL) { + putenv((char *) "C_TAP_BUILD="); + putenv((char *) "BUILD="); + free(c_tap_build_env); + free(build_env); + } + exit(status); +} diff --git a/tests/style/obsolete-strings-t b/tests/style/obsolete-strings-t new file mode 100755 index 000000000000..430f07219cef --- /dev/null +++ b/tests/style/obsolete-strings-t @@ -0,0 +1,104 @@ +#!/usr/bin/perl +# +# Check for obsolete strings in source files. +# +# Examine all source files in a distribution for obsolete strings and report +# on files that fail this check. This catches various transitions I want to +# do globally in all my packages, like changing my personal URLs to https. +# +# The canonical version of this file is maintained in the rra-c-util package, +# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. +# +# Copyright 2016, 2018-2020 Russ Allbery <eagle@eyrie.org> +# +# 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 + +use 5.010; +use strict; +use warnings; + +use lib "$ENV{C_TAP_SOURCE}/tap/perl"; + +use Test::RRA qw(skip_unless_author); +use Test::RRA::Automake qw(all_files automake_setup); + +use File::Basename qw(basename); +use Test::More; + +# Bad patterns to search for. +my @BAD_REGEXES = (qr{ http:// \S+ [.]eyrie[.]org }xms); +my @BAD_STRINGS = qw(rra@stanford.edu RRA_MAINTAINER_TESTS); + +# File names to exclude from this check. +my %EXCLUDE + = map { $_ => 1 } qw(NEWS changelog obsolete-strings.t obsolete-strings-t); + +# Only run this test for the package author, since it doesn't indicate any +# user-noticable flaw in the package itself. +skip_unless_author('Obsolete strings tests'); + +# Set up Automake testing. +automake_setup(); + +# Check a single file for one of the bad patterns. +# +# $path - Path to the file +# +# Returns: undef +sub check_file { + my ($path) = @_; + my $filename = basename($path); + + # Ignore excluded and binary files. + return if $EXCLUDE{$filename}; + return if !-T $path; + + # Scan the file. + open(my $fh, '<', $path) or BAIL_OUT("Cannot open $path"); + while (defined(my $line = <$fh>)) { + for my $regex (@BAD_REGEXES) { + if ($line =~ $regex) { + ok(0, "$path contains $regex"); + close($fh) or BAIL_OUT("Cannot close $path"); + return; + } + } + for my $string (@BAD_STRINGS) { + if (index($line, $string) != -1) { + ok(0, "$path contains $string"); + close($fh) or BAIL_OUT("Cannot close $path"); + return; + } + } + } + close($fh) or BAIL_OUT("Cannot close $path"); + ok(1, $path); + return; +} + +# Scan every file for any of the bad patterns or strings. We don't declare a +# plan since we skip a lot of files and don't want to precalculate the file +# list. +my @paths = all_files(); +for my $path (@paths) { + check_file($path); +} +done_testing(); diff --git a/tests/tap/basic.c b/tests/tap/basic.c new file mode 100644 index 000000000000..b5f42d0211a4 --- /dev/null +++ b/tests/tap/basic.c @@ -0,0 +1,1029 @@ +/* + * Some utility routines for writing tests. + * + * Here are a variety of utility routines for writing tests compatible with + * the TAP protocol. All routines of the form ok() or is*() take a test + * number and some number of appropriate arguments, check to be sure the + * results match the expected output using the arguments, and print out + * something appropriate for that test number. Other utility routines help in + * constructing more complex tests, skipping tests, reporting errors, setting + * up the TAP output format, or finding things in the test environment. + * + * This file is part of C TAP Harness. The current version plus supporting + * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2009-2019 Russ Allbery <eagle@eyrie.org> + * Copyright 2001-2002, 2004-2008, 2011-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 <errno.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#ifdef _WIN32 +# include <direct.h> +#else +# include <sys/stat.h> +#endif +#include <sys/types.h> +#include <unistd.h> + +#include <tests/tap/basic.h> + +/* Windows provides mkdir and rmdir under different names. */ +#ifdef _WIN32 +# define mkdir(p, m) _mkdir(p) +# define rmdir(p) _rmdir(p) +#endif + +/* + * The test count. Always contains the number that will be used for the next + * test status. This is exported to callers of the library. + */ +unsigned long testnum = 1; + +/* + * Status information stored so that we can give a test summary at the end of + * the test case. We store the planned final test and the count of failures. + * We can get the highest test count from testnum. + */ +static unsigned long _planned = 0; +static unsigned long _failed = 0; + +/* + * Store the PID of the process that called plan() and only summarize + * results when that process exits, so as to not misreport results in forked + * processes. + */ +static pid_t _process = 0; + +/* + * If true, we're doing lazy planning and will print out the plan based on the + * last test number at the end of testing. + */ +static int _lazy = 0; + +/* + * If true, the test was aborted by calling bail(). Currently, this is only + * used to ensure that we pass a false value to any cleanup functions even if + * all tests to that point have passed. + */ +static int _aborted = 0; + +/* + * Registered cleanup functions. These are stored as a linked list and run in + * registered order by finish when the test program exits. Each function is + * passed a boolean value indicating whether all tests were successful. + */ +struct cleanup_func { + test_cleanup_func func; + test_cleanup_func_with_data func_with_data; + void *data; + struct cleanup_func *next; +}; +static struct cleanup_func *cleanup_funcs = NULL; + +/* + * Registered diag files. Any output found in these files will be printed out + * as if it were passed to diag() before any other output we do. This allows + * background processes to log to a file and have that output interleaved with + * the test output. + */ +struct diag_file { + char *name; + FILE *file; + char *buffer; + size_t bufsize; + struct diag_file *next; +}; +static struct diag_file *diag_files = NULL; + +/* + * Print a specified prefix and then the test description. Handles turning + * the argument list into a va_args structure suitable for passing to + * print_desc, which has to be done in a macro. Assumes that format is the + * argument immediately before the variadic arguments. + */ +#define PRINT_DESC(prefix, format) \ + do { \ + if (format != NULL) { \ + va_list args; \ + printf("%s", prefix); \ + va_start(args, format); \ + vprintf(format, args); \ + va_end(args); \ + } \ + } while (0) + + +/* + * Form a new string by concatenating multiple strings. The arguments must be + * terminated by (const char *) 0. + * + * This function only exists because we can't assume asprintf. We can't + * simulate asprintf with snprintf because we're only assuming SUSv3, which + * does not require that snprintf with a NULL buffer return the required + * length. When those constraints are relaxed, this should be ripped out and + * replaced with asprintf or a more trivial replacement with snprintf. + */ +static char * +concat(const char *first, ...) +{ + va_list args; + char *result; + const char *string; + size_t offset; + size_t length = 0; + + /* + * Find the total memory required. Ensure we don't overflow length. See + * the comment for breallocarray for why we're using UINT_MAX here. + */ + va_start(args, first); + for (string = first; string != NULL; string = va_arg(args, const char *)) { + if (length >= UINT_MAX - strlen(string)) + bail("strings too long in concat"); + length += strlen(string); + } + va_end(args); + length++; + + /* Create the string. */ + result = bcalloc_type(length, char); + va_start(args, first); + offset = 0; + for (string = first; string != NULL; string = va_arg(args, const char *)) { + memcpy(result + offset, string, strlen(string)); + offset += strlen(string); + } + va_end(args); + result[offset] = '\0'; + return result; +} + + +/* + * Helper function for check_diag_files to handle a single line in a diag + * file. + * + * The general scheme here used is as follows: read one line of output. If we + * get NULL, check for an error. If there was one, bail out of the test + * program; otherwise, return, and the enclosing loop will check for EOF. + * + * If we get some data, see if it ends in a newline. If it doesn't end in a + * newline, we have one of two cases: our buffer isn't large enough, in which + * case we resize it and try again, or we have incomplete data in the file, in + * which case we rewind the file and will try again next time. + * + * Returns a boolean indicating whether the last line was incomplete. + */ +static int +handle_diag_file_line(struct diag_file *file, fpos_t where) +{ + int size; + size_t length; + + /* Read the next line from the file. */ + size = file->bufsize > INT_MAX ? INT_MAX : (int) file->bufsize; + if (fgets(file->buffer, size, file->file) == NULL) { + if (ferror(file->file)) + sysbail("cannot read from %s", file->name); + return 0; + } + + /* + * See if the line ends in a newline. If not, see which error case we + * have. + */ + length = strlen(file->buffer); + if (file->buffer[length - 1] != '\n') { + int incomplete = 0; + + /* Check whether we ran out of buffer space and resize if so. */ + if (length < file->bufsize - 1) + incomplete = 1; + else { + file->bufsize += BUFSIZ; + file->buffer = + breallocarray_type(file->buffer, file->bufsize, char); + } + + /* + * On either incomplete lines or too small of a buffer, rewind + * and read the file again (on the next pass, if incomplete). + * It's simpler than trying to double-buffer the file. + */ + if (fsetpos(file->file, &where) < 0) + sysbail("cannot set position in %s", file->name); + return incomplete; + } + + /* We saw a complete line. Print it out. */ + printf("# %s", file->buffer); + return 0; +} + + +/* + * Check all registered diag_files for any output. We only print out the + * output if we see a complete line; otherwise, we wait for the next newline. + */ +static void +check_diag_files(void) +{ + struct diag_file *file; + fpos_t where; + int incomplete; + + /* + * Walk through each file and read each line of output available. + */ + for (file = diag_files; file != NULL; file = file->next) { + clearerr(file->file); + + /* Store the current position in case we have to rewind. */ + if (fgetpos(file->file, &where) < 0) + sysbail("cannot get position in %s", file->name); + + /* Continue until we get EOF or an incomplete line of data. */ + incomplete = 0; + while (!feof(file->file) && !incomplete) { + incomplete = handle_diag_file_line(file, where); + } + } +} + + +/* + * Our exit handler. Called on completion of the test to report a summary of + * results provided we're still in the original process. This also handles + * printing out the plan if we used plan_lazy(), although that's suppressed if + * we never ran a test (due to an early bail, for example), and running any + * registered cleanup functions. + */ +static void +finish(void) +{ + int success, primary; + struct cleanup_func *current; + unsigned long highest = testnum - 1; + struct diag_file *file, *tmp; + + /* Check for pending diag_file output. */ + check_diag_files(); + + /* Free the diag_files. */ + file = diag_files; + while (file != NULL) { + tmp = file; + file = file->next; + fclose(tmp->file); + free(tmp->name); + free(tmp->buffer); + free(tmp); + } + diag_files = NULL; + + /* + * Determine whether all tests were successful, which is needed before + * calling cleanup functions since we pass that fact to the functions. + */ + if (_planned == 0 && _lazy) + _planned = highest; + success = (!_aborted && _planned == highest && _failed == 0); + + /* + * If there are any registered cleanup functions, we run those first. We + * always run them, even if we didn't run a test. Don't do anything + * except free the diag_files and call cleanup functions if we aren't the + * primary process (the process in which plan or plan_lazy was called), + * and tell the cleanup functions that fact. + */ + primary = (_process == 0 || getpid() == _process); + while (cleanup_funcs != NULL) { + if (cleanup_funcs->func_with_data) { + void *data = cleanup_funcs->data; + + cleanup_funcs->func_with_data(success, primary, data); + } else { + cleanup_funcs->func(success, primary); + } + current = cleanup_funcs; + cleanup_funcs = cleanup_funcs->next; + free(current); + } + if (!primary) + return; + + /* Don't do anything further if we never planned a test. */ + if (_planned == 0) + return; + + /* If we're aborting due to bail, don't print summaries. */ + if (_aborted) + return; + + /* Print out the lazy plan if needed. */ + fflush(stderr); + if (_lazy && _planned > 0) + printf("1..%lu\n", _planned); + + /* Print out a summary of the results. */ + if (_planned > highest) + diag("Looks like you planned %lu test%s but only ran %lu", _planned, + (_planned > 1 ? "s" : ""), highest); + else if (_planned < highest) + diag("Looks like you planned %lu test%s but ran %lu extra", _planned, + (_planned > 1 ? "s" : ""), highest - _planned); + else if (_failed > 0) + diag("Looks like you failed %lu test%s of %lu", _failed, + (_failed > 1 ? "s" : ""), _planned); + else if (_planned != 1) + diag("All %lu tests successful or skipped", _planned); + else + diag("%lu test successful or skipped", _planned); +} + + +/* + * Initialize things. Turns on line buffering on stdout and then prints out + * the number of tests in the test suite. We intentionally don't check for + * pending diag_file output here, since it should really come after the plan. + */ +void +plan(unsigned long count) +{ + if (setvbuf(stdout, NULL, _IOLBF, BUFSIZ) != 0) + sysdiag("cannot set stdout to line buffered"); + fflush(stderr); + printf("1..%lu\n", count); + testnum = 1; + _planned = count; + _process = getpid(); + if (atexit(finish) != 0) { + sysdiag("cannot register exit handler"); + diag("cleanups will not be run"); + } +} + + +/* + * Initialize things for lazy planning, where we'll automatically print out a + * plan at the end of the program. Turns on line buffering on stdout as well. + */ +void +plan_lazy(void) +{ + if (setvbuf(stdout, NULL, _IOLBF, BUFSIZ) != 0) + sysdiag("cannot set stdout to line buffered"); + testnum = 1; + _process = getpid(); + _lazy = 1; + if (atexit(finish) != 0) + sysbail("cannot register exit handler to display plan"); +} + + +/* + * Skip the entire test suite and exits. Should be called instead of plan(), + * not after it, since it prints out a special plan line. Ignore diag_file + * output here, since it's not clear if it's allowed before the plan. + */ +void +skip_all(const char *format, ...) +{ + fflush(stderr); + printf("1..0 # skip"); + PRINT_DESC(" ", format); + putchar('\n'); + exit(0); +} + + +/* + * Takes a boolean success value and assumes the test passes if that value + * is true and fails if that value is false. + */ +int +ok(int success, const char *format, ...) +{ + fflush(stderr); + check_diag_files(); + printf("%sok %lu", success ? "" : "not ", testnum++); + if (!success) + _failed++; + PRINT_DESC(" - ", format); + putchar('\n'); + return success; +} + + +/* + * Same as ok(), but takes the format arguments as a va_list. + */ +int +okv(int success, const char *format, va_list args) +{ + fflush(stderr); + check_diag_files(); + printf("%sok %lu", success ? "" : "not ", testnum++); + if (!success) + _failed++; + if (format != NULL) { + printf(" - "); + vprintf(format, args); + } + putchar('\n'); + return success; +} + + +/* + * Skip a test. + */ +void +skip(const char *reason, ...) +{ + fflush(stderr); + check_diag_files(); + printf("ok %lu # skip", testnum++); + PRINT_DESC(" ", reason); + putchar('\n'); +} + + +/* + * Report the same status on the next count tests. + */ +int +ok_block(unsigned long count, int success, const char *format, ...) +{ + unsigned long i; + + fflush(stderr); + check_diag_files(); + for (i = 0; i < count; i++) { + printf("%sok %lu", success ? "" : "not ", testnum++); + if (!success) + _failed++; + PRINT_DESC(" - ", format); + putchar('\n'); + } + return success; +} + + +/* + * Skip the next count tests. + */ +void +skip_block(unsigned long count, const char *reason, ...) +{ + unsigned long i; + + fflush(stderr); + check_diag_files(); + for (i = 0; i < count; i++) { + printf("ok %lu # skip", testnum++); + PRINT_DESC(" ", reason); + putchar('\n'); + } +} + + +/* + * Takes two boolean values and requires the truth value of both match. + */ +int +is_bool(int left, int right, const char *format, ...) +{ + int success; + + fflush(stderr); + check_diag_files(); + success = (!!left == !!right); + if (success) + printf("ok %lu", testnum++); + else { + diag(" left: %s", !!left ? "true" : "false"); + diag("right: %s", !!right ? "true" : "false"); + printf("not ok %lu", testnum++); + _failed++; + } + PRINT_DESC(" - ", format); + putchar('\n'); + return success; +} + + +/* + * Takes two integer values and requires they match. + */ +int +is_int(long left, long right, const char *format, ...) +{ + int success; + + fflush(stderr); + check_diag_files(); + success = (left == right); + if (success) + printf("ok %lu", testnum++); + else { + diag(" left: %ld", left); + diag("right: %ld", right); + printf("not ok %lu", testnum++); + _failed++; + } + PRINT_DESC(" - ", format); + putchar('\n'); + return success; +} + + +/* + * Takes two strings and requires they match (using strcmp). NULL arguments + * are permitted and handled correctly. + */ +int +is_string(const char *left, const char *right, const char *format, ...) +{ + int success; + + fflush(stderr); + check_diag_files(); + + /* Compare the strings, being careful of NULL. */ + if (left == NULL) + success = (right == NULL); + else if (right == NULL) + success = 0; + else + success = (strcmp(left, right) == 0); + + /* Report the results. */ + if (success) + printf("ok %lu", testnum++); + else { + diag(" left: %s", left == NULL ? "(null)" : left); + diag("right: %s", right == NULL ? "(null)" : right); + printf("not ok %lu", testnum++); + _failed++; + } + PRINT_DESC(" - ", format); + putchar('\n'); + return success; +} + + +/* + * Takes two unsigned longs and requires they match. On failure, reports them + * in hex. + */ +int +is_hex(unsigned long left, unsigned long right, const char *format, ...) +{ + int success; + + fflush(stderr); + check_diag_files(); + success = (left == right); + if (success) + printf("ok %lu", testnum++); + else { + diag(" left: %lx", (unsigned long) left); + diag("right: %lx", (unsigned long) right); + printf("not ok %lu", testnum++); + _failed++; + } + PRINT_DESC(" - ", format); + putchar('\n'); + return success; +} + + +/* + * Takes pointers to a regions of memory and requires that len bytes from each + * match. Otherwise reports any bytes which didn't match. + */ +int +is_blob(const void *left, const void *right, size_t len, const char *format, + ...) +{ + int success; + size_t i; + + fflush(stderr); + check_diag_files(); + success = (memcmp(left, right, len) == 0); + if (success) + printf("ok %lu", testnum++); + else { + const unsigned char *left_c = (const unsigned char *) left; + const unsigned char *right_c = (const unsigned char *) right; + + for (i = 0; i < len; i++) { + if (left_c[i] != right_c[i]) + diag("offset %lu: left %02x, right %02x", (unsigned long) i, + left_c[i], right_c[i]); + } + printf("not ok %lu", testnum++); + _failed++; + } + PRINT_DESC(" - ", format); + putchar('\n'); + return success; +} + + +/* + * Bail out with an error. + */ +void +bail(const char *format, ...) +{ + va_list args; + + _aborted = 1; + fflush(stderr); + check_diag_files(); + fflush(stdout); + printf("Bail out! "); + va_start(args, format); + vprintf(format, args); + va_end(args); + printf("\n"); + exit(255); +} + + +/* + * Bail out with an error, appending strerror(errno). + */ +void +sysbail(const char *format, ...) +{ + va_list args; + int oerrno = errno; + + _aborted = 1; + fflush(stderr); + check_diag_files(); + fflush(stdout); + printf("Bail out! "); + va_start(args, format); + vprintf(format, args); + va_end(args); + printf(": %s\n", strerror(oerrno)); + exit(255); +} + + +/* + * Report a diagnostic to stderr. Always returns 1 to allow embedding in + * compound statements. + */ +int +diag(const char *format, ...) +{ + va_list args; + + fflush(stderr); + check_diag_files(); + fflush(stdout); + printf("# "); + va_start(args, format); + vprintf(format, args); + va_end(args); + printf("\n"); + return 1; +} + + +/* + * Report a diagnostic to stderr, appending strerror(errno). Always returns 1 + * to allow embedding in compound statements. + */ +int +sysdiag(const char *format, ...) +{ + va_list args; + int oerrno = errno; + + fflush(stderr); + check_diag_files(); + fflush(stdout); + printf("# "); + va_start(args, format); + vprintf(format, args); + va_end(args); + printf(": %s\n", strerror(oerrno)); + return 1; +} + + +/* + * Register a new file for diag_file processing. + */ +void +diag_file_add(const char *name) +{ + struct diag_file *file, *prev; + + file = bcalloc_type(1, struct diag_file); + file->name = bstrdup(name); + file->file = fopen(file->name, "r"); + if (file->file == NULL) + sysbail("cannot open %s", name); + file->buffer = bcalloc_type(BUFSIZ, char); + file->bufsize = BUFSIZ; + if (diag_files == NULL) + diag_files = file; + else { + for (prev = diag_files; prev->next != NULL; prev = prev->next) + ; + prev->next = file; + } +} + + +/* + * Remove a file from diag_file processing. If the file is not found, do + * nothing, since there are some situations where it can be removed twice + * (such as if it's removed from a cleanup function, since cleanup functions + * are called after freeing all the diag_files). + */ +void +diag_file_remove(const char *name) +{ + struct diag_file *file; + struct diag_file **prev = &diag_files; + + for (file = diag_files; file != NULL; file = file->next) { + if (strcmp(file->name, name) == 0) { + *prev = file->next; + fclose(file->file); + free(file->name); + free(file->buffer); + free(file); + return; + } + prev = &file->next; + } +} + + +/* + * Allocate cleared memory, reporting a fatal error with bail on failure. + */ +void * +bcalloc(size_t n, size_t size) +{ + void *p; + + p = calloc(n, size); + if (p == NULL) + sysbail("failed to calloc %lu", (unsigned long) (n * size)); + return p; +} + + +/* + * Allocate memory, reporting a fatal error with bail on failure. + */ +void * +bmalloc(size_t size) +{ + void *p; + + p = malloc(size); + if (p == NULL) + sysbail("failed to malloc %lu", (unsigned long) size); + return p; +} + + +/* + * Reallocate memory, reporting a fatal error with bail on failure. + */ +void * +brealloc(void *p, size_t size) +{ + p = realloc(p, size); + if (p == NULL) + sysbail("failed to realloc %lu bytes", (unsigned long) size); + return p; +} + + +/* + * The same as brealloc, but determine the size by multiplying an element + * count by a size, similar to calloc. The multiplication is checked for + * integer overflow. + * + * We should technically use SIZE_MAX here for the overflow check, but + * SIZE_MAX is C99 and we're only assuming C89 + SUSv3, which does not + * guarantee that it exists. They do guarantee that UINT_MAX exists, and we + * can assume that UINT_MAX <= SIZE_MAX. + * + * (In theory, C89 and C99 permit size_t to be smaller than unsigned int, but + * I disbelieve in the existence of such systems and they will have to cope + * without overflow checks.) + */ +void * +breallocarray(void *p, size_t n, size_t size) +{ + if (n > 0 && UINT_MAX / n <= size) + bail("reallocarray too large"); + if (n == 0) + n = 1; + p = realloc(p, n * size); + if (p == NULL) + sysbail("failed to realloc %lu bytes", (unsigned long) (n * size)); + return p; +} + + +/* + * Copy a string, reporting a fatal error with bail on failure. + */ +char * +bstrdup(const char *s) +{ + char *p; + size_t len; + + len = strlen(s) + 1; + p = (char *) malloc(len); + if (p == NULL) + sysbail("failed to strdup %lu bytes", (unsigned long) len); + memcpy(p, s, len); + return p; +} + + +/* + * Copy up to n characters of a string, reporting a fatal error with bail on + * failure. Don't use the system strndup function, since it may not exist and + * the TAP library doesn't assume any portability support. + */ +char * +bstrndup(const char *s, size_t n) +{ + const char *p; + char *copy; + size_t length; + + /* Don't assume that the source string is nul-terminated. */ + for (p = s; (size_t)(p - s) < n && *p != '\0'; p++) + ; + length = (size_t)(p - s); + copy = (char *) malloc(length + 1); + if (copy == NULL) + sysbail("failed to strndup %lu bytes", (unsigned long) length); + memcpy(copy, s, length); + copy[length] = '\0'; + return copy; +} + + +/* + * Locate a test file. Given the partial path to a file, look under + * C_TAP_BUILD and then C_TAP_SOURCE for the file and return the full path to + * the file. Returns NULL if the file doesn't exist. A non-NULL return + * should be freed with test_file_path_free(). + */ +char * +test_file_path(const char *file) +{ + char *base; + char *path = NULL; + const char *envs[] = {"C_TAP_BUILD", "C_TAP_SOURCE", NULL}; + int i; + + for (i = 0; envs[i] != NULL; i++) { + base = getenv(envs[i]); + if (base == NULL) + continue; + path = concat(base, "/", file, (const char *) 0); + if (access(path, R_OK) == 0) + break; + free(path); + path = NULL; + } + return path; +} + + +/* + * Free a path returned from test_file_path(). This function exists primarily + * for Windows, where memory must be freed from the same library domain that + * it was allocated from. + */ +void +test_file_path_free(char *path) +{ + free(path); +} + + +/* + * Create a temporary directory, tmp, under C_TAP_BUILD if set and the current + * directory if it does not. Returns the path to the temporary directory in + * newly allocated memory, and calls bail on any failure. The return value + * should be freed with test_tmpdir_free. + * + * This function uses sprintf because it attempts to be independent of all + * other portability layers. The use immediately after a memory allocation + * should be safe without using snprintf or strlcpy/strlcat. + */ +char * +test_tmpdir(void) +{ + const char *build; + char *path = NULL; + + build = getenv("C_TAP_BUILD"); + if (build == NULL) + build = "."; + path = concat(build, "/tmp", (const char *) 0); + if (access(path, X_OK) < 0) + if (mkdir(path, 0777) < 0) + sysbail("error creating temporary directory %s", path); + return path; +} + + +/* + * Free a path returned from test_tmpdir() and attempt to remove the + * directory. If we can't delete the directory, don't worry; something else + * that hasn't yet cleaned up may still be using it. + */ +void +test_tmpdir_free(char *path) +{ + if (path != NULL) + rmdir(path); + free(path); +} + +static void +register_cleanup(test_cleanup_func func, + test_cleanup_func_with_data func_with_data, void *data) +{ + struct cleanup_func *cleanup, **last; + + cleanup = bcalloc_type(1, struct cleanup_func); + cleanup->func = func; + cleanup->func_with_data = func_with_data; + cleanup->data = data; + cleanup->next = NULL; + last = &cleanup_funcs; + while (*last != NULL) + last = &(*last)->next; + *last = cleanup; +} + +/* + * Register a cleanup function that is called when testing ends. All such + * registered functions will be run by finish. + */ +void +test_cleanup_register(test_cleanup_func func) +{ + register_cleanup(func, NULL, NULL); +} + +/* + * Same as above, but also allows an opaque pointer to be passed to the cleanup + * function. + */ +void +test_cleanup_register_with_data(test_cleanup_func_with_data func, void *data) +{ + register_cleanup(NULL, func, data); +} diff --git a/tests/tap/basic.h b/tests/tap/basic.h new file mode 100644 index 000000000000..45f15f2892a7 --- /dev/null +++ b/tests/tap/basic.h @@ -0,0 +1,192 @@ +/* + * Basic utility routines for the TAP protocol. + * + * This file is part of C TAP Harness. The current version plus supporting + * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2009-2019 Russ Allbery <eagle@eyrie.org> + * Copyright 2001-2002, 2004-2008, 2011-2012, 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 + */ + +#ifndef TAP_BASIC_H +#define TAP_BASIC_H 1 + +#include <stdarg.h> /* va_list */ +#include <stddef.h> /* size_t */ +#include <tests/tap/macros.h> + +/* + * Used for iterating through arrays. ARRAY_SIZE returns the number of + * elements in the array (useful for a < upper bound in a for loop) and + * ARRAY_END returns a pointer to the element past the end (ISO C99 makes it + * legal to refer to such a pointer as long as it's never dereferenced). + */ +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) +#define ARRAY_END(array) (&(array)[ARRAY_SIZE(array)]) + +BEGIN_DECLS + +/* + * The test count. Always contains the number that will be used for the next + * test status. + */ +extern unsigned long testnum; + +/* Print out the number of tests and set standard output to line buffered. */ +void plan(unsigned long count); + +/* + * Prepare for lazy planning, in which the plan will be printed automatically + * at the end of the test program. + */ +void plan_lazy(void); + +/* Skip the entire test suite. Call instead of plan. */ +void skip_all(const char *format, ...) + __attribute__((__noreturn__, __format__(printf, 1, 2))); + +/* + * Basic reporting functions. The okv() function is the same as ok() but + * takes the test description as a va_list to make it easier to reuse the + * reporting infrastructure when writing new tests. ok() and okv() return the + * value of the success argument. + */ +int ok(int success, const char *format, ...) + __attribute__((__format__(printf, 2, 3))); +int okv(int success, const char *format, va_list args) + __attribute__((__format__(printf, 2, 0))); +void skip(const char *reason, ...) __attribute__((__format__(printf, 1, 2))); + +/* + * Report the same status on, or skip, the next count tests. ok_block() + * returns the value of the success argument. + */ +int ok_block(unsigned long count, int success, const char *format, ...) + __attribute__((__format__(printf, 3, 4))); +void skip_block(unsigned long count, const char *reason, ...) + __attribute__((__format__(printf, 2, 3))); + +/* + * Compare two values. Returns true if the test passes and false if it fails. + * is_bool takes an int since the bool type isn't fully portable yet, but + * interprets both arguments for their truth value, not for their numeric + * value. + */ +int is_bool(int, int, const char *format, ...) + __attribute__((__format__(printf, 3, 4))); +int is_int(long, long, const char *format, ...) + __attribute__((__format__(printf, 3, 4))); +int is_string(const char *, const char *, const char *format, ...) + __attribute__((__format__(printf, 3, 4))); +int is_hex(unsigned long, unsigned long, const char *format, ...) + __attribute__((__format__(printf, 3, 4))); +int is_blob(const void *, const void *, size_t, const char *format, ...) + __attribute__((__format__(printf, 4, 5))); + +/* Bail out with an error. sysbail appends strerror(errno). */ +void bail(const char *format, ...) + __attribute__((__noreturn__, __nonnull__, __format__(printf, 1, 2))); +void sysbail(const char *format, ...) + __attribute__((__noreturn__, __nonnull__, __format__(printf, 1, 2))); + +/* Report a diagnostic to stderr prefixed with #. */ +int diag(const char *format, ...) + __attribute__((__nonnull__, __format__(printf, 1, 2))); +int sysdiag(const char *format, ...) + __attribute__((__nonnull__, __format__(printf, 1, 2))); + +/* + * Register or unregister a file that contains supplementary diagnostics. + * Before any other output, all registered files will be read, line by line, + * and each line will be reported as a diagnostic as if it were passed to + * diag(). Nul characters are not supported in these files and will result in + * truncated output. + */ +void diag_file_add(const char *file) __attribute__((__nonnull__)); +void diag_file_remove(const char *file) __attribute__((__nonnull__)); + +/* Allocate memory, reporting a fatal error with bail on failure. */ +void *bcalloc(size_t, size_t) + __attribute__((__alloc_size__(1, 2), __malloc__, __warn_unused_result__)); +void *bmalloc(size_t) + __attribute__((__alloc_size__(1), __malloc__, __warn_unused_result__)); +void *breallocarray(void *, size_t, size_t) + __attribute__((__alloc_size__(2, 3), __malloc__, __warn_unused_result__)); +void *brealloc(void *, size_t) + __attribute__((__alloc_size__(2), __malloc__, __warn_unused_result__)); +char *bstrdup(const char *) + __attribute__((__malloc__, __nonnull__, __warn_unused_result__)); +char *bstrndup(const char *, size_t) + __attribute__((__malloc__, __nonnull__, __warn_unused_result__)); + +/* + * Macros that cast the return value from b* memory functions, making them + * usable in C++ code and providing some additional type safety. + */ +#define bcalloc_type(n, type) ((type *) bcalloc((n), sizeof(type))) +#define breallocarray_type(p, n, type) \ + ((type *) breallocarray((p), (n), sizeof(type))) + +/* + * Find a test file under C_TAP_BUILD or C_TAP_SOURCE, returning the full + * path. The returned path should be freed with test_file_path_free(). + */ +char *test_file_path(const char *file) + __attribute__((__malloc__, __nonnull__, __warn_unused_result__)); +void test_file_path_free(char *path); + +/* + * Create a temporary directory relative to C_TAP_BUILD and return the path. + * The returned path should be freed with test_tmpdir_free(). + */ +char *test_tmpdir(void) __attribute__((__malloc__, __warn_unused_result__)); +void test_tmpdir_free(char *path); + +/* + * Register a cleanup function that is called when testing ends. All such + * registered functions will be run during atexit handling (and are therefore + * subject to all the same constraints and caveats as atexit functions). + * + * The function must return void and will be passed two arguments: an int that + * will be true if the test completed successfully and false otherwise, and an + * int that will be true if the cleanup function is run in the primary process + * (the one that called plan or plan_lazy) and false otherwise. If + * test_cleanup_register_with_data is used instead, a generic pointer can be + * provided and will be passed to the cleanup function as a third argument. + * + * test_cleanup_register_with_data is the better API and should have been the + * only API. test_cleanup_register was an API error preserved for backward + * cmpatibility. + */ +typedef void (*test_cleanup_func)(int, int); +typedef void (*test_cleanup_func_with_data)(int, int, void *); + +void test_cleanup_register(test_cleanup_func) __attribute__((__nonnull__)); +void test_cleanup_register_with_data(test_cleanup_func_with_data, void *) + __attribute__((__nonnull__)); + +END_DECLS + +#endif /* TAP_BASIC_H */ diff --git a/tests/tap/kadmin.c b/tests/tap/kadmin.c new file mode 100644 index 000000000000..8e70f9d0ec27 --- /dev/null +++ b/tests/tap/kadmin.c @@ -0,0 +1,138 @@ +/* + * Kerberos test setup requiring the kadmin API. + * + * This file collects Kerberos test setup functions that use the kadmin API to + * put principals into particular configurations for testing. Currently, the + * only implemented functionality is to mark a password as expired. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2017 Russ Allbery <eagle@eyrie.org> + * Copyright 2011 + * 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 <config.h> +#ifdef HAVE_KADM5CLNT +# include <portable/kadmin.h> +# include <portable/krb5.h> +#endif +#include <portable/system.h> + +#include <time.h> + +#include <tests/tap/basic.h> +#include <tests/tap/kadmin.h> +#include <tests/tap/kerberos.h> + +/* Used for unused parameters to silence gcc warnings. */ +#define UNUSED __attribute__((__unused__)) + + +/* + * Given the principal to set an expiration on, set that principal to have an + * expired password. This requires that the realm admin server be configured + * either in DNS (with SRV records) or in krb5.conf (possibly the one + * KRB5_CONFIG is pointing to). Authentication is done using the keytab + * stored in config/admin-keytab. + * + * Returns true on success. Returns false if necessary configuration is + * missing so that the caller can choose whether to call bail or skip_all. If + * the configuration is present but the operation fails, bails. + */ +#ifdef HAVE_KADM5CLNT +bool +kerberos_expire_password(const char *principal, time_t expires) +{ + char *path, *user; + const char *realm; + krb5_context ctx; + krb5_principal admin = NULL; + krb5_principal princ = NULL; + kadm5_ret_t code; + kadm5_config_params params; + kadm5_principal_ent_rec ent; + void *handle; + bool okay = false; + + /* Set up for making our call. */ + path = test_file_path("config/admin-keytab"); + if (path == NULL) + return false; + code = krb5_init_context(&ctx); + if (code != 0) + bail_krb5(ctx, code, "error initializing Kerberos"); + admin = kerberos_keytab_principal(ctx, path); + realm = krb5_principal_get_realm(ctx, admin); + code = krb5_set_default_realm(ctx, realm); + if (code != 0) + bail_krb5(ctx, code, "cannot set default realm"); + code = krb5_unparse_name(ctx, admin, &user); + if (code != 0) + bail_krb5(ctx, code, "cannot unparse admin principal"); + code = krb5_parse_name(ctx, principal, &princ); + if (code != 0) + bail_krb5(ctx, code, "cannot parse principal %s", principal); + + /* + * If the actual kadmin calls fail, we may be built with MIT Kerberos + * against a Heimdal server or vice versa. Return false to skip the + * tests. + */ + memset(¶ms, 0, sizeof(params)); + params.realm = (char *) realm; + params.mask = KADM5_CONFIG_REALM; + code = kadm5_init_with_skey_ctx(ctx, user, path, KADM5_ADMIN_SERVICE, + ¶ms, KADM5_STRUCT_VERSION, + KADM5_API_VERSION, &handle); + if (code != 0) { + diag_krb5(ctx, code, "error initializing kadmin"); + goto done; + } + memset(&ent, 0, sizeof(ent)); + ent.principal = princ; + ent.pw_expiration = (krb5_timestamp) expires; + code = kadm5_modify_principal(handle, &ent, KADM5_PW_EXPIRATION); + if (code == 0) + okay = true; + else + diag_krb5(ctx, code, "error setting password expiration"); + +done: + kadm5_destroy(handle); + krb5_free_unparsed_name(ctx, user); + krb5_free_principal(ctx, admin); + krb5_free_principal(ctx, princ); + krb5_free_context(ctx); + test_file_path_free(path); + return okay; +} +#else /* !HAVE_KADM5CLNT */ +bool +kerberos_expire_password(const char *principal UNUSED, time_t expires UNUSED) +{ + return false; +} +#endif /* !HAVE_KADM5CLNT */ diff --git a/tests/tap/kadmin.h b/tests/tap/kadmin.h new file mode 100644 index 000000000000..c4dc657237da --- /dev/null +++ b/tests/tap/kadmin.h @@ -0,0 +1,58 @@ +/* + * Utility functions for tests needing Kerberos admin actions. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2011, 2013 + * 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 + */ + +#ifndef TAP_KADMIN_H +#define TAP_KADMIN_H 1 + +#include <config.h> +#include <portable/stdbool.h> + +#include <time.h> + +#include <tests/tap/macros.h> + +BEGIN_DECLS + +/* + * Given the principal to set an expiration on and the expiration time, set + * that principal's key to expire at that time. Authentication is done using + * the keytab stored in config/admin-keytab. + * + * Returns true on success. Returns false if necessary configuration is + * missing so that the caller can choose whether to call bail or skip_all. If + * the configuration is present but the operation fails, bails. + */ +bool kerberos_expire_password(const char *, time_t) + __attribute__((__nonnull__)); + +END_DECLS + +#endif /* !TAP_KADMIN_H */ diff --git a/tests/tap/kerberos.c b/tests/tap/kerberos.c new file mode 100644 index 000000000000..765d80290a64 --- /dev/null +++ b/tests/tap/kerberos.c @@ -0,0 +1,544 @@ +/* + * Utility functions for tests that use Kerberos. + * + * The core function is kerberos_setup, which loads Kerberos test + * configuration and returns a struct of information. It also supports + * obtaining initial tickets from the configured keytab and setting up + * KRB5CCNAME and KRB5_KTNAME if a Kerberos keytab is present. Also included + * are utility functions for setting up a krb5.conf file and reporting + * Kerberos errors or warnings during testing. + * + * Some of the functionality here is only available if the Kerberos libraries + * are available. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2017 Russ Allbery <eagle@eyrie.org> + * Copyright 2006-2007, 2009-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 <config.h> +#ifdef HAVE_KRB5 +# include <portable/krb5.h> +#endif +#include <portable/system.h> + +#include <sys/stat.h> + +#include <tests/tap/basic.h> +#include <tests/tap/kerberos.h> +#include <tests/tap/macros.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + +/* + * Disable the requirement that format strings be literals, since it's easier + * to handle the possible patterns for kinit commands as an array. + */ +#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 2) || defined(__clang__) +# pragma GCC diagnostic ignored "-Wformat-nonliteral" +#endif + + +/* + * These variables hold the allocated configuration struct, the environment to + * point to a different Kerberos ticket cache, keytab, and configuration file, + * and the temporary directories used. We store them so that we can free them + * on exit for cleaner valgrind output, making it easier to find real memory + * leaks in the tested programs. + */ +static struct kerberos_config *config = NULL; +static char *krb5ccname = NULL; +static char *krb5_ktname = NULL; +static char *krb5_config = NULL; +static char *tmpdir_ticket = NULL; +static char *tmpdir_conf = NULL; + + +/* + * Obtain Kerberos tickets and fill in the principal config entry. + * + * There are two implementations of this function, one if we have native + * Kerberos libraries available and one if we don't. Uses keytab to obtain + * credentials, and fills in the cache member of the provided config struct. + */ +#ifdef HAVE_KRB5 + +static void +kerberos_kinit(void) +{ + char *name, *krbtgt; + krb5_error_code code; + krb5_context ctx; + krb5_ccache ccache; + krb5_principal kprinc; + krb5_keytab keytab; + krb5_get_init_creds_opt *opts; + krb5_creds creds; + const char *realm; + + /* + * Determine the principal corresponding to that keytab. We copy the + * memory to ensure that it's allocated in the right memory domain on + * systems where that may matter (like Windows). + */ + code = krb5_init_context(&ctx); + if (code != 0) + bail_krb5(ctx, code, "error initializing Kerberos"); + kprinc = kerberos_keytab_principal(ctx, config->keytab); + code = krb5_unparse_name(ctx, kprinc, &name); + if (code != 0) + bail_krb5(ctx, code, "error unparsing name"); + krb5_free_principal(ctx, kprinc); + config->principal = bstrdup(name); + krb5_free_unparsed_name(ctx, name); + + /* Now do the Kerberos initialization. */ + code = krb5_cc_default(ctx, &ccache); + if (code != 0) + bail_krb5(ctx, code, "error setting ticket cache"); + code = krb5_parse_name(ctx, config->principal, &kprinc); + if (code != 0) + bail_krb5(ctx, code, "error parsing principal %s", config->principal); + realm = krb5_principal_get_realm(ctx, kprinc); + basprintf(&krbtgt, "krbtgt/%s@%s", realm, realm); + code = krb5_kt_resolve(ctx, config->keytab, &keytab); + if (code != 0) + bail_krb5(ctx, code, "cannot open keytab %s", config->keytab); + code = krb5_get_init_creds_opt_alloc(ctx, &opts); + if (code != 0) + bail_krb5(ctx, code, "cannot allocate credential options"); + krb5_get_init_creds_opt_set_default_flags(ctx, NULL, realm, opts); + krb5_get_init_creds_opt_set_forwardable(opts, 0); + krb5_get_init_creds_opt_set_proxiable(opts, 0); + code = krb5_get_init_creds_keytab(ctx, &creds, kprinc, keytab, 0, krbtgt, + opts); + if (code != 0) + bail_krb5(ctx, code, "cannot get Kerberos tickets"); + code = krb5_cc_initialize(ctx, ccache, kprinc); + if (code != 0) + bail_krb5(ctx, code, "error initializing ticket cache"); + code = krb5_cc_store_cred(ctx, ccache, &creds); + if (code != 0) + bail_krb5(ctx, code, "error storing credentials"); + krb5_cc_close(ctx, ccache); + krb5_free_cred_contents(ctx, &creds); + krb5_kt_close(ctx, keytab); + krb5_free_principal(ctx, kprinc); + krb5_get_init_creds_opt_free(ctx, opts); + krb5_free_context(ctx); + free(krbtgt); +} + +#else /* !HAVE_KRB5 */ + +static void +kerberos_kinit(void) +{ + static const char *const format[] = { + "kinit --no-afslog -k -t %s %s >/dev/null 2>&1 </dev/null", + "kinit -k -t %s %s >/dev/null 2>&1 </dev/null", + "kinit -t %s %s >/dev/null 2>&1 </dev/null", + "kinit -k -K %s %s >/dev/null 2>&1 </dev/null"}; + FILE *file; + char *path; + char principal[BUFSIZ], *command; + size_t i; + int status; + + /* Read the principal corresponding to the keytab. */ + path = test_file_path("config/principal"); + if (path == NULL) { + test_file_path_free(config->keytab); + config->keytab = NULL; + return; + } + file = fopen(path, "r"); + if (file == NULL) { + test_file_path_free(path); + return; + } + test_file_path_free(path); + if (fgets(principal, sizeof(principal), file) == NULL) + bail("cannot read %s", path); + fclose(file); + if (principal[strlen(principal) - 1] != '\n') + bail("no newline in %s", path); + principal[strlen(principal) - 1] = '\0'; + config->principal = bstrdup(principal); + + /* Now do the Kerberos initialization. */ + for (i = 0; i < ARRAY_SIZE(format); i++) { + basprintf(&command, format[i], config->keytab, principal); + status = system(command); + free(command); + if (status != -1 && WEXITSTATUS(status) == 0) + break; + } + if (status == -1 || WEXITSTATUS(status) != 0) + bail("cannot get Kerberos tickets"); +} + +#endif /* !HAVE_KRB5 */ + + +/* + * Free all the memory associated with our Kerberos setup, but don't remove + * the ticket cache. This is used when cleaning up on exit from a non-primary + * process so that test programs that fork don't remove the ticket cache still + * used by the main program. + */ +static void +kerberos_free(void) +{ + test_tmpdir_free(tmpdir_ticket); + tmpdir_ticket = NULL; + if (config != NULL) { + test_file_path_free(config->keytab); + free(config->principal); + free(config->cache); + free(config->userprinc); + free(config->username); + free(config->password); + free(config->pkinit_principal); + free(config->pkinit_cert); + free(config); + config = NULL; + } + if (krb5ccname != NULL) { + putenv((char *) "KRB5CCNAME="); + free(krb5ccname); + krb5ccname = NULL; + } + if (krb5_ktname != NULL) { + putenv((char *) "KRB5_KTNAME="); + free(krb5_ktname); + krb5_ktname = NULL; + } +} + + +/* + * Clean up at the end of a test. This removes the ticket cache and resets + * and frees the memory allocated for the environment variables so that + * valgrind output on test suites is cleaner. Most of the work is done by + * kerberos_free, but this function also deletes the ticket cache. + */ +void +kerberos_cleanup(void) +{ + char *path; + + if (tmpdir_ticket != NULL) { + basprintf(&path, "%s/krb5cc_test", tmpdir_ticket); + unlink(path); + free(path); + } + kerberos_free(); +} + + +/* + * The cleanup handler for the TAP framework. Call kerberos_cleanup if we're + * in the primary process and kerberos_free if not. The first argument, which + * indicates whether the test succeeded or not, is ignored, since we need to + * do the same thing either way. + */ +static void +kerberos_cleanup_handler(int success UNUSED, int primary) +{ + if (primary) + kerberos_cleanup(); + else + kerberos_free(); +} + + +/* + * Obtain Kerberos tickets for the principal specified in config/principal + * using the keytab specified in config/keytab, both of which are presumed to + * be in tests in either the build or the source tree. Also sets KRB5_KTNAME + * and KRB5CCNAME. + * + * Returns the contents of config/principal in newly allocated memory or NULL + * if Kerberos tests are apparently not configured. If Kerberos tests are + * configured but something else fails, calls bail. + */ +struct kerberos_config * +kerberos_setup(enum kerberos_needs needs) +{ + char *path; + char buffer[BUFSIZ]; + FILE *file = NULL; + + /* If we were called before, clean up after the previous run. */ + if (config != NULL) + kerberos_cleanup(); + config = bcalloc(1, sizeof(struct kerberos_config)); + + /* + * If we have a config/keytab file, set the KRB5CCNAME and KRB5_KTNAME + * environment variables and obtain initial tickets. + */ + config->keytab = test_file_path("config/keytab"); + if (config->keytab == NULL) { + if (needs == TAP_KRB_NEEDS_KEYTAB || needs == TAP_KRB_NEEDS_BOTH) + skip_all("Kerberos tests not configured"); + } else { + tmpdir_ticket = test_tmpdir(); + basprintf(&config->cache, "%s/krb5cc_test", tmpdir_ticket); + basprintf(&krb5ccname, "KRB5CCNAME=%s/krb5cc_test", tmpdir_ticket); + basprintf(&krb5_ktname, "KRB5_KTNAME=%s", config->keytab); + putenv(krb5ccname); + putenv(krb5_ktname); + kerberos_kinit(); + } + + /* + * If we have a config/password file, read it and fill out the relevant + * members of our config struct. + */ + path = test_file_path("config/password"); + if (path != NULL) + file = fopen(path, "r"); + if (file == NULL) { + if (needs == TAP_KRB_NEEDS_PASSWORD || needs == TAP_KRB_NEEDS_BOTH) + skip_all("Kerberos tests not configured"); + } else { + if (fgets(buffer, sizeof(buffer), file) == NULL) + bail("cannot read %s", path); + if (buffer[strlen(buffer) - 1] != '\n') + bail("no newline in %s", path); + buffer[strlen(buffer) - 1] = '\0'; + config->userprinc = bstrdup(buffer); + if (fgets(buffer, sizeof(buffer), file) == NULL) + bail("cannot read password from %s", path); + fclose(file); + if (buffer[strlen(buffer) - 1] != '\n') + bail("password too long in %s", path); + buffer[strlen(buffer) - 1] = '\0'; + config->password = bstrdup(buffer); + + /* + * Strip the realm from the principal and set realm and username. + * This is not strictly correct; it doesn't cope with escaped @-signs + * or enterprise names. + */ + config->username = bstrdup(config->userprinc); + config->realm = strchr(config->username, '@'); + if (config->realm == NULL) + bail("test principal has no realm"); + *config->realm = '\0'; + config->realm++; + } + test_file_path_free(path); + + /* + * If we have PKINIT configuration, read it and fill out the relevant + * members of our config struct. + */ + path = test_file_path("config/pkinit-principal"); + if (path != NULL) + file = fopen(path, "r"); + if (path != NULL && file != NULL) { + if (fgets(buffer, sizeof(buffer), file) == NULL) + bail("cannot read %s", path); + if (buffer[strlen(buffer) - 1] != '\n') + bail("no newline in %s", path); + buffer[strlen(buffer) - 1] = '\0'; + fclose(file); + test_file_path_free(path); + path = test_file_path("config/pkinit-cert"); + if (path != NULL) { + config->pkinit_principal = bstrdup(buffer); + config->pkinit_cert = bstrdup(path); + } + } + test_file_path_free(path); + if (config->pkinit_cert == NULL && (needs & TAP_KRB_NEEDS_PKINIT) != 0) + skip_all("PKINIT tests not configured"); + + /* + * Register the cleanup function so that the caller doesn't have to do + * explicit cleanup. + */ + test_cleanup_register(kerberos_cleanup_handler); + + /* Return the configuration. */ + return config; +} + + +/* + * Clean up the krb5.conf file generated by kerberos_generate_conf and free + * the memory used to set the environment variable. This doesn't fail if the + * file and variable are already gone, allowing it to be harmlessly run + * multiple times. + * + * Normally called via an atexit handler. + */ +void +kerberos_cleanup_conf(void) +{ + char *path; + + if (tmpdir_conf != NULL) { + basprintf(&path, "%s/krb5.conf", tmpdir_conf); + unlink(path); + free(path); + test_tmpdir_free(tmpdir_conf); + tmpdir_conf = NULL; + } + putenv((char *) "KRB5_CONFIG="); + free(krb5_config); + krb5_config = NULL; +} + + +/* + * Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it. + * The [appdefaults] section will be stripped out and the default realm will + * be set to the realm specified, if not NULL. This will use config/krb5.conf + * in preference, so users can configure the tests by creating that file if + * the system file isn't suitable. + * + * Depends on data/generate-krb5-conf being present in the test suite. + */ +void +kerberos_generate_conf(const char *realm) +{ + char *path; + const char *argv[3]; + + if (tmpdir_conf != NULL) + kerberos_cleanup_conf(); + path = test_file_path("data/generate-krb5-conf"); + if (path == NULL) + bail("cannot find generate-krb5-conf"); + argv[0] = path; + argv[1] = realm; + argv[2] = NULL; + run_setup(argv); + test_file_path_free(path); + tmpdir_conf = test_tmpdir(); + basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir_conf); + putenv(krb5_config); + if (atexit(kerberos_cleanup_conf) != 0) + sysdiag("cannot register cleanup function"); +} + + +/* + * The remaining functions in this file are only available if Kerberos + * libraries are available. + */ +#ifdef HAVE_KRB5 + + +/* + * Report a Kerberos error and bail out. Takes a long instead of a + * krb5_error_code because it can also handle a kadm5_ret_t (which may be a + * different size). + */ +void +bail_krb5(krb5_context ctx, long code, const char *format, ...) +{ + const char *k5_msg = NULL; + char *message; + va_list args; + + if (ctx != NULL) + k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code); + va_start(args, format); + bvasprintf(&message, format, args); + va_end(args); + if (k5_msg == NULL) + bail("%s", message); + else + bail("%s: %s", message, k5_msg); +} + + +/* + * Report a Kerberos error as a diagnostic to stderr. Takes a long instead of + * a krb5_error_code because it can also handle a kadm5_ret_t (which may be a + * different size). + */ +void +diag_krb5(krb5_context ctx, long code, const char *format, ...) +{ + const char *k5_msg = NULL; + char *message; + va_list args; + + if (ctx != NULL) + k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code); + va_start(args, format); + bvasprintf(&message, format, args); + va_end(args); + if (k5_msg == NULL) + diag("%s", message); + else + diag("%s: %s", message, k5_msg); + free(message); + if (k5_msg != NULL) + krb5_free_error_message(ctx, k5_msg); +} + + +/* + * Find the principal of the first entry of a keytab and return it. The + * caller is responsible for freeing the result with krb5_free_principal. + * Exit on error. + */ +krb5_principal +kerberos_keytab_principal(krb5_context ctx, const char *path) +{ + krb5_keytab keytab; + krb5_kt_cursor cursor; + krb5_keytab_entry entry; + krb5_principal princ; + krb5_error_code status; + + status = krb5_kt_resolve(ctx, path, &keytab); + if (status != 0) + bail_krb5(ctx, status, "error opening %s", path); + status = krb5_kt_start_seq_get(ctx, keytab, &cursor); + if (status != 0) + bail_krb5(ctx, status, "error reading %s", path); + status = krb5_kt_next_entry(ctx, keytab, &entry, &cursor); + if (status != 0) + bail("no principal found in keytab file %s", path); + status = krb5_copy_principal(ctx, entry.principal, &princ); + if (status != 0) + bail_krb5(ctx, status, "error copying principal from %s", path); + krb5_kt_free_entry(ctx, &entry); + krb5_kt_end_seq_get(ctx, keytab, &cursor); + krb5_kt_close(ctx, keytab); + return princ; +} + +#endif /* HAVE_KRB5 */ diff --git a/tests/tap/kerberos.h b/tests/tap/kerberos.h new file mode 100644 index 000000000000..53dd09619c96 --- /dev/null +++ b/tests/tap/kerberos.h @@ -0,0 +1,135 @@ +/* + * Utility functions for tests that use Kerberos. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org> + * Copyright 2006-2007, 2009, 2011-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 + */ + +#ifndef TAP_KERBEROS_H +#define TAP_KERBEROS_H 1 + +#include <config.h> +#include <tests/tap/macros.h> + +#ifdef HAVE_KRB5 +# include <portable/krb5.h> +#endif + +/* Holds the information parsed from the Kerberos test configuration. */ +struct kerberos_config { + char *keytab; /* Path to the keytab. */ + char *principal; /* Principal whose keys are in the keytab. */ + char *cache; /* Path to the Kerberos ticket cache. */ + char *userprinc; /* The fully-qualified principal. */ + char *username; /* The local (non-realm) part of principal. */ + char *realm; /* The realm part of the principal. */ + char *password; /* The password. */ + char *pkinit_principal; /* Principal for PKINIT authentication. */ + char *pkinit_cert; /* Path to certificates for PKINIT. */ +}; + +/* + * Whether to skip all tests (by calling skip_all) in kerberos_setup if + * certain configuration information isn't available. "_BOTH" means that the + * tests require both keytab and password, but PKINIT is not required. + */ +enum kerberos_needs +{ + /* clang-format off */ + TAP_KRB_NEEDS_NONE = 0x00, + TAP_KRB_NEEDS_KEYTAB = 0x01, + TAP_KRB_NEEDS_PASSWORD = 0x02, + TAP_KRB_NEEDS_BOTH = 0x01 | 0x02, + TAP_KRB_NEEDS_PKINIT = 0x04 + /* clang-format on */ +}; + +BEGIN_DECLS + +/* + * Set up Kerberos, returning the test configuration information. This + * obtains Kerberos tickets from config/keytab, if one is present, and stores + * them in a Kerberos ticket cache, sets KRB5_KTNAME and KRB5CCNAME. It also + * loads the principal and password from config/password, if it exists, and + * stores the principal, password, username, and realm in the returned struct. + * + * If there is no config/keytab file, KRB5_KTNAME and KRB5CCNAME won't be set + * and the keytab field will be NULL. If there is no config/password file, + * the principal field will be NULL. If the files exist but loading them + * fails, or authentication fails, kerberos_setup calls bail. + * + * kerberos_cleanup will be run as a cleanup function normally, freeing all + * resources and cleaning up temporary files on process exit. It can, + * however, be called directly if for some reason the caller needs to delete + * the Kerberos environment again. However, normally the caller can just call + * kerberos_setup again. + */ +struct kerberos_config *kerberos_setup(enum kerberos_needs) + __attribute__((__malloc__)); +void kerberos_cleanup(void); + +/* + * Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it. + * The [appdefaults] section will be stripped out and the default realm will + * be set to the realm specified, if not NULL. This will use config/krb5.conf + * in preference, so users can configure the tests by creating that file if + * the system file isn't suitable. + * + * Depends on data/generate-krb5-conf being present in the test suite. + * + * kerberos_cleanup_conf will clean up after this function, but usually + * doesn't need to be called directly since it's registered as an atexit + * handler. + */ +void kerberos_generate_conf(const char *realm); +void kerberos_cleanup_conf(void); + +/* These interfaces are only available with native Kerberos support. */ +#ifdef HAVE_KRB5 + +/* Bail out with an error, appending the Kerberos error message. */ +void bail_krb5(krb5_context, long, const char *format, ...) + __attribute__((__noreturn__, __nonnull__(3), __format__(printf, 3, 4))); + +/* Report a diagnostic with Kerberos error to stderr prefixed with #. */ +void diag_krb5(krb5_context, long, const char *format, ...) + __attribute__((__nonnull__(3), __format__(printf, 3, 4))); + +/* + * Given a Kerberos context and the path to a keytab, retrieve the principal + * for the first entry in the keytab and return it. Calls bail on failure. + * The returned principal should be freed with krb5_free_principal. + */ +krb5_principal kerberos_keytab_principal(krb5_context, const char *path) + __attribute__((__nonnull__)); + +#endif /* HAVE_KRB5 */ + +END_DECLS + +#endif /* !TAP_MESSAGES_H */ diff --git a/tests/tap/libtap.sh b/tests/tap/libtap.sh new file mode 100644 index 000000000000..1827a689e380 --- /dev/null +++ b/tests/tap/libtap.sh @@ -0,0 +1,248 @@ +# Shell function library for test cases. +# +# Note that while many of the functions in this library could benefit from +# using "local" to avoid possibly hammering global variables, Solaris /bin/sh +# doesn't support local and this library aspires to be portable to Solaris +# Bourne shell. Instead, all private variables are prefixed with "tap_". +# +# This file provides a TAP-compatible shell function library useful for +# writing test cases. It is part of C TAP Harness, which can be found at +# <https://www.eyrie.org/~eagle/software/c-tap-harness/>. +# +# Written by Russ Allbery <eagle@eyrie.org> +# Copyright 2009-2012, 2016 Russ Allbery <eagle@eyrie.org> +# Copyright 2006-2008, 2013 +# 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 + +# Print out the number of test cases we expect to run. +plan () { + count=1 + planned="$1" + failed=0 + echo "1..$1" + trap finish 0 +} + +# Prepare for lazy planning. +plan_lazy () { + count=1 + planned=0 + failed=0 + trap finish 0 +} + +# Report the test status on exit. +finish () { + tap_highest=`expr "$count" - 1` + if [ "$planned" = 0 ] ; then + echo "1..$tap_highest" + planned="$tap_highest" + fi + tap_looks='# Looks like you' + if [ "$planned" -gt 0 ] ; then + if [ "$planned" -gt "$tap_highest" ] ; then + if [ "$planned" -gt 1 ] ; then + echo "$tap_looks planned $planned tests but only ran" \ + "$tap_highest" + else + echo "$tap_looks planned $planned test but only ran" \ + "$tap_highest" + fi + elif [ "$planned" -lt "$tap_highest" ] ; then + tap_extra=`expr "$tap_highest" - "$planned"` + if [ "$planned" -gt 1 ] ; then + echo "$tap_looks planned $planned tests but ran" \ + "$tap_extra extra" + else + echo "$tap_looks planned $planned test but ran" \ + "$tap_extra extra" + fi + elif [ "$failed" -gt 0 ] ; then + if [ "$failed" -gt 1 ] ; then + echo "$tap_looks failed $failed tests of $planned" + else + echo "$tap_looks failed $failed test of $planned" + fi + elif [ "$planned" -gt 1 ] ; then + echo "# All $planned tests successful or skipped" + else + echo "# $planned test successful or skipped" + fi + fi +} + +# Skip the entire test suite. Should be run instead of plan. +skip_all () { + tap_desc="$1" + if [ -n "$tap_desc" ] ; then + echo "1..0 # skip $tap_desc" + else + echo "1..0 # skip" + fi + exit 0 +} + +# ok takes a test description and a command to run and prints success if that +# command is successful, false otherwise. The count starts at 1 and is +# updated each time ok is printed. +ok () { + tap_desc="$1" + if [ -n "$tap_desc" ] ; then + tap_desc=" - $tap_desc" + fi + shift + if "$@" ; then + echo ok "$count$tap_desc" + else + echo not ok "$count$tap_desc" + failed=`expr $failed + 1` + fi + count=`expr $count + 1` +} + +# Skip the next test. Takes the reason why the test is skipped. +skip () { + echo "ok $count # skip $*" + count=`expr $count + 1` +} + +# Report the same status on a whole set of tests. Takes the count of tests, +# the description, and then the command to run to determine the status. +ok_block () { + tap_i=$count + tap_end=`expr $count + $1` + shift + while [ "$tap_i" -lt "$tap_end" ] ; do + ok "$@" + tap_i=`expr $tap_i + 1` + done +} + +# Skip a whole set of tests. Takes the count and then the reason for skipping +# the test. +skip_block () { + tap_i=$count + tap_end=`expr $count + $1` + shift + while [ "$tap_i" -lt "$tap_end" ] ; do + skip "$@" + tap_i=`expr $tap_i + 1` + done +} + +# Portable variant of printf '%s\n' "$*". In the majority of cases, this +# function is slower than printf, because the latter is often implemented +# as a builtin command. The value of the variable IFS is ignored. +# +# This macro must not be called via backticks inside double quotes, since this +# will result in bizarre escaping behavior and lots of extra backslashes on +# Solaris. +puts () { + cat << EOH +$@ +EOH +} + +# Run a program expected to succeed, and print ok if it does and produces the +# correct output. Takes the description, expected exit status, the expected +# output, the command to run, and then any arguments for that command. +# Standard output and standard error are combined when analyzing the output of +# the command. +# +# If the command may contain system-specific error messages in its output, +# add strip_colon_error before the command to post-process its output. +ok_program () { + tap_desc="$1" + shift + tap_w_status="$1" + shift + tap_w_output="$1" + shift + tap_output=`"$@" 2>&1` + tap_status=$? + if [ $tap_status = $tap_w_status ] \ + && [ x"$tap_output" = x"$tap_w_output" ] ; then + ok "$tap_desc" true + else + echo "# saw: ($tap_status) $tap_output" + echo "# not: ($tap_w_status) $tap_w_output" + ok "$tap_desc" false + fi +} + +# Strip a colon and everything after it off the output of a command, as long +# as that colon comes after at least one whitespace character. (This is done +# to avoid stripping the name of the program from the start of an error +# message.) This is used to remove system-specific error messages (coming +# from strerror, for example). +strip_colon_error() { + tap_output=`"$@" 2>&1` + tap_status=$? + tap_output=`puts "$tap_output" | sed 's/^\([^ ]* [^:]*\):.*/\1/'` + puts "$tap_output" + return $tap_status +} + +# Bail out with an error message. +bail () { + echo 'Bail out!' "$@" + exit 255 +} + +# Output a diagnostic on standard error, preceded by the required # mark. +diag () { + echo '#' "$@" +} + +# Search for the given file first in $C_TAP_BUILD and then in $C_TAP_SOURCE +# and echo the path where the file was found, or the empty string if the file +# wasn't found. +# +# This macro uses puts, so don't run it using backticks inside double quotes +# or bizarre quoting behavior will happen with Solaris sh. +test_file_path () { + if [ -n "$C_TAP_BUILD" ] && [ -f "$C_TAP_BUILD/$1" ] ; then + puts "$C_TAP_BUILD/$1" + elif [ -n "$C_TAP_SOURCE" ] && [ -f "$C_TAP_SOURCE/$1" ] ; then + puts "$C_TAP_SOURCE/$1" + else + echo '' + fi +} + +# Create $C_TAP_BUILD/tmp for use by tests for storing temporary files and +# return the path (via standard output). +# +# This macro uses puts, so don't run it using backticks inside double quotes +# or bizarre quoting behavior will happen with Solaris sh. +test_tmpdir () { + if [ -z "$C_TAP_BUILD" ] ; then + tap_tmpdir="./tmp" + else + tap_tmpdir="$C_TAP_BUILD"/tmp + fi + if [ ! -d "$tap_tmpdir" ] ; then + mkdir "$tap_tmpdir" || bail "Error creating $tap_tmpdir" + fi + puts "$tap_tmpdir" +} diff --git a/tests/tap/macros.h b/tests/tap/macros.h new file mode 100644 index 000000000000..c2c8b5c7315d --- /dev/null +++ b/tests/tap/macros.h @@ -0,0 +1,99 @@ +/* + * Helpful macros for TAP header files. + * + * This is not, strictly speaking, related to TAP, but any TAP add-on is + * probably going to need these macros, so define them in one place so that + * everyone can pull them in. + * + * This file is part of C TAP Harness. The current version plus supporting + * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>. + * + * Copyright 2008, 2012-2013, 2015 Russ Allbery <eagle@eyrie.org> + * + * 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 + */ + +#ifndef TAP_MACROS_H +#define TAP_MACROS_H 1 + +/* + * __attribute__ is available in gcc 2.5 and later, but only with gcc 2.7 + * could you use the __format__ form of the attributes, which is what we use + * (to avoid confusion with other macros), and only with gcc 2.96 can you use + * the attribute __malloc__. 2.96 is very old, so don't bother trying to get + * the other attributes to work with GCC versions between 2.7 and 2.96. + */ +#ifndef __attribute__ +# if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 96) +# define __attribute__(spec) /* empty */ +# endif +#endif + +/* + * We use __alloc_size__, but it was only available in fairly recent versions + * of GCC. Suppress warnings about the unknown attribute if GCC is too old. + * We know that we're GCC at this point, so we can use the GCC variadic macro + * extension, which will still work with versions of GCC too old to have C99 + * variadic macro support. + */ +#if !defined(__attribute__) && !defined(__alloc_size__) +# if defined(__GNUC__) && !defined(__clang__) +# if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 3) +# define __alloc_size__(spec, args...) /* empty */ +# endif +# endif +#endif + +/* Suppress __warn_unused_result__ if gcc is too old. */ +#if !defined(__attribute__) && !defined(__warn_unused_result__) +# if __GNUC__ < 3 || (__GNUC__ == 3 && __GNUC_MINOR__ < 4) +# define __warn_unused_result__ /* empty */ +# endif +#endif + +/* + * LLVM and Clang pretend to be GCC but don't support all of the __attribute__ + * settings that GCC does. For them, suppress warnings about unknown + * attributes on declarations. This unfortunately will affect the entire + * compilation context, but there's no push and pop available. + */ +#if !defined(__attribute__) && (defined(__llvm__) || defined(__clang__)) +# pragma GCC diagnostic ignored "-Wattributes" +#endif + +/* Used for unused parameters to silence gcc warnings. */ +#define UNUSED __attribute__((__unused__)) + +/* + * BEGIN_DECLS is used at the beginning of declarations so that C++ + * compilers don't mangle their names. END_DECLS is used at the end. + */ +#undef BEGIN_DECLS +#undef END_DECLS +#ifdef __cplusplus +# define BEGIN_DECLS extern "C" { +# define END_DECLS } +#else +# define BEGIN_DECLS /* empty */ +# define END_DECLS /* empty */ +#endif + +#endif /* TAP_MACROS_H */ diff --git a/tests/tap/perl/Test/RRA.pm b/tests/tap/perl/Test/RRA.pm new file mode 100644 index 000000000000..6ea65c5701c3 --- /dev/null +++ b/tests/tap/perl/Test/RRA.pm @@ -0,0 +1,324 @@ +# Helper functions for test programs written in Perl. +# +# This module provides a collection of helper functions used by test programs +# written in Perl. This is a general collection of functions that can be used +# by both C packages with Automake and by stand-alone Perl modules. See +# Test::RRA::Automake for additional functions specifically for C Automake +# distributions. +# +# SPDX-License-Identifier: MIT + +package Test::RRA; + +use 5.010; +use base qw(Exporter); +use strict; +use warnings; + +use Carp qw(croak); +use File::Temp; + +# Abort if Test::More was loaded before Test::RRA to be sure that we get the +# benefits of the Test::More probing below. +if ($INC{'Test/More.pm'}) { + croak('Test::More loaded before Test::RRA'); +} + +# Red Hat's base perl package doesn't include Test::More (one has to install +# the perl-core package in addition). Try to detect this and skip any Perl +# tests if Test::More is not present. This relies on Test::RRA being included +# before Test::More. +eval { + require Test::More; + Test::More->import(); +}; +if ($@) { + print "1..0 # SKIP Test::More required for test\n" + or croak('Cannot write to stdout'); + exit 0; +} + +# Declare variables that should be set in BEGIN for robustness. +our (@EXPORT_OK, $VERSION); + +# Set $VERSION and everything export-related in a BEGIN block for robustness +# against circular module loading (not that we load any modules, but +# consistency is good). +BEGIN { + @EXPORT_OK = qw( + is_file_contents skip_unless_author skip_unless_automated use_prereq + ); + + # This version should match the corresponding rra-c-util release, but with + # two digits for the minor version, including a leading zero if necessary, + # so that it will sort properly. + $VERSION = '10.00'; +} + +# Compare a string to the contents of a file, similar to the standard is() +# function, but to show the line-based unified diff between them if they +# differ. +# +# $got - The output that we received +# $expected - The path to the file containing the expected output +# $message - The message to use when reporting the test results +# +# Returns: undef +# Throws: Exception on failure to read or write files or run diff +sub is_file_contents { + my ($got, $expected, $message) = @_; + + # If they're equal, this is simple. + open(my $fh, '<', $expected) or BAIL_OUT("Cannot open $expected: $!\n"); + my $data = do { local $/ = undef; <$fh> }; + close($fh) or BAIL_OUT("Cannot close $expected: $!\n"); + if ($got eq $data) { + is($got, $data, $message); + return; + } + + # Otherwise, we show a diff, but only if we have IPC::System::Simple and + # diff succeeds. Otherwise, we fall back on showing the full expected and + # seen output. + eval { + require IPC::System::Simple; + + my $tmp = File::Temp->new(); + my $tmpname = $tmp->filename; + print {$tmp} $got or BAIL_OUT("Cannot write to $tmpname: $!\n"); + my @command = ('diff', '-u', $expected, $tmpname); + my $diff = IPC::System::Simple::capturex([0 .. 1], @command); + diag($diff); + }; + if ($@) { + diag('Expected:'); + diag($expected); + diag('Seen:'); + diag($data); + } + + # Report failure. + ok(0, $message); + return; +} + +# Skip this test unless author tests are requested. Takes a short description +# of what tests this script would perform, which is used in the skip message. +# Calls plan skip_all, which will terminate the program. +# +# $description - Short description of the tests +# +# Returns: undef +sub skip_unless_author { + my ($description) = @_; + if (!$ENV{AUTHOR_TESTING}) { + plan(skip_all => "$description only run for author"); + } + return; +} + +# Skip this test unless doing automated testing or release testing. This is +# used for tests that should be run by CPAN smoke testing or during releases, +# but not for manual installs by end users. Takes a short description of what +# tests this script would perform, which is used in the skip message. Calls +# plan skip_all, which will terminate the program. +# +# $description - Short description of the tests +# +# Returns: undef +sub skip_unless_automated { + my ($description) = @_; + for my $env (qw(AUTOMATED_TESTING RELEASE_TESTING AUTHOR_TESTING)) { + return if $ENV{$env}; + } + plan(skip_all => "$description normally skipped"); + return; +} + +# Attempt to load a module and skip the test if the module could not be +# loaded. If the module could be loaded, call its import function manually. +# If the module could not be loaded, calls plan skip_all, which will terminate +# the program. +# +# The special logic here is based on Test::More and is required to get the +# imports to happen in the caller's namespace. +# +# $module - Name of the module to load +# @imports - Any arguments to import, possibly including a version +# +# Returns: undef +sub use_prereq { + my ($module, @imports) = @_; + + # If the first import looks like a version, pass it as a bare string. + my $version = q{}; + if (@imports >= 1 && $imports[0] =~ m{ \A \d+ (?: [.][\d_]+ )* \z }xms) { + $version = shift(@imports); + } + + # Get caller information to put imports in the correct package. + my ($package) = caller; + + # Do the import with eval, and try to isolate it from the surrounding + # context as much as possible. Based heavily on Test::More::_eval. + ## no critic (BuiltinFunctions::ProhibitStringyEval) + ## no critic (ValuesAndExpressions::ProhibitImplicitNewlines) + my ($result, $error, $sigdie); + { + local $@ = undef; + local $! = undef; + local $SIG{__DIE__} = undef; + $result = eval qq{ + package $package; + use $module $version \@imports; + 1; + }; + $error = $@; + $sigdie = $SIG{__DIE__} || undef; + } + + # If the use failed for any reason, skip the test. + if (!$result || $error) { + my $name = length($version) > 0 ? "$module $version" : $module; + plan(skip_all => "$name required for test"); + } + + # If the module set $SIG{__DIE__}, we cleared that via local. Restore it. + ## no critic (Variables::RequireLocalizedPunctuationVars) + if (defined($sigdie)) { + $SIG{__DIE__} = $sigdie; + } + return; +} + +1; +__END__ + +=for stopwords +Allbery Allbery's DESC bareword sublicense MERCHANTABILITY NONINFRINGEMENT +rra-c-util CPAN diff + +=head1 NAME + +Test::RRA - Support functions for Perl tests + +=head1 SYNOPSIS + + use Test::RRA + qw(skip_unless_author skip_unless_automated use_prereq); + + # Skip this test unless author tests are requested. + skip_unless_author('Coding style tests'); + + # Skip this test unless doing automated or release testing. + skip_unless_automated('POD syntax tests'); + + # Load modules, skipping the test if they're not available. + use_prereq('Perl6::Slurp', 'slurp'); + use_prereq('Test::Script::Run', '0.04'); + +=head1 DESCRIPTION + +This module collects utility functions that are useful for Perl test scripts. +It assumes Russ Allbery's Perl module layout and test conventions and will +only be useful for other people if they use the same conventions. + +This module B<must> be loaded before Test::More or it will abort during +import. It will skip the test (by printing a skip message to standard output +and exiting with status 0, equivalent to C<plan skip_all>) during import if +Test::More is not available. This allows tests written in Perl using this +module to be skipped if run on a system with Perl but not Test::More, such as +Red Hat systems with the C<perl> package but not the C<perl-core> package +installed. + +=head1 FUNCTIONS + +None of these functions are imported by default. The ones used by a script +should be explicitly imported. + +=over 4 + +=item is_file_contents(GOT, EXPECTED, MESSAGE) + +Check a string against the contents of a file, showing the differences if any +using diff (if IPC::System::Simple and diff are available). GOT is the output +the test received. EXPECTED is the path to a file containing the expected +output (not the output itself). MESSAGE is a message to display alongside the +test results. + +=item skip_unless_author(DESC) + +Checks whether AUTHOR_TESTING is set in the environment and skips the whole +test (by calling C<plan skip_all> from Test::More) if it is not. DESC is a +description of the tests being skipped. A space and C<only run for author> +will be appended to it and used as the skip reason. + +=item skip_unless_automated(DESC) + +Checks whether AUTHOR_TESTING, AUTOMATED_TESTING, or RELEASE_TESTING are set +in the environment and skips the whole test (by calling C<plan skip_all> from +Test::More) if they are not. This should be used by tests that should not run +during end-user installs of the module, but which should run as part of CPAN +smoke testing and release testing. + +DESC is a description of the tests being skipped. A space and C<normally +skipped> will be appended to it and used as the skip reason. + +=item use_prereq(MODULE[, VERSION][, IMPORT ...]) + +Attempts to load MODULE with the given VERSION and import arguments. If this +fails for any reason, the test will be skipped (by calling C<plan skip_all> +from Test::More) with a skip reason saying that MODULE is required for the +test. + +VERSION will be passed to C<use> as a version bareword if it looks like a +version number. The remaining IMPORT arguments will be passed as the value of +an array. + +=back + +=head1 AUTHOR + +Russ Allbery <eagle@eyrie.org> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2016, 2018-2019, 2021 Russ Allbery <eagle@eyrie.org> + +Copyright 2013-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. + +=head1 SEE ALSO + +Test::More(3), Test::RRA::Automake(3), Test::RRA::Config(3) + +This module is maintained in the rra-c-util package. The current version is +available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>. + +The functions to control when tests are run use environment variables defined +by the L<Lancaster +Consensus|https://github.com/Perl-Toolchain-Gang/toolchain-site/blob/master/lancaster-consensus.md>. + +=cut + +# Local Variables: +# copyright-at-end-flag: t +# End: diff --git a/tests/tap/perl/Test/RRA/Automake.pm b/tests/tap/perl/Test/RRA/Automake.pm new file mode 100644 index 000000000000..261feab81e27 --- /dev/null +++ b/tests/tap/perl/Test/RRA/Automake.pm @@ -0,0 +1,487 @@ +# Helper functions for Perl test programs in Automake distributions. +# +# This module provides a collection of helper functions used by test programs +# written in Perl and included in C source distributions that use Automake. +# They embed knowledge of how I lay out my source trees and test suites with +# Autoconf and Automake. They may be usable by others, but doing so will +# require closely following the conventions implemented by the rra-c-util +# utility collection. +# +# All the functions here assume that C_TAP_BUILD and C_TAP_SOURCE are set in +# the environment. This is normally done via the C TAP Harness runtests +# wrapper. +# +# SPDX-License-Identifier: MIT + +package Test::RRA::Automake; + +use 5.010; +use base qw(Exporter); +use strict; +use warnings; + +use Exporter; +use File::Find qw(find); +use File::Spec; +use Test::More; +use Test::RRA::Config qw($LIBRARY_PATH); + +# Used below for use lib calls. +my ($PERL_BLIB_ARCH, $PERL_BLIB_LIB); + +# Determine the path to the build tree of any embedded Perl module package in +# this source package. We do this in a BEGIN block because we're going to use +# the results in a use lib command below. +BEGIN { + $PERL_BLIB_ARCH = File::Spec->catdir(qw(perl blib arch)); + $PERL_BLIB_LIB = File::Spec->catdir(qw(perl blib lib)); + + # If C_TAP_BUILD is set, we can come up with better values. + if (defined($ENV{C_TAP_BUILD})) { + my ($vol, $dirs) = File::Spec->splitpath($ENV{C_TAP_BUILD}, 1); + my @dirs = File::Spec->splitdir($dirs); + pop(@dirs); + $PERL_BLIB_ARCH = File::Spec->catdir(@dirs, qw(perl blib arch)); + $PERL_BLIB_LIB = File::Spec->catdir(@dirs, qw(perl blib lib)); + } +} + +# Prefer the modules built as part of our source package. Otherwise, we may +# not find Perl modules while testing, or find the wrong versions. +use lib $PERL_BLIB_ARCH; +use lib $PERL_BLIB_LIB; + +# Declare variables that should be set in BEGIN for robustness. +our (@EXPORT_OK, $VERSION); + +# Set $VERSION and everything export-related in a BEGIN block for robustness +# against circular module loading (not that we load any modules, but +# consistency is good). +BEGIN { + @EXPORT_OK = qw( + all_files automake_setup perl_dirs test_file_path test_tmpdir + ); + + # This version should match the corresponding rra-c-util release, but with + # two digits for the minor version, including a leading zero if necessary, + # so that it will sort properly. + $VERSION = '10.00'; +} + +# Directories to skip globally when looking for all files, or for directories +# that could contain Perl files. +my @GLOBAL_SKIP = qw( + .git .pc _build autom4te.cache build-aux perl/_build perl/blib +); + +# Additional paths to skip when building a list of all files in the +# distribution. This primarily skips build artifacts that aren't interesting +# to any of the tests. These match any path component. +my @FILES_SKIP = qw( + .deps .dirstamp .libs aclocal.m4 config.h config.h.in config.h.in~ + config.log config.status configure configure~ +); + +# The temporary directory created by test_tmpdir, if any. If this is set, +# attempt to remove the directory stored here on program exit (but ignore +# failure to do so). +my $TMPDIR; + +# Returns a list of all files in the distribution. +# +# Returns: List of files +sub all_files { + my @files; + + # Turn the skip lists into hashes for ease of querying. + my %skip = map { $_ => 1 } @GLOBAL_SKIP; + my %files_skip = map { $_ => 1 } @FILES_SKIP; + + # Wanted function for find. Prune anything matching either of the skip + # lists, or *.lo files, and then add all regular files to the list. + my $wanted = sub { + my $file = $_; + my $path = $File::Find::name; + $path =~ s{ \A [.]/ }{}xms; + if ($skip{$path} || $files_skip{$file} || $file =~ m{ [.]lo\z }xms) { + $File::Find::prune = 1; + return; + } + if (!-d $file) { + push(@files, $path); + } + }; + + # Do the recursive search and return the results. + find($wanted, q{.}); + return @files; +} + +# Perform initial test setup for running a Perl test in an Automake package. +# This verifies that C_TAP_BUILD and C_TAP_SOURCE are set and then changes +# directory to the C_TAP_SOURCE directory by default. Sets LD_LIBRARY_PATH if +# the $LIBRARY_PATH configuration option is set. Calls BAIL_OUT if +# C_TAP_BUILD or C_TAP_SOURCE are missing or if anything else fails. +# +# $args_ref - Reference to a hash of arguments to configure behavior: +# chdir_build - If set to a true value, changes to C_TAP_BUILD instead of +# C_TAP_SOURCE +# +# Returns: undef +sub automake_setup { + my ($args_ref) = @_; + + # Bail if C_TAP_BUILD or C_TAP_SOURCE are not set. + if (!$ENV{C_TAP_BUILD}) { + BAIL_OUT('C_TAP_BUILD not defined (run under runtests)'); + } + if (!$ENV{C_TAP_SOURCE}) { + BAIL_OUT('C_TAP_SOURCE not defined (run under runtests)'); + } + + # C_TAP_BUILD or C_TAP_SOURCE will be the test directory. Change to the + # parent. + my $start; + if ($args_ref->{chdir_build}) { + $start = $ENV{C_TAP_BUILD}; + } else { + $start = $ENV{C_TAP_SOURCE}; + } + my ($vol, $dirs) = File::Spec->splitpath($start, 1); + my @dirs = File::Spec->splitdir($dirs); + pop(@dirs); + + # Simplify relative paths at the end of the directory. + my $ups = 0; + my $i = $#dirs; + while ($i > 2 && $dirs[$i] eq File::Spec->updir) { + $ups++; + $i--; + } + for (1 .. $ups) { + pop(@dirs); + pop(@dirs); + } + my $root = File::Spec->catpath($vol, File::Spec->catdir(@dirs), q{}); + chdir($root) or BAIL_OUT("cannot chdir to $root: $!"); + + # If C_TAP_BUILD is a subdirectory of C_TAP_SOURCE, add it to the global + # ignore list. + my ($buildvol, $builddirs) = File::Spec->splitpath($ENV{C_TAP_BUILD}, 1); + my @builddirs = File::Spec->splitdir($builddirs); + pop(@builddirs); + if ($buildvol eq $vol && @builddirs == @dirs + 1) { + while (@dirs && $builddirs[0] eq $dirs[0]) { + shift(@builddirs); + shift(@dirs); + } + if (@builddirs == 1) { + push(@GLOBAL_SKIP, $builddirs[0]); + } + } + + # Set LD_LIBRARY_PATH if the $LIBRARY_PATH configuration option is set. + ## no critic (Variables::RequireLocalizedPunctuationVars) + if (defined($LIBRARY_PATH)) { + @builddirs = File::Spec->splitdir($builddirs); + pop(@builddirs); + my $libdir = File::Spec->catdir(@builddirs, $LIBRARY_PATH); + my $path = File::Spec->catpath($buildvol, $libdir, q{}); + if (-d "$path/.libs") { + $path .= '/.libs'; + } + if ($ENV{LD_LIBRARY_PATH}) { + $ENV{LD_LIBRARY_PATH} .= ":$path"; + } else { + $ENV{LD_LIBRARY_PATH} = $path; + } + } + return; +} + +# Returns a list of directories that may contain Perl scripts and that should +# be passed to Perl test infrastructure that expects a list of directories to +# recursively check. The list will be all eligible top-level directories in +# the package except for the tests directory, which is broken out to one +# additional level. Calls BAIL_OUT on any problems +# +# $args_ref - Reference to a hash of arguments to configure behavior: +# skip - A reference to an array of directories to skip +# +# Returns: List of directories possibly containing Perl scripts to test +sub perl_dirs { + my ($args_ref) = @_; + + # Add the global skip list. We also ignore the perl directory if it + # exists since, in my packages, it is treated as a Perl module + # distribution and has its own standalone test suite. + my @skip = $args_ref->{skip} ? @{ $args_ref->{skip} } : (); + push(@skip, @GLOBAL_SKIP, 'perl'); + + # Separate directories to skip under tests from top-level directories. + my @skip_tests = grep { m{ \A tests/ }xms } @skip; + @skip = grep { !m{ \A tests }xms } @skip; + for my $skip_dir (@skip_tests) { + $skip_dir =~ s{ \A tests/ }{}xms; + } + + # Convert the skip lists into hashes for convenience. + my %skip = map { $_ => 1 } @skip, 'tests'; + my %skip_tests = map { $_ => 1 } @skip_tests; + + # Build the list of top-level directories to test. + opendir(my $rootdir, q{.}) or BAIL_OUT("cannot open .: $!"); + my @dirs = grep { -d && !$skip{$_} } readdir($rootdir); + closedir($rootdir); + @dirs = File::Spec->no_upwards(@dirs); + + # Add the list of subdirectories of the tests directory. + if (-d 'tests') { + opendir(my $testsdir, q{tests}) or BAIL_OUT("cannot open tests: $!"); + + # Skip if found in %skip_tests or if not a directory. + my $is_skipped = sub { + my ($dir) = @_; + return 1 if $skip_tests{$dir}; + $dir = File::Spec->catdir('tests', $dir); + return -d $dir ? 0 : 1; + }; + + # Build the filtered list of subdirectories of tests. + my @test_dirs = grep { !$is_skipped->($_) } readdir($testsdir); + closedir($testsdir); + @test_dirs = File::Spec->no_upwards(@test_dirs); + + # Add the tests directory to the start of the directory name. + push(@dirs, map { File::Spec->catdir('tests', $_) } @test_dirs); + } + return @dirs; +} + +# Find a configuration file for the test suite. Searches relative to +# C_TAP_BUILD first and then C_TAP_SOURCE and returns whichever is found +# first. Calls BAIL_OUT if the file could not be found. +# +# $file - Partial path to the file +# +# Returns: Full path to the file +sub test_file_path { + my ($file) = @_; + BASE: + for my $base ($ENV{C_TAP_BUILD}, $ENV{C_TAP_SOURCE}) { + next if !defined($base); + if (-e "$base/$file") { + return "$base/$file"; + } + } + BAIL_OUT("cannot find $file"); + return; +} + +# Create a temporary directory for tests to use for transient files and return +# the path to that directory. The directory is automatically removed on +# program exit. The directory permissions use the current umask. Calls +# BAIL_OUT if the directory could not be created. +# +# Returns: Path to a writable temporary directory +sub test_tmpdir { + my $path; + + # If we already figured out what directory to use, reuse the same path. + # Otherwise, create a directory relative to C_TAP_BUILD if set. + if (defined($TMPDIR)) { + $path = $TMPDIR; + } else { + my $base; + if (defined($ENV{C_TAP_BUILD})) { + $base = $ENV{C_TAP_BUILD}; + } else { + $base = File::Spec->curdir; + } + $path = File::Spec->catdir($base, 'tmp'); + } + + # Create the directory if it doesn't exist. + if (!-d $path) { + if (!mkdir($path, 0777)) { + BAIL_OUT("cannot create directory $path: $!"); + } + } + + # Store the directory name for cleanup and return it. + $TMPDIR = $path; + return $path; +} + +# On program exit, remove $TMPDIR if set and if possible. Report errors with +# diag but otherwise ignore them. +END { + if (defined($TMPDIR) && -d $TMPDIR) { + local $! = undef; + if (!rmdir($TMPDIR)) { + diag("cannot remove temporary directory $TMPDIR: $!"); + } + } +} + +1; +__END__ + +=for stopwords +Allbery Automake Automake-aware Automake-based rra-c-util ARGS subdirectories +sublicense MERCHANTABILITY NONINFRINGEMENT umask + +=head1 NAME + +Test::RRA::Automake - Automake-aware support functions for Perl tests + +=head1 SYNOPSIS + + use Test::RRA::Automake qw(automake_setup perl_dirs test_file_path); + automake_setup({ chdir_build => 1 }); + + # Paths to directories that may contain Perl scripts. + my @dirs = perl_dirs({ skip => [qw(lib)] }); + + # Configuration for Kerberos tests. + my $keytab = test_file_path('config/keytab'); + +=head1 DESCRIPTION + +This module collects utility functions that are useful for test scripts +written in Perl and included in a C Automake-based package. They assume the +layout of a package that uses rra-c-util and C TAP Harness for the test +structure. + +Loading this module will also add the directories C<perl/blib/arch> and +C<perl/blib/lib> to the Perl library search path, relative to C_TAP_BUILD if +that environment variable is set. This is harmless for C Automake projects +that don't contain an embedded Perl module, and for those projects that do, +this will allow subsequent C<use> calls to find modules that are built as part +of the package build process. + +The automake_setup() function should be called before calling any other +functions provided by this module. + +=head1 FUNCTIONS + +None of these functions are imported by default. The ones used by a script +should be explicitly imported. On failure, all of these functions call +BAIL_OUT (from Test::More). + +=over 4 + +=item all_files() + +Returns a list of all "interesting" files in the distribution that a test +suite may want to look at. This excludes various products of the build system, +the build directory if it's under the source directory, and a few other +uninteresting directories like F<.git>. The returned paths will be paths +relative to the root of the package. + +=item automake_setup([ARGS]) + +Verifies that the C_TAP_BUILD and C_TAP_SOURCE environment variables are set +and then changes directory to the top of the source tree (which is one +directory up from the C_TAP_SOURCE path, since C_TAP_SOURCE points to the top +of the tests directory). + +If ARGS is given, it should be a reference to a hash of configuration options. +Only one option is supported: C<chdir_build>. If it is set to a true value, +automake_setup() changes directories to the top of the build tree instead. + +=item perl_dirs([ARGS]) + +Returns a list of directories that may contain Perl scripts that should be +tested by test scripts that test all Perl in the source tree (such as syntax +or coding style checks). The paths will be simple directory names relative to +the current directory or two-part directory names under the F<tests> +directory. (Directories under F<tests> are broken out separately since it's +common to want to apply different policies to different subdirectories of +F<tests>.) + +If ARGS is given, it should be a reference to a hash of configuration options. +Only one option is supported: C<skip>, whose value should be a reference to an +array of additional top-level directories or directories starting with +C<tests/> that should be skipped. + +=item test_file_path(FILE) + +Given FILE, which should be a relative path, locates that file relative to the +test directory in either the source or build tree. FILE will be checked for +relative to the environment variable C_TAP_BUILD first, and then relative to +C_TAP_SOURCE. test_file_path() returns the full path to FILE or calls +BAIL_OUT if FILE could not be found. + +=item test_tmpdir() + +Create a temporary directory for tests to use for transient files and return +the path to that directory. The directory is created relative to the +C_TAP_BUILD environment variable, which must be set. Permissions on the +directory are set using the current umask. test_tmpdir() returns the full +path to the temporary directory or calls BAIL_OUT if it could not be created. + +The directory is automatically removed if possible on program exit. Failure +to remove the directory on exit is reported with diag() and otherwise ignored. + +=back + +=head1 ENVIRONMENT + +=over 4 + +=item C_TAP_BUILD + +The root of the tests directory in Automake build directory for this package, +used to find files as documented above. + +=item C_TAP_SOURCE + +The root of the tests directory in the source tree for this package, used to +find files as documented above. + +=back + +=head1 AUTHOR + +Russ Allbery <eagle@eyrie.org> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2014-2015, 2018-2021 Russ Allbery <eagle@eyrie.org> + +Copyright 2013 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. + +=head1 SEE ALSO + +Test::More(3), Test::RRA(3), Test::RRA::Config(3) + +This module is maintained in the rra-c-util package. The current version is +available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>. + +The C TAP Harness test driver and libraries for TAP-based C testing are +available from L<https://www.eyrie.org/~eagle/software/c-tap-harness/>. + +=cut + +# Local Variables: +# copyright-at-end-flag: t +# End: diff --git a/tests/tap/perl/Test/RRA/Config.pm b/tests/tap/perl/Test/RRA/Config.pm new file mode 100644 index 000000000000..77f967e35b52 --- /dev/null +++ b/tests/tap/perl/Test/RRA/Config.pm @@ -0,0 +1,224 @@ +# Configuration for Perl test cases. +# +# In order to reuse the same Perl test cases in multiple packages, I use a +# configuration file to store some package-specific data. This module loads +# that configuration and provides the namespace for the configuration +# settings. +# +# SPDX-License-Identifier: MIT + +package Test::RRA::Config; + +use 5.010; +use base qw(Exporter); +use strict; +use warnings; + +use Test::More; + +# Declare variables that should be set in BEGIN for robustness. +our (@EXPORT_OK, $VERSION); + +# Set $VERSION and everything export-related in a BEGIN block for robustness +# against circular module loading (not that we load any modules, but +# consistency is good). +BEGIN { + @EXPORT_OK = qw( + $COVERAGE_LEVEL @COVERAGE_SKIP_TESTS @CRITIC_IGNORE $LIBRARY_PATH + $MINIMUM_VERSION %MINIMUM_VERSION @MODULE_VERSION_IGNORE + @POD_COVERAGE_EXCLUDE @STRICT_IGNORE @STRICT_PREREQ + ); + + # This version should match the corresponding rra-c-util release, but with + # two digits for the minor version, including a leading zero if necessary, + # so that it will sort properly. + $VERSION = '10.00'; +} + +# If C_TAP_BUILD or C_TAP_SOURCE are set in the environment, look for +# data/perl.conf under those paths for a C Automake package. Otherwise, look +# in t/data/perl.conf for a standalone Perl module or tests/data/perl.conf for +# Perl tests embedded in a larger distribution. Don't use Test::RRA::Automake +# since it may not exist. +our $PATH; +for my $base ($ENV{C_TAP_BUILD}, $ENV{C_TAP_SOURCE}, './t', './tests') { + next if !defined($base); + my $path = "$base/data/perl.conf"; + if (-r $path) { + $PATH = $path; + last; + } +} +if (!defined($PATH)) { + BAIL_OUT('cannot find data/perl.conf'); +} + +# Pre-declare all of our variables and set any defaults. +our $COVERAGE_LEVEL = 100; +our @COVERAGE_SKIP_TESTS; +our @CRITIC_IGNORE; +our $LIBRARY_PATH; +our $MINIMUM_VERSION = '5.010'; +our %MINIMUM_VERSION; +our @MODULE_VERSION_IGNORE; +our @POD_COVERAGE_EXCLUDE; +our @STRICT_IGNORE; +our @STRICT_PREREQ; + +# Load the configuration. +if (!do($PATH)) { + my $error = $@ || $! || 'loading file did not return true'; + BAIL_OUT("cannot load $PATH: $error"); +} + +1; +__END__ + +=for stopwords +Allbery rra-c-util Automake perlcritic .libs namespace subdirectory sublicense +MERCHANTABILITY NONINFRINGEMENT regexes + +=head1 NAME + +Test::RRA::Config - Perl test configuration + +=head1 SYNOPSIS + + use Test::RRA::Config qw($MINIMUM_VERSION); + print "Required Perl version is $MINIMUM_VERSION\n"; + +=head1 DESCRIPTION + +Test::RRA::Config encapsulates per-package configuration for generic Perl test +programs that are shared between multiple packages using the rra-c-util +infrastructure. It handles locating and loading the test configuration file +for both C Automake packages and stand-alone Perl modules. + +Test::RRA::Config looks for a file named F<data/perl.conf> relative to the +root of the test directory. That root is taken from the environment variables +C_TAP_BUILD or C_TAP_SOURCE (in that order) if set, which will be the case for +C Automake packages using C TAP Harness. If neither is set, it expects the +root of the test directory to be a directory named F<t> relative to the +current directory, which will be the case for stand-alone Perl modules. + +The following variables are supported: + +=over 4 + +=item $COVERAGE_LEVEL + +The coverage level achieved by the test suite for Perl test coverage testing +using Test::Strict, as a percentage. The test will fail if test coverage less +than this percentage is achieved. If not given, defaults to 100. + +=item @COVERAGE_SKIP_TESTS + +Directories under F<t> whose tests should be skipped when doing coverage +testing. This can be tests that won't contribute to coverage or tests that +don't run properly under Devel::Cover for some reason (such as ones that use +taint checking). F<docs> and F<style> will always be skipped regardless of +this setting. + +=item @CRITIC_IGNORE + +Additional files or directories to ignore when doing recursive perlcritic +testing. To ignore files that will be installed, the path should start with +F<blib>. + +=item $LIBRARY_PATH + +Add this directory (or a F<.libs> subdirectory) relative to the top of the +source tree to LD_LIBRARY_PATH when checking the syntax of Perl modules. This +may be required to pick up libraries that are used by in-tree Perl modules so +that Perl scripts can pass a syntax check. + +=item $MINIMUM_VERSION + +Default minimum version requirement for included Perl scripts. If not given, +defaults to 5.010. + +=item %MINIMUM_VERSION + +Minimum version exceptions for specific directories. The keys should be +minimum versions of Perl to enforce. The value for each key should be a +reference to an array of either top-level directory names or directory names +starting with F<tests/>. All files in those directories will have that +minimum Perl version constraint imposed instead of $MINIMUM_VERSION. + +=item @MODULE_VERSION_IGNORE + +File names to ignore when checking that all modules in a distribution have the +same version. Sometimes, some specific modules need separate, special version +handling, such as modules defining database schemata for DBIx::Class, and +can't follow the version of the larger package. + +=item @POD_COVERAGE_EXCLUDE + +Regexes that match method names that should be excluded from POD coverage +testing. Normally, all methods have to be documented in the POD for a Perl +module, but methods matching any of these regexes will be considered private +and won't require documentation. + +=item @STRICT_IGNORE + +Additional directories to ignore when doing recursive Test::Strict testing for +C<use strict> and C<use warnings>. The contents of this directory must be +either top-level directory names or directory names starting with F<tests/>. + +=item @STRICT_PREREQ + +A list of Perl modules that have to be available in order to do meaningful +Test::Strict testing. If any of the modules cannot be loaded via C<use>, +Test::Strict checking will be skipped. There is currently no way to require +specific versions of the modules. + +=back + +No variables are exported by default, but the variables can be imported into +the local namespace to avoid long variable names. + +=head1 AUTHOR + +Russ Allbery <eagle@eyrie.org> + +=head1 COPYRIGHT AND LICENSE + +Copyright 2015-2016, 2019, 2021 Russ Allbery <eagle@eyrie.org> + +Copyright 2013-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. + +=head1 SEE ALSO + +perlcritic(1), Test::MinimumVersion(3), Test::RRA(3), Test::RRA::Automake(3), +Test::Strict(3) + +This module is maintained in the rra-c-util package. The current version is +available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>. + +The C TAP Harness test driver and libraries for TAP-based C testing are +available from L<https://www.eyrie.org/~eagle/software/c-tap-harness/>. + +=cut + +# Local Variables: +# copyright-at-end-flag: t +# End: diff --git a/tests/tap/process.c b/tests/tap/process.c new file mode 100644 index 000000000000..2f797f8f7567 --- /dev/null +++ b/tests/tap/process.c @@ -0,0 +1,532 @@ +/* + * Utility functions for tests that use subprocesses. + * + * Provides utility functions for subprocess manipulation. Specifically, + * provides a function, run_setup, which runs a command and bails if it fails, + * using its error message as the bail output, and is_function_output, which + * runs a function in a subprocess and checks its output and exit status + * against expected values. + * + * Requires an Autoconf probe for sys/select.h and a replacement for a missing + * mkstemp. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2002, 2004-2005, 2013, 2016-2017 Russ Allbery <eagle@eyrie.org> + * Copyright 2009-2011, 2013-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 <config.h> +#include <portable/system.h> + +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#ifdef HAVE_SYS_SELECT_H +# include <sys/select.h> +#endif +#include <sys/stat.h> +#ifdef HAVE_SYS_TIME_H +# include <sys/time.h> +#endif +#include <sys/wait.h> +#include <time.h> + +#include <tests/tap/basic.h> +#include <tests/tap/process.h> +#include <tests/tap/string.h> + +/* May be defined by the build system. */ +#ifndef PATH_FAKEROOT +# define PATH_FAKEROOT "" +#endif + +/* How long to wait for the process to start in seconds. */ +#define PROCESS_WAIT 10 + +/* + * Used to store information about a background process. This contains + * everything required to stop the process and clean up after it. + */ +struct process { + pid_t pid; /* PID of child process */ + char *pidfile; /* PID file to delete on process stop */ + char *tmpdir; /* Temporary directory for log file */ + char *logfile; /* Log file of process output */ + bool is_child; /* Whether we can waitpid for process */ + struct process *next; /* Next process in global list */ +}; + +/* + * Global list of started processes, which will be cleaned up automatically on + * program exit if they haven't been explicitly stopped with process_stop + * prior to that point. + */ +static struct process *processes = NULL; + + +/* + * Given a function, an expected exit status, and expected output, runs that + * function in a subprocess, capturing stdout and stderr via a pipe, and + * returns the function output in newly allocated memory. Also captures the + * process exit status. + */ +static void +run_child_function(test_function_type function, void *data, int *status, + char **output) +{ + int fds[2]; + pid_t child; + char *buf; + ssize_t count, ret, buflen; + int rval; + + /* Flush stdout before we start to avoid odd forking issues. */ + fflush(stdout); + + /* Set up the pipe and call the function, collecting its output. */ + if (pipe(fds) == -1) + sysbail("can't create pipe"); + child = fork(); + if (child == (pid_t) -1) { + sysbail("can't fork"); + } else if (child == 0) { + /* In child. Set up our stdout and stderr. */ + close(fds[0]); + if (dup2(fds[1], 1) == -1) + _exit(255); + if (dup2(fds[1], 2) == -1) + _exit(255); + + /* Now, run the function and exit successfully if it returns. */ + (*function)(data); + fflush(stdout); + _exit(0); + } else { + /* + * In the parent; close the extra file descriptor, read the output if + * any, and then collect the exit status. + */ + close(fds[1]); + buflen = BUFSIZ; + buf = bmalloc(buflen); + count = 0; + do { + ret = read(fds[0], buf + count, buflen - count - 1); + if (SSIZE_MAX - count <= ret) + bail("maximum output size exceeded in run_child_function"); + if (ret > 0) + count += ret; + if (count >= buflen - 1) { + buflen += BUFSIZ; + buf = brealloc(buf, buflen); + } + } while (ret > 0); + buf[count] = '\0'; + if (waitpid(child, &rval, 0) == (pid_t) -1) + sysbail("waitpid failed"); + close(fds[0]); + } + + /* Store the output and return. */ + *status = rval; + *output = buf; +} + + +/* + * Given a function, data to pass to that function, an expected exit status, + * and expected output, runs that function in a subprocess, capturing stdout + * and stderr via a pipe, and compare the combination of stdout and stderr + * with the expected output and the exit status with the expected status. + * Expects the function to always exit (not die from a signal). + */ +void +is_function_output(test_function_type function, void *data, int status, + const char *output, const char *format, ...) +{ + char *buf, *msg; + int rval; + va_list args; + + run_child_function(function, data, &rval, &buf); + + /* Now, check the results against what we expected. */ + va_start(args, format); + bvasprintf(&msg, format, args); + va_end(args); + ok(WIFEXITED(rval), "%s (exited)", msg); + is_int(status, WEXITSTATUS(rval), "%s (status)", msg); + is_string(output, buf, "%s (output)", msg); + free(buf); + free(msg); +} + + +/* + * A helper function for run_setup. This is a function to run an external + * command, suitable for passing into run_child_function. The expected + * argument must be an argv array, with argv[0] being the command to run. + */ +static void +exec_command(void *data) +{ + char *const *argv = data; + + execvp(argv[0], argv); +} + + +/* + * Given a command expressed as an argv struct, with argv[0] the name or path + * to the command, run that command. If it exits with a non-zero status, use + * the part of its output up to the first newline as the error message when + * calling bail. + */ +void +run_setup(const char *const argv[]) +{ + char *output, *p; + int status; + + run_child_function(exec_command, (void *) argv, &status, &output); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + p = strchr(output, '\n'); + if (p != NULL) + *p = '\0'; + if (output[0] != '\0') + bail("%s", output); + else + bail("setup command failed with no output"); + } + free(output); +} + + +/* + * Free the resources associated with tracking a process, without doing + * anything to the process. This is kept separate so that we can free + * resources during shutdown in a non-primary process. + */ +static void +process_free(struct process *process) +{ + struct process **prev; + + /* Do nothing if called with a NULL argument. */ + if (process == NULL) + return; + + /* Remove the process from the global list. */ + prev = &processes; + while (*prev != NULL && *prev != process) + prev = &(*prev)->next; + if (*prev == process) + *prev = process->next; + + /* Free resources. */ + free(process->pidfile); + free(process->logfile); + test_tmpdir_free(process->tmpdir); + free(process); +} + + +/* + * Kill a process and wait for it to exit. Returns the status of the process. + * Calls bail on a system failure or a failure of the process to exit. + * + * We are quite aggressive with error reporting here because child processes + * that don't exit or that don't exist often indicate some form of test + * failure. + */ +static int +process_kill(struct process *process) +{ + int result, i; + int status = -1; + struct timeval tv; + unsigned long pid = process->pid; + + /* If the process is not a child, just kill it and hope. */ + if (!process->is_child) { + if (kill(process->pid, SIGTERM) < 0 && errno != ESRCH) + sysbail("cannot send SIGTERM to process %lu", pid); + return 0; + } + + /* Check if the process has already exited. */ + result = waitpid(process->pid, &status, WNOHANG); + if (result < 0) + sysbail("cannot wait for child process %lu", pid); + else if (result > 0) + return status; + + /* + * Kill the process and wait for it to exit. I don't want to go to the + * work of setting up a SIGCHLD handler or a full event loop here, so we + * effectively poll every tenth of a second for process exit (and + * hopefully faster when it does since the SIGCHLD may interrupt our + * select, although we're racing with it. + */ + if (kill(process->pid, SIGTERM) < 0 && errno != ESRCH) + sysbail("cannot send SIGTERM to child process %lu", pid); + for (i = 0; i < PROCESS_WAIT * 10; i++) { + tv.tv_sec = 0; + tv.tv_usec = 100000; + select(0, NULL, NULL, NULL, &tv); + result = waitpid(process->pid, &status, WNOHANG); + if (result < 0) + sysbail("cannot wait for child process %lu", pid); + else if (result > 0) + return status; + } + + /* The process still hasn't exited. Bail. */ + bail("child process %lu did not exit on SIGTERM", pid); + + /* Not reached, but some compilers may get confused. */ + return status; +} + + +/* + * Stop a particular process given its process struct. This kills the + * process, waits for it to exit if possible (giving it at most five seconds), + * and then removes it from the global processes struct so that it isn't + * stopped again during global shutdown. + */ +void +process_stop(struct process *process) +{ + int status; + unsigned long pid = process->pid; + + /* Stop the process. */ + status = process_kill(process); + + /* Call diag to flush logs as well as provide exit status. */ + if (process->is_child) + diag("stopped process %lu (exit status %d)", pid, status); + else + diag("stopped process %lu", pid); + + /* Remove the log and PID file. */ + diag_file_remove(process->logfile); + unlink(process->pidfile); + unlink(process->logfile); + + /* Free resources. */ + process_free(process); +} + + +/* + * Stop all running processes. This is called as a cleanup handler during + * process shutdown. The first argument, which says whether the test was + * successful, is ignored, since the same actions should be performed + * regardless. The second argument says whether this is the primary process, + * in which case we do the full shutdown. Otherwise, we only free resources + * but don't stop the process. + */ +static void +process_stop_all(int success UNUSED, int primary) +{ + while (processes != NULL) { + if (primary) + process_stop(processes); + else + process_free(processes); + } +} + + +/* + * Read the PID of a process from a file. This is necessary when running + * under fakeroot to get the actual PID of the remctld process. + */ +static pid_t +read_pidfile(const char *path) +{ + FILE *file; + char buffer[BUFSIZ]; + long pid; + + file = fopen(path, "r"); + if (file == NULL) + sysbail("cannot open %s", path); + if (fgets(buffer, sizeof(buffer), file) == NULL) + sysbail("cannot read from %s", path); + fclose(file); + pid = strtol(buffer, NULL, 10); + if (pid <= 0) + bail("cannot read PID from %s", path); + return (pid_t) pid; +} + + +/* + * Start a process and return its status information. The status information + * is also stored in the global processes linked list so that it can be + * stopped automatically on program exit. + * + * The boolean argument says whether to start the process under fakeroot. If + * true, PATH_FAKEROOT must be defined, generally by Autoconf. If it's not + * found, call skip_all. + * + * This is a helper function for process_start and process_start_fakeroot. + */ +static struct process * +process_start_internal(const char *const argv[], const char *pidfile, + bool fakeroot) +{ + size_t i; + int log_fd; + const char *name; + struct timeval tv; + struct process *process; + const char **fakeroot_argv = NULL; + const char *path_fakeroot = PATH_FAKEROOT; + + /* Check prerequisites. */ + if (fakeroot && path_fakeroot[0] == '\0') + skip_all("fakeroot not found"); + + /* Create the process struct and log file. */ + process = bcalloc(1, sizeof(struct process)); + process->pidfile = bstrdup(pidfile); + process->tmpdir = test_tmpdir(); + name = strrchr(argv[0], '/'); + if (name != NULL) + name++; + else + name = argv[0]; + basprintf(&process->logfile, "%s/%s.log.XXXXXX", process->tmpdir, name); + log_fd = mkstemp(process->logfile); + if (log_fd < 0) + sysbail("cannot create log file for %s", argv[0]); + + /* If using fakeroot, rewrite argv accordingly. */ + if (fakeroot) { + for (i = 0; argv[i] != NULL; i++) + ; + fakeroot_argv = bcalloc(2 + i + 1, sizeof(const char *)); + fakeroot_argv[0] = path_fakeroot; + fakeroot_argv[1] = "--"; + for (i = 0; argv[i] != NULL; i++) + fakeroot_argv[i + 2] = argv[i]; + fakeroot_argv[i + 2] = NULL; + argv = fakeroot_argv; + } + + /* + * Fork off the child process, redirect its standard output and standard + * error to the log file, and then exec the program. + */ + process->pid = fork(); + if (process->pid < 0) + sysbail("fork failed"); + else if (process->pid == 0) { + if (dup2(log_fd, STDOUT_FILENO) < 0) + sysbail("cannot redirect standard output"); + if (dup2(log_fd, STDERR_FILENO) < 0) + sysbail("cannot redirect standard error"); + close(log_fd); + if (execv(argv[0], (char *const *) argv) < 0) + sysbail("exec of %s failed", argv[0]); + } + close(log_fd); + free(fakeroot_argv); + + /* + * In the parent. Wait for the child to start by watching for the PID + * file to appear in 100ms intervals. + */ + for (i = 0; i < PROCESS_WAIT * 10 && access(pidfile, F_OK) != 0; i++) { + tv.tv_sec = 0; + tv.tv_usec = 100000; + select(0, NULL, NULL, NULL, &tv); + } + + /* + * If the PID file still hasn't appeared after ten seconds, attempt to + * kill the process and then bail. + */ + if (access(pidfile, F_OK) != 0) { + kill(process->pid, SIGTERM); + alarm(5); + waitpid(process->pid, NULL, 0); + alarm(0); + bail("cannot start %s", argv[0]); + } + + /* + * Read the PID back from the PID file. This usually isn't necessary for + * non-forking daemons, but always doing this makes this function general, + * and it's required when running under fakeroot. + */ + if (fakeroot) + process->pid = read_pidfile(pidfile); + process->is_child = !fakeroot; + + /* Register the log file as a source of diag messages. */ + diag_file_add(process->logfile); + + /* + * Add the process to our global list and set our cleanup handler if this + * is the first process we started. + */ + if (processes == NULL) + test_cleanup_register(process_stop_all); + process->next = processes; + processes = process; + + /* All done. */ + return process; +} + + +/* + * Start a process and return the opaque process struct. The process must + * create pidfile with its PID when startup is complete. + */ +struct process * +process_start(const char *const argv[], const char *pidfile) +{ + return process_start_internal(argv, pidfile, false); +} + + +/* + * Start a process under fakeroot and return the opaque process struct. If + * fakeroot is not available, calls skip_all. The process must create pidfile + * with its PID when startup is complete. + */ +struct process * +process_start_fakeroot(const char *const argv[], const char *pidfile) +{ + return process_start_internal(argv, pidfile, true); +} diff --git a/tests/tap/process.h b/tests/tap/process.h new file mode 100644 index 000000000000..4210c209ed0b --- /dev/null +++ b/tests/tap/process.h @@ -0,0 +1,95 @@ +/* + * Utility functions for tests that use subprocesses. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Written by Russ Allbery <eagle@eyrie.org> + * Copyright 2009-2010, 2013 + * 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 + */ + +#ifndef TAP_PROCESS_H +#define TAP_PROCESS_H 1 + +#include <config.h> +#include <tests/tap/macros.h> + +/* Opaque data type for process_start and friends. */ +struct process; + +BEGIN_DECLS + +/* + * Run a function in a subprocess and check the exit status and expected + * output (stdout and stderr combined) against the provided values. Expects + * the function to always exit (not die from a signal). data is optional data + * that's passed into the function as its only argument. + * + * This reports as three separate tests: whether the function exited rather + * than was killed, whether the exit status was correct, and whether the + * output was correct. + */ +typedef void (*test_function_type)(void *); +void is_function_output(test_function_type, void *data, int status, + const char *output, const char *format, ...) + __attribute__((__format__(printf, 5, 6), __nonnull__(1))); + +/* + * Run a setup program. Takes the program to run and its arguments as an argv + * vector, where argv[0] must be either the full path to the program or the + * program name if the PATH should be searched. If the program does not exit + * successfully, call bail, with the error message being the output from the + * program. + */ +void run_setup(const char *const argv[]) __attribute__((__nonnull__)); + +/* + * process_start starts a process in the background, returning an opaque data + * struct that can be used to stop the process later. The standard output and + * standard error of the process will be sent to a log file registered with + * diag_file_add, so its output will be properly interleaved with the test + * case output. + * + * The process should create a PID file in the path given as the second + * argument when it's finished initialization. + * + * process_start_fakeroot is the same but starts the process under fakeroot. + * PATH_FAKEROOT must be defined (generally by Autoconf). If fakeroot is not + * found, process_start_fakeroot will call skip_all, so be sure to call this + * function before plan. + * + * process_stop can be called to explicitly stop the process. If it isn't + * called by the test program, it will be called automatically when the + * program exits. + */ +struct process *process_start(const char *const argv[], const char *pidfile) + __attribute__((__nonnull__)); +struct process *process_start_fakeroot(const char *const argv[], + const char *pidfile) + __attribute__((__nonnull__)); +void process_stop(struct process *); + +END_DECLS + +#endif /* TAP_PROCESS_H */ diff --git a/tests/tap/string.c b/tests/tap/string.c new file mode 100644 index 000000000000..71cf571e6f03 --- /dev/null +++ b/tests/tap/string.c @@ -0,0 +1,67 @@ +/* + * String utilities for the TAP protocol. + * + * Additional string utilities that can't be included with C TAP Harness + * because they rely on additional portability code from rra-c-util. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Copyright 2011-2012 Russ Allbery <eagle@eyrie.org> + * + * 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 <config.h> +#include <portable/system.h> + +#include <tests/tap/basic.h> +#include <tests/tap/string.h> + + +/* + * vsprintf into a newly allocated string, reporting a fatal error with bail + * on failure. + */ +void +bvasprintf(char **strp, const char *fmt, va_list args) +{ + int status; + + status = vasprintf(strp, fmt, args); + if (status < 0) + sysbail("failed to allocate memory for vasprintf"); +} + + +/* + * sprintf into a newly allocated string, reporting a fatal error with bail on + * failure. + */ +void +basprintf(char **strp, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + bvasprintf(strp, fmt, args); + va_end(args); +} diff --git a/tests/tap/string.h b/tests/tap/string.h new file mode 100644 index 000000000000..651c38a26f06 --- /dev/null +++ b/tests/tap/string.h @@ -0,0 +1,51 @@ +/* + * String utilities for the TAP protocol. + * + * Additional string utilities that can't be included with C TAP Harness + * because they rely on additional portability code from rra-c-util. + * + * The canonical version of this file is maintained in the rra-c-util package, + * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. + * + * Copyright 2011-2012 Russ Allbery <eagle@eyrie.org> + * + * 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 + */ + +#ifndef TAP_STRING_H +#define TAP_STRING_H 1 + +#include <config.h> +#include <tests/tap/macros.h> + +#include <stdarg.h> /* va_list */ + +BEGIN_DECLS + +/* sprintf into an allocated string, calling bail on any failure. */ +void basprintf(char **, const char *, ...) + __attribute__((__nonnull__, __format__(printf, 2, 3))); +void bvasprintf(char **, const char *, va_list) + __attribute__((__nonnull__, __format__(printf, 2, 0))); + +END_DECLS + +#endif /* !TAP_STRING_H */ diff --git a/tests/valgrind/logs-t b/tests/valgrind/logs-t new file mode 100755 index 000000000000..5444d00a5730 --- /dev/null +++ b/tests/valgrind/logs-t @@ -0,0 +1,83 @@ +#!/usr/bin/perl +# +# Check for errors in valgrind logs. +# +# The canonical version of this file is maintained in the rra-c-util package, +# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>. +# +# Copyright 2018-2019, 2021 Russ Allbery <eagle@eyrie.org> +# +# 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 + +use 5.010; +use strict; +use warnings; + +use lib "$ENV{C_TAP_SOURCE}/tap/perl"; + +use Test::RRA; +use Test::RRA::Automake qw(automake_setup); + +use File::Spec; +use Test::More; + +# Skip this test if C_TAP_VALGRIND was not set. +if (!exists $ENV{C_TAP_VALGRIND}) { + plan skip_all => 'Not testing under valgrind'; +} + +# Set up Automake testing. +automake_setup({ chdir_build => 1 }); + +# Gather the list of valgrind logs (and skip this test if there are none). +opendir(my $logdir, File::Spec->catfile('tests', 'tmp', 'valgrind')) + or plan skip_all => 'No valgrind logs in tests/tmp/valgrind'; +my @logs = grep { m{ \A log [.] }xms } readdir $logdir; +closedir($logdir) or BAIL_OUT("cannot close directory: $!"); + +# Check each log file. +plan tests => scalar(@logs); +for my $file (@logs) { + my $path = File::Spec->catfile('tests', 'tmp', 'valgrind', $file); + open(my $log, '<', $path) or BAIL_OUT("cannot open $path: $!"); + my $okay = 1; + my @log; + while (defined(my $line = <$log>)) { + push(@log, $line); + if ($line =~ m{ ERROR [ ] SUMMARY: [ ] (\d+) [ ] errors }xms) { + $okay = ($1 == 0); + } + } + close($log) or BAIL_OUT("cannot close $path: $!"); + if ($okay) { + unlink($path); + } else { + for my $line (@log) { + print '# ', $line + or BAIL_OUT("cannot print to standard output: $!"); + } + } + ok($okay, $path); +} + +# Remove tests/tmp/valgrind if it's now empty. +rmdir(File::Spec->catfile('tests', 'tmp', 'valgrind')); +rmdir(File::Spec->catfile('tests', 'tmp')); |