From 0d75d3bc63cd726b2bf883fb110b3b0ef7032dc7 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 3 Jun 2026 11:25:08 -0400 Subject: [PATCH 1/6] [_777] bats wrapper for running original pam_interactive test script --- .../test/scripts/test011_pam_interactive.bats | 46 +++++++++++++++++++ .../single_node/test_script_parameters | 1 + 2 files changed, 47 insertions(+) create mode 100755 irods/test/scripts/test011_pam_interactive.bats diff --git a/irods/test/scripts/test011_pam_interactive.bats b/irods/test/scripts/test011_pam_interactive.bats new file mode 100755 index 000000000..b5b269b11 --- /dev/null +++ b/irods/test/scripts/test011_pam_interactive.bats @@ -0,0 +1,46 @@ +#!/usr/bin/env bats + +# The tests in this BATS module must be run as a (passwordless) sudo-enabled user. +# It is also required that the python irodsclient be installed under irods' ~/.local environment. + +. $BATS_TEST_DIRNAME/test_support_functions + +setup() { + [ -f /tmp/test011_flag ] || { + rm -fr ~/.irods + /prc/test_harness/utility/iinit.py host localhost \ + port 1247 \ + zone tempZone \ + user rods \ + password rods \ + + ## Because iRODS 5+ negotiates for SSL automatically: + CLIENT_JSON=~/.irods/irods_environment.json + jq '.irods_client_server_policy="CS_NEG_REFUSE"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \ + mv $CLIENT_JSON.$$ $CLIENT_JSON + + sudo apt install irods-auth-plugin-pam-interactive-{client,server} + + setup_pam_login_for_user "rods" alice + + # Tests require only the irods_environment.json + rm -f ~/.irods/.irodsA + + ## Switch over to scheme to be tested. + jq '.irods_authentication_scheme="pam_interactive"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \ + mv $CLIENT_JSON.$$ $CLIENT_JSON + } + touch /tmp/test011_flag +} + +original_test_suite() +{ + local USER="alice" + local PASSWORD="rods" + sudo chpasswd <<<"$USER:$PASSWORD" + python -m unittest irods.test.pam_interactive_test_must_run_manually +} + +@test "original_pam_interactive_tests" { + original_test_suite +} diff --git a/test_harness/single_node/test_script_parameters b/test_harness/single_node/test_script_parameters index 4b94d58e6..b4b2d5940 100644 --- a/test_harness/single_node/test_script_parameters +++ b/test_harness/single_node/test_script_parameters @@ -21,6 +21,7 @@ declare -A wrappers=( [test008_prc_write_irodsA_utility_in_native_mode.bats]=../login_auth_test.sh [test009_test_special_characters_in_pam_passwords_auth_framework.bats]=../login_auth_test.sh [test010_issue_362_rogue_chars_in_pam_password.bats]=../login_auth_test.sh + [test011_pam_interactive.bats]=../login_auth_test.sh ) # keys for Image and User refer to the basename after resolution to a wrapper if one is used From c1bb6c5e6e68da21d2ca5c96a047b5f333409799 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 14 Aug 2026 05:22:10 -0400 Subject: [PATCH 2/6] developments prior to debug session with Kory --- irods/auth/pam_interactive.py | 3 +- .../files_for_test012/pam_clear_token.c | 36 ++++++ .../scripts/files_for_test012/pam_interactive | 17 +++ .../scripts/files_for_test012/pam_password | 7 ++ .../test012_pam_interactive_multistep.bats | 117 ++++++++++++++++++ irods/test/scripts/test_support_functions | 2 +- .../single_node/test_script_parameters | 1 + 7 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 irods/test/scripts/files_for_test012/pam_clear_token.c create mode 100644 irods/test/scripts/files_for_test012/pam_interactive create mode 100644 irods/test/scripts/files_for_test012/pam_password create mode 100755 irods/test/scripts/test012_pam_interactive_multistep.bats diff --git a/irods/auth/pam_interactive.py b/irods/auth/pam_interactive.py index 9b53ed068..ac5876bf4 100644 --- a/irods/auth/pam_interactive.py +++ b/irods/auth/pam_interactive.py @@ -38,6 +38,7 @@ _logger = logging.getLogger(__name__) +_logger.debug("hello from paminteractive") def login(conn, **extra_opt): """The entry point for the pam_interactive authentication scheme.""" @@ -230,7 +231,7 @@ def native_auth(self, request): def next(self, request): prompt = request.get("msg", {}).get("prompt", "") if prompt: - _logger.info("Server prompt: %s", prompt) + _logger.debug("Server prompt: %s", prompt) server_req = request.copy() self._patch_state(server_req) diff --git a/irods/test/scripts/files_for_test012/pam_clear_token.c b/irods/test/scripts/files_for_test012/pam_clear_token.c new file mode 100644 index 000000000..8590287ce --- /dev/null +++ b/irods/test/scripts/files_for_test012/pam_clear_token.c @@ -0,0 +1,36 @@ +/* +To build, you need the PAM development library. Once installed, +run the following: + + gcc -fPIC -fno-stack-protector -o pam_clear_token.o -c main.c + gcc -shared -o pam_clear_token.so pam_clear_token.o +*/ + +#include +#include +#include + +#include + +PAM_EXTERN int pam_sm_authenticate(pam_handle_t* pamh, int flags, int argc, const char** argv) +{ + (void) flags; + (void) argc; + (void) argv; + + // Clear the current auth token. + pam_set_item(pamh, PAM_AUTHTOK, NULL); + pam_set_item(pamh, PAM_OLDAUTHTOK, NULL); + + return PAM_SUCCESS; +} + +PAM_EXTERN int pam_sm_setcred(pam_handle_t* pamh, int flags, int argc, const char** argv) +{ + (void) pamh; + (void) flags; + (void) argc; + (void) argv; + + return PAM_SUCCESS; +} diff --git a/irods/test/scripts/files_for_test012/pam_interactive b/irods/test/scripts/files_for_test012/pam_interactive new file mode 100644 index 000000000..40a3a8b6e --- /dev/null +++ b/irods/test/scripts/files_for_test012/pam_interactive @@ -0,0 +1,17 @@ +# This file is for testing PAM authentication with iRODS +# using the pam_interactive authentication scheme. + +# Prompt for the first password, from /etc/shadow. +auth required pam_unix.so + +# This is a custom PAM module that clears the success token. Without +# this, the "auth" lines which follow are skipped. +auth required /t012/pam_clear_token.so + +# Prompt for the second password, from the user database file created +# earlier. The use of "crypt=crypt" is required for this to work. It +# tells the module that the passwords are encrypted. +auth required pam_userdb.so db=/t012/pam_userdb crypt=crypt + +# Do the normal user account stuff. +account required pam_unix.so diff --git a/irods/test/scripts/files_for_test012/pam_password b/irods/test/scripts/files_for_test012/pam_password new file mode 100644 index 000000000..44d4f19ff --- /dev/null +++ b/irods/test/scripts/files_for_test012/pam_password @@ -0,0 +1,7 @@ +# This file is for testing PAM authentication with iRODS +# using the pam_password authentication scheme. + +auth required pam_env.so +auth sufficient pam_unix.so +auth requisite pam_succeed_if.so uid >= 500 quiet +auth required pam_deny.so diff --git a/irods/test/scripts/test012_pam_interactive_multistep.bats b/irods/test/scripts/test012_pam_interactive_multistep.bats new file mode 100755 index 000000000..b366c57c9 --- /dev/null +++ b/irods/test/scripts/test012_pam_interactive_multistep.bats @@ -0,0 +1,117 @@ +#!/usr/bin/env bats + +# The tests in this BATS module must be run as a (passwordless) sudo-enabled user. +# It is also required that the python irodsclient be installed under irods' ~/.local environment. + +SKIP_IINIT_FOR_PASSWORD=yes + +. $BATS_TEST_DIRNAME/test_support_functions + +export TESTUSER="john" +export FIRST_PASSWORD="=i;r@o\\d&s" # somerods +export SECOND_PASSWORD="otherrods" + +setup() { + [ -f /tmp/test012_flag ] || { + rm -fr ~/.irods + /prc/test_harness/utility/iinit.py host localhost \ + port 1247 \ + zone tempZone \ + user rods \ + password rods \ + + sudo apt update + sudo apt install -y db-util libpam0g-dev jq + + ## Because iRODS 5+ negotiates for SSL automatically: + CLIENT_JSON=~/.irods/irods_environment.json + jq '.irods_client_server_policy="CS_NEG_REFUSE"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \ + mv $CLIENT_JSON.$$ $CLIENT_JSON + + sudo apt install irods-auth-plugin-pam-interactive-{client,server} + SERVER_CONFIG=server_config.json + + sudo -s <<-EOF + jq '.plugin_configuration.authentication.pam_interactive = { + "pam_stack_name": "pam_interactive" + }' <"/etc/irods/${SERVER_CONFIG}" >"/tmp/${SERVER_CONFIG}" + cp -rp "/etc/irods/${SERVER_CONFIG}"{,.orig} + mv -f "/tmp/${SERVER_CONFIG}" "/etc/irods/${SERVER_CONFIG}" + EOF + waitsrv() { + while true; do + sleep 5 + ils >& /dev/null && break + done + } + + { sudo kill -HUP `sudo cat /tmp/irods.pid` && waitsrv; } || { + echo "Couldn't properly bounce server after configuration change."; exit 1; } + + setup_pam_login_for_user "${FIRST_PASSWORD}" $TESTUSER + sudo cp $BATS_TEST_DIRNAME/files_for_test012/pam_password /etc/pam.d/irods + sudo cp $BATS_TEST_DIRNAME/files_for_test012/pam_interactive /etc/pam.d/ + sudo mkdir /t012 && sudo gcc -o /t012/pam_clear_token.so -fno-stack-protector -shared -fPIC $BATS_TEST_DIRNAME/files_for_test012/pam_clear_token.c + + db_file=/t012/pam_userdb.db + sudo db_load -T -t hash "$db_file" <<<"${TESTUSER}"$'\n'"${SECOND_PASSWORD}" + sudo chown root:root "$db_file" + sudo chmod 600 "$db_file" + + # Tests require only the irods_environment.json + rm -f ~/.irods/.irodsA + + ## Switch over to scheme to be tested. + jq '.irods_authentication_scheme="pam_interactive"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \ + mv $CLIENT_JSON.$$ $CLIENT_JSON + } + touch /tmp/test012_flag +} + +@test "pam_interactive_test_multistep_with_correct_passwords" { +: +echo " +import getpass +import irods +import os +from unittest.mock import patch +from irods.auth import FORCE_PASSWORD_PROMPT + +def getpass_new_callable(answers=()): + class iterate_answers: + def __init__(self,answers = answers): + self.answers = answers + self.count = 0 + def __call__(self,*_): + count = self.count + self.count += 1 + ans = self.answers[count] + print ('*** giving answer:', ans) + return ans + return lambda : iterate_answers() + +home = None + +with patch( + 'getpass.getpass', + new_callable=getpass_new_callable(answers=[os.environ['FIRST_PASSWORD'],os.environ['SECOND_PASSWORD']]) +) as m: + try: + sess = irods.helpers.make_session(test_server_version=False) + sess.set_auth_option_for_scheme('pam_interactive', FORCE_PASSWORD_PROMPT, True) + home = sess.collections.get(f'/{sess.zone}/home/{sess.username}') + finally: + pw_count = m.count + +#if pw_count < 2: +# print(f'************************ {pw_count = } < 2') +# exit(3) +if home is None: + exit(2) +username = os.environ['TESTUSER'] +if not home.path.endswith(f'/{username}'): + exit(1) +" >/tmp/test012.py +############################### +python /tmp/test012.py >&3 2>&1 +} diff --git a/irods/test/scripts/test_support_functions b/irods/test/scripts/test_support_functions index a7e40bfe4..875e53e7c 100644 --- a/irods/test/scripts/test_support_functions +++ b/irods/test/scripts/test_support_functions @@ -120,7 +120,7 @@ _begin_pam_environment_and_password() { echo "$ENV" > ~/.irods/irods_environment.json if [ -n "$1" -a -z "$SKIP_IINIT_FOR_PASSWORD" ]; then - iinit <<<"$1" 2>/tmp/iinit_as_alice.log + iinit ${IINIT_TTL:+--ttl $IINIT_TTL}<<<"$1" 2>/tmp/iinit_as_alice.log fi } diff --git a/test_harness/single_node/test_script_parameters b/test_harness/single_node/test_script_parameters index b4b2d5940..f7b977296 100644 --- a/test_harness/single_node/test_script_parameters +++ b/test_harness/single_node/test_script_parameters @@ -22,6 +22,7 @@ declare -A wrappers=( [test009_test_special_characters_in_pam_passwords_auth_framework.bats]=../login_auth_test.sh [test010_issue_362_rogue_chars_in_pam_password.bats]=../login_auth_test.sh [test011_pam_interactive.bats]=../login_auth_test.sh + [test012_pam_interactive_multistep.bats]=../login_auth_test.sh ) # keys for Image and User refer to the basename after resolution to a wrapper if one is used From c6d6eae589977c92f96ee9f589572f71b0a4bd85 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 13 Aug 2026 09:00:24 -0400 Subject: [PATCH 3/6] db_load stuff --- irods/test/scripts/test012_pam_interactive_multistep.bats | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/irods/test/scripts/test012_pam_interactive_multistep.bats b/irods/test/scripts/test012_pam_interactive_multistep.bats index b366c57c9..d2b6261be 100755 --- a/irods/test/scripts/test012_pam_interactive_multistep.bats +++ b/irods/test/scripts/test012_pam_interactive_multistep.bats @@ -11,6 +11,10 @@ export TESTUSER="john" export FIRST_PASSWORD="=i;r@o\\d&s" # somerods export SECOND_PASSWORD="otherrods" +ssl_hash() { + openssl passwd -6 "$1" +} + setup() { [ -f /tmp/test012_flag ] || { rm -fr ~/.irods @@ -54,7 +58,7 @@ setup() { sudo mkdir /t012 && sudo gcc -o /t012/pam_clear_token.so -fno-stack-protector -shared -fPIC $BATS_TEST_DIRNAME/files_for_test012/pam_clear_token.c db_file=/t012/pam_userdb.db - sudo db_load -T -t hash "$db_file" <<<"${TESTUSER}"$'\n'"${SECOND_PASSWORD}" + sudo db_load -T -t hash "$db_file" <<<"${TESTUSER}"$'\n'"$(ssl_hash _${SECOND_PASSWORD})" sudo chown root:root "$db_file" sudo chmod 600 "$db_file" From 8ba2194dc22ba81e17f6d22c6a76a6c5122e471f Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 12 Aug 2026 11:01:11 -0400 Subject: [PATCH 4/6] got both tests in test012_pam_interactive_multistep.bats now pass One test for correct 2nd password, another for deliberately munged 2nd password. (nb, the 2nd password test does an extra check to a pam_userdb.db file to prove multistep authentication work flows are functioning for the pam_interactive auth scheme.) --- .../test/scripts/test011_pam_interactive.bats | 2 +- .../test012_pam_interactive_multistep.bats | 73 +++++++++++++------ 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/irods/test/scripts/test011_pam_interactive.bats b/irods/test/scripts/test011_pam_interactive.bats index b5b269b11..a721dcbeb 100755 --- a/irods/test/scripts/test011_pam_interactive.bats +++ b/irods/test/scripts/test011_pam_interactive.bats @@ -19,7 +19,7 @@ setup() { jq '.irods_client_server_policy="CS_NEG_REFUSE"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \ mv $CLIENT_JSON.$$ $CLIENT_JSON - sudo apt install irods-auth-plugin-pam-interactive-{client,server} + sudo apt install -y irods-auth-plugin-pam-interactive-{client,server} setup_pam_login_for_user "rods" alice diff --git a/irods/test/scripts/test012_pam_interactive_multistep.bats b/irods/test/scripts/test012_pam_interactive_multistep.bats index d2b6261be..0f4de2f28 100755 --- a/irods/test/scripts/test012_pam_interactive_multistep.bats +++ b/irods/test/scripts/test012_pam_interactive_multistep.bats @@ -10,6 +10,7 @@ SKIP_IINIT_FOR_PASSWORD=yes export TESTUSER="john" export FIRST_PASSWORD="=i;r@o\\d&s" # somerods export SECOND_PASSWORD="otherrods" +export CLIENT_AUTH_ERROR_EXITCODE=123 ssl_hash() { openssl passwd -6 "$1" @@ -32,7 +33,7 @@ setup() { jq '.irods_client_server_policy="CS_NEG_REFUSE"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \ mv $CLIENT_JSON.$$ $CLIENT_JSON - sudo apt install irods-auth-plugin-pam-interactive-{client,server} + sudo apt install -y irods-auth-plugin-pam-interactive-{client,server} SERVER_CONFIG=server_config.json sudo -s <<-EOF @@ -57,11 +58,6 @@ setup() { sudo cp $BATS_TEST_DIRNAME/files_for_test012/pam_interactive /etc/pam.d/ sudo mkdir /t012 && sudo gcc -o /t012/pam_clear_token.so -fno-stack-protector -shared -fPIC $BATS_TEST_DIRNAME/files_for_test012/pam_clear_token.c - db_file=/t012/pam_userdb.db - sudo db_load -T -t hash "$db_file" <<<"${TESTUSER}"$'\n'"$(ssl_hash _${SECOND_PASSWORD})" - sudo chown root:root "$db_file" - sudo chmod 600 "$db_file" - # Tests require only the irods_environment.json rm -f ~/.irods/.irodsA @@ -72,14 +68,20 @@ setup() { touch /tmp/test012_flag } -@test "pam_interactive_test_multistep_with_correct_passwords" { -: -echo " +encode_2nd_password() { + db_file=/t012/pam_userdb.db + sudo db_load -T -t hash "$db_file" <<<"${TESTUSER}"$'\n'"$(ssl_hash ${1})" + sudo chown root:root "$db_file" + sudo chmod 600 "$db_file" +} + +SCRIPT=" import getpass -import irods import os + +import irods +from irods.auth import ClientAuthError from unittest.mock import patch -from irods.auth import FORCE_PASSWORD_PROMPT def getpass_new_callable(answers=()): class iterate_answers: @@ -96,26 +98,55 @@ def getpass_new_callable(answers=()): home = None +pw_count = 0 + with patch( 'getpass.getpass', new_callable=getpass_new_callable(answers=[os.environ['FIRST_PASSWORD'],os.environ['SECOND_PASSWORD']]) ) as m: try: - sess = irods.helpers.make_session(test_server_version=False) - sess.set_auth_option_for_scheme('pam_interactive', FORCE_PASSWORD_PROMPT, True) - home = sess.collections.get(f'/{sess.zone}/home/{sess.username}') + sess = irods.helpers.make_session(test_server_version=False) + sess.set_auth_option_for_scheme('pam_interactive', irods.auth.FORCE_PASSWORD_PROMPT, True) + home = sess.collections.get(f'/{sess.zone}/home/{sess.username}') + except ClientAuthError as exc: + # Note: The write to stdout, and the specific exit code, are necessary for the test assertions. + # in test "pam_interactive_test_multistep_with_incorrect_2nd_password" below. + print(f'ERROR: {exc!r}') + exit(int(os.environ['CLIENT_AUTH_ERROR_EXITCODE'])) finally: - pw_count = m.count + pw_count = m.count + +# Assert both passwords were prompted for. +if pw_count < 2: + print(f'************************ {pw_count = } < 2') + exit(3) -#if pw_count < 2: -# print(f'************************ {pw_count = } < 2') -# exit(3) +# Assert home is defined, ie a session was successfully created and used to retrieve a collection object if home is None: exit(2) + username = os.environ['TESTUSER'] + +# Assert home contains the expected username. if not home.path.endswith(f'/{username}'): exit(1) -" >/tmp/test012.py -############################### -python /tmp/test012.py >&3 2>&1 +" + +@test "pam_interactive_test_multistep_with_incorrect_2nd_password" { + + # We are using a deliberately munged password. + encode_2nd_password "_${SECOND_PASSWORD}" + local STATUS="" + OUTPUT=$(python -c "$SCRIPT" 2>&1) || STATUS=$? + + # Here, we assert the process's exit and output conform to expectation. We want to + # enforce that the stdout output stream contains the thrown exception name ("ClientAuthError") + # as well as that the process exits with a particular error status. + [ $STATUS = $CLIENT_AUTH_ERROR_EXITCODE ] + [[ $OUTPUT =~ ClientAuthError ]] +} + +@test "pam_interactive_test_multistep_with_correct_2nd_password" { + encode_2nd_password "${SECOND_PASSWORD}" + python -c "$SCRIPT" } From d969affee4608fadc6a69e7dc8c0e59214045330 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 14 Aug 2026 09:32:46 -0400 Subject: [PATCH 5/6] add dummy file to force rerunning tests in github actions --- dummy | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 dummy diff --git a/dummy b/dummy new file mode 100644 index 000000000..e69de29bb From 66e67e661dbeada916465f3229f9418343efece3 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 17 Aug 2026 09:03:17 -0400 Subject: [PATCH 6/6] skip test if server not latest version --- irods/test/scripts/test011_pam_interactive.bats | 5 +++++ irods/test/scripts/test012_pam_interactive_multistep.bats | 5 +++++ irods/test/scripts/test_support_functions | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/irods/test/scripts/test011_pam_interactive.bats b/irods/test/scripts/test011_pam_interactive.bats index a721dcbeb..41112495e 100755 --- a/irods/test/scripts/test011_pam_interactive.bats +++ b/irods/test/scripts/test011_pam_interactive.bats @@ -19,6 +19,11 @@ setup() { jq '.irods_client_server_policy="CS_NEG_REFUSE"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \ mv $CLIENT_JSON.$$ $CLIENT_JSON + # if plugin installation would upgrade server, then skip test. + if irods_server_package_upgradable; then + skip + fi + sudo apt install -y irods-auth-plugin-pam-interactive-{client,server} setup_pam_login_for_user "rods" alice diff --git a/irods/test/scripts/test012_pam_interactive_multistep.bats b/irods/test/scripts/test012_pam_interactive_multistep.bats index 0f4de2f28..6d0156374 100755 --- a/irods/test/scripts/test012_pam_interactive_multistep.bats +++ b/irods/test/scripts/test012_pam_interactive_multistep.bats @@ -33,6 +33,11 @@ setup() { jq '.irods_client_server_policy="CS_NEG_REFUSE"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \ mv $CLIENT_JSON.$$ $CLIENT_JSON + # if plugin installation would upgrade server, then skip test. + if irods_server_package_upgradable; then + skip + fi + sudo apt install -y irods-auth-plugin-pam-interactive-{client,server} SERVER_CONFIG=server_config.json diff --git a/irods/test/scripts/test_support_functions b/irods/test/scripts/test_support_functions index 875e53e7c..8fbaf6b7c 100644 --- a/irods/test/scripts/test_support_functions +++ b/irods/test/scripts/test_support_functions @@ -242,3 +242,11 @@ if relto: print(fm_tuple(svt)) " $1 $2 } + +irods_server_package_upgradable() { + apt update >& /dev/null + if { apt list --upgradable | grep "irods-server"; } >&/dev/null; then + return 0 + fi + return 1 +}