Skip to content

Commit dcd6c13

Browse files
d-w-moorealanking
authored andcommitted
[#517,#596,#621] generate .irodsA for pam_password and native authentication.
This commit introduces iinit-like capability to generate the .irodsA file, when not previously existing, for the pam_password authentication scheme. Also, free functions are introduced which create the .irodsA file from a cleartext password value in the native and pam_password authentication schemes.
1 parent 5e7334d commit dcd6c13

12 files changed

+296
-23
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,47 @@ the `encryption_*` and `ssl_*` options
162162
directly to the constructor as keyword arguments, even though it is
163163
required when they are placed in the environment file.
164164

165+
Creating PAM or Native Credentials File (.irodsA)
166+
-------------------------------------------------
167+
168+
Two free functions exist for creating encoded authentication files:
169+
```
170+
irods.client_init.write_native_credentials_to_secrets_file
171+
irods.client_init.write_pam_credentials_to_secrets_file
172+
```
173+
174+
Each takes a cleartext password and writes an appropriately processed version of it
175+
into an .irodsA (secrets) file in the login environment.
176+
177+
Note, in the `pam_password` case, this involves sending the cleartext password
178+
to the server (SSL should thus be enabled!) and then writing the scrambled token that
179+
returns from the transaction.
180+
181+
If an .irodsA file exists already, it will be overwritten.
182+
183+
Examples:
184+
For the `native` authentication scheme, we can use the currently set iRODS password to create .irodsA file from Python thus:
185+
186+
```python
187+
import irods.client_init as iinit
188+
iinit.write_native_credentials_to_secrets_file(irods_password)
189+
```
190+
191+
For the `pam_password` authentication scheme, we must first ensure an `irods_environment.json` file exists in the
192+
client environment (necessary for establishing SSL/TLS connection parameters as well as obtaining a PAM token from the server after connecting)
193+
and then make the call to write .irodsA using the Bash commands:
194+
195+
```bash
196+
$ cat > ~/.irods/irods_environment.json << EOF
197+
{
198+
"irods_user_name":"rods",
199+
"irods_host":"server-hostname",
200+
... [all other connection settings, including SSL parameters, needed for communication with iRODS] ...
201+
}
202+
EOF
203+
$ python -c "import irods.client_init as iinit; iinit.write_pam_credentials_to_secrets_file(pam_cleartext_password)"
204+
```
205+
165206
PAM logins
166207
----------
167208

@@ -171,6 +212,16 @@ iCommands.
171212
Caveat for iRODS 4.3+: when upgrading from 4.2, the "irods_authentication_scheme" setting must be changed from "pam" to "pam_password" in
172213
`~/.irods/irods_environment.json` for all file-based client environments.
173214

215+
To use the PRC PAM login credentials update function for the client login environment, we can set these two configuration variables:
216+
217+
```
218+
legacy_auth.pam.password_for_auto_renew "my_pam_password"
219+
legacy_auth.pam.store_password_to_environment True
220+
```
221+
222+
Optionally, the `legacy_auth.pam.time_to_live_in_hours` may also be set to determine the time-to-live for the new password.
223+
Leaving it at the default value defers this decision to the server.
224+
174225
Maintaining a connection
175226
------------------------
176227

irods/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
import logging
44
import os
55

6+
def env_filename_from_keyword_args(kwargs):
7+
try:
8+
env_file = kwargs.pop('irods_env_file')
9+
except KeyError:
10+
try:
11+
env_file = os.environ['IRODS_ENVIRONMENT_FILE']
12+
except KeyError:
13+
env_file = os.path.expanduser('~/.irods/irods_environment.json')
14+
return env_file
15+
16+
def derived_auth_filename(env_filename):
17+
if not env_filename:
18+
return ''
19+
default_irods_authentication_file = os.path.join(os.path.dirname(env_filename),'.irodsA')
20+
return os.environ.get('IRODS_AUTHENTICATION_FILE', default_irods_authentication_file)
21+
622
# This has no effect if basicConfig() was previously called.
723
logging.basicConfig()
824

irods/account.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
1+
from irods import derived_auth_filename
2+
13
class iRODSAccount(object):
24

5+
@property
6+
def derived_auth_file(self):
7+
return derived_auth_filename(self.env_file)
8+
39
def __init__(self, irods_host, irods_port, irods_user_name, irods_zone_name,
410
irods_authentication_scheme='native',
511
password=None, client_user=None,
6-
server_dn=None, client_zone=None, **kwargs):
12+
server_dn=None, client_zone=None,
13+
env_file = '',
14+
**kwargs):
15+
716

817
# Allowed overrides when cloning sessions. (Currently hostname only.)
918
for k,v in kwargs.pop('_overrides',{}).items():
1019
if k =='irods_host':
1120
irods_host = v
1221

22+
self.env_file = env_file
1323
tuplify = lambda _: _ if isinstance(_,(list,tuple)) else (_,)
1424
schemes = [_.lower() for _ in tuplify(irods_authentication_scheme)]
1525

irods/client_init.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from irods import (env_filename_from_keyword_args, derived_auth_filename)
2+
import irods.client_configuration as cfg
3+
import irods.password_obfuscation as obf
4+
import irods.helpers as h
5+
import getpass
6+
import os
7+
import sys
8+
9+
def write_native_credentials_to_secrets_file(password, **kw):
10+
env_file = env_filename_from_keyword_args(kw)
11+
auth_file = derived_auth_filename(env_file)
12+
old_mask = None
13+
try:
14+
old_mask = os.umask(0o77)
15+
open(auth_file,'w').write(obf.encode(password))
16+
finally:
17+
if old_mask is not None:
18+
os.umask(old_mask)
19+
20+
return True
21+
22+
def write_pam_credentials_to_secrets_file( password ,**kw):
23+
s = h.make_session()
24+
s.pool.account.password = password
25+
with cfg.loadlines( [dict(setting='legacy_auth.pam.password_for_auto_renew',value=None),
26+
dict(setting='legacy_auth.pam.store_password_to_environment',value=False)] ):
27+
to_encode = s.pam_pw_negotiated
28+
if to_encode:
29+
open(s.pool.account.derived_auth_file,'w').write(obf.encode(to_encode[0]))
30+
return True
31+
return False
32+
33+
if __name__ == '__main__':
34+
vector = {
35+
'pam_password': write_pam_credentials_to_secrets_file,
36+
'native': write_native_credentials_to_secrets_file
37+
}
38+
39+
if sys.argv[1] in vector:
40+
vector[sys.argv[1]](getpass.getpass(prompt=f'{sys.argv[1]} password: '))
41+
else:
42+
print('did not recognize authentication scheme argument',file = sys.stderr)

irods/connection.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -461,16 +461,14 @@ def _login_pam(self):
461461

462462
import irods.client_configuration as cfg
463463
inline_password = (self.account.authentication_scheme == self.account._original_authentication_scheme)
464-
# By default, let server determine the TTL.
465-
time_to_live_in_hours = 0
464+
time_to_live_in_hours = cfg.legacy_auth.pam.time_to_live_in_hours
466465
# For certain characters in the pam password, if they need escaping with '\' then do so.
467466
new_pam_password = PAM_PW_ESC_PATTERN.sub(lambda m: '\\'+m.group(1), self.account.password)
468-
if not inline_password:
467+
if not inline_password and cfg.legacy_auth.pam.password_for_auto_renew is not None:
469468
# Login using PAM password from .irodsA
470469
try:
471470
self._login_native()
472471
except (ex.CAT_PASSWORD_EXPIRED, ex.CAT_INVALID_USER, ex.CAT_INVALID_AUTHENTICATION) as exc:
473-
time_to_live_in_hours = cfg.legacy_auth.pam.time_to_live_in_hours
474472
if cfg.legacy_auth.pam.password_for_auto_renew:
475473
new_pam_password = cfg.legacy_auth.pam.password_for_auto_renew
476474
# Fall through and retry the native login later, after creating a new PAM password
@@ -532,8 +530,9 @@ def _login_pam(self):
532530
self._login_native(password = auth_out.result_)
533531

534532
# Store new password in .irodsA if requested.
535-
if self.account._auth_file and cfg.legacy_auth.pam.store_password_to_environment:
536-
with open(self.account._auth_file,'w') as f:
533+
auth_file = (self.account._auth_file or self.account.derived_auth_file)
534+
if auth_file and cfg.legacy_auth.pam.store_password_to_environment:
535+
with open(auth_file,'w') as f:
537536
f.write(obf.encode(auth_out.result_))
538537
logger.debug('new PAM pw write succeeded')
539538

irods/session.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,9 @@ def cleanup(self, new_host = ''):
212212
self.__configured = self.configure(**self.do_configure)
213213

214214
def _configure_account(self, **kwargs):
215-
215+
env_file = None
216216
try:
217217
env_file = kwargs['irods_env_file']
218-
219218
except KeyError:
220219
# For backward compatibility
221220
for key in ['host', 'port', 'authentication_scheme']:
@@ -234,6 +233,9 @@ def _configure_account(self, **kwargs):
234233
# Update with new keywords arguments only
235234
creds.update((key, value) for key, value in kwargs.items() if key not in creds)
236235

236+
if env_file:
237+
creds['env_file'] = env_file
238+
237239
# Get auth scheme
238240
try:
239241
auth_scheme = creds['irods_authentication_scheme']
@@ -261,10 +263,13 @@ def _configure_account(self, **kwargs):
261263
missing_file_path = []
262264
error_args = []
263265
pw = creds['password'] = self.get_irods_password(session_ = self, file_path_if_not_found = missing_file_path, **creds)
264-
if not pw and creds.get('irods_user_name') != 'anonymous':
265-
if missing_file_path:
266-
error_args += ["Authentication file not found at {!r}".format(missing_file_path[0])]
267-
raise NonAnonymousLoginWithoutPassword(*error_args)
266+
# For native authentication, a missing password should be flagged as an error for non-anonymous logins.
267+
# However, the pam_password case has its own internal checks.
268+
if auth_scheme.lower() not in PAM_AUTH_SCHEMES:
269+
if not pw and creds.get('irods_user_name') != 'anonymous':
270+
if missing_file_path:
271+
error_args += ["Authentication file not found at {!r}".format(missing_file_path[0])]
272+
raise NonAnonymousLoginWithoutPassword(*error_args)
268273

269274
return iRODSAccount(**creds)
270275

irods/test/PRC_issue_362.bats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# The tests in this BATS module must be run as a (passwordless) sudo-enabled user.
22
# It is also required that the python irodsclient be installed under irods' ~/.local environment.
33

4-
. $BATS_TEST_DIRNAME/scripts/funcs
4+
. $BATS_TEST_DIRNAME/scripts/test_support_functions
55

66
setup() {
77

irods/test/helpers.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from irods.session import iRODSSession
2222
from irods.message import (iRODSMessage, IRODS_VERSION)
2323
from irods.password_obfuscation import encode
24+
from irods import env_filename_from_keyword_args
2425
from six.moves import range
2526

2627
class iRODSUserLogins(object):
@@ -148,7 +149,6 @@ def recast(k):
148149
os.chmod(auth,0o600)
149150
return (config, auth)
150151

151-
152152
# Create a connection for test, based on ~/.irods environment by default.
153153

154154
def make_session(test_server_version = True, **kwargs):
@@ -166,13 +166,7 @@ def make_session(test_server_version = True, **kwargs):
166166
**kwargs: Keyword arguments. Fed directly to the iRODSSession
167167
constructor. """
168168

169-
try:
170-
env_file = kwargs.pop('irods_env_file')
171-
except KeyError:
172-
try:
173-
env_file = os.environ['IRODS_ENVIRONMENT_FILE']
174-
except KeyError:
175-
env_file = os.path.expanduser('~/.irods/irods_environment.json')
169+
env_file = env_filename_from_keyword_args( kwargs )
176170
session = iRODSSession( irods_env_file = env_file, **kwargs )
177171
if test_server_version:
178172
connected_version = session.server_version[:3]

irods/test/pam.bats/test001_pam_password_expiration.bats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bats
22

3-
. "$BATS_TEST_DIRNAME"/funcs
3+
. "$BATS_TEST_DIRNAME"/test_support_functions
44
PYTHON=python3
55

66
# Setup/prerequisites are same as for login_auth_test.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bats
2+
#
3+
# Test creation of .irodsA for iRODS native authentication using the free function,
4+
# irods.client_init.write_pam_credentials_to_secrets_file
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+
13+
ALICES_OLD_PAM_PASSWD="test123"
14+
ALICES_NEW_PAM_PASSWD="new_pass"
15+
16+
setup()
17+
{
18+
setup_pam_login_for_alice "$ALICES_OLD_PAM_PASSWD"
19+
}
20+
21+
teardown()
22+
{
23+
finalize_pam_login_for_alice
24+
test_specific_cleanup
25+
}
26+
27+
@test create_secrets_file {
28+
29+
# Old .irodsA is already created, so we delete it and alter the pam password.
30+
sudo chpasswd <<<"alice:$ALICES_NEW_PAM_PASSWD"
31+
rm -f ~/.irods/.irodsA
32+
$PYTHON -c "import irods.client_init; irods.client_init.write_pam_credentials_to_secrets_file('$ALICES_NEW_PAM_PASSWD')"
33+
34+
# Define the core Python to be run, basically a minimal code block ensuring that we can authenticate to iRODS
35+
# without an exception being raised.
36+
37+
local SCRIPT="
38+
import irods.test.helpers as h
39+
ses = h.make_session()
40+
ses.collections.get(h.home_collection(ses))
41+
print ('env_auth_scheme=%s' % ses.pool.account._original_authentication_scheme)
42+
"
43+
OUTPUT=$($PYTHON -c "$SCRIPT")
44+
# Assert passing value
45+
[ $OUTPUT = "env_auth_scheme=pam_password" ]
46+
47+
}

0 commit comments

Comments
 (0)