summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuss Allbery <eagle@eyrie.org>2015-05-24 14:36:46 -0700
committerRuss Allbery <eagle@eyrie.org>2015-05-24 14:36:46 -0700
commit6f6cf2e1f31a86f59877b33ce09a09d506db1844 (patch)
tree6b1ed8071b574c547860cf874e049e2ded5bafba
parent6031a98194cdb472ed11a7eb10f53be5a4d8273f (diff)
Rewrite the module/basic test to use PAM scripts
Import the rest of PAM scripted testing from rra-c-util, add some additional infrastructure required to use it for this module, and convert the basic test to use the new mechanism. This still uses the hack to simplify input logging. I'll back out of that as soon as the entire test suite has been converted.
-rw-r--r--Makefile.am13
-rw-r--r--TODO6
-rw-r--r--tests/data/scripts/basic/establish13
-rw-r--r--tests/data/scripts/basic/establish-debug21
-rw-r--r--tests/data/scripts/basic/no-ticket16
-rw-r--r--tests/data/scripts/basic/no-ticket-debug33
-rw-r--r--tests/data/scripts/basic/noop18
-rw-r--r--tests/data/scripts/basic/noop-debug34
-rw-r--r--tests/data/scripts/basic/open-session12
-rw-r--r--tests/data/scripts/basic/open-session-debug20
-rw-r--r--tests/data/scripts/basic/refresh11
-rw-r--r--tests/data/scripts/basic/refresh-debug16
-rw-r--r--tests/data/scripts/basic/reinit11
-rw-r--r--tests/data/scripts/basic/reinit-debug16
-rw-r--r--tests/data/scripts/basic/unknown24
-rw-r--r--tests/data/scripts/basic/unknown-debug37
-rw-r--r--tests/fakepam/config.c679
-rw-r--r--tests/fakepam/general.c9
-rw-r--r--tests/fakepam/script.c408
-rw-r--r--tests/fakepam/script.h79
-rw-r--r--tests/fakepam/stubs.c32
-rw-r--r--tests/module/basic-t.c261
22 files changed, 1556 insertions, 213 deletions
diff --git a/Makefile.am b/Makefile.am
index 7e7c160..7f8ac57 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -12,8 +12,9 @@ EXTRA_DIST = .gitignore LICENSE autogen examples/debian/common-account \
examples/redhat/system-auth examples/solaris/pam.conf \
pam_afs_session.map pam_afs_session.pod pam_afs_session.sym \
tests/README tests/TESTS tests/data/fake-aklog tests/data/krb5.conf \
- tests/docs/pod-spelling-t tests/docs/pod-t tests/fakepam/README \
- tests/kafs/basic-t tests/module/full-t tests/tap/libtap.sh
+ tests/data/scripts tests/docs/pod-spelling-t tests/docs/pod-t \
+ tests/fakepam/README tests/kafs/basic-t tests/module/full-t \
+ tests/tap/libtap.sh
# The following library order matters for annoying reasons. libafsauthent
# contains its own com_err implementation, which we do not want to pick up.
@@ -118,9 +119,11 @@ tests_runtests_CPPFLAGS = -DSOURCE='"$(abs_top_srcdir)/tests"' \
-DBUILD='"$(abs_top_builddir)/tests"'
check_LIBRARIES = tests/fakepam/libfakepam.a tests/module/libfakekafs.a \
tests/tap/libtap.a
-tests_fakepam_libfakepam_a_SOURCES = tests/fakepam/data.c \
- tests/fakepam/general.c tests/fakepam/internal.h \
- tests/fakepam/logging.c tests/fakepam/pam.h
+tests_fakepam_libfakepam_a_SOURCES = tests/fakepam/config.c \
+ tests/fakepam/data.c tests/fakepam/general.c \
+ tests/fakepam/internal.h tests/fakepam/logging.c \
+ tests/fakepam/pam.h tests/fakepam/script.c tests/fakepam/script.h \
+ tests/fakepam/stubs.c
tests_module_libfakekafs_a_SOURCES = tests/module/fakekafs.c
tests_tap_libtap_a_CPPFLAGS = -I$(abs_top_srcdir)/tests
tests_tap_libtap_a_SOURCES = tests/tap/basic.c tests/tap/basic.h \
diff --git a/TODO b/TODO
index 06af0da..0ac1397 100644
--- a/TODO
+++ b/TODO
@@ -8,3 +8,9 @@ Portability:
configuration from krb5.conf on Mac OS X Lion. Find a way to link
directly to the underlying Heimdal libraries so that the module can
call the real krb5_appdefault_* functions.
+
+Testing:
+
+ * Add support for setting an environment variable to the fakepam testing
+ library and use that to test behavior when KRB5CCNAME is only set in
+ the PAM environment, not the general environment.
diff --git a/tests/data/scripts/basic/establish b/tests/data/scripts/basic/establish
new file mode 100644
index 0000000..cc94a85
--- /dev/null
+++ b/tests/data/scripts/basic/establish
@@ -0,0 +1,13 @@
+# Test pam_setcred credential establishment. -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0
+ session = program=%0
+
+[run]
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
diff --git a/tests/data/scripts/basic/establish-debug b/tests/data/scripts/basic/establish-debug
new file mode 100644
index 0000000..f9183c7
--- /dev/null
+++ b/tests/data/scripts/basic/establish-debug
@@ -0,0 +1,21 @@
+# Test pam_setcred credential establishment (debug). -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0 debug
+ session = program=%0 debug
+
+[run]
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_setcred: entry (0x2)
+ DEBUG running %0 as UID %1
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (0x2)
+ DEBUG skipping, apparently already ran
+ DEBUG pam_sm_setcred: exit (success)
diff --git a/tests/data/scripts/basic/no-ticket b/tests/data/scripts/basic/no-ticket
new file mode 100644
index 0000000..0dedc0c
--- /dev/null
+++ b/tests/data/scripts/basic/no-ticket
@@ -0,0 +1,16 @@
+# Test behavior without a ticket. -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0
+ session = program=%0
+
+[run]
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
diff --git a/tests/data/scripts/basic/no-ticket-debug b/tests/data/scripts/basic/no-ticket-debug
new file mode 100644
index 0000000..75df798
--- /dev/null
+++ b/tests/data/scripts/basic/no-ticket-debug
@@ -0,0 +1,33 @@
+# Test behavior without a ticket (debug). -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0 debug
+ session = program=%0 debug
+
+[run]
+ 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 pam_sm_setcred: entry (0x2)
+ DEBUG skipping tokens, no Kerberos ticket cache
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (0x10)
+ DEBUG skipping tokens, no Kerberos ticket cache
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (0x8)
+ DEBUG skipping tokens, no Kerberos ticket cache
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_open_session: entry (0x0)
+ DEBUG skipping tokens, no Kerberos ticket cache
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry (0x0)
+ DEBUG skipping, no open session
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/basic/noop b/tests/data/scripts/basic/noop
new file mode 100644
index 0000000..2a367c3
--- /dev/null
+++ b/tests/data/scripts/basic/noop
@@ -0,0 +1,18 @@
+# Test authenticate and session no-op behavior. -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = nopag notokens
+ session = nopag notokens
+
+[run]
+ authenticate = PAM_SUCCESS
+ 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_IGNORE
diff --git a/tests/data/scripts/basic/noop-debug b/tests/data/scripts/basic/noop-debug
new file mode 100644
index 0000000..e103b53
--- /dev/null
+++ b/tests/data/scripts/basic/noop-debug
@@ -0,0 +1,34 @@
+# Test authenticate and session no-op behavior (debug). -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = nopag notokens debug
+ session = nopag notokens debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ 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_IGNORE
+
+[output]
+ DEBUG pam_sm_setcred: entry (0x4)
+ DEBUG skipping as configured
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (0x2)
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (0x10)
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (0x8)
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_open_session: entry (0x0)
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry (0x0)
+ DEBUG skipping as configured
+ DEBUG pam_sm_close_session: exit (ignore)
diff --git a/tests/data/scripts/basic/open-session b/tests/data/scripts/basic/open-session
new file mode 100644
index 0000000..31e76f0
--- /dev/null
+++ b/tests/data/scripts/basic/open-session
@@ -0,0 +1,12 @@
+# Test pam_open_session. -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ session = program=%0
+
+[run]
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
diff --git a/tests/data/scripts/basic/open-session-debug b/tests/data/scripts/basic/open-session-debug
new file mode 100644
index 0000000..1eda6b8
--- /dev/null
+++ b/tests/data/scripts/basic/open-session-debug
@@ -0,0 +1,20 @@
+# Test pam_open_session (debug). -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ session = program=%0 debug
+
+[run]
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_open_session: entry (0x0)
+ DEBUG running %0 as UID %1
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry (0x0)
+ DEBUG destroying tokens
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/basic/refresh b/tests/data/scripts/basic/refresh
new file mode 100644
index 0000000..c897528
--- /dev/null
+++ b/tests/data/scripts/basic/refresh
@@ -0,0 +1,11 @@
+# Test pam_setcred credential refresh. -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0
+
+[run]
+ setcred(REFRESH_CRED) = PAM_SUCCESS
diff --git a/tests/data/scripts/basic/refresh-debug b/tests/data/scripts/basic/refresh-debug
new file mode 100644
index 0000000..b2f0859
--- /dev/null
+++ b/tests/data/scripts/basic/refresh-debug
@@ -0,0 +1,16 @@
+# Test pam_setcred credential refresh (debug). -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0 debug
+
+[run]
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_setcred: entry (0x10)
+ DEBUG running %0 as UID %1
+ DEBUG pam_sm_setcred: exit (success)
diff --git a/tests/data/scripts/basic/reinit b/tests/data/scripts/basic/reinit
new file mode 100644
index 0000000..eccd0ca
--- /dev/null
+++ b/tests/data/scripts/basic/reinit
@@ -0,0 +1,11 @@
+# Test pam_setcred credential reinitialization. -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0
+
+[run]
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
diff --git a/tests/data/scripts/basic/reinit-debug b/tests/data/scripts/basic/reinit-debug
new file mode 100644
index 0000000..3d91c09
--- /dev/null
+++ b/tests/data/scripts/basic/reinit-debug
@@ -0,0 +1,16 @@
+# Test pam_setcred credential reinitialization (debug). -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0 debug
+
+[run]
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_setcred: entry (0x8)
+ DEBUG running %0 as UID %1
+ DEBUG pam_sm_setcred: exit (success)
diff --git a/tests/data/scripts/basic/unknown b/tests/data/scripts/basic/unknown
new file mode 100644
index 0000000..f2f4a2c
--- /dev/null
+++ b/tests/data/scripts/basic/unknown
@@ -0,0 +1,24 @@
+# Test behavior with unknown user. -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0
+ session = program=%0
+
+[run]
+ authenticate = PAM_SUCCESS
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_USER_UNKNOWN
+ setcred(REFRESH_CRED) = PAM_USER_UNKNOWN
+ setcred(REINITIALIZE_CRED) = PAM_USER_UNKNOWN
+ open_session = PAM_SESSION_ERR
+ close_session = PAM_SUCCESS
+
+[output]
+ ERR cannot find UID for pam-afs-session-unknown-user: %1
+ ERR cannot find UID for pam-afs-session-unknown-user: %1
+ ERR cannot find UID for pam-afs-session-unknown-user: %1
+ ERR cannot find UID for pam-afs-session-unknown-user: %1
diff --git a/tests/data/scripts/basic/unknown-debug b/tests/data/scripts/basic/unknown-debug
new file mode 100644
index 0000000..282a155
--- /dev/null
+++ b/tests/data/scripts/basic/unknown-debug
@@ -0,0 +1,37 @@
+# Test behavior with unknown user (debug). -*- conf -*-
+#
+# Copyright 2015 Russ Allbery <eagle@eyrie.org>
+#
+# See LICENSE for licensing terms.
+
+[options]
+ auth = program=%0 debug
+ session = program=%0 debug
+
+[run]
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_USER_UNKNOWN
+ setcred(REFRESH_CRED) = PAM_USER_UNKNOWN
+ setcred(REINITIALIZE_CRED) = PAM_USER_UNKNOWN
+ open_session = PAM_SESSION_ERR
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_setcred: entry (0x4)
+ DEBUG skipping, no open session
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (0x2)
+ ERR cannot find UID for pam-afs-session-unknown-user: %1
+ DEBUG pam_sm_setcred: exit (failure)
+ DEBUG pam_sm_setcred: entry (0x10)
+ ERR cannot find UID for pam-afs-session-unknown-user: %1
+ DEBUG pam_sm_setcred: exit (failure)
+ DEBUG pam_sm_setcred: entry (0x8)
+ ERR cannot find UID for pam-afs-session-unknown-user: %1
+ DEBUG pam_sm_setcred: exit (failure)
+ DEBUG pam_sm_open_session: entry (0x0)
+ ERR cannot find UID for pam-afs-session-unknown-user: %1
+ DEBUG pam_sm_open_session: exit (failure)
+ DEBUG pam_sm_close_session: entry (0x0)
+ DEBUG skipping, no open session
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/fakepam/config.c b/tests/fakepam/config.c
new file mode 100644
index 0000000..9e769d6
--- /dev/null
+++ b/tests/fakepam/config.c
@@ -0,0 +1,679 @@
+/*
+ * 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 <http://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by 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.
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.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[] = {
+ { "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 },
+};
+
+/* Mapping of PAM flag names without the leading PAM_ to values. */
+static const struct {
+ const char *name;
+ int value;
+} FLAGS[] = {
+ { "CHANGE_EXPIRED_AUTHTOK", PAM_CHANGE_EXPIRED_AUTHTOK },
+ { "DISALLOW_NULL_AUTHTOK", PAM_DISALLOW_NULL_AUTHTOK },
+ { "DELETE_CRED", PAM_DELETE_CRED },
+ { "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 },
+};
+
+/* Mapping of strings to PAM groups. */
+static const struct {
+ const char *name;
+ enum group_type group;
+} GROUPS[] = {
+ { "account", GROUP_ACCOUNT },
+ { "auth", GROUP_AUTH },
+ { "password", GROUP_PASSWORD },
+ { "session", GROUP_SESSION },
+};
+
+/* Mapping of strings to PAM return values. */
+static const struct {
+ const char *name;
+ int status;
+} RETURNS[] = {
+ { "PAM_AUTH_ERR", PAM_AUTH_ERR },
+ { "PAM_AUTHINFO_UNAVAIL", PAM_AUTHINFO_UNAVAIL },
+ { "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 },
+};
+
+/* Mapping of PAM prompt styles to their values. */
+static const struct {
+ const char *name;
+ int style;
+} STYLES[] = {
+ { "echo_off", PAM_PROMPT_ECHO_OFF },
+ { "echo_on", PAM_PROMPT_ECHO_ON },
+ { "error_msg", PAM_ERROR_MSG },
+ { "info", PAM_TEXT_INFO },
+};
+
+/* Mappings of strings to syslog priorities. */
+static const struct {
+ const char *name;
+ int priority;
+} PRIORITIES[] = {
+ { "DEBUG", LOG_DEBUG },
+ { "INFO", LOG_INFO },
+ { "NOTICE", LOG_NOTICE },
+ { "ERR", LOG_ERR },
+ { "CRIT", LOG_CRIT },
+};
+
+
+/*
+ * 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 enum group_type
+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':
+ 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;
+ 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 || 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, *next;
+ char *line, *token, *call;
+ size_t length;
+
+ 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 || 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 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 = NULL;
+ 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, length;
+
+ 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 || 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", prompt->prompt);
+ *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->actions = NULL;
+ 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, "[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/general.c b/tests/fakepam/general.c
index 3d59acc..9c0d091 100644
--- a/tests/fakepam/general.c
+++ b/tests/fakepam/general.c
@@ -34,6 +34,7 @@
#include <portable/pam.h>
#include <portable/system.h>
+#include <errno.h>
#include <pwd.h>
#include <tests/fakepam/pam.h>
@@ -130,8 +131,10 @@ 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
+ else {
+ errno = 0;
return NULL;
+ }
}
#else
struct passwd *
@@ -139,7 +142,9 @@ getpwnam(const char *name)
{
if (pwd_info != NULL && strcmp(pwd_info->pw_name, name) == 0)
return pwd_info;
- else
+ else {
+ errno = 0;
return NULL;
+ }
}
#endif
diff --git a/tests/fakepam/script.c b/tests/fakepam/script.c
new file mode 100644
index 0000000..5f9dead
--- /dev/null
+++ b/tests/fakepam/script.c
@@ -0,0 +1,408 @@
+/*
+ * 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 <http://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by 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.
+ */
+
+#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
+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: %s\n", wanted, seen);
+ va_start(args, format);
+ okv(0, format, args);
+ va_end(args);
+ return;
+ }
+ memset(&regex, 0, sizeof(regex));
+ status = regcomp(&regex, wanted, REG_EXTENDED | REG_NOSUB);
+ if (status != 0) {
+ regerror(status, &regex, err, sizeof(err));
+ bail("invalid regex /%s/: %s", wanted, err);
+ }
+ status = regexec(&regex, 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, &regex, err, sizeof(err));
+ bail("regexec failed for regex /%s/: %s", wanted, err);
+ }
+ regfree(&regex);
+}
+#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
+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, 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 SOURCE or BUILD, the user (may be NULL), and the stored
+ * password (may be NULL), run that script, outputing 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, PAM_SUCCESS);
+ 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 0000000..e90e6b2
--- /dev/null
+++ b/tests/fakepam/script.h
@@ -0,0 +1,79 @@
+/*
+ * 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 <http://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by 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.
+ */
+
+#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 SOURCE or 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/fakepam/stubs.c b/tests/fakepam/stubs.c
new file mode 100644
index 0000000..a6349ad
--- /dev/null
+++ b/tests/fakepam/stubs.c
@@ -0,0 +1,32 @@
+/*
+ * Stubs to support testing a module without some PAM groups.
+ *
+ * pam-afs-session doesn't support account management or password changes, but
+ * these are wired into the PAM testing apparatus. Provide stub functions so
+ * that the test programs will link.
+ *
+ * Copyright 2015 Russ Allbery <eagle@eyrie.org>
+ *
+ * See LICENSE for licensing terms.
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+
+#include <tests/tap/macros.h>
+
+
+int
+pam_sm_acct_mgmt(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED,
+ const char **argv UNUSED)
+{
+ return PAM_SUCCESS;
+}
+
+
+int
+pam_sm_chauthtok(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED,
+ const char **argv UNUSED)
+{
+ return PAM_SUCCESS;
+}
diff --git a/tests/module/basic-t.c b/tests/module/basic-t.c
index 2e42c99..fdb52d0 100644
--- a/tests/module/basic-t.c
+++ b/tests/module/basic-t.c
@@ -10,41 +10,33 @@
#include <config.h>
#include <portable/kafs.h>
-#include <portable/pam.h>
#include <portable/system.h>
+#include <errno.h>
#include <pwd.h>
-#include <syslog.h>
#include <tests/fakepam/pam.h>
-#include <tests/module/util.h>
+#include <tests/fakepam/script.h>
#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
-/*
- * We run the entire test suite twice, once with debug disabled and once with
- * debug enabled. This is the wrapper around all the test cases to enable
- * that without code duplication.
- */
-static void
-run_tests(bool debug)
+int
+main(void)
{
- pam_handle_t *pamh;
- int status;
- char *skipping, *skiptokens, *skipsession, *program, *running, *already;
- char *destroy, *unknown;
- char *aklog = test_file_path ("data/fake-aklog");
+ struct script_config config;
struct passwd *user;
- struct pam_conv conv = { NULL, NULL };
- const char *debug_desc = debug ? " w/debug" : "";
- const char *argv_nothing[] = { "nopag", "notokens", "debug", NULL };
- const char *argv_normal[] = { "program=", "debug", NULL };
-
- /* Determine the user so that setuid will work. */
- user = getpwuid(getuid());
- if (user == NULL)
- bail("cannot find username of current user");
- pam_set_pwd(user);
+ char *aklog, *uid, *script;
+ size_t i;
+ const char *const session_types[] = {
+ "establish", "establish-debug", "refresh", "refresh-debug",
+ "reinit", "reinit-debug", "open-session", "open-session-debug"
+ };
+
+ /* Skip the entire test if AFS isn't available. */
+ if (!k_hasafs())
+ skip_all("AFS not available");
+ plan_lazy();
/*
* Clear KRB5CCNAME out of the environment to avoid running aklog when we
@@ -53,203 +45,60 @@ run_tests(bool debug)
if (putenv((char *) "KRB5CCNAME") < 0)
sysbail("cannot clear KRB5CCNAME from the environment");
- /* Build some messages that we'll use multiple times. */
- if (asprintf(&skipping, "%d skipping as configured", LOG_DEBUG) < 0)
- sysbail("cannot allocate memory");
- if (asprintf(&skiptokens, "%d skipping tokens, no Kerberos ticket cache",
- LOG_DEBUG) < 0)
- sysbail("cannot allocate memory");
- if (asprintf(&skipsession, "%d skipping, no open session", LOG_DEBUG) < 0)
- sysbail("cannot allocate memory");
- if (asprintf(&program, "program=%s", aklog) < 0)
- sysbail("cannot allocate memory");
- if (asprintf(&running, "%d running %s as UID %lu", LOG_DEBUG, aklog,
- (unsigned long) getuid()) < 0)
- sysbail("cannot allocate memory");
- if (asprintf(&already, "%d skipping, apparently already ran",
- LOG_DEBUG) < 0)
- sysbail("cannot allocate memory");
- if (asprintf(&destroy, "%d destroying tokens", LOG_DEBUG) < 0)
- sysbail("cannot allocate memory");
+ /* Determine the user so that setuid will work. */
+ user = getpwuid(getuid());
+ if (user == NULL)
+ bail("cannot find username of current user");
+ pam_set_pwd(user);
- /* Do nothing and check for correct output status. */
- status = pam_start("test", "testuser", &conv, &pamh);
- if (status != PAM_SUCCESS)
- sysbail("cannot create PAM handle");
- TEST_PAM(pam_sm_authenticate, 0, argv_nothing,
- "", PAM_SUCCESS,
- "do nothing");
- TEST_PAM(pam_sm_setcred, 0, argv_nothing,
- "", PAM_SUCCESS,
- "do nothing");
- TEST_PAM(pam_sm_setcred, PAM_DELETE_CRED, argv_nothing,
- (debug ? skipping : ""), PAM_SUCCESS,
- "delete do nothing");
- TEST_PAM(pam_sm_setcred, PAM_REINITIALIZE_CRED, argv_nothing,
- "", PAM_SUCCESS,
- "reinitialize do nothing");
- TEST_PAM(pam_sm_setcred, PAM_REFRESH_CRED, argv_nothing,
- "", PAM_SUCCESS,
- "refresh do nothing");
- TEST_PAM(pam_sm_open_session, 0, argv_nothing,
- "", PAM_SUCCESS,
- "do nothing");
- TEST_PAM(pam_sm_close_session, 0, argv_nothing,
- (debug ? skipping : ""), PAM_IGNORE,
- "do nothing");
- pam_end(pamh, status);
+ /* Configure the path to aklog. */
+ memset(&config, 0, sizeof(config));
+ aklog = test_file_path("data/fake-aklog");
+ config.extra[0] = aklog;
- /* Test behavior with an unknown user. */
- status = pam_start("test", "pam-afs-session-unknown-user", &conv, &pamh);
- if (status != PAM_SUCCESS)
- sysbail("cannot create PAM handle");
- pam_modutil_getpwnam(pamh, "pam-afs-session-unknown-user");
- if (asprintf(&unknown, "%d cannot find UID for"
- " pam-afs-session-unknown-user: %s", LOG_ERR,
- strerror(errno)) < 0)
- sysbail("cannot allocate memory");
- if (pam_putenv(pamh, "KRB5CCNAME=krb5cc_test") != PAM_SUCCESS)
- sysbail("cannot set PAM environment variable");
- argv_normal[0] = program;
- TEST_PAM(pam_sm_authenticate, 0, argv_normal,
- "", PAM_SUCCESS,
- "unknown user");
- TEST_PAM(pam_sm_setcred, 0, argv_normal,
- unknown, PAM_USER_UNKNOWN,
- "unknown user");
- TEST_PAM(pam_sm_setcred, PAM_DELETE_CRED, argv_normal,
- (debug ? skipsession : ""), PAM_SUCCESS,
- "delete unknown user");
- TEST_PAM(pam_sm_setcred, PAM_REINITIALIZE_CRED, argv_normal,
- unknown, PAM_USER_UNKNOWN,
- "reinitialize unknown user");
- TEST_PAM(pam_sm_setcred, PAM_REFRESH_CRED, argv_normal,
- unknown, PAM_USER_UNKNOWN,
- "refresh unknown user");
- TEST_PAM(pam_sm_open_session, 0, argv_normal,
- unknown, PAM_SESSION_ERR,
- "unknown user");
- TEST_PAM(pam_sm_close_session, 0, argv_normal,
- (debug ? skipsession : ""), PAM_SUCCESS,
- "close unknown user");
- pam_end(pamh, status);
+ /* Initial no-op tests. */
+ config.user = "testuser";
+ run_script("data/scripts/basic/noop", &config);
+ run_script("data/scripts/basic/noop-debug", &config);
/*
* Test behavior without a Kerberos ticket. This doesn't test actual
* creation of a PAG.
*/
unlink("aklog-args");
- status = pam_start("test", "testuser", &conv, &pamh);
- if (status != PAM_SUCCESS)
- sysbail("cannot create PAM handle");
- argv_normal[0] = program;
- TEST_PAM(pam_sm_authenticate, 0, argv_normal,
- "", PAM_SUCCESS,
- "no ticket");
- TEST_PAM(pam_sm_setcred, 0, argv_normal,
- (debug ? skiptokens : ""), PAM_SUCCESS,
- "no ticket");
- TEST_PAM(pam_sm_setcred, PAM_REINITIALIZE_CRED, argv_normal,
- (debug ? skiptokens : ""), PAM_SUCCESS,
- "reinitialize no ticket");
- TEST_PAM(pam_sm_setcred, PAM_REFRESH_CRED, argv_normal,
- (debug ? skiptokens : ""), PAM_SUCCESS,
- "refresh no ticket");
- TEST_PAM(pam_sm_open_session, 0, argv_normal,
- (debug ? skiptokens : ""), PAM_SUCCESS,
- "no ticket");
- TEST_PAM(pam_sm_close_session, 0, argv_normal,
- (debug ? skipsession : ""), PAM_SUCCESS,
- "no ticket");
- pam_end(pamh, status);
- ok(access("aklog-args", F_OK) < 0, "aklog was not run");
-
- /*
- * Fake the presence of a Kerberos ticket and see that aklog runs, and
- * test suppression of multiple calls to pam_sm_setcred.
- */
- unlink("aklog-args");
- status = pam_start("test", user->pw_name, &conv, &pamh);
- if (status != PAM_SUCCESS)
- sysbail("cannot create PAM handle");
- if (pam_putenv(pamh, "KRB5CCNAME=krb5cc_test") != PAM_SUCCESS)
- sysbail("cannot set PAM environment variable");
- TEST_PAM(pam_sm_setcred, 0, argv_normal,
- (debug ? running : ""), PAM_SUCCESS,
- "normal");
- ok(access("aklog-args", F_OK) == 0, "aklog was run");
- unlink("aklog-args");
- TEST_PAM(pam_sm_setcred, PAM_REINITIALIZE_CRED, argv_normal,
- (debug ? running : ""), PAM_SUCCESS,
- "normal reinitialize");
- ok(access("aklog-args", F_OK) == 0, "aklog was run");
- unlink("aklog-args");
- TEST_PAM(pam_sm_setcred, PAM_REFRESH_CRED, argv_normal,
- (debug ? running : ""), PAM_SUCCESS,
- "normal refresh");
- ok(access("aklog-args", F_OK) == 0, "aklog was run");
- unlink("aklog-args");
- TEST_PAM(pam_sm_setcred, 0, argv_normal,
- (debug ? already : ""), PAM_SUCCESS,
- "normal");
+ run_script("data/scripts/basic/no-ticket", &config);
+ run_script("data/scripts/basic/no-ticket-debug", &config);
ok(access("aklog-args", F_OK) < 0, "aklog was not run");
- TEST_PAM(pam_sm_close_session, 0, argv_normal,
- (debug ? destroy : ""), PAM_SUCCESS,
- "normal");
- pam_end(pamh, status);
/*
- * Fake the presence of a Kerberos ticket in the environment rather than
- * in the PAM environment. We should still run aklog.
+ * Remaining tests run with the module fooled into thinking we have a
+ * Kerberos ticket cache.
*/
- unlink("aklog-args");
- status = pam_start("test", user->pw_name, &conv, &pamh);
- if (status != PAM_SUCCESS)
- sysbail("cannot create PAM handle");
if (putenv((char *) "KRB5CCNAME=krb5cc_test") < 0)
sysbail("cannot set KRB5CCNAME in the environment");
- TEST_PAM(pam_sm_setcred, 0, argv_normal,
- (debug ? running : ""), PAM_SUCCESS,
- "normal");
- ok(access("aklog-args", F_OK) == 0, "aklog was run");
- unlink("aklog-args");
- TEST_PAM(pam_sm_setcred, PAM_REINITIALIZE_CRED, argv_normal,
- (debug ? running : ""), PAM_SUCCESS,
- "normal reinitialize");
- ok(access("aklog-args", F_OK) == 0, "aklog was run");
- unlink("aklog-args");
- TEST_PAM(pam_sm_setcred, PAM_REFRESH_CRED, argv_normal,
- (debug ? running : ""), PAM_SUCCESS,
- "normal refresh");
- ok(access("aklog-args", F_OK) == 0, "aklog was run");
+
+ /* Unknown user. Be sure to get the strerror message. */
+ config.user = "pam-afs-session-unknown-user";
+ config.extra[1] = strerror(0);
+ run_script("data/scripts/basic/unknown", &config);
+ run_script("data/scripts/basic/unknown-debug", &config);
+ config.extra[1] = NULL;
+
+ /* Check that aklog runs in various ways of opening a session. */
+ config.user = user->pw_name;
+ basprintf(&uid, "%lu", (unsigned long) getuid());
+ config.extra[1] = uid;
+ for (i = 0; i < ARRAY_SIZE(session_types); i++) {
+ unlink("aklog-args");
+ basprintf(&script, "data/scripts/basic/%s", session_types[i]);
+ run_script(script, &config);
+ ok(access("aklog-args", F_OK) == 0, "aklog was run");
+ }
unlink("aklog-args");
- TEST_PAM(pam_sm_close_session, 0, argv_normal,
- (debug ? destroy : ""), PAM_SUCCESS,
- "normal");
- pam_end(pamh, status);
+ config.extra[1] = NULL;
+ free(uid);
+ /* Clean up. */
test_file_path_free(aklog);
- free(program);
- free(skipping);
- free(skiptokens);
- free(skipsession);
- free(running);
- free(already);
- free(destroy);
- free(unknown);
-}
-
-
-int
-main(void)
-{
- if (!k_hasafs())
- skip_all("AFS not available");
-
- plan(66 * 2);
-
- run_tests(false);
- run_tests(true);
-
return 0;
}