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/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/irods/test/scripts/test012.py b/irods/test/scripts/test012.py new file mode 100755 index 000000000..97a61335f --- /dev/null +++ b/irods/test/scripts/test012.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import getpass +import irods +import os +from unittest.mock import patch +from irods.auth import FORCE_PASSWORD_PROMPT + +# Relies on preexisting irods_environment and certs copied from server: +# { +# "irods_authentication_scheme": "pam_interactive", +# "irods_user_name": "john", +# "_irods_user_name": "rods", +# "irods_client_server_negotiation": "request_server_negotiation", +# "irods_client_server_policy": "CS_NEG_REQUIRE", +# "irods_connection_pool_refresh_time_in_seconds": 300, +# "irods_cwd": "/tempZone/home/rods", +# "irods_default_hash_scheme": "SHA256", +# "irods_default_number_of_transfer_threads": 4, +# "irods_default_resource": "demoResc", +# "irods_encryption_algorithm": "AES-256-CBC", +# "irods_encryption_key_size": 32, +# "irods_encryption_num_hash_rounds": 16, +# "irods_encryption_salt_size": 8, +# "irods_home": "/tempZone/home/rods", +# "irods_host": "localhost", +# "irods_match_hash_policy": "compatible", +# "irods_maximum_size_for_single_buffer_in_megabytes": 32, +# "irods_port": 1247, +# "irods_ssl_ca_certificate_file": "/home/daniel/tls_certs/irods_server.crt", +# "irods_ssl_verify_server": "none", +# "irods_transfer_buffer_size_for_parallel_transfer_in_megabytes": 4, +# "irods_zone_name": "tempZone", +# "schema_name": "service_account_environment", +# "schema_version": "v5" +# } + +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 + +FIRST_PASSWORD = r'=i;r@o\d&s' +SECOND_PASSWORD = "otherrods" +TESTUSER = 'john' + +with patch( + 'getpass.getpass', + new_callable=getpass_new_callable(answers=[FIRST_PASSWORD,SECOND_PASSWORD]) +): + 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}') + +print(f'{home.path = }') +if home is None: + exit(2) +if not home.path.endswith(f'/{TESTUSER}'): + exit(1) 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..c8de54de9 --- /dev/null +++ b/irods/test/scripts/test012_pam_interactive_multistep.bats @@ -0,0 +1,91 @@ +#!/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 + + ## 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 "${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" { +: +python -c" +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']]) +): + 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}') + +if home is None: + exit(2) +username = os.environ['TESTUSER'] +if not home.path.endswith(f'/{username}'): + exit(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/docker_container_driver.sh b/test_harness/single_node/docker_container_driver.sh index 996f54778..57a5f324d 100755 --- a/test_harness/single_node/docker_container_driver.sh +++ b/test_harness/single_node/docker_container_driver.sh @@ -12,6 +12,7 @@ # Options: +ENV_VARS=() IRODS_CONTROL_PATH="" KILL_TEST_CONTAINER=1 RUN_AS_USER="" @@ -26,6 +27,9 @@ usage() { $0 [options] /external/path/to/script" echo 'Options : + -E SETTING set in docker environment: argument is a single env setting like: "ENVVAR=" where + can may contain whitespace. "-E" may occur multiple times. + -e SETTINGS set in docker environment: argument is a list of env settings (format: VAR=VAL) separated by spaces -V (extra verbosity). Prints out useful stuff including VERSION information -u USERNAME run test in container as this USERNAME -i invoke container with -i and -t @@ -60,6 +64,13 @@ while [[ $1 = -* ]]; do elif [ "$1" = -r ]; then REMOVE_OPTION="$2" shift 2 + elif [ "$1" = -E ]; then + ENV_VARS+=(-e "$2") + shift 2 + elif [ "$1" = -e ]; then + ENV_VAR_ARRAY=($2) + ENV_VARS="${ENV_VAR_ARRAY[*]/#/-e }" + shift 2 elif [ "$1" = -w ]; then EXPLICIT_WORKDIR="$2" shift 2 @@ -126,7 +137,7 @@ INNER_MOUNT=/prc # Start the container. echo image="[$image]" -CONTAINER=$($DOCKER run -d -v "$reporoot:$INNER_MOUNT:ro" $INTERACTIVE_OPTION $REMOVE_OPTION \ +CONTAINER=$($DOCKER run "${ENV_VARS[@]}" -d -v "$reporoot:$INNER_MOUNT:ro" $INTERACTIVE_OPTION $REMOVE_OPTION \ -e "IRODS_CONTROL_PATH=$IRODS_CONTROL_PATH" $image) # Wait for iRODS and database to start up. diff --git a/test_harness/single_node/test_script_parameters b/test_harness/single_node/test_script_parameters index 4b94d58e6..f7b977296 100644 --- a/test_harness/single_node/test_script_parameters +++ b/test_harness/single_node/test_script_parameters @@ -21,6 +21,8 @@ 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 + [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