Skip to content

Commit 89d8150

Browse files
committed
[#777] automate tests for pam_interactive scheme
1 parent 9554c9f commit 89d8150

8 files changed

Lines changed: 280 additions & 2 deletions

File tree

irods/auth/pam_interactive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def native_auth(self, request):
230230
def next(self, request):
231231
prompt = request.get("msg", {}).get("prompt", "")
232232
if prompt:
233-
_logger.info("Server prompt: %s", prompt)
233+
_logger.debug("Server prompt: %s", prompt)
234234

235235
server_req = request.copy()
236236
self._patch_state(server_req)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
To build, you need the PAM development library. Once installed,
3+
run the following:
4+
5+
gcc -fPIC -fno-stack-protector -o pam_clear_token.o -c main.c
6+
gcc -shared -o pam_clear_token.so pam_clear_token.o
7+
*/
8+
9+
#include <security/pam_modules.h>
10+
#include <security/pam_ext.h>
11+
#include <security/pam_appl.h>
12+
13+
#include <stdlib.h>
14+
15+
PAM_EXTERN int pam_sm_authenticate(pam_handle_t* pamh, int flags, int argc, const char** argv)
16+
{
17+
(void) flags;
18+
(void) argc;
19+
(void) argv;
20+
21+
// Clear the current auth token.
22+
pam_set_item(pamh, PAM_AUTHTOK, NULL);
23+
pam_set_item(pamh, PAM_OLDAUTHTOK, NULL);
24+
25+
return PAM_SUCCESS;
26+
}
27+
28+
PAM_EXTERN int pam_sm_setcred(pam_handle_t* pamh, int flags, int argc, const char** argv)
29+
{
30+
(void) pamh;
31+
(void) flags;
32+
(void) argc;
33+
(void) argv;
34+
35+
return PAM_SUCCESS;
36+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# This file is for testing PAM authentication with iRODS
2+
# using the pam_interactive authentication scheme.
3+
4+
# Prompt for the first password, from /etc/shadow.
5+
auth required pam_unix.so
6+
7+
# This is a custom PAM module that clears the success token. Without
8+
# this, the "auth" lines which follow are skipped.
9+
auth required /t012/pam_clear_token.so
10+
11+
# Prompt for the second password, from the user database file created
12+
# earlier. The use of "crypt=crypt" is required for this to work. It
13+
# tells the module that the passwords are encrypted.
14+
auth required pam_userdb.so db=/t012/pam_userdb crypt=crypt
15+
16+
# Do the normal user account stuff.
17+
account required pam_unix.so
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# This file is for testing PAM authentication with iRODS
2+
# using the pam_password authentication scheme.
3+
4+
auth required pam_env.so
5+
auth sufficient pam_unix.so
6+
auth requisite pam_succeed_if.so uid >= 500 quiet
7+
auth required pam_deny.so
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env bats
2+
3+
# The tests in this BATS module must be run as a (passwordless) sudo-enabled user.
4+
# It is also required that the python irodsclient be installed under irods' ~/.local environment.
5+
6+
. $BATS_TEST_DIRNAME/test_support_functions
7+
8+
setup() {
9+
[ -f /tmp/test011_flag ] || {
10+
rm -fr ~/.irods
11+
/prc/test_harness/utility/iinit.py host localhost \
12+
port 1247 \
13+
zone tempZone \
14+
user rods \
15+
password rods \
16+
17+
## Because iRODS 5+ negotiates for SSL automatically:
18+
CLIENT_JSON=~/.irods/irods_environment.json
19+
jq '.irods_client_server_policy="CS_NEG_REFUSE"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \
20+
mv $CLIENT_JSON.$$ $CLIENT_JSON
21+
22+
# if plugin installation would upgrade server, then skip test.
23+
if irods_server_package_upgradable; then
24+
skip
25+
fi
26+
27+
sudo apt install -y irods-auth-plugin-pam-interactive-{client,server}
28+
29+
setup_pam_login_for_user "rods" alice
30+
31+
# Tests require only the irods_environment.json
32+
rm -f ~/.irods/.irodsA
33+
34+
## Switch over to scheme to be tested.
35+
jq '.irods_authentication_scheme="pam_interactive"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \
36+
mv $CLIENT_JSON.$$ $CLIENT_JSON
37+
}
38+
touch /tmp/test011_flag
39+
}
40+
41+
original_test_suite()
42+
{
43+
local USER="alice"
44+
local PASSWORD="rods"
45+
sudo chpasswd <<<"$USER:$PASSWORD"
46+
python -m unittest irods.test.pam_interactive_test_must_run_manually
47+
}
48+
49+
@test "original_pam_interactive_tests" {
50+
original_test_suite
51+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env bats
2+
3+
# The tests in this BATS module must be run as a (passwordless) sudo-enabled user.
4+
# It is also required that the python irodsclient be installed under irods' ~/.local environment.
5+
6+
SKIP_IINIT_FOR_PASSWORD=yes
7+
8+
. $BATS_TEST_DIRNAME/test_support_functions
9+
10+
export TESTUSER="john"
11+
export FIRST_PASSWORD="=i;r@o\\d&s" # somerods
12+
export SECOND_PASSWORD="otherrods"
13+
export CLIENT_AUTH_ERROR_EXITCODE=123
14+
15+
ssl_hash() {
16+
openssl passwd -6 "$1"
17+
}
18+
19+
setup() {
20+
[ -f /tmp/test012_flag ] || {
21+
rm -fr ~/.irods
22+
/prc/test_harness/utility/iinit.py host localhost \
23+
port 1247 \
24+
zone tempZone \
25+
user rods \
26+
password rods \
27+
28+
sudo apt update
29+
sudo apt install -y db-util libpam0g-dev jq
30+
31+
## Because iRODS 5+ negotiates for SSL automatically:
32+
CLIENT_JSON=~/.irods/irods_environment.json
33+
jq '.irods_client_server_policy="CS_NEG_REFUSE"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \
34+
mv $CLIENT_JSON.$$ $CLIENT_JSON
35+
36+
# if plugin installation would upgrade server, then skip test.
37+
if irods_server_package_upgradable; then
38+
skip
39+
fi
40+
41+
sudo apt install -y irods-auth-plugin-pam-interactive-{client,server}
42+
SERVER_CONFIG=server_config.json
43+
44+
sudo -s <<-EOF
45+
jq '.plugin_configuration.authentication.pam_interactive = {
46+
"pam_stack_name": "pam_interactive"
47+
}' <"/etc/irods/${SERVER_CONFIG}" >"/tmp/${SERVER_CONFIG}"
48+
cp -rp "/etc/irods/${SERVER_CONFIG}"{,.orig}
49+
mv -f "/tmp/${SERVER_CONFIG}" "/etc/irods/${SERVER_CONFIG}"
50+
EOF
51+
waitsrv() {
52+
while true; do
53+
sleep 5
54+
ils >& /dev/null && break
55+
done
56+
}
57+
58+
{ sudo kill -HUP `sudo cat /tmp/irods.pid` && waitsrv; } || {
59+
echo "Couldn't properly bounce server after configuration change."; exit 1; }
60+
61+
setup_pam_login_for_user "${FIRST_PASSWORD}" $TESTUSER
62+
sudo cp $BATS_TEST_DIRNAME/files_for_test012/pam_password /etc/pam.d/irods
63+
sudo cp $BATS_TEST_DIRNAME/files_for_test012/pam_interactive /etc/pam.d/
64+
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
65+
66+
## Switch over to scheme to be tested.
67+
jq '.irods_authentication_scheme="pam_interactive"' >$CLIENT_JSON.$$ <$CLIENT_JSON && \
68+
mv $CLIENT_JSON.$$ $CLIENT_JSON
69+
}
70+
touch /tmp/test012_flag
71+
72+
# Tests require only the irods_environment.json
73+
rm -f ~/.irods/.irodsA
74+
}
75+
76+
encode_2nd_password() {
77+
db_file=/t012/pam_userdb.db
78+
sudo db_load -T -t hash "$db_file" <<<"${TESTUSER}"$'\n'"$(ssl_hash ${1})"
79+
sudo chown root:root "$db_file"
80+
sudo chmod 600 "$db_file"
81+
}
82+
83+
SCRIPT="
84+
import getpass
85+
import os
86+
87+
import irods
88+
from irods.auth import ClientAuthError
89+
from unittest.mock import patch
90+
91+
def getpass_new_callable(answers=()):
92+
class iterate_answers:
93+
def __init__(self,answers = answers):
94+
self.answers = answers
95+
self.count = 0
96+
def __call__(self,*_):
97+
count = self.count
98+
self.count += 1
99+
ans = self.answers[count]
100+
print ('*** giving answer:', ans)
101+
return ans
102+
return lambda : iterate_answers()
103+
104+
home = None
105+
106+
pw_count = 0
107+
108+
with patch(
109+
'getpass.getpass',
110+
new_callable=getpass_new_callable(answers=[os.environ['FIRST_PASSWORD'],os.environ['SECOND_PASSWORD']])
111+
) as m:
112+
try:
113+
sess = irods.helpers.make_session(test_server_version=False)
114+
sess.set_auth_option_for_scheme('pam_interactive', irods.auth.FORCE_PASSWORD_PROMPT, True)
115+
home = sess.collections.get(f'/{sess.zone}/home/{sess.username}')
116+
except ClientAuthError as exc:
117+
# Note: The write to stdout, and the specific exit code, are necessary for the test assertions.
118+
# in test "pam_interactive_test_multistep_with_incorrect_2nd_password" below.
119+
print(f'ERROR: {exc!r}')
120+
exit(int(os.environ['CLIENT_AUTH_ERROR_EXITCODE']))
121+
finally:
122+
pw_count = m.count
123+
124+
# Assert both passwords were prompted for.
125+
if pw_count < 2:
126+
print(f'************************ {pw_count = } < 2')
127+
exit(3)
128+
129+
# Assert home is defined, ie a session was successfully created and used to retrieve a collection object
130+
if home is None:
131+
exit(2)
132+
133+
username = os.environ['TESTUSER']
134+
135+
# Assert home contains the expected username.
136+
if not home.path.endswith(f'/{username}'):
137+
exit(1)
138+
"
139+
140+
@test "pam_interactive_test_multistep_with_incorrect_2nd_password" {
141+
142+
# We are using a deliberately munged password.
143+
encode_2nd_password "_${SECOND_PASSWORD}"
144+
local STATUS=""
145+
OUTPUT=$(python -c "$SCRIPT" 2>&1) || STATUS=$?
146+
147+
# Here, we assert the process's exit and output conform to expectation. We want to
148+
# enforce that the stdout output stream contains the thrown exception name ("ClientAuthError")
149+
# as well as that the process exits with a particular error status.
150+
[ $STATUS = $CLIENT_AUTH_ERROR_EXITCODE ]
151+
[[ $OUTPUT =~ ClientAuthError ]]
152+
}
153+
154+
@test "pam_interactive_test_multistep_with_correct_2nd_password" {
155+
encode_2nd_password "${SECOND_PASSWORD}"
156+
python -c "$SCRIPT"
157+
}

irods/test/scripts/test_support_functions

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ _begin_pam_environment_and_password() {
120120
echo "$ENV" > ~/.irods/irods_environment.json
121121

122122
if [ -n "$1" -a -z "$SKIP_IINIT_FOR_PASSWORD" ]; then
123-
iinit <<<"$1" 2>/tmp/iinit_as_alice.log
123+
iinit ${IINIT_TTL:+--ttl $IINIT_TTL}<<<"$1" 2>/tmp/iinit_as_alice.log
124124
fi
125125
}
126126

@@ -242,3 +242,11 @@ if relto:
242242
print(fm_tuple(svt))
243243
" $1 $2
244244
}
245+
246+
irods_server_package_upgradable() {
247+
apt update >& /dev/null
248+
if { apt list --upgradable | grep "irods-server"; } >&/dev/null; then
249+
return 0
250+
fi
251+
return 1
252+
}

test_harness/single_node/test_script_parameters

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ declare -A wrappers=(
2121
[test008_prc_write_irodsA_utility_in_native_mode.bats]=../login_auth_test.sh
2222
[test009_test_special_characters_in_pam_passwords_auth_framework.bats]=../login_auth_test.sh
2323
[test010_issue_362_rogue_chars_in_pam_password.bats]=../login_auth_test.sh
24+
[test011_pam_interactive.bats]=../login_auth_test.sh
25+
[test012_pam_interactive_multistep.bats]=../login_auth_test.sh
2426
)
2527

2628
# keys for Image and User refer to the basename after resolution to a wrapper if one is used

0 commit comments

Comments
 (0)