Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added dummy
Empty file.
9 changes: 8 additions & 1 deletion irods/auth/pam_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -72,12 +73,17 @@
resp['user_name'] = self.conn.account.proxy_user
resp['zone_name'] = self.conn.account.proxy_zone

#TODO check handling of FORCE_PASSWORD_PROMPT -

Check failure on line 76 in irods/auth/pam_interactive.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
# 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
Expand Down Expand Up @@ -199,6 +205,7 @@
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)

Expand Down Expand Up @@ -230,7 +237,7 @@
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)
Expand Down
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 -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
}
152 changes: 152 additions & 0 deletions irods/test/scripts/test012_pam_interactive_multistep.bats
Original file line number Diff line number Diff line change
@@ -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"
}
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ select = [
# flake8-tidy-imports
"TID",
# flake8-todos
"TD",
#"TD",
# flake8-type-checking
"TC",
# flake8-unused-arguments
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