Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enable client to create PAM credentials auth_file #517

Closed
d-w-moore opened this issue Feb 10, 2024 · 13 comments
Closed

enable client to create PAM credentials auth_file #517

d-w-moore opened this issue Feb 10, 2024 · 13 comments
Assignees
Milestone

Comments

@d-w-moore
Copy link
Collaborator

d-w-moore commented Feb 10, 2024

As of v2.0.0, the Python client is capable of renewing the PAM credentials in the client environment secrets file (.irodsA), in the event of e.g. an expired time-to-live or even if it simply did not previously exist

@alanking alanking added this to the 2.1.0 milestone Feb 12, 2024
@IanVermes
Copy link

Also interested in this change.

@d-w-moore d-w-moore self-assigned this May 3, 2024
@d-w-moore
Copy link
Collaborator Author

d-w-moore commented May 3, 2024

The way this issue is framed, it deals with PAM credentials in particular ... so I'm wondering if the community interest exists (or should apply) to the native login case too. If so, we'd need another configuration option to store the native password - to enable PRC's automatic generation of .irodsA for that case as well. @IanVermes @trel any opinion?

@trel
Copy link
Member

trel commented May 3, 2024

you're proposing a configuration option for each login type? rather than having one that both would honor?

can you propose a name for this one (or two) configuration option(s)?

can we get away with having no new user-facing configuration options?

@IanVermes
Copy link

IanVermes commented May 4, 2024

I was hoping for an official, Pythonic way of emulating the IRODs program iinit when used with a PAM configuration. Ie. mimicking it. If there was official support from this python-irodsclient that would be nice.

Here is how I have replicated the functionality of iinit: ideally I would have liked my iinit_via_python function to call _persist_irods_auth_file with minimal complexity.

However to be able to create a valid .irodsA file I need to read and persist the iRODSSession.pam_pw_negotiated attribute value to be able to use it in later function calls. Unfortunately that attribute on the iRODSSession object is not stable and will be masked or null (see _freeze_global_pam_pw_negotiated function below). I resort to using a global variable (yes - I agree this is horrible and hope to replace it).

Despite this, the .irodsA file this code generates is compliant with the itools my colleagues and team use; which was the goal.

I'm using python-irodsclient = "1.1.5"

import typing as t
import warnings
...
from irods.exception import iRODSException
from irods import password_obfuscation
from irods.session import iRODSSession
...

...
__GLOBAL_PAM_PW_NEGOTIATED = None
...

def iinit_via_python(
    password: str,
    irods_env_file: t.Union[str, Path],
    ssl_ca_certificate_file: t.Union[str, Path],
):
    """
    Execute the iinit command via the irods-python library.
    """

    # Cast the iRODS environment file and SSL CA certificate file to strings.
    irods_env_file = str(irods_env_file)
    ssl_ca_certificate_file = str(ssl_ca_certificate_file)

    # NB: An explict ssl certificate file kwarg will override the value in the
    # irods_env_file
    with get_irods_session(
        password=password,
        irods_env_file=irods_env_file,
        ssl_ca_certificate_file=ssl_ca_certificate_file,
    ) as session:
        token_list = session.pam_pw_negotiated
        logging.info(f"Received iRODS auth token")

        # Compute flags - global variable set when session is created by context
        use_normal_token = isinstance(token_list, list) and len(token_list) > 0
        use_global_token = (
            __GLOBAL_PAM_PW_NEGOTIATED is not None
            and isinstance(__GLOBAL_PAM_PW_NEGOTIATED, list)
            and len(__GLOBAL_PAM_PW_NEGOTIATED) > 0
        )
        # Find and process the token
        if use_normal_token:
            token = token_list[0]
        elif not use_normal_token and use_global_token:
            logging.info(
                "iRODS session pam_pw_negotiated was reset, "
                "using frozen value from global variable"
            )
            token = __GLOBAL_PAM_PW_NEGOTIATED[0]
        else:
            raise ValueError("No token returned from iRODS.")
        encoded_password = password_obfuscation.encode(token)
        _persist_irods_auth_file(encoded_password)


@contextmanager
def get_irods_session(
    password: t.Optional[str] = None,
    irods_env_file: t.Optional[t.Union[str, Path]] = None,
    ssl_ca_certificate_file: t.Optional[t.Union[str, Path]] = None,
) -> t.Generator[iRODSSession, None, None]:
    """
    Get an iRODS session object within a managed context. If the iRODS environment file or SSL CA
    certificate file are not provided, their default locations will be used. If
    the files are not found, an error will be raised.

    Raises:
        MyAppError: If the iRODS session is not authenticated and connected.

    Optional parameters:

    - password: The iRODS password, if absent the session will try to
        authenticate with an existing token.
    - irods_env_file: The path to the iRODS environment file, e.g.
      ~/.irods/irods_environment.json. If absent, the default location will be
      used.
    - ssl_ca_certificate_file: The path to the SSL CA certificate file used. If
      absent, the value in the irods_env_file will be used, or falling back to
      the default location.

    Usage:
    >>> with get_irods_session() as session:
    ...     # Perform operations with the session
    ...     pass
    """
    irods_env_file = _resolve_irods_env_file(irods_env_file)
    ssl_ca_certificate_file = _resolve_ssl_ca_certificate_file(
        irods_env_file, ssl_ca_certificate_file
    )

    # Prepare the session keyword arguments.
    kwargs = {
        "irods_env_file": str(irods_env_file),
        "ssl_context": get_ssl_context(),
        "ssl_ca_certificate_file": str(ssl_ca_certificate_file),
    }
    if password is not None:
        kwargs["password"] = password

    # Create the iRODS session and check if it is connected.
    try:
        session = iRODSSession(**kwargs)
        try:
            _freeze_global_pam_pw_negotiated(session)
            yield session
        finally:
            session.cleanup()
    except iRODSException as err:
        ... # custom handling
        raise
    except Exception as err:
        ... # no special handling
        raise
    return


def _freeze_global_pam_pw_negotiated(session: iRODSSession):
    """
    Why does this exist? Aren't globals horrible? Yes, they are but we have challenges.

    If you perform ANY query on the session object, the pam_pw_negotiated attribute will be reset and empty. So we want
    to create a global variable that will store the value of pam_pw_negotiated so that we can use it later.
    """
    warnings.warn("Not threadsafe! Encapsulate 'iinit_via_python' logic as a class to avoid global var to manage 'pam_pw_negotiated' or see if upstream solved problem", RuntimeWarning)
    global __GLOBAL_PAM_PW_NEGOTIATED
    __GLOBAL_PAM_PW_NEGOTIATED = session.pam_pw_negotiated
    return


def _persist_irods_auth_file(encoded_password: str) -> None:
    # Default iRODs home directory and authentication file paths.
    irods_auth_file = _resolve_irods_auth_file()  # An expanded user path equivalent of ~/.irods/.irodsA
    irods_dir = irods_auth_file.parent

    # Create the iRODS home directory if it does not exist.
    if not irods_dir.exists():
        logging.info(f"Creating iRODS home directory {str(irods_dir)}")
        irods_dir.mkdir(parents=True, exist_ok=True, mode=0o700)

    # Write the token to the iRODS authentication file or raise an error.
    logging.info(f"Persisting iRODS encoded auth token to {str(irods_auth_file)}")
    irods_auth_file.unlink(missing_ok=True)
    with open(irods_auth_file, "w") as auth_file:
        auth_file.write(encoded_password)
    os.chmod(irods_auth_file, 0o600)
    return

@trel
Copy link
Member

trel commented May 5, 2024

You mention

I'm using python-irodsclient = "1.1.5"

Can you confirm this is still necessary with python-irodsclient = "2.0.1"?

@d-w-moore
Copy link
Collaborator Author

you're proposing a configuration option for each login type? rather than having one that both would honor?

can you propose a name for this one (or two) configuration option(s)?

can we get away with having no new user-facing configuration options?

This is possibly just a half-baked idea of mine. It's perhaps better to go with a library and/or command that is explicitly iinit-like, which some (notably @IanVermes) have expressed a wish for. (See below). There might be a way for routine login attempts to call into that library from the client's login attempts, and that is the part that would require storing cleartext passwords as user-facing configuration. Although Ian has apparently figured out how to avoid that for PAM! (Assuming we're not covering the case of a PAM password the user has modified in the database since the last session).

@IanVermes
Copy link

IanVermes commented May 14, 2024

@trel Sorry for the delay in answering. I tried and the above approach works in python-irodsclient = "2.0.1" and python-irodsclient = "1.1.5"

@d-w-moore

Assuming we're not covering the case of a PAM password the user has modified in the database since the last session

Is this different to what one would expect from iinit with a PAM strategy? If the user's password has changed, authentication with irods tools or the python-irods client would still be attempted with the .irodsA file, even if its stale. I'd hope the server would reject the connection attempt as the token in the .irodsA file could not be cryptographically matched against a new password... but this is only a guess.

@d-w-moore
Copy link
Collaborator Author

d-w-moore commented May 14, 2024

Is this different to what one would expect from iinit with a PAM strategy?

If the user's password has changed, authentication with irods tools or the python-irods client would still be attempted with the .irodsA file, even if its stale. I'd hope the server would reject the connection attempt as the token in the .irodsA file could not be cryptographically matched against a new password... but this is only a guess.

Yes, solid security is certainly the rule : ) . Or the aim - my attempts at uninterrupted client access via PAM (ie . not requiring a user to re-enter a new password the way that iinit/icommands would if the old PAM password is expired) have thus far involved storing the PAM user's cleartext password in a configuration variable. The security of that depends on protecting a ~/.python_irodsclient containing a PAM password from being seen by other users. Whereas with your solution, the password doesn't need to be stored anywhere, only the server token given as a challenge response to the cleartext password.

@d-w-moore
Copy link
Collaborator Author

This can wait for 2.1.1 ... its testing is contingent on #502, which I've also noted should have a milestone of 2.1.1

@korydraughn korydraughn modified the milestones: 2.1.0, 2.1.1 Jun 14, 2024
d-w-moore added a commit to d-w-moore/python-irodsclient that referenced this issue Oct 11, 2024
… 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.
alanking pushed a commit that referenced this issue Oct 11, 2024
…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.
@mstfdkmn
Copy link

mstfdkmn commented Nov 4, 2024

We realize this might be a bit delayed, but wondering whether there are any plans to integrate the pam_interactive option into the PRC soon?

@korydraughn
Copy link
Contributor

Yes, the plan is to add support for pam_interactive to the PRC. We do not have a specific date for that, but the goal is to add it sooner rather than later.

@trel
Copy link
Member

trel commented Nov 4, 2024

let's create an issue for it - and link it from here.

@korydraughn
Copy link
Contributor

Created #653

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

6 participants