irods / python-irodsclient

A Python API for iRODS
Other
63 stars 73 forks source link

TTL issue for pam_password with v2.0.0 #536

Closed mstfdkmn closed 5 months ago

mstfdkmn commented 5 months ago

Hi, we have an iinit snippet (iinit.exe too) that is used by our windows users to setup the necessary files to authenticate against irods. It writes the irods environment file and the .irodsA file. So that the obfuscated password file helps users grant access for 60 hours. Basically it mimics iinit of iCommands. Before the v2.0.0 all was working normal. However it seems with the v2.0.0 there is something broken in the flow. I explain it below.

iinit snippet to be executed in an interactive shell/interpreter:

config = """
{
    "irods_host": "irods.host",
    "irods_port": 1247,
    "irods_zone_name": "tempZone",
    "irods_authentication_scheme": "pam_password",
    "irods_encryption_algorithm": "AES-256-CBC",
    "irods_encryption_salt_size": 8,
    "irods_encryption_key_size": 32,
    "irods_encryption_num_hash_rounds": 8,
    "irods_user_name": "u0137480",
    "irods_ssl_ca_certificate_file": "",
    "irods_ssl_verify_server": "cert",
    "irods_client_server_negotiation": "request_server_negotiation",
    "irods_client_server_policy": "CS_NEG_REQUIRE",
    "irods_default_resource": "default",
    "irods_cwd": "/tempZone/home",
    "irods_authentication_uid": 1000
}
"""
password = "some-password...."

import os, os.path
from irods.session import iRODSSession
from irods.password_obfuscation import encode

def iinit(config, password):
    def put(file, contents):
        os.makedirs(os.path.dirname(file), exist_ok=True)
        with open(file, "w") as f:
            f.write(contents)
    env_file = os.getenv('IRODS_ENVIRONMENT_FILE', os.path.expanduser('~/.irods/irods_environment.json'))
    put(env_file, config)
    with iRODSSession(irods_env_file=env_file, password=password) as session:
        put(iRODSSession.get_irods_password_file(), encode(session.pam_pw_negotiated[0], uid=1000))
        print("You have successfully authenticated. Use iRODSSession(irods_env_file=" + repr(env_file) +") to start interacting with iRODS.")

iinit(config, password)

any script that contains session connection to be executed:

import os, os.path
from irods.session import iRODSSession

env_file = os.getenv('IRODS_ENVIRONMENT_FILE', os.path.expanduser('~/.irods/irods_environment.json'))
with iRODSSession(irods_env_file=env_file) as session:
    pass

coll = session.collections.get("/tempZone/home/u0137480")
coll.id

If the script that contains a session connection is executed immediately just after the iinit script, the flows work normal. But if it is executed later (my impression is 120 sec), we are getting the error here:

>>> env_file = os.getenv('IRODS_ENVIRONMENT_FILE', os.path.expanduser('~/.irods/irods_environment.json'))
>>> with iRODSSession(irods_env_file=env_file) as session:
...     pass
>>> coll = session.collections.get("/gbiomed/home/u0137480")
>>> coll.id
10075

>>> # Some time later (120 seconds)

>>> env_file = os.getenv('IRODS_ENVIRONMENT_FILE', os.path.expanduser('~/.irods/irods_environment.json'))
>>> with iRODSSession(irods_env_file=env_file) as session:
...           pass
>>> coll = session.collections.get("/gbiomed/home/u0137480")

Traceback (most recent call last): 
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\session.py", line 290, in server_version
return tuple(ast.literal_eval(reported_vsn))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\ast.py", line 66, in literal_eval
node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                                                                                                          File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\ast.py", line 52, in parse
return compile(source, filename, mode, flags,
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<unknown>", line 0
SyntaxError: invalid syntax
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\session.py", line 296, in __server_version
conn = next(iter(self.pool.active))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
StopIteration
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\pool.py", line 62, in get_connection
conn = self.idle.pop()
^^^^^^^^^^^^^^^
KeyError: 'pop from an empty set'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\connection.py", line 468, in _login_pam
self._login_native()
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\connection.py", line 615, in _login_native
self.recv()
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\connection.py", line 151, in recv
raise get_exception_by_code(msg.int_info, err_msg)
irods.exception.CAT_PASSWORD_EXPIRED: None
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\manager\collection_manager.py", line 21, in get
query = self.sess.query(Collection).filter(*filters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\session.py", line 268, in query
return Query(self, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\query.py", line 45, in __init__
if self.sess.server_version >= col.min_version:
^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\session.py", line 292, in server_version
return self.__server_version()
^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\session.py", line 299, in __server_version
conn = self.pool.get_connection()
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\pool.py", line 17, in method_
ret = method(self,*s,**kw)
^^^^^^^^^^^^^^^^^^^^
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\pool.py", line 78, in get_connection
conn = Connection(self, self.account)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\connection.py", line 75, in __init__
auth_module.login(self)
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\auth\pam_password.py", line 2, in login
conn._login_pam()
File "C:\Workdir\MyApps\Anaconda\envs\pam\Lib\site-packages\irods\connection.py", line 477, in _login_pam
raise RuntimeError(message)
RuntimeError: Time To Live has expired for the PAM password, and no new password is given in legacy_auth.pam.password_for_auto_renew.  Please run iinit.

Confused with this. Btw, we don't use the native authentication in our flow.

Could you look into this issue? Meanwhile, please let us know if there is any workaround or if we are doing something missing. We tried several thing but didn't work. Thanks.

korydraughn commented 5 months ago

What version of the PRC are you using? What version of iRODS are you experiencing this against?

mstfdkmn commented 5 months ago

What version of the PRC are you using?

>>> import irods
>>> irods.__version__
'2.0.0'

What version of iRODS are you experiencing this against?

>>> session.server_version
(4, 3, 1)
korydraughn commented 5 months ago

Oh right, the PRC version is in the title.

Anyway, have you adjusted your PAM TTL settings according to the following section?

If yes, can you share what you have for each option?

mstfdkmn commented 5 months ago

these are:

[irods@gbiomed ~]$ iadmin get_grid_configuration authentication password_max_time
1209600
[irods@gbiomed ~]$ iadmin get_grid_configuration authentication password_min_time
121
[irods@gbiomed ~]$ iadmin get_grid_configuration authentication password_extend_lifetime
1
peterverraedt commented 5 months ago

The real issue here is that in the v1 client the default password lifetime is 60 hours https://github.com/irods/python-irodsclient/blob/v1.1.9/irods/connection.py#L440 (although the naming of that variable is confusing).

In v2 the default is that the server should decide on the lifetime https://github.com/irods/python-irodsclient/blob/ed2e73cb79dcf08efedeed4a19f3e2a43db90f9c/irods/connection.py#L461-L462 apparently without possibility for the client to overwrite it and ask another value. And the default lifetime of the server is the minimal one, 121 seconds.

trel commented 5 months ago

Everything is more configurable with 'seconds', so that was the new standard - so I think that part is expected/desired.

trel commented 5 months ago

you're sure it was being treated as 'hours' in 1.1.9? that seems... surprising.

korydraughn commented 5 months ago

One thing that sticks out is the message at the end of stacktrace(?).

RuntimeError: Time To Live has expired for the PAM password, and no new password is given in legacy_auth.pam.password_for_auto_renew.  Please run iinit.

That is generated here: https://github.com/irods/python-irodsclient/blob/1d8433e760abbd26859873054812fee6b9a187f3/irods/connection.py#L475-L477

Seems you may want to review this section. There are several PAM related option described there and they are referenced in the code leading to that exception.

korydraughn commented 5 months ago

See the following for the full function impl. Notice the lines starting from line 470. https://github.com/irods/python-irodsclient/blob/1d8433e760abbd26859873054812fee6b9a187f3/irods/connection.py#L457-L480

peterverraedt commented 5 months ago

you're sure it was being treated as 'hours' in 1.1.9? that seems... surprising.

https://github.com/irods/irods/blob/main/plugins/database/src/db_plugin.cpp#L7102

The number passed over the wire is multiplied by 3600, so it always has been hours. So this is the regression for the python client: by default v1 attempted to generate a native password with validity of 60 hours, and v2 takes the shorter 121 seconds from server side.

peterverraedt commented 5 months ago

Actually correct code snippet is

https://github.com/irods/irods/blob/main/plugins/database/src/db_plugin.cpp#L7241-L7252

korydraughn commented 5 months ago

I think adding a settings file will allow you to make progress. The option you want to set in that file appears to be legacy_auth.pam.time_to_live_in_hours.

korydraughn commented 5 months ago

You may also need legacy_auth.pam.password_for_auto_renew.

d-w-moore commented 5 months ago

Confused with this. Btw, we don't use the native authentication in our flow.

Note this line too, showing that eventually _login_pam routes through _login_native anyway, with a transformed value it receives from the server, as part of its own internal workings. Yes , it's been that way for a while! : )

mstfdkmn commented 5 months ago

I am closing this - because we decided to use the native scheme. And apparently we are touching on an issue that existed in older versions.