Skip to content
Open
36 changes: 36 additions & 0 deletions irods/test/scripts/files_for_test012/pam_clear_token.c
Original file line number Diff line number Diff line change
@@ -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 <security/pam_modules.h>
#include <security/pam_ext.h>
#include <security/pam_appl.h>

#include <stdlib.h>

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;
}
17 changes: 17 additions & 0 deletions irods/test/scripts/files_for_test012/pam_interactive
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions irods/test/scripts/files_for_test012/pam_password
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions irods/test/scripts/test011_pam_interactive.bats
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 69 additions & 0 deletions irods/test/scripts/test012.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3

Check failure on line 1 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff undocumented-public-module

undocumented-public-module: Missing docstring in public module [check:undocumented-public-module]

import getpass

Check failure on line 3 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff unused-import

unused-import: `getpass` imported but unused [check:unused-import]
import irods
import os

Check failure on line 5 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff unused-import

unused-import: `os` imported but unused [check:unused-import]
from unittest.mock import patch
from irods.auth import FORCE_PASSWORD_PROMPT

Check failure on line 7 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff unsorted-imports

unsorted-imports: Import block is un-sorted or un-formatted [check:unsorted-imports]

# Relies on preexisting irods_environment and certs copied from server:
# {

Check failure on line 10 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff commented-out-code

commented-out-code: Found commented-out code [check:commented-out-code]
# "irods_authentication_scheme": "pam_interactive",

Check failure on line 11 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff commented-out-code

commented-out-code: Found commented-out code [check:commented-out-code]
# "irods_user_name": "john",

Check failure on line 12 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff commented-out-code

commented-out-code: Found commented-out code [check:commented-out-code]
# "_irods_user_name": "rods",

Check failure on line 13 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff commented-out-code

commented-out-code: Found commented-out code [check:commented-out-code]
# "irods_client_server_negotiation": "request_server_negotiation",

Check failure on line 14 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff commented-out-code

commented-out-code: Found commented-out code [check:commented-out-code]
# "irods_client_server_policy": "CS_NEG_REQUIRE",

Check failure on line 15 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff commented-out-code

commented-out-code: Found commented-out code [check:commented-out-code]
# "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])
):

Check failure on line 60 in irods/test/scripts/test012.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
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)
91 changes: 91 additions & 0 deletions irods/test/scripts/test012_pam_interactive_multistep.bats
Original file line number Diff line number Diff line change
@@ -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)
"
}
2 changes: 1 addition & 1 deletion irods/test/scripts/test_support_functions
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
13 changes: 12 additions & 1 deletion test_harness/single_node/docker_container_driver.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

# Options:

ENV_VARS=()
IRODS_CONTROL_PATH=""
KILL_TEST_CONTAINER=1
RUN_AS_USER=""
Expand All @@ -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=<value>" where
<value> 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions test_harness/single_node/test_script_parameters
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading