From 0d75d3bc63cd726b2bf883fb110b3b0ef7032dc7 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 3 Jun 2026 11:25:08 -0400 Subject: [PATCH 01/11] [_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 02/11] 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 96bf4d7a4051e7209bf5b08157968644fb1f7e03 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 10 Aug 2026 01:04:40 -0400 Subject: [PATCH 03/11] TODOs added in debug session --- irods/auth/pam_interactive.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/irods/auth/pam_interactive.py b/irods/auth/pam_interactive.py index ac5876bf4..ac638df8c 100644 --- a/irods/auth/pam_interactive.py +++ b/irods/auth/pam_interactive.py @@ -73,12 +73,17 @@ def auth_client_start(self, request): resp['user_name'] = self.conn.account.proxy_user resp['zone_name'] = self.conn.account.proxy_zone + #TODO check handling of FORCE_PASSWORD_PROMPT - + # This is close to what the C++ plugin (client-side) does # If not forcing a prompt, check for existing credentials (.irodsA) to attempt native auth directly if not resp.get(FORCE_PASSWORD_PROMPT, False): if self.conn.account.password and self.conn.account.derived_auth_file: resp[__NEXT_OPERATION__] = PERFORM_NATIVE_AUTH return resp + # TODO + # iRODS4j removes the passworda property from the response object + # Otherwise, begin the full interactive flow resp[__NEXT_OPERATION__] = AUTH_CLIENT_AUTH_REQUEST return resp @@ -200,6 +205,7 @@ def authenticated(self, request): if not self.depot: raise RuntimeError("auth storage object was either not set, or allowed to expire prematurely.") + # TODO: review (iRODS4j doesn't do this). if request.get(STORE_PASSWORD_IN_MEMORY): self.depot.use_client_auth_file(None) From 27587da0283ab23236061283997f940ff4383d92 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 13 Aug 2026 09:30:35 -0400 Subject: [PATCH 04/11] tweak to comply more with C++/irod4j native auth approach --- irods/auth/pam_interactive.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/irods/auth/pam_interactive.py b/irods/auth/pam_interactive.py index ac638df8c..41e3719a2 100644 --- a/irods/auth/pam_interactive.py +++ b/irods/auth/pam_interactive.py @@ -223,8 +223,18 @@ def authenticated(self, request): def native_auth(self, request): resp = request.copy() + # TODO: removing AUTH_PASSWORD_KEY as done in C++ and irods4j clients + AUTH_PASSWORD_KEY = "a_pw" # <--- TODO: clean up by importing + + resp.pop(AUTH_PASSWORD_KEY, "") + + # TODO may need to define user_name and zone_name properties + native_auth_request = {"zone_name": resp["zone_name"], + "user_name": resp["user_name"], + "password": resp["request_result"]} + # The native auth function will use the depot to retrieve the password token - _authenticate_native(self.conn, request) + _authenticate_native(self.conn, native_auth_request) resp[__NEXT_OPERATION__] = __FLOW_COMPLETE__ self.loggedIn = 1 From b8bd6b0221e98f9c83aef5aefbf920230715e132 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 13 Aug 2026 09:00:24 -0400 Subject: [PATCH 05/11] 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 ae8fbd1477135f28a5cfaee3a39a46d925562d49 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 12 Aug 2026 11:01:11 -0400 Subject: [PATCH 06/11] 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 4ddccfc51ce3c80e3017a1f878b87e8390eb06e6 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 13 Aug 2026 09:31:54 -0400 Subject: [PATCH 07/11] Revert "tweak to comply more with C++/irod4j native auth approach" This reverts commit 702f7236c6153d1d1f9463277ba13c471e539ed7. --- irods/auth/pam_interactive.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/irods/auth/pam_interactive.py b/irods/auth/pam_interactive.py index 41e3719a2..ac638df8c 100644 --- a/irods/auth/pam_interactive.py +++ b/irods/auth/pam_interactive.py @@ -223,18 +223,8 @@ def authenticated(self, request): def native_auth(self, request): resp = request.copy() - # TODO: removing AUTH_PASSWORD_KEY as done in C++ and irods4j clients - AUTH_PASSWORD_KEY = "a_pw" # <--- TODO: clean up by importing - - resp.pop(AUTH_PASSWORD_KEY, "") - - # TODO may need to define user_name and zone_name properties - native_auth_request = {"zone_name": resp["zone_name"], - "user_name": resp["user_name"], - "password": resp["request_result"]} - # The native auth function will use the depot to retrieve the password token - _authenticate_native(self.conn, native_auth_request) + _authenticate_native(self.conn, request) resp[__NEXT_OPERATION__] = __FLOW_COMPLETE__ self.loggedIn = 1 From 5243be3f02e4bec52122e496b0e00f4325839345 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 14 Aug 2026 09:32:46 -0400 Subject: [PATCH 08/11] 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 a0b4aca679ba082c879458e88dad876ab7288821 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 14 Aug 2026 09:45:55 -0400 Subject: [PATCH 09/11] remove todo requirements in ruff --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index abd88a899..e07af1717 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,7 +152,7 @@ select = [ # flake8-tidy-imports "TID", # flake8-todos - "TD", +#"TD", # flake8-type-checking "TC", # flake8-unused-arguments From 264714b08ebb744bee6c59d0909c07dd5d0511c1 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 14 Aug 2026 09:51:12 -0400 Subject: [PATCH 10/11] tweak to remove new scripts. will tests all pass now? --- .../test/scripts/test011_pam_interactive.bats | 46 ------ .../test012_pam_interactive_multistep.bats | 152 ------------------ 2 files changed, 198 deletions(-) delete mode 100755 irods/test/scripts/test011_pam_interactive.bats delete mode 100755 irods/test/scripts/test012_pam_interactive_multistep.bats diff --git a/irods/test/scripts/test011_pam_interactive.bats b/irods/test/scripts/test011_pam_interactive.bats deleted file mode 100755 index a721dcbeb..000000000 --- a/irods/test/scripts/test011_pam_interactive.bats +++ /dev/null @@ -1,46 +0,0 @@ -#!/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 -y 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/irods/test/scripts/test012_pam_interactive_multistep.bats b/irods/test/scripts/test012_pam_interactive_multistep.bats deleted file mode 100755 index 0f4de2f28..000000000 --- a/irods/test/scripts/test012_pam_interactive_multistep.bats +++ /dev/null @@ -1,152 +0,0 @@ -#!/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" -export CLIENT_AUTH_ERROR_EXITCODE=123 - -ssl_hash() { - openssl passwd -6 "$1" -} - -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 -y 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 - - # 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 -} - -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 os - -import irods -from irods.auth import ClientAuthError -from unittest.mock import patch - -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 - -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', 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 - -# Assert both passwords were prompted for. -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) -" - -@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 3cd2b4c8310840df4ffd42e8939072c4be849b3b Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 14 Aug 2026 10:20:09 -0400 Subject: [PATCH 11/11] Revert "tweak to remove new scripts. will tests all pass now?" This reverts commit 264714b08ebb744bee6c59d0909c07dd5d0511c1. --- .../test/scripts/test011_pam_interactive.bats | 46 ++++++ .../test012_pam_interactive_multistep.bats | 152 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100755 irods/test/scripts/test011_pam_interactive.bats create mode 100755 irods/test/scripts/test012_pam_interactive_multistep.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..a721dcbeb --- /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 -y 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/irods/test/scripts/test012_pam_interactive_multistep.bats b/irods/test/scripts/test012_pam_interactive_multistep.bats new file mode 100755 index 000000000..0f4de2f28 --- /dev/null +++ b/irods/test/scripts/test012_pam_interactive_multistep.bats @@ -0,0 +1,152 @@ +#!/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" +export CLIENT_AUTH_ERROR_EXITCODE=123 + +ssl_hash() { + openssl passwd -6 "$1" +} + +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 -y 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 + + # 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 +} + +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 os + +import irods +from irods.auth import ClientAuthError +from unittest.mock import patch + +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 + +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', 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 + +# Assert both passwords were prompted for. +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) +" + +@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" +}