Skip to content

Commit

Permalink
[#517,#596,#621] generate .irodsA for pam_password and native authent…
Browse files Browse the repository at this point in the history
…ication.

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.
  • Loading branch information
d-w-moore authored and alanking committed Oct 11, 2024
1 parent 5e7334d commit dcd6c13
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 23 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,47 @@ the `encryption_*` and `ssl_*` options
directly to the constructor as keyword arguments, even though it is
required when they are placed in the environment file.

Creating PAM or Native Credentials File (.irodsA)
-------------------------------------------------

Two free functions exist for creating encoded authentication files:
```
irods.client_init.write_native_credentials_to_secrets_file
irods.client_init.write_pam_credentials_to_secrets_file
```

Each takes a cleartext password and writes an appropriately processed version of it
into an .irodsA (secrets) file in the login environment.

Note, in the `pam_password` case, this involves sending the cleartext password
to the server (SSL should thus be enabled!) and then writing the scrambled token that
returns from the transaction.

If an .irodsA file exists already, it will be overwritten.

Examples:
For the `native` authentication scheme, we can use the currently set iRODS password to create .irodsA file from Python thus:

```python
import irods.client_init as iinit
iinit.write_native_credentials_to_secrets_file(irods_password)
```

For the `pam_password` authentication scheme, we must first ensure an `irods_environment.json` file exists in the
client environment (necessary for establishing SSL/TLS connection parameters as well as obtaining a PAM token from the server after connecting)
and then make the call to write .irodsA using the Bash commands:

```bash
$ cat > ~/.irods/irods_environment.json << EOF
{
"irods_user_name":"rods",
"irods_host":"server-hostname",
... [all other connection settings, including SSL parameters, needed for communication with iRODS] ...
}
EOF
$ python -c "import irods.client_init as iinit; iinit.write_pam_credentials_to_secrets_file(pam_cleartext_password)"
```

PAM logins
----------

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

To use the PRC PAM login credentials update function for the client login environment, we can set these two configuration variables:

```
legacy_auth.pam.password_for_auto_renew "my_pam_password"
legacy_auth.pam.store_password_to_environment True
```

Optionally, the `legacy_auth.pam.time_to_live_in_hours` may also be set to determine the time-to-live for the new password.
Leaving it at the default value defers this decision to the server.

Maintaining a connection
------------------------

Expand Down
16 changes: 16 additions & 0 deletions irods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@
import logging
import os

def env_filename_from_keyword_args(kwargs):
try:
env_file = kwargs.pop('irods_env_file')
except KeyError:
try:
env_file = os.environ['IRODS_ENVIRONMENT_FILE']
except KeyError:
env_file = os.path.expanduser('~/.irods/irods_environment.json')
return env_file

def derived_auth_filename(env_filename):
if not env_filename:
return ''
default_irods_authentication_file = os.path.join(os.path.dirname(env_filename),'.irodsA')
return os.environ.get('IRODS_AUTHENTICATION_FILE', default_irods_authentication_file)

# This has no effect if basicConfig() was previously called.
logging.basicConfig()

Expand Down
12 changes: 11 additions & 1 deletion irods/account.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
from irods import derived_auth_filename

class iRODSAccount(object):

@property
def derived_auth_file(self):
return derived_auth_filename(self.env_file)

def __init__(self, irods_host, irods_port, irods_user_name, irods_zone_name,
irods_authentication_scheme='native',
password=None, client_user=None,
server_dn=None, client_zone=None, **kwargs):
server_dn=None, client_zone=None,
env_file = '',
**kwargs):


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

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

Expand Down
42 changes: 42 additions & 0 deletions irods/client_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from irods import (env_filename_from_keyword_args, derived_auth_filename)
import irods.client_configuration as cfg
import irods.password_obfuscation as obf
import irods.helpers as h
import getpass
import os
import sys

def write_native_credentials_to_secrets_file(password, **kw):
env_file = env_filename_from_keyword_args(kw)
auth_file = derived_auth_filename(env_file)
old_mask = None
try:
old_mask = os.umask(0o77)
open(auth_file,'w').write(obf.encode(password))
finally:
if old_mask is not None:
os.umask(old_mask)

return True

def write_pam_credentials_to_secrets_file( password ,**kw):
s = h.make_session()
s.pool.account.password = password
with cfg.loadlines( [dict(setting='legacy_auth.pam.password_for_auto_renew',value=None),
dict(setting='legacy_auth.pam.store_password_to_environment',value=False)] ):
to_encode = s.pam_pw_negotiated
if to_encode:
open(s.pool.account.derived_auth_file,'w').write(obf.encode(to_encode[0]))
return True
return False

if __name__ == '__main__':
vector = {
'pam_password': write_pam_credentials_to_secrets_file,
'native': write_native_credentials_to_secrets_file
}

if sys.argv[1] in vector:
vector[sys.argv[1]](getpass.getpass(prompt=f'{sys.argv[1]} password: '))
else:
print('did not recognize authentication scheme argument',file = sys.stderr)
11 changes: 5 additions & 6 deletions irods/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,16 +461,14 @@ def _login_pam(self):

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

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

Expand Down
17 changes: 11 additions & 6 deletions irods/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,9 @@ def cleanup(self, new_host = ''):
self.__configured = self.configure(**self.do_configure)

def _configure_account(self, **kwargs):

env_file = None
try:
env_file = kwargs['irods_env_file']

except KeyError:
# For backward compatibility
for key in ['host', 'port', 'authentication_scheme']:
Expand All @@ -234,6 +233,9 @@ def _configure_account(self, **kwargs):
# Update with new keywords arguments only
creds.update((key, value) for key, value in kwargs.items() if key not in creds)

if env_file:
creds['env_file'] = env_file

# Get auth scheme
try:
auth_scheme = creds['irods_authentication_scheme']
Expand Down Expand Up @@ -261,10 +263,13 @@ def _configure_account(self, **kwargs):
missing_file_path = []
error_args = []
pw = creds['password'] = self.get_irods_password(session_ = self, file_path_if_not_found = missing_file_path, **creds)
if not pw and creds.get('irods_user_name') != 'anonymous':
if missing_file_path:
error_args += ["Authentication file not found at {!r}".format(missing_file_path[0])]
raise NonAnonymousLoginWithoutPassword(*error_args)
# For native authentication, a missing password should be flagged as an error for non-anonymous logins.
# However, the pam_password case has its own internal checks.
if auth_scheme.lower() not in PAM_AUTH_SCHEMES:
if not pw and creds.get('irods_user_name') != 'anonymous':
if missing_file_path:
error_args += ["Authentication file not found at {!r}".format(missing_file_path[0])]
raise NonAnonymousLoginWithoutPassword(*error_args)

return iRODSAccount(**creds)

Expand Down
2 changes: 1 addition & 1 deletion irods/test/PRC_issue_362.bats
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 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/scripts/funcs
. $BATS_TEST_DIRNAME/scripts/test_support_functions

setup() {

Expand Down
10 changes: 2 additions & 8 deletions irods/test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from irods.session import iRODSSession
from irods.message import (iRODSMessage, IRODS_VERSION)
from irods.password_obfuscation import encode
from irods import env_filename_from_keyword_args
from six.moves import range

class iRODSUserLogins(object):
Expand Down Expand Up @@ -148,7 +149,6 @@ def recast(k):
os.chmod(auth,0o600)
return (config, auth)


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

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

try:
env_file = kwargs.pop('irods_env_file')
except KeyError:
try:
env_file = os.environ['IRODS_ENVIRONMENT_FILE']
except KeyError:
env_file = os.path.expanduser('~/.irods/irods_environment.json')
env_file = env_filename_from_keyword_args( kwargs )
session = iRODSSession( irods_env_file = env_file, **kwargs )
if test_server_version:
connected_version = session.server_version[:3]
Expand Down
2 changes: 1 addition & 1 deletion irods/test/pam.bats/test001_pam_password_expiration.bats
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bats

. "$BATS_TEST_DIRNAME"/funcs
. "$BATS_TEST_DIRNAME"/test_support_functions
PYTHON=python3

# Setup/prerequisites are same as for login_auth_test.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bats
#
# Test creation of .irodsA for iRODS native authentication using the free function,
# irods.client_init.write_pam_credentials_to_secrets_file

. "$BATS_TEST_DIRNAME"/test_support_functions
PYTHON=python3

# Setup/prerequisites are same as for login_auth_test.
# Run as ubuntu user with sudo; python_irodsclient must be installed (in either ~/.local or a virtualenv)
#

ALICES_OLD_PAM_PASSWD="test123"
ALICES_NEW_PAM_PASSWD="new_pass"

setup()
{
setup_pam_login_for_alice "$ALICES_OLD_PAM_PASSWD"
}

teardown()
{
finalize_pam_login_for_alice
test_specific_cleanup
}

@test create_secrets_file {

# Old .irodsA is already created, so we delete it and alter the pam password.
sudo chpasswd <<<"alice:$ALICES_NEW_PAM_PASSWD"
rm -f ~/.irods/.irodsA
$PYTHON -c "import irods.client_init; irods.client_init.write_pam_credentials_to_secrets_file('$ALICES_NEW_PAM_PASSWD')"

# Define the core Python to be run, basically a minimal code block ensuring that we can authenticate to iRODS
# without an exception being raised.

local SCRIPT="
import irods.test.helpers as h
ses = h.make_session()
ses.collections.get(h.home_collection(ses))
print ('env_auth_scheme=%s' % ses.pool.account._original_authentication_scheme)
"
OUTPUT=$($PYTHON -c "$SCRIPT")
# Assert passing value
[ $OUTPUT = "env_auth_scheme=pam_password" ]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bats
#
# Test creation of .irodsA for iRODS pam_password authentication using the free function,
# irods.client_init.write_native_credentials_to_secrets_file

. "$BATS_TEST_DIRNAME"/test_support_functions
PYTHON=python3

# Setup/prerequisites are same as for login_auth_test.
# Run as ubuntu user with sudo; python_irodsclient must be installed (in either ~/.local or a virtualenv)
#

@test create_irods_secrets_file {

rm -fr ~/.irods
mkdir ~/.irods
cat > ~/.irods/irods_environment.json <<-EOF
{ "irods_host":"$(hostname)",
"irods_port":1247,
"irods_user_name":"rods",
"irods_zone_name":"tempZone"
}
EOF
$PYTHON -c "import irods.client_init; irods.client_init.write_native_credentials_to_secrets_file('rods')"

# Define the core Python to be run, basically a minimal code block ensuring that we can authenticate to iRODS
# without an exception being raised.

local SCRIPT="
import irods.test.helpers as h
ses = h.make_session()
ses.collections.get(h.home_collection(ses))
print ('env_auth_scheme=%s' % ses.pool.account._original_authentication_scheme)
"
OUTPUT=$($PYTHON -c "$SCRIPT")
# Assert passing value
[ $OUTPUT = "env_auth_scheme=native" ]
}
Loading

0 comments on commit dcd6c13

Please sign in to comment.