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 | |
pam-krb5: Import/add pam-krb5 from eyeire.orgvendor/pam-krb5/4.11vendor/pam-krb5
From https://www.eyrie.org/~eagle/software/pam-krb5/:
pam-krb5 provides a Kerberos PAM module that supports authentication,
user ticket cache handling, simple authorization (via .k5login or
checking Kerberos principals against local usernames), and password
changing. It can be configured through either options in the PAM
configuration itself or through entries in the system krb5.conf file,
and it tries to work around PAM implementation flaws in commonly-used
PAM-enabled applications such as OpenSSH and xdm. It supports both
PKINIT and FAST to the extent that the underlying Kerberos libraries
support these features.
The reason for this import is to provide an MIT KRB5 compatible
pam_krb5 PAM module. The existing pam_krb5 in FreeBS only works
with Heimdal.
Sponsored by: The FreeBSD Foundation
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')); |
