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
21 changes: 19 additions & 2 deletions 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

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff missing-todo-link

missing-todo-link: Missing issue link for this TODO [check:missing-todo-link]

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff missing-todo-colon

missing-todo-colon: Missing colon in TODO [check:missing-todo-colon]
# 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

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff missing-todo-link

missing-todo-link: Missing issue link for this TODO [check:missing-todo-link]

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff missing-todo-description

missing-todo-description: Missing issue description after `TODO` [check:missing-todo-description]

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff missing-todo-colon

missing-todo-colon: Missing colon in TODO [check:missing-todo-colon]
# 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).

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff missing-todo-link

missing-todo-link: Missing issue link for this TODO [check:missing-todo-link]
if request.get(STORE_PASSWORD_IN_MEMORY):
self.depot.use_client_auth_file(None)

Expand All @@ -216,8 +223,18 @@
def native_auth(self, request):
resp = request.copy()

# TODO: removing AUTH_PASSWORD_KEY as done in C++ and irods4j clients

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff missing-todo-link

missing-todo-link: Missing issue link for this TODO [check:missing-todo-link]
AUTH_PASSWORD_KEY = "a_pw" # <--- TODO: clean up by importing

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff hardcoded-password-string

hardcoded-password-string: Possible hardcoded password assigned to: "AUTH_PASSWORD_KEY" [check:hardcoded-password-string]

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff non-lowercase-variable-in-function

non-lowercase-variable-in-function: Variable `AUTH_PASSWORD_KEY` in function should be lowercase [check:non-lowercase-variable-in-function]

resp.pop(AUTH_PASSWORD_KEY, "")

# TODO may need to define user_name and zone_name properties

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff missing-todo-colon

missing-todo-colon: Missing colon in TODO [check:missing-todo-colon]
native_auth_request = {"zone_name": resp["zone_name"],
"user_name": resp["user_name"],
"password": resp["request_result"]}

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting

# 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
Expand All @@ -230,7 +247,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 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

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])
):

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)
117 changes: 117 additions & 0 deletions irods/test/scripts/test012_pam_interactive_multistep.bats
Original file line number Diff line number Diff line change
@@ -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
}
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
Loading
Loading