Skip to content

Commit f4ef3a1

Browse files
d-w-moorealanking
authored andcommitted
[#650] new escaped character processing.
This fixes the approach to escaping characters from the set: [@&;=] that iRODS historically has had problems accommodating in PAM passwords in the past. Presently, for iRODS 4.2 and 4.3 we only need to consider ";" and "=" as problematic characters, due to the conflict with the use of those characters in the KVP-formatted context parameter when the AUTH_PLUG_REQ_AN api is used.
1 parent a371d7a commit f4ef3a1

4 files changed

Lines changed: 91 additions & 11 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,13 @@ variables serving as overrides:
559559
- Default Value: `False`
560560
- Environment Variable Override: `PYTHON_IRODSCLIENT_CONFIG__LEGACY_AUTH__PAM__STORE_PASSWORD_TO_ENVIRONMENT`
561561

562+
- Setting: Force the use of PAM_AUTH_REQUEST_AN API for entering a new PAM password into the catalog. This API accommodates longer passwords and avoids the step of parsing a semicolon-delimited
563+
"context" parameter.
564+
- Dotted Name: `legacy_auth.pam.force_use_of_dedicated_pam_api`
565+
- Type: `bool`
566+
- Default Value: `False`
567+
- Environment Variable Override: `PYTHON_IRODSCLIENT_CONFIG__LEGACY_AUTH__PAM__FORCE_USE_OF_DEDICATED_PAM_API`
568+
562569
- Setting: Default choice of XML parser for all new threads.
563570
- Dotted Name: `connections.xml_parser_default`
564571
- Type: `str`

irods/client_configuration/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ def __init__(self):
8888
class LegacyAuth(iRODSConfiguration):
8989
__slots__ = ('pam',)
9090
class Pam(iRODSConfiguration):
91-
__slots__ = ('time_to_live_in_hours', 'password_for_auto_renew', 'store_password_to_environment')
91+
__slots__ = ('time_to_live_in_hours', 'password_for_auto_renew', 'store_password_to_environment', 'force_use_of_dedicated_pam_api')
9292
def __init__(self):
9393
self.time_to_live_in_hours = 0 # -> We default to the server's TTL preference.
9494
self.password_for_auto_renew = ''
9595
self.store_password_to_environment = False
96+
self.force_use_of_dedicated_pam_api = False
9697
def __init__(self):
9798
self.pam = self.Pam()
9899

irods/connection.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from ast import literal_eval as safe_eval
1313
import re
1414

15-
PAM_PW_ESC_PATTERN = re.compile(r'([@=&;])')
1615

1716

1817
from irods.message import (
@@ -471,8 +470,7 @@ def _login_pam(self):
471470
import irods.client_configuration as cfg
472471
inline_password = (self.account.authentication_scheme == self.account._original_authentication_scheme)
473472
time_to_live_in_hours = cfg.legacy_auth.pam.time_to_live_in_hours
474-
# For certain characters in the pam password, if they need escaping with '\' then do so.
475-
new_pam_password = PAM_PW_ESC_PATTERN.sub(lambda m: '\\'+m.group(1), self.account.password)
473+
new_pam_password = self.account.password
476474
if not inline_password and cfg.legacy_auth.pam.password_for_auto_renew is not None:
477475
# Login using PAM password from .irodsA
478476
try:
@@ -487,9 +485,13 @@ def _login_pam(self):
487485
# Login succeeded, so we're within the time-to-live and can return without error.
488486
return
489487

488+
# Some characters need to be escaped for the key-value format and parser.
489+
KVP_ESCAPED_CHARS = r'\;='
490+
kvp_escape = lambda s:''.join((fr'\{c}' if c in KVP_ESCAPED_CHARS else c) for c in s)
491+
490492
# Generate a new PAM password.
491493
ctx_user = '%s=%s' % (AUTH_USER_KEY, self.account.client_user)
492-
ctx_pwd = '%s=%s' % (AUTH_PWD_KEY, new_pam_password)
494+
ctx_pwd = '%s=%s' % (AUTH_PWD_KEY, kvp_escape(new_pam_password))
493495
ctx_ttl = '%s=%s' % (AUTH_TTL_KEY, str(time_to_live_in_hours))
494496

495497
ctx = ";".join([ctx_user, ctx_pwd, ctx_ttl])
@@ -498,10 +500,12 @@ def _login_pam(self):
498500
if getattr(self,'DISALLOWING_PAM_PLAINTEXT',True):
499501
raise PlainTextPAMPasswordError
500502

501-
# In general authentication API, a ';' and '=' in the password would be misinterpreted due to those
502-
# characters' special meaning in the context string parameter.
503-
use_dedicated_pam_api = len(ctx) >= MAX_NAME_LEN or \
504-
{';','='}.intersection(set(new_pam_password))
503+
# Normally, we use the AUTH_PLUG_REQ_AN api (generalized to handle both PAM and GSI, as evidenced in the gsi_client_auth_request() method.)
504+
# However, it has a practical limit to the number of characters in a context_ parameter (defined in packStruct as "str[MAX_NAME_LEN]").
505+
# Whereas PAM_AUTH_REQUEST_AN is an older api and defines pamPassword as a "str*" entry, with apparently no length limit.
506+
507+
use_dedicated_pam_api = cfg.legacy_auth.pam.force_use_of_dedicated_pam_api or (
508+
len(ctx) >= MAX_NAME_LEN )
505509

506510
if use_dedicated_pam_api:
507511
message_body = PamAuthRequest( pamUser = self.account.client_user,
@@ -510,10 +514,12 @@ def _login_pam(self):
510514
else:
511515
message_body = PluginAuthMessage( auth_scheme_ = PAM_AUTH_SCHEME, context_ = ctx)
512516

517+
api_name = ('PAM_AUTH_REQUEST_AN' if use_dedicated_pam_api else 'AUTH_PLUG_REQ_AN')
518+
513519
auth_req = iRODSMessage(
514520
msg_type='RODS_API_REQ',
515521
msg=message_body,
516-
int_info=api_number['PAM_AUTH_REQUEST_AN' if use_dedicated_pam_api else 'AUTH_PLUG_REQ_AN']
522+
int_info=api_number[api_name]
517523
)
518524

519525
self.send(auth_req)
@@ -545,7 +551,7 @@ def _login_pam(self):
545551
f.write(obf.encode(auth_out.result_))
546552
logger.debug('new PAM pw write succeeded')
547553

548-
logger.info("PAM authorization validated")
554+
logger.info("PAM authorization validated (via %s)", api_name)
549555

550556
def read_file(self, desc, size=-1, buffer=None):
551557
if size < 0:
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env bats
2+
3+
# Test creation and use of PAM password authentication info in .irodsA when the password itself contains
4+
# special characters (those known to have created issues in the past).
5+
6+
. "$BATS_TEST_DIRNAME"/test_support_functions
7+
PYTHON=python3
8+
9+
# Setup/prerequisites are same as for login_auth_test.
10+
# Run as ubuntu user with sudo; python_irodsclient must be installed (in either ~/.local or a virtualenv)
11+
12+
ALICES_OLD_PAM_PASSWD="test123"
13+
ALICES_NEW_PAM_PASSWD="new_&@;=_pass"
14+
15+
setup()
16+
{
17+
setup_pam_login_for_alice "$ALICES_OLD_PAM_PASSWD"
18+
}
19+
20+
teardown()
21+
{
22+
finalize_pam_login_for_alice
23+
test_specific_cleanup
24+
}
25+
26+
@test create_secrets_file {
27+
# Old .irodsA is already created, so we delete it and alter the pam password.
28+
sudo chpasswd <<<"alice:$ALICES_NEW_PAM_PASSWD"
29+
local logfile i
30+
for force_flexible_pam_login_api in False True; do
31+
logfile=/tmp/prc_logs.$((++i))
32+
sudo su - irods -c 'iadmin rpp alice'
33+
rm -f ~/.irods/.irodsA
34+
$PYTHON -c "import irods.client_init
35+
import logging.config
36+
logging.config.dictConfig(
37+
{'handlers': {'file': {'class': 'logging.FileHandler',
38+
'filename': '$logfile'}},
39+
'loggers': {'irods.connection': {'handlers': ['file'],
40+
'level': 'INFO'}},
41+
'version': 1}
42+
)
43+
irods.client_configuration.legacy_auth.pam.force_use_of_dedicated_pam_api = $force_flexible_pam_login_api
44+
irods.client_init.write_pam_credentials_to_secrets_file('$ALICES_NEW_PAM_PASSWD')"
45+
46+
# Make sure the iinit-like routines created the catalog entry for the PAM password using the algorithm we expected it to call.
47+
log_content=$(cat $logfile)
48+
declare -A pam_api=([True]=PAM_AUTH_REQUEST_AN
49+
[False]=AUTH_PLUG_REQ_AN)
50+
[[ $log_content =~ "PAM authorization validated (via ${pam_api[$force_flexible_pam_login_api]})" ]]
51+
52+
# Define the core Python to be run, basically a minimal code block ensuring that we can authenticate to iRODS
53+
# without an exception being raised.
54+
55+
local SCRIPT="
56+
import irods
57+
import irods.test.helpers as h
58+
ses = h.make_session()
59+
ses.collections.get(h.home_collection(ses))
60+
print ('env_auth_scheme=%s' % ses.pool.account._original_authentication_scheme)
61+
"
62+
OUTPUT=$($PYTHON -c "$SCRIPT")
63+
# Assert passing value
64+
[[ $OUTPUT = "env_auth_scheme=pam"* ]]
65+
done
66+
}

0 commit comments

Comments
 (0)