Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

python-irodsclient

Package Overview
Dependencies
Maintainers
1
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

python-irodsclient - pypi Package Compare versions

Comparing version
3.2.0
to
3.3.0
+43
irods/test/client_configuration_test.py
import unittest
import irods.client_configuration as cfg
# Test assignments on the negative and positive space of the
# client configuration.
class TestClientConfigurationAttributes(unittest.TestCase):
def test_configuration_writes_and_miswrites__issue_708(self):
# For caching configuration objects
configuration_level = {}
leaf_names = []
for dotted_name, value, is_conf in cfg._var_items_as_generator(): # noqa: SLF001
with self.subTest(dotted_name=dotted_name):
name_parts = dotted_name.split('.')
namespace = '.'.join(name_parts[:-1])
attribute_name = name_parts[-1]
if isinstance(value, cfg.iRODSConfiguration):
# Store a parent object corresponding to a namespace. For any leaf value
# subsequently found in the top-down descent, to be sitting directly within
# that namespace, the "else is_conf" part of this if/else will run the core
# part of the test on the corresponding configuration setting.
configuration_level[dotted_name] = value
elif is_conf:
# A configuration setting was actually found (i.e. a "leaf" value within a dotted name.)
# Store the leaf name for proof positive of subtests actually run.
leaf_names.append(attribute_name)
# Test the positive space, i.e. the 'hit'. This simply tests that the
# setting may be written to without error:
setattr(configuration_level[namespace], attribute_name, value)
# Test the negative space, i.e. a deliberate 'miss'. This time we must fail;
# otherwise we'd get a silent miswrite in the form of a write to the incorrect attribute.
# (The new __slots__ members are there to prevent this):
with self.assertRaises(AttributeError):
setattr(configuration_level[namespace], attribute_name + '_1', value)
# These cases were identified as likely ones for possible failed writes via misspelling.
self.assertIn('irods_query_limit', leaf_names)
self.assertIn('xml_parser_default', leaf_names)
#! /usr/bin/env python
import os
import sys
import tempfile
import unittest
import textwrap
import json
import shutil
import ssl
import irods.test.helpers as helpers
from irods.connection import Connection
from irods.session import iRODSSession, NonAnonymousLoginWithoutPassword
from irods.rule import Rule
from irods.models import User
from socket import gethostname
from irods.password_obfuscation import encode as pw_encode
from irods.connection import PlainTextPAMPasswordError
from irods.access import iRODSAccess
import irods.exception as ex
import contextlib
import socket
from re import compile as regex
from typing import Dict, Optional
import gc
from irods.test.setup_ssl import create_ssl_dir
#
# Allow override to specify the PAM password in effect for the test rodsuser.
#
TEST_PAM_PW_OVERRIDE = os.environ.get("PYTHON_IRODSCLIENT_TEST_PAM_PW_OVERRIDE", "")
TEST_PAM_PW = TEST_PAM_PW_OVERRIDE or "test123"
TEST_IRODS_PW = "apass"
TEST_RODS_USER = "alissa"
from re import Pattern as regex_type
def json_file_update(fname, keys_to_delete=(), **kw):
with open(fname, "r") as f:
j = json.load(f)
j.update(**kw)
for k in keys_to_delete:
if k in j:
del j[k]
elif isinstance(k, regex_type):
jk = [i for i in j.keys() if k.search(i)]
for ky in jk:
del j[ky]
with open(fname, "w") as out:
json.dump(j, out, indent=4)
def env_dir_fullpath(authtype):
return os.path.join(os.environ["HOME"], ".irods." + authtype)
def json_env_fullpath(authtype):
return os.path.join(env_dir_fullpath(authtype), "irods_environment.json")
def secrets_fullpath(authtype):
return os.path.join(env_dir_fullpath(authtype), ".irodsA")
RODSADMIN_ENV_PATH = os.path.expanduser("~/.irods/irods_environment.json")
SERVER_ENV_SSL_SETTINGS = {
"irods_ssl_certificate_chain_file": "/etc/irods/ssl/irods.crt",
"irods_ssl_certificate_key_file": "/etc/irods/ssl/irods.key",
"irods_ssl_dh_params_file": "/etc/irods/ssl/dhparams.pem",
"irods_ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt",
"irods_ssl_verify_server": "cert",
}
CLIENT_OPTIONS_FOR_SSL = {
"irods_client_server_policy": "CS_NEG_REQUIRE",
"irods_client_server_negotiation": "request_server_negotiation",
"irods_ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt",
"irods_ssl_verify_server": "cert",
"irods_encryption_key_size": 16,
"irods_encryption_salt_size": 8,
"irods_encryption_num_hash_rounds": 16,
"irods_encryption_algorithm": "AES-256-CBC",
}
def client_env_keys_from_admin_env(user_name, auth_scheme=""):
cli_env = {}
with open(RODSADMIN_ENV_PATH) as f:
srv_env = json.load(f)
for k in ["irods_host", "irods_zone_name", "irods_port"]:
cli_env[k] = srv_env[k]
cli_env["irods_user_name"] = user_name
if auth_scheme:
cli_env["irods_authentication_scheme"] = auth_scheme
return cli_env
# For testing only!
# Note pam_password_in_plaintext* functions are for test only as they will allow transmitting passwords on a potentially
# interceptible, unencrypted channel.
@contextlib.contextmanager
def pam_password_in_plaintext_4_3(allow=True):
import irods.helpers
from irods.auth.pam_password import ENSURE_SSL_IS_ACTIVE
# We'll temporarily replace the original iRODSSession constructor with a new version which changes pam_password options
# to allow pam password SSL
old_init = iRODSSession.__init__
def new_init(self, *arg, **kw):
old_init(self, *arg, **kw)
self.set_auth_option_for_scheme("pam_password", ENSURE_SSL_IS_ACTIVE, not (allow))
with irods.helpers.temporarily_assign_attribute(iRODSSession, "__init__", new_init):
yield
@contextlib.contextmanager
def pam_password_in_plaintext_4_2(allow=True):
saved = bool(Connection.DISALLOWING_PAM_PLAINTEXT)
try:
Connection.DISALLOWING_PAM_PLAINTEXT = not (allow)
yield
finally:
Connection.DISALLOWING_PAM_PLAINTEXT = saved
@contextlib.contextmanager
def pam_password_in_plaintext(allow=True, nop=False):
if nop:
yield
return
with pam_password_in_plaintext_4_2(allow=allow):
with pam_password_in_plaintext_4_3(allow=allow):
yield
class TestLogins(unittest.TestCase):
"""
Ideally, these tests should move into CI, but that would require the server
(currently a different node than the client) to have SSL certs created and
enabled.
Until then, we require these tests to be run manually on a server node,
with:
python -m unittest "irods.test.login_auth_test[.XX[.YY]]'
Additionally:
1. The PAM/SSL tests under the TestLogins class should be run on a
single-node iRODS system, by the service account user. This ensures
the /etc/irods directory is local and writable.
2. ./setup_ssl.py (sets up SSL keys etc. in /etc/irods/ssl) should be run
first to create (or overwrite, if appropriate) the /etc/irods/ssl directory
and its contents.
3. Must add & override configuration entries in /var/lib/irods/irods_environment
Per https://slides.com/irods/ugm2018-ssl-and-pam-configuration#/3/7
"""
user_auth_envs = {
".irods.pam": {"USER": TEST_RODS_USER, "PASSWORD": TEST_PAM_PW, "AUTH": "pam"},
".irods.native": {
"USER": TEST_RODS_USER,
"PASSWORD": TEST_IRODS_PW,
"AUTH": "native",
},
}
env_save: Dict[str, Optional[str]] = {}
@contextlib.contextmanager
def setenv(self, var, newvalue):
try:
self.env_save[var] = os.environ.get(var, None)
os.environ[var] = newvalue
yield newvalue
finally:
oldvalue = self.env_save[var]
if oldvalue is None:
del os.environ[var]
else:
os.environ[var] = oldvalue
def create_env_dirs(self):
dirs = {}
retval = []
# -- create environment configurations and secrets
with pam_password_in_plaintext():
for dirname, lookup in self.user_auth_envs.items():
if lookup["AUTH"] in ("pam", "pam_password"):
ses = iRODSSession(
host=gethostname(),
user=lookup["USER"],
zone="tempZone",
authentication_scheme=lookup["AUTH"],
password=lookup["PASSWORD"],
port=1247,
**(
{**SERVER_ENV_SSL_SETTINGS, **CLIENT_OPTIONS_FOR_SSL}
if self.admin.server_version >= (5,)
else {}
),
)
try:
pam_hashes = ses.pam_pw_negotiated
except AttributeError:
pam_hashes = []
if not pam_hashes:
print("Warning ** PAM pw couldnt be generated")
break
scrambled_pw = pw_encode(pam_hashes[0])
# elif lookup['AUTH'] == 'XXXXXX': # TODO: insert other authentication schemes here
elif lookup["AUTH"] in ("native", "", None):
scrambled_pw = pw_encode(lookup["PASSWORD"])
cl_env = client_env_keys_from_admin_env(TEST_RODS_USER)
if lookup.get("AUTH", None) is not None: # - specify auth scheme only if given
cl_env["irods_authentication_scheme"] = lookup["AUTH"]
dirbase = os.path.join(os.environ["HOME"], dirname)
dirs[dirbase] = {"secrets": scrambled_pw, "client_environment": cl_env}
# -- create the environment directories and write into them the configurations just created
for absdir in dirs.keys():
shutil.rmtree(absdir, ignore_errors=True)
os.mkdir(absdir)
with open(os.path.join(absdir, "irods_environment.json"), "w") as envfile:
envfile.write("{}")
json_file_update(envfile.name, **dirs[absdir]["client_environment"])
with open(os.path.join(absdir, ".irodsA"), "w") as secrets_file:
secrets_file.write(dirs[absdir]["secrets"])
os.chmod(secrets_file.name, 0o600)
retval = dirs.keys()
return retval
PAM_SCHEME_STRING = "pam"
@classmethod
def setUpClass(cls):
import irods.client_configuration as cfg
cls.admin = helpers.make_session()
if cls.admin.server_version >= (4, 3) and not cfg.legacy_auth.force_legacy_auth:
cls.PAM_SCHEME_STRING = cls.user_auth_envs[".irods.pam"]["AUTH"] = "pam_password"
@classmethod
def tearDownClass(cls):
cls.admin.cleanup()
def setUp(self):
super(TestLogins, self).setUp()
def tearDown(self):
for envdir in getattr(self, "envdirs", []):
shutil.rmtree(envdir, ignore_errors=True)
super(TestLogins, self).tearDown()
def validate_session(self, session, verbose=False, **options):
# - try to get the home collection
home_coll = "/{0.zone}/home/{0.username}".format(session)
self.assertTrue(session.collections.get(home_coll).path == home_coll)
if verbose:
print(home_coll)
# - check user is as expected
self.assertEqual(session.username, TEST_RODS_USER)
# - check socket type (normal vs SSL) against whether ssl requested
use_ssl = options.pop("ssl", None)
if use_ssl is not None:
my_connect = [s for s in (session.pool.active | session.pool.idle)][0]
self.assertEqual(bool(use_ssl), my_connect.socket.__class__ is ssl.SSLSocket)
@contextlib.contextmanager
def _setup_rodsuser_and_optional_pw(self, name, make_irods_pw=False):
try:
self.admin.users.create(name, "rodsuser")
if make_irods_pw:
self.admin.users.modify(name, "password", TEST_IRODS_PW)
yield
finally:
self.admin.users.remove(name)
def tst0(self, ssl_opt, auth_opt, env_opt, name=TEST_RODS_USER, make_irods_pw=False):
session = None
_auth_opt = auth_opt
if auth_opt in ("pam", "pam_password"):
auth_opt = self.PAM_SCHEME_STRING
with self._setup_rodsuser_and_optional_pw(name=name, make_irods_pw=make_irods_pw):
self.envdirs = self.create_env_dirs()
if not self.envdirs:
raise RuntimeError("Could not create one or more client environments")
auth_opt_explicit = "native" if _auth_opt == "" else _auth_opt
verbosity = False
# verbosity='' # -- debug - sanity check by printing out options applied
out = {"": ""}
if env_opt:
with (
self.setenv("IRODS_ENVIRONMENT_FILE", json_env_fullpath(auth_opt_explicit)) as env_file,
self.setenv("IRODS_AUTHENTICATION_FILE", secrets_fullpath(auth_opt_explicit)),
):
cli_env_extras = {} if not (ssl_opt) else dict(CLIENT_OPTIONS_FOR_SSL)
if auth_opt:
cli_env_extras.update(irods_authentication_scheme=auth_opt)
remove = []
else:
remove = [regex("authentication_")]
with helpers.file_backed_up(env_file):
json_file_update(env_file, keys_to_delete=remove, **cli_env_extras)
with pam_password_in_plaintext(nop=ssl_opt):
session = iRODSSession(irods_env_file=env_file)
with open(env_file) as f:
out = json.load(f)
self.validate_session(session, verbose=verbosity, ssl=ssl_opt)
session.cleanup()
out["ARGS"] = "no"
else:
session_options = {}
if auth_opt:
session_options.update(authentication_scheme=auth_opt)
if ssl_opt:
SSL_cert = CLIENT_OPTIONS_FOR_SSL["irods_ssl_ca_certificate_file"]
session_options.update(
ssl_context=ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH,
capath=None,
cadata=None,
cafile=SSL_cert,
),
**CLIENT_OPTIONS_FOR_SSL,
)
lookup = self.user_auth_envs[".irods." + ("native" if not (_auth_opt) else _auth_opt)]
with pam_password_in_plaintext(nop=ssl_opt):
session = iRODSSession(
host=gethostname(),
user=lookup["USER"],
zone="tempZone",
password=lookup["PASSWORD"],
port=1247,
**session_options,
)
out = session_options
self.validate_session(session, verbose=verbosity, ssl=ssl_opt)
session.cleanup()
out["ARGS"] = "yes"
if verbosity == "":
print("--- ssl:", ssl_opt, "/ auth:", repr(auth_opt), "/ env:", env_opt)
print(
"--- > ",
json.dumps({k: v for k, v in out.items() if k != "ssl_context"}, indent=4),
)
print("---")
if session:
session.cleanup()
return session
# == test defaulting to 'native'
def test_01(self):
self.tst0(ssl_opt=True, auth_opt="", env_opt=False, make_irods_pw=True)
def test_02(self):
self.tst0(ssl_opt=False, auth_opt="", env_opt=False, make_irods_pw=True)
def test_03(self):
self.tst0(ssl_opt=True, auth_opt="", env_opt=True, make_irods_pw=True)
def test_04(self):
self.tst0(ssl_opt=False, auth_opt="", env_opt=True, make_irods_pw=True)
# == test explicit scheme 'native'
def test_1(self):
self.tst0(ssl_opt=True, auth_opt="native", env_opt=False, make_irods_pw=True)
def test_2(self):
self.tst0(ssl_opt=False, auth_opt="native", env_opt=False, make_irods_pw=True)
def test_3(self):
self.tst0(ssl_opt=True, auth_opt="native", env_opt=True, make_irods_pw=True)
def test_4(self):
self.tst0(ssl_opt=False, auth_opt="native", env_opt=True, make_irods_pw=True)
# == test explicit scheme 'pam'
def test_5(self):
self.tst0(ssl_opt=True, auth_opt="pam", env_opt=False)
def test_6(self):
if self.admin.server_version >= (5,):
self.skipTest("iRODS 5 does not permit sending the raw PAM password on an unencrypted connection.")
try:
session = self.tst0(ssl_opt=False, auth_opt="pam", env_opt=False)
except PlainTextPAMPasswordError:
pass
else:
# -- no exception raised (this is expected behavior in 4.3+ with the new authentication framework,
# but for 4.2 and previous, we expect the PlainTextPAMPasswordError to be raised.
if session.server_version_without_auth() < (4, 3):
self.fail("PlainTextPAMPasswordError should have been raised")
def test_7(self):
self.tst0(ssl_opt=True, auth_opt="pam", env_opt=True)
def test_8(self):
self.tst0(ssl_opt=False, auth_opt="pam", env_opt=True)
@unittest.skipUnless(
TEST_PAM_PW_OVERRIDE,
"Skipping unless pam password is overridden (e.g. to test special characters)",
)
def test_escaped_pam_password_chars__362(self):
with self._setup_rodsuser_and_optional_pw(name=TEST_RODS_USER):
context = ssl._create_unverified_context(
purpose=ssl.Purpose.SERVER_AUTH,
capath=None,
cadata=None,
cafile=None,
)
ssl_settings = {
"client_server_negotiation": "request_server_negotiation",
"client_server_policy": "CS_NEG_REQUIRE",
"encryption_algorithm": "AES-256-CBC",
"encryption_key_size": 32,
"encryption_num_hash_rounds": 16,
"encryption_salt_size": 8,
"ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt",
"ssl_context": context,
}
irods_session = iRODSSession(
host=self.admin.host,
port=self.admin.port,
zone=self.admin.zone,
user=TEST_RODS_USER,
password=TEST_PAM_PW_OVERRIDE,
authentication_scheme="pam",
**ssl_settings,
)
home_coll = "/{0.zone}/home/{0.username}".format(irods_session)
self.assertEqual(irods_session.collections.get(home_coll).path, home_coll)
class TestAnonymousUser(unittest.TestCase):
def setUp(self):
admin = self.admin = helpers.make_session()
user = self.user = admin.users.create("anonymous", "rodsuser", admin.zone)
self.home = "/{admin.zone}/home/{user.name}".format(**locals())
admin.collections.create(self.home)
acl = iRODSAccess("own", self.home, user.name)
admin.acls.set(acl, admin=True)
self.env_file = os.path.expanduser("~/.irods.anon/irods_environment.json")
self.env_dir = os.path.dirname(self.env_file)
self.auth_file = os.path.expanduser("~/.irods.anon/.irodsA")
os.mkdir(os.path.dirname(self.env_file))
json.dump(
{
"irods_host": admin.host,
"irods_port": admin.port,
"irods_user_name": user.name,
"irods_zone_name": admin.zone,
},
open(self.env_file, "w"),
indent=4,
)
def tearDown(self):
self.admin.collections.remove(self.home, recurse=True, force=True)
self.admin.users.remove(self.user.name)
shutil.rmtree(self.env_dir, ignore_errors=True)
def test_login_from_environment(self):
orig_env = os.environ.copy()
try:
os.environ["IRODS_ENVIRONMENT_FILE"] = self.env_file
os.environ["IRODS_AUTHENTICATION_FILE"] = self.auth_file
ses = helpers.make_session()
ses.collections.get(self.home)
finally:
os.environ.clear()
os.environ.update(orig_env)
class TestMiscellaneous(unittest.TestCase):
def test_nonanonymous_login_without_auth_file_fails__290(self):
ses = self.admin
if ses.users.get(ses.username).type != "rodsadmin":
self.skipTest("Only a rodsadmin may run this test.")
try:
ENV_DIR = tempfile.mkdtemp()
ses.users.create("bob", "rodsuser")
ses.users.modify("bob", "password", "bpass")
d = dict(
password="bpass",
user="bob",
host=ses.host,
port=ses.port,
zone=ses.zone,
)
(bob_env, bob_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d)
login_options = {
"irods_env_file": bob_env,
"irods_authentication_file": bob_auth,
}
with helpers.make_session(**login_options) as s:
s.users.get("bob")
os.unlink(bob_auth)
# -- Check that we raise an appropriate exception pointing to the missing auth file path --
with self.assertRaisesRegex(NonAnonymousLoginWithoutPassword, bob_auth):
with helpers.make_session(**login_options) as s:
s.users.get("bob")
finally:
try:
shutil.rmtree(ENV_DIR, ignore_errors=True)
ses.users.get("bob").remove()
except ex.UserDoesNotExist:
pass
def setUp(self):
admin = self.admin = helpers.make_session()
if admin.users.get(admin.username).type != "rodsadmin":
self.skipTest("need admin privilege")
admin.users.create("alice", "rodsuser")
def tearDown(self):
self.admin.users.remove("alice")
self.admin.cleanup()
def test_destruct_session_with_no_pool_315(self):
destruct_flag = [False]
class mySess(iRODSSession):
def __del__(self):
self.pool = None
super(mySess, self).__del__() # call parent destructor(s) - will raise
# an error before the #315 fix
destruct_flag[:] = [True]
admin = self.admin
admin.users.modify("alice", "password", "apass")
my_sess = mySess(
user="alice",
password="apass",
host=admin.host,
port=admin.port,
zone=admin.zone,
)
my_sess.cleanup()
del my_sess
gc.collect()
self.assertEqual(destruct_flag, [True])
def test_non_anon_native_login_omitting_password_fails_1__290(self):
# rodsuser with password unset
with self.assertRaises(ex.CAT_INVALID_USER):
self._non_anon_native_login_omitting_password_fails_N__290()
def test_non_anon_native_login_omitting_password_fails_2__290(self):
# rodsuser with a password set
self.admin.users.modify("alice", "password", "apass")
with self.assertRaises(ex.CAT_INVALID_AUTHENTICATION):
self._non_anon_native_login_omitting_password_fails_N__290()
def _non_anon_native_login_omitting_password_fails_N__290(self):
admin = self.admin
with iRODSSession(zone=admin.zone, port=admin.port, host=admin.host, user="alice") as alice:
alice.collections.get(helpers.home_collection(alice))
class TestWithSSL(unittest.TestCase):
"""
The tests within this class should be run by an account other than the
service account. Otherwise there is risk of corrupting the server setup.
"""
def setUp(self):
if os.path.expanduser("~") == "/var/lib/irods":
self.skipTest("TestWithSSL may not be run by user irods")
if not os.path.exists("/etc/irods/ssl"):
self.skipTest("Running setup_ssl.py as irods user is prerequisite for this test.")
with helpers.make_session() as session:
if not session.host in ("localhost", socket.gethostname()):
self.skipTest("Test must be run co-resident with server")
def test_ssl_with_server_verify_set_to_none_281(self):
env_file = os.path.expanduser("~/.irods/irods_environment.json")
my_ssl_directory = ""
try:
with helpers.file_backed_up(env_file):
with open(env_file) as env_file_handle:
env = json.load(env_file_handle)
my_ssl_directory = tempfile.mkdtemp(dir=os.path.expanduser("~"))
# Elect for efficiency in DH param generation, eg. when setting up for testing.
create_ssl_dir(ssl_dir=my_ssl_directory, use_strong_primes_for_dh_generation=False)
settings_to_update = {
key: value.replace("/etc/irods/ssl", my_ssl_directory)
for key, value in env.items()
if type(value) is str and value.startswith("/etc/irods/ssl")
}
settings_to_update["irods_ssl_verify_server"] = "none"
env.update(settings_to_update)
with open(env_file, "w") as f:
json.dump(env, f)
with helpers.make_session() as session:
session.collections.get("/{session.zone}/home/{session.username}".format(**locals()))
finally:
if my_ssl_directory:
shutil.rmtree(my_ssl_directory)
if __name__ == "__main__":
# let the tests find the parent irods lib
sys.path.insert(0, os.path.abspath("../.."))
unittest.main()
#! /usr/bin/env python
import os
import sys
import tempfile
import unittest
import textwrap
import json
import shutil
import ssl
import irods.test.helpers as helpers
from irods.connection import Connection
from irods.session import iRODSSession, NonAnonymousLoginWithoutPassword
from irods.rule import Rule
from irods.models import User
from socket import gethostname
from irods.password_obfuscation import encode as pw_encode
from irods.connection import PlainTextPAMPasswordError
from irods.access import iRODSAccess
import irods.exception as ex
import contextlib
import socket
from re import compile as regex
from typing import Dict, Optional
import gc
from irods.test.setup_ssl import create_ssl_dir
#
# Allow override to specify the PAM password in effect for the test rodsuser.
#
TEST_PAM_PW_OVERRIDE = os.environ.get("PYTHON_IRODSCLIENT_TEST_PAM_PW_OVERRIDE", "")
TEST_PAM_PW = TEST_PAM_PW_OVERRIDE or "test123"
TEST_IRODS_PW = "apass"
TEST_RODS_USER = "alissa"
from re import Pattern as regex_type
def json_file_update(fname, keys_to_delete=(), **kw):
with open(fname, "r") as f:
j = json.load(f)
j.update(**kw)
for k in keys_to_delete:
if k in j:
del j[k]
elif isinstance(k, regex_type):
jk = [i for i in j.keys() if k.search(i)]
for ky in jk:
del j[ky]
with open(fname, "w") as out:
json.dump(j, out, indent=4)
def env_dir_fullpath(authtype):
return os.path.join(os.environ["HOME"], ".irods." + authtype)
def json_env_fullpath(authtype):
return os.path.join(env_dir_fullpath(authtype), "irods_environment.json")
def secrets_fullpath(authtype):
return os.path.join(env_dir_fullpath(authtype), ".irodsA")
RODSADMIN_ENV_PATH = os.path.expanduser("~/.irods/irods_environment.json")
SERVER_ENV_SSL_SETTINGS = {
"irods_ssl_certificate_chain_file": "/etc/irods/ssl/irods.crt",
"irods_ssl_certificate_key_file": "/etc/irods/ssl/irods.key",
"irods_ssl_dh_params_file": "/etc/irods/ssl/dhparams.pem",
"irods_ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt",
"irods_ssl_verify_server": "cert",
}
CLIENT_OPTIONS_FOR_SSL = {
"irods_client_server_policy": "CS_NEG_REQUIRE",
"irods_client_server_negotiation": "request_server_negotiation",
"irods_ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt",
"irods_ssl_verify_server": "cert",
"irods_encryption_key_size": 16,
"irods_encryption_salt_size": 8,
"irods_encryption_num_hash_rounds": 16,
"irods_encryption_algorithm": "AES-256-CBC",
}
def client_env_keys_from_admin_env(user_name, auth_scheme=""):
cli_env = {}
with open(RODSADMIN_ENV_PATH) as f:
srv_env = json.load(f)
for k in ["irods_host", "irods_zone_name", "irods_port"]:
cli_env[k] = srv_env[k]
cli_env["irods_user_name"] = user_name
if auth_scheme:
cli_env["irods_authentication_scheme"] = auth_scheme
return cli_env
# For testing only!
# Note pam_password_in_plaintext* functions are for test only as they will allow transmitting passwords on a potentially
# interceptible, unencrypted channel.
@contextlib.contextmanager
def pam_password_in_plaintext_4_3(allow=True):
import irods.helpers
from irods.auth.pam_password import ENSURE_SSL_IS_ACTIVE
# We'll temporarily replace the original iRODSSession constructor with a new version which changes pam_password options
# to allow pam password SSL
old_init = iRODSSession.__init__
def new_init(self, *arg, **kw):
old_init(self, *arg, **kw)
self.set_auth_option_for_scheme("pam_password", ENSURE_SSL_IS_ACTIVE, not (allow))
with irods.helpers.temporarily_assign_attribute(iRODSSession, "__init__", new_init):
yield
@contextlib.contextmanager
def pam_password_in_plaintext_4_2(allow=True):
saved = bool(Connection.DISALLOWING_PAM_PLAINTEXT)
try:
Connection.DISALLOWING_PAM_PLAINTEXT = not (allow)
yield
finally:
Connection.DISALLOWING_PAM_PLAINTEXT = saved
@contextlib.contextmanager
def pam_password_in_plaintext(allow=True, nop=False):
if nop:
yield
return
with pam_password_in_plaintext_4_2(allow=allow):
with pam_password_in_plaintext_4_3(allow=allow):
yield
class TestLogins(unittest.TestCase):
"""
Ideally, these tests should move into CI, but that would require the server
(currently a different node than the client) to have SSL certs created and
enabled.
Until then, we require these tests to be run manually on a server node,
with:
python -m unittest "irods.test.login_auth_test[.XX[.YY]]'
Additionally:
1. The PAM/SSL tests under the TestLogins class should be run on a
single-node iRODS system, by the service account user. This ensures
the /etc/irods directory is local and writable.
2. ./setup_ssl.py (sets up SSL keys etc. in /etc/irods/ssl) should be run
first to create (or overwrite, if appropriate) the /etc/irods/ssl directory
and its contents.
3. Must add & override configuration entries in /var/lib/irods/irods_environment
Per https://slides.com/irods/ugm2018-ssl-and-pam-configuration#/3/7
"""
user_auth_envs = {
".irods.pam": {"USER": TEST_RODS_USER, "PASSWORD": TEST_PAM_PW, "AUTH": "pam"},
".irods.native": {
"USER": TEST_RODS_USER,
"PASSWORD": TEST_IRODS_PW,
"AUTH": "native",
},
}
env_save: Dict[str, Optional[str]] = {}
@contextlib.contextmanager
def setenv(self, var, newvalue):
try:
self.env_save[var] = os.environ.get(var, None)
os.environ[var] = newvalue
yield newvalue
finally:
oldvalue = self.env_save[var]
if oldvalue is None:
del os.environ[var]
else:
os.environ[var] = oldvalue
def create_env_dirs(self):
dirs = {}
retval = []
# -- create environment configurations and secrets
with pam_password_in_plaintext():
for dirname, lookup in self.user_auth_envs.items():
if lookup["AUTH"] in ("pam", "pam_password"):
ses = iRODSSession(
host=gethostname(),
user=lookup["USER"],
zone="tempZone",
authentication_scheme=lookup["AUTH"],
password=lookup["PASSWORD"],
port=1247,
**(
{**SERVER_ENV_SSL_SETTINGS, **CLIENT_OPTIONS_FOR_SSL}
if self.admin.server_version >= (5,)
else {}
),
)
try:
pam_hashes = ses.pam_pw_negotiated
except AttributeError:
pam_hashes = []
if not pam_hashes:
print("Warning ** PAM pw couldnt be generated")
break
scrambled_pw = pw_encode(pam_hashes[0])
# elif lookup['AUTH'] == 'XXXXXX': # TODO: insert other authentication schemes here
elif lookup["AUTH"] in ("native", "", None):
scrambled_pw = pw_encode(lookup["PASSWORD"])
cl_env = client_env_keys_from_admin_env(TEST_RODS_USER)
if lookup.get("AUTH", None) is not None: # - specify auth scheme only if given
cl_env["irods_authentication_scheme"] = lookup["AUTH"]
dirbase = os.path.join(os.environ["HOME"], dirname)
dirs[dirbase] = {"secrets": scrambled_pw, "client_environment": cl_env}
# -- create the environment directories and write into them the configurations just created
for absdir in dirs.keys():
shutil.rmtree(absdir, ignore_errors=True)
os.mkdir(absdir)
with open(os.path.join(absdir, "irods_environment.json"), "w") as envfile:
envfile.write("{}")
json_file_update(envfile.name, **dirs[absdir]["client_environment"])
with open(os.path.join(absdir, ".irodsA"), "w") as secrets_file:
secrets_file.write(dirs[absdir]["secrets"])
os.chmod(secrets_file.name, 0o600)
retval = dirs.keys()
return retval
PAM_SCHEME_STRING = "pam"
@classmethod
def setUpClass(cls):
import irods.client_configuration as cfg
cls.admin = helpers.make_session()
if cls.admin.server_version >= (4, 3) and not cfg.legacy_auth.force_legacy_auth:
cls.PAM_SCHEME_STRING = cls.user_auth_envs[".irods.pam"]["AUTH"] = "pam_password"
@classmethod
def tearDownClass(cls):
cls.admin.cleanup()
def setUp(self):
super(TestLogins, self).setUp()
def tearDown(self):
for envdir in getattr(self, "envdirs", []):
shutil.rmtree(envdir, ignore_errors=True)
super(TestLogins, self).tearDown()
def validate_session(self, session, verbose=False, **options):
# - try to get the home collection
home_coll = "/{0.zone}/home/{0.username}".format(session)
self.assertTrue(session.collections.get(home_coll).path == home_coll)
if verbose:
print(home_coll)
# - check user is as expected
self.assertEqual(session.username, TEST_RODS_USER)
# - check socket type (normal vs SSL) against whether ssl requested
use_ssl = options.pop("ssl", None)
if use_ssl is not None:
my_connect = [s for s in (session.pool.active | session.pool.idle)][0]
self.assertEqual(bool(use_ssl), my_connect.socket.__class__ is ssl.SSLSocket)
@contextlib.contextmanager
def _setup_rodsuser_and_optional_pw(self, name, make_irods_pw=False):
try:
self.admin.users.create(name, "rodsuser")
if make_irods_pw:
self.admin.users.modify(name, "password", TEST_IRODS_PW)
yield
finally:
self.admin.users.remove(name)
def tst0(self, ssl_opt, auth_opt, env_opt, name=TEST_RODS_USER, make_irods_pw=False):
session = None
_auth_opt = auth_opt
if auth_opt in ("pam", "pam_password"):
auth_opt = self.PAM_SCHEME_STRING
with self._setup_rodsuser_and_optional_pw(name=name, make_irods_pw=make_irods_pw):
self.envdirs = self.create_env_dirs()
if not self.envdirs:
raise RuntimeError("Could not create one or more client environments")
auth_opt_explicit = "native" if _auth_opt == "" else _auth_opt
verbosity = False
# verbosity='' # -- debug - sanity check by printing out options applied
out = {"": ""}
if env_opt:
with (
self.setenv("IRODS_ENVIRONMENT_FILE", json_env_fullpath(auth_opt_explicit)) as env_file,
self.setenv("IRODS_AUTHENTICATION_FILE", secrets_fullpath(auth_opt_explicit)),
):
cli_env_extras = {} if not (ssl_opt) else dict(CLIENT_OPTIONS_FOR_SSL)
if auth_opt:
cli_env_extras.update(irods_authentication_scheme=auth_opt)
remove = []
else:
remove = [regex("authentication_")]
with helpers.file_backed_up(env_file):
json_file_update(env_file, keys_to_delete=remove, **cli_env_extras)
with pam_password_in_plaintext(nop=ssl_opt):
session = iRODSSession(irods_env_file=env_file)
with open(env_file) as f:
out = json.load(f)
self.validate_session(session, verbose=verbosity, ssl=ssl_opt)
session.cleanup()
out["ARGS"] = "no"
else:
session_options = {}
if auth_opt:
session_options.update(authentication_scheme=auth_opt)
if ssl_opt:
SSL_cert = CLIENT_OPTIONS_FOR_SSL["irods_ssl_ca_certificate_file"]
session_options.update(
ssl_context=ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH,
capath=None,
cadata=None,
cafile=SSL_cert,
),
**CLIENT_OPTIONS_FOR_SSL,
)
lookup = self.user_auth_envs[".irods." + ("native" if not (_auth_opt) else _auth_opt)]
with pam_password_in_plaintext(nop=ssl_opt):
session = iRODSSession(
host=gethostname(),
user=lookup["USER"],
zone="tempZone",
password=lookup["PASSWORD"],
port=1247,
**session_options,
)
out = session_options
self.validate_session(session, verbose=verbosity, ssl=ssl_opt)
session.cleanup()
out["ARGS"] = "yes"
if verbosity == "":
print("--- ssl:", ssl_opt, "/ auth:", repr(auth_opt), "/ env:", env_opt)
print(
"--- > ",
json.dumps({k: v for k, v in out.items() if k != "ssl_context"}, indent=4),
)
print("---")
if session:
session.cleanup()
return session
# == test defaulting to 'native'
def test_01(self):
self.tst0(ssl_opt=True, auth_opt="", env_opt=False, make_irods_pw=True)
def test_02(self):
self.tst0(ssl_opt=False, auth_opt="", env_opt=False, make_irods_pw=True)
def test_03(self):
self.tst0(ssl_opt=True, auth_opt="", env_opt=True, make_irods_pw=True)
def test_04(self):
self.tst0(ssl_opt=False, auth_opt="", env_opt=True, make_irods_pw=True)
# == test explicit scheme 'native'
def test_1(self):
self.tst0(ssl_opt=True, auth_opt="native", env_opt=False, make_irods_pw=True)
def test_2(self):
self.tst0(ssl_opt=False, auth_opt="native", env_opt=False, make_irods_pw=True)
def test_3(self):
self.tst0(ssl_opt=True, auth_opt="native", env_opt=True, make_irods_pw=True)
def test_4(self):
self.tst0(ssl_opt=False, auth_opt="native", env_opt=True, make_irods_pw=True)
# == test explicit scheme 'pam'
def test_5(self):
self.tst0(ssl_opt=True, auth_opt="pam", env_opt=False)
def test_6(self):
if self.admin.server_version >= (5,):
self.skipTest("iRODS 5 does not permit sending the raw PAM password on an unencrypted connection.")
try:
session = self.tst0(ssl_opt=False, auth_opt="pam", env_opt=False)
except PlainTextPAMPasswordError:
pass
else:
# -- no exception raised (this is expected behavior in 4.3+ with the new authentication framework,
# but for 4.2 and previous, we expect the PlainTextPAMPasswordError to be raised.
if session.server_version_without_auth() < (4, 3):
self.fail("PlainTextPAMPasswordError should have been raised")
def test_7(self):
self.tst0(ssl_opt=True, auth_opt="pam", env_opt=True)
def test_8(self):
self.tst0(ssl_opt=False, auth_opt="pam", env_opt=True)
@unittest.skipUnless(
TEST_PAM_PW_OVERRIDE,
"Skipping unless pam password is overridden (e.g. to test special characters)",
)
def test_escaped_pam_password_chars__362(self):
with self._setup_rodsuser_and_optional_pw(name=TEST_RODS_USER):
context = ssl._create_unverified_context(
purpose=ssl.Purpose.SERVER_AUTH,
capath=None,
cadata=None,
cafile=None,
)
ssl_settings = {
"client_server_negotiation": "request_server_negotiation",
"client_server_policy": "CS_NEG_REQUIRE",
"encryption_algorithm": "AES-256-CBC",
"encryption_key_size": 32,
"encryption_num_hash_rounds": 16,
"encryption_salt_size": 8,
"ssl_ca_certificate_file": "/etc/irods/ssl/irods.crt",
"ssl_context": context,
}
irods_session = iRODSSession(
host=self.admin.host,
port=self.admin.port,
zone=self.admin.zone,
user=TEST_RODS_USER,
password=TEST_PAM_PW_OVERRIDE,
authentication_scheme="pam",
**ssl_settings,
)
home_coll = "/{0.zone}/home/{0.username}".format(irods_session)
self.assertEqual(irods_session.collections.get(home_coll).path, home_coll)
class TestAnonymousUser(unittest.TestCase):
def setUp(self):
admin = self.admin = helpers.make_session()
user = self.user = admin.users.create("anonymous", "rodsuser", admin.zone)
self.home = "/{admin.zone}/home/{user.name}".format(**locals())
admin.collections.create(self.home)
acl = iRODSAccess("own", self.home, user.name)
admin.acls.set(acl, admin=True)
self.env_file = os.path.expanduser("~/.irods.anon/irods_environment.json")
self.env_dir = os.path.dirname(self.env_file)
self.auth_file = os.path.expanduser("~/.irods.anon/.irodsA")
os.mkdir(os.path.dirname(self.env_file))
json.dump(
{
"irods_host": admin.host,
"irods_port": admin.port,
"irods_user_name": user.name,
"irods_zone_name": admin.zone,
},
open(self.env_file, "w"),
indent=4,
)
def tearDown(self):
self.admin.collections.remove(self.home, recurse=True, force=True)
self.admin.users.remove(self.user.name)
shutil.rmtree(self.env_dir, ignore_errors=True)
def test_login_from_environment(self):
orig_env = os.environ.copy()
try:
os.environ["IRODS_ENVIRONMENT_FILE"] = self.env_file
os.environ["IRODS_AUTHENTICATION_FILE"] = self.auth_file
ses = helpers.make_session()
ses.collections.get(self.home)
finally:
os.environ.clear()
os.environ.update(orig_env)
class TestMiscellaneous(unittest.TestCase):
def test_nonanonymous_login_without_auth_file_fails__290(self):
ses = self.admin
if ses.users.get(ses.username).type != "rodsadmin":
self.skipTest("Only a rodsadmin may run this test.")
try:
ENV_DIR = tempfile.mkdtemp()
ses.users.create("bob", "rodsuser")
ses.users.modify("bob", "password", "bpass")
d = dict(
password="bpass",
user="bob",
host=ses.host,
port=ses.port,
zone=ses.zone,
)
(bob_env, bob_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d)
login_options = {
"irods_env_file": bob_env,
"irods_authentication_file": bob_auth,
}
with helpers.make_session(**login_options) as s:
s.users.get("bob")
os.unlink(bob_auth)
# -- Check that we raise an appropriate exception pointing to the missing auth file path --
with self.assertRaisesRegex(NonAnonymousLoginWithoutPassword, bob_auth):
with helpers.make_session(**login_options) as s:
s.users.get("bob")
finally:
try:
shutil.rmtree(ENV_DIR, ignore_errors=True)
ses.users.get("bob").remove()
except ex.UserDoesNotExist:
pass
def setUp(self):
admin = self.admin = helpers.make_session()
if admin.users.get(admin.username).type != "rodsadmin":
self.skipTest("need admin privilege")
admin.users.create("alice", "rodsuser")
def tearDown(self):
self.admin.users.remove("alice")
self.admin.cleanup()
def test_destruct_session_with_no_pool_315(self):
destruct_flag = [False]
class mySess(iRODSSession):
def __del__(self):
self.pool = None
super(mySess, self).__del__() # call parent destructor(s) - will raise
# an error before the #315 fix
destruct_flag[:] = [True]
admin = self.admin
admin.users.modify("alice", "password", "apass")
my_sess = mySess(
user="alice",
password="apass",
host=admin.host,
port=admin.port,
zone=admin.zone,
)
my_sess.cleanup()
del my_sess
gc.collect()
self.assertEqual(destruct_flag, [True])
def test_non_anon_native_login_omitting_password_fails_1__290(self):
# rodsuser with password unset
with self.assertRaises(ex.CAT_INVALID_USER):
self._non_anon_native_login_omitting_password_fails_N__290()
def test_non_anon_native_login_omitting_password_fails_2__290(self):
# rodsuser with a password set
self.admin.users.modify("alice", "password", "apass")
with self.assertRaises(ex.CAT_INVALID_AUTHENTICATION):
self._non_anon_native_login_omitting_password_fails_N__290()
def _non_anon_native_login_omitting_password_fails_N__290(self):
admin = self.admin
with iRODSSession(zone=admin.zone, port=admin.port, host=admin.host, user="alice") as alice:
alice.collections.get(helpers.home_collection(alice))
class TestWithSSL(unittest.TestCase):
"""
The tests within this class should be run by an account other than the
service account. Otherwise there is risk of corrupting the server setup.
"""
def setUp(self):
if os.path.expanduser("~") == "/var/lib/irods":
self.skipTest("TestWithSSL may not be run by user irods")
if not os.path.exists("/etc/irods/ssl"):
self.skipTest("Running setup_ssl.py as irods user is prerequisite for this test.")
with helpers.make_session() as session:
if not session.host in ("localhost", socket.gethostname()):
self.skipTest("Test must be run co-resident with server")
def test_ssl_with_server_verify_set_to_none_281(self):
env_file = os.path.expanduser("~/.irods/irods_environment.json")
my_ssl_directory = ""
try:
with helpers.file_backed_up(env_file):
with open(env_file) as env_file_handle:
env = json.load(env_file_handle)
my_ssl_directory = tempfile.mkdtemp(dir=os.path.expanduser("~"))
# Elect for efficiency in DH param generation, eg. when setting up for testing.
create_ssl_dir(ssl_dir=my_ssl_directory, use_strong_primes_for_dh_generation=False)
settings_to_update = {
key: value.replace("/etc/irods/ssl", my_ssl_directory)
for key, value in env.items()
if type(value) is str and value.startswith("/etc/irods/ssl")
}
settings_to_update["irods_ssl_verify_server"] = "none"
env.update(settings_to_update)
with open(env_file, "w") as f:
json.dump(env, f)
with helpers.make_session() as session:
session.collections.get("/{session.zone}/home/{session.username}".format(**locals()))
finally:
if my_ssl_directory:
shutil.rmtree(my_ssl_directory)
if __name__ == "__main__":
# let the tests find the parent irods lib
sys.path.insert(0, os.path.abspath("../.."))
unittest.main()
import os
import re
import signal
import subprocess
import sys
import tempfile
import irods.helpers
from irods.test import modules as test_modules
from irods.parallel import abort_parallel_transfers
OBJECT_SIZE = 4 * 1024**3
OBJECT_NAME = "data_get_issue__722"
LOCAL_TEMPFILE_NAME = "data_object_for_issue_722.dat"
def test(test_case, signal_names=("SIGTERM", "SIGINT")):
"""Creates a child process executing a long get() and ensures the process can be
terminated using SIGINT or SIGTERM.
"""
from .tools import wait_till_true
program = os.path.join(test_modules.__path__[0], os.path.basename(__file__))
for signal_name in signal_names:
with test_case.subTest(f"Testing with signal {signal_name}"):
# Call into this same module as a command. This will initiate another Python process that
# performs a lengthy data object "get" operation (see the main body of the script, below.)
process = subprocess.Popen(
[sys.executable, program],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
)
# Wait for download process to reach the point of spawning data transfer threads. In Python 3.9+ versions
# of the concurrent.futures module, these are nondaemon threads and will block the exit of the main thread
# unless measures are taken.
localfile = process.stdout.readline().strip()
# Use timeout of 10 minutes for test transfer, which should be more than enough.
test_case.assertTrue(
wait_till_true(
lambda: os.path.exists(localfile) and os.stat(localfile).st_size > OBJECT_SIZE // 2,
),
"Parallel download from data_objects.get() probably experienced a fatal error before spawning auxiliary data transfer threads.",
)
sig = getattr(signal, signal_name)
signal_offset_return_code = lambda s: 128 - s if s < 0 else s
signal_plus_128 = lambda sig: 128 + sig
# Interrupt the subprocess with the given signal.
process.send_signal(sig)
# Assert that this signal is what killed the subprocess, rather than a timed out process "wait" or a natural exit
# due to misproper or incomplete handling of the signal.
try:
translated_return_code = signal_offset_return_code(process.wait(timeout=15))
test_case.assertIn(
translated_return_code,
[1, signal_plus_128(sig)],
f"Expected subprocess return code of {signal_plus_128(sig) = }; got {translated_return_code = }",
)
except subprocess.TimeoutExpired:
test_case.fail(
"Subprocess timed out before terminating. "
"Non-daemon thread(s) probably prevented subprocess's main thread from exiting."
)
# Assert that in the case of SIGINT, the process registered a KeyboardInterrupt.
if sig == signal.SIGINT:
test_case.assertTrue(
re.search("KeyboardInterrupt", process.stderr.read()),
"Did not find expected string 'KeyboardInterrupt' in log output.",
)
if __name__ == "__main__":
# These lines are run only if the module is launched as a process.
session = irods.helpers.make_session()
hc = irods.helpers.home_collection(session)
TESTFILE_FILL = b"_" * (1024 * 1024)
object_path = f"{hc}/{OBJECT_NAME}"
# Create the object to be downloaded.
with session.data_objects.open(object_path, "w") as f:
for y in range(OBJECT_SIZE // len(TESTFILE_FILL)):
f.write(TESTFILE_FILL)
local_path = None
# Establish where (ie absolute path) to place the downloaded file, i.e. the get() target.
try:
with tempfile.NamedTemporaryFile(prefix="local_file_issue_722.dat", delete=True) as t:
local_path = t.name
# Tell the parent process the name of the local file, ie the result of the "get" from iRODS.
# That parent process is the unittest, which will use the filename to verify the threads are started
# and we're somewhere mid-transfer.
print(local_path)
sys.stdout.flush()
def handler(sig, *_):
abort_parallel_transfers()
if sig == signal.SIGTERM:
os._exit(128 + sig)
signal.signal(signal.SIGTERM, handler)
try:
# download the object
session.data_objects.get(object_path, local_path)
except KeyboardInterrupt:
abort_parallel_transfers()
raise
finally:
# Clean up, whether or not the download succeeded.
if local_path is not None and os.path.exists(local_path):
os.unlink(local_path)
if session.data_objects.exists(object_path):
session.data_objects.unlink(object_path, force=True)
import datetime
import os
import re
import signal
import subprocess
import sys
import irods.helpers
from irods.session import iRODSSession
from irods.test.helpers import unique_name
from irods.test import modules as test_modules
from irods.parallel import abort_parallel_transfers
OBJECT_SIZE = 4 * 1024**3
LOCAL_TEMPFILE_NAME = "data_object_for_issue_722.dat"
def test(test_case, signal_names=("SIGTERM", "SIGINT")):
"""Creates a child process executing a long put() and ensures the process can be terminated using SIGINT or SIGTERM."""
from .tools import wait_till_true
program = os.path.join(test_modules.__path__[0], os.path.basename(__file__))
session = getattr(test_case, "sess", None) or irods.helpers.make_session()
for signal_name in signal_names:
with test_case.subTest(f"Testing with signal {signal_name}"):
try:
# Call into this same module as a command. This will initiate another Python process that
# performs a lengthy data object "get" operation (see the main body of the script, below.)
process = subprocess.Popen(
# -k: Keep object around for replica status testing.
[sys.executable, program, "-k"],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
)
# Wait for download process to reach the point of spawning data transfer threads. In Python 3.9+ versions
# of the concurrent.futures module, these are non-daemon threads and will block the exit of the main thread
# unless measures are taken.
logical_path = process.stdout.readline().strip()
# Use timeout of 10 minutes for test transfer, which should be more than enough.
test_case.assertTrue(
wait_till_true(
lambda: (
session.data_objects.exists(logical_path)
and named_irods_data_object(session, logical_path, delete=False).data.size
> OBJECT_SIZE // 2
),
),
"Parallel download from data_objects.put() probably experienced a fatal error before spawning auxiliary data transfer threads.",
)
sig = getattr(signal, signal_name)
signal_offset_return_code = lambda s: 128 - s if s < 0 else s
signal_plus_128 = lambda sig: 128 + sig
# Interrupt the subprocess with the given signal.
process.send_signal(sig)
# Assert that this signal is what killed the subprocess, rather than a timed out process "wait" or a natural exit
# due to misproper or incomplete handling of the signal.
try:
translated_return_code = signal_offset_return_code(process.wait(timeout=15))
test_case.assertIn(
translated_return_code,
[1, signal_plus_128(sig)],
f"Expected subprocess return code of {signal_plus_128(sig) = }; got {translated_return_code = }",
)
except subprocess.TimeoutExpired:
test_case.fail(
"Subprocess timed out before terminating. "
"Non-daemon thread(s) probably prevented subprocess's main thread from exiting."
)
# Assert that in the case of SIGINT, the process registered a KeyboardInterrupt.
if sig == signal.SIGINT:
test_case.assertTrue(
re.search("KeyboardInterrupt", process.stderr.read()),
"Did not find expected string 'KeyboardInterrupt' in log output.",
)
# Assert that the status is left as not LOCKED.
test_case.assertTrue(
wait_till_true(lambda: int(session.data_objects.get(logical_path).replica_status) < 2)
)
finally:
if logical_path and (d := irods.helpers.get_data_object(session, logical_path)):
d.unlink(force=True)
class named_irods_data_object:
def __init__(self, /, session: iRODSSession, path: str = "", delete: bool = True):
self.sess = session
self.delete = delete
if not path:
path = irods.helpers.home_collection(session) + "/" + unique_name(datetime.datetime.now())
self.path = path
@property
def data(self):
return irods.helpers.get_data_object(self.sess, self.path)
def __del__(self):
if self.delete:
self.remove()
def remove(self):
if d := self.data:
d.unlink(force=True)
def create(self):
self.sess.data_objects.create(self.path)
return self
if __name__ == "__main__":
import getopt
opts, _ = getopt.getopt(sys.argv[1:], "k")
keep_data_object = "-k" in (_[0] for _ in opts)
# These lines are run only if the module is launched as a process.
test_session = irods.helpers.make_session()
hc = irods.helpers.home_collection(test_session)
TESTFILE_FILL = b"_" * (1024 * 1024)
object_path = named_irods_data_object(test_session, delete=True).create().path
local_path = object_path.split("/")[-1]
# Create the object to uploaded.
with open(local_path, "wb") as f:
for y in range(OBJECT_SIZE // len(TESTFILE_FILL)):
f.write(TESTFILE_FILL)
try:
# Tell the parent process the name of the data object logical path, the target of the "put" to iRODS.
# That parent process is the unittest, which will use the logical path to verify the threads are started
# and we're somewhere mid-transfer.
print(object_path)
sys.stdout.flush()
def handler(sig, *_):
abort_parallel_transfers()
if sig == signal.SIGTERM:
os._exit(128 + sig)
signal.signal(signal.SIGTERM, handler)
try:
# Upload the object
test_session.data_objects.put(local_path, object_path)
except KeyboardInterrupt:
abort_parallel_transfers()
raise
finally:
# Clean up, whether or not the upload succeeded.
if local_path is not None and os.path.exists(local_path):
os.unlink(local_path)
if not keep_data_object:
named_irods_data_object(test_session, path=object_path, delete=True)
import time
_clock_polling_interval = max(0.01, time.clock_getres(time.CLOCK_BOOTTIME))
LARGE_TEST_TIMEOUT = 10 * 60.0 # ten minutes.
def wait_till_true(callback, timeout=LARGE_TEST_TIMEOUT, msg=""):
"""Wait for test purposes until a condition becomes true , as determined by the
return value of the provided test function.
By default, we wait at most LARGE_TEST_TIMEOUT seconds for the callback function to return true,
and then quit or time out. Alternatively, a timeout of None translates as a request not to time out.
If the msg value passed in is a nonzero-length string, it can be used to raise a timeout exception;
otherwise timing out causes a normal exit, relaying as the return value the last value returned
from the test callback function.
"""
start_time = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
while not (truth_value := callback()):
if timeout is not None and (time.clock_gettime_ns(time.CLOCK_BOOTTIME) - start_time) * 1e-9 > timeout:
if msg:
raise TimeoutError(msg)
else:
break
time.sleep(_clock_polling_interval)
return truth_value
import unittest
import os
import json
from irods.client_init import write_pam_interactive_irodsA_file
from unittest.mock import patch
from irods.auth import ClientAuthError, FORCE_PASSWORD_PROMPT
from irods.auth.pam_interactive import (
_pam_interactive_ClientAuthState,
PERFORM_WAITING,
PERFORM_AUTHENTICATED,
PERFORM_NEXT,
)
from irods.test.helpers import make_session
from irods.auth.pam_interactive import __NEXT_OPERATION__, __FLOW_COMPLETE__
from irods.auth.pam_interactive import _auth_api_request
class PamInteractiveTest(unittest.TestCase):
def setUp(self):
# These tests assume the irods_environment.json file is set up correctly
# and that the iRODS user 'alice' exists in the 'tempZone' zone and has the password 'rods'.
# There should also be a linux user 'alice' with the same password.
self.sess = None
self.auth_client = _pam_interactive_ClientAuthState(None, None, scheme="pam_interactive")
self.env_file_path = os.path.expanduser("~/.irods/irods_environment.json")
self.auth_file_path = os.path.expanduser("~/.irods/.irodsA")
with open(self.env_file_path) as f:
env = json.load(f)
self.user = env.get("irods_user_name", "alice")
self.zone = env.get("irods_zone_name", "tempZone")
self.password = "rods"
def tearDown(self):
if self.sess:
self.sess.cleanup()
if os.path.exists(self.auth_file_path):
os.remove(self.auth_file_path)
def test_pam_interactive_login_basic(self):
with patch("getpass.getpass", return_value=self.password):
self.sess = make_session(
test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive"
)
# Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded
home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}")
self.assertEqual(home.name, self.sess.username)
def test_pam_interactive_auth_file_creation(self):
with patch("getpass.getpass", return_value=self.password):
write_pam_interactive_irodsA_file(env_file=self.env_file_path)
self.assertTrue(os.path.exists(self.auth_file_path), ".irodsA file was not created")
with patch("getpass.getpass", return_value=self.password) as mock_getpass:
self.sess = make_session(
test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive"
)
# Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded
home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}")
self.assertEqual(home.name, self.sess.username)
mock_getpass.assert_not_called()
def test_forced_interactive_flow(self):
with patch("getpass.getpass", return_value=self.password):
write_pam_interactive_irodsA_file(env_file=self.env_file_path)
self.assertTrue(os.path.exists(self.auth_file_path), ".irodsA file was not created")
with patch("getpass.getpass", return_value=self.password) as mock_getpass:
self.sess = make_session(
test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive"
)
self.sess.set_auth_option_for_scheme("pam_interactive", FORCE_PASSWORD_PROMPT, True)
# Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded
home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}")
self.assertEqual(home.name, self.sess.username)
mock_getpass.assert_called_once()
def test_failed_login_incorrect_password(self):
with patch("getpass.getpass", return_value="wrong_password"):
with self.assertRaises(ClientAuthError):
self.sess = make_session(
test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive"
)
self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") # trigger auth flow
with patch("getpass.getpass", return_value="wrong_password"):
with self.assertRaises(ClientAuthError):
write_pam_interactive_irodsA_file(env_file=self.env_file_path)
def test_get_default_value(self):
test_cases = [
("simple_path", {"msg": {"default_path": "/username"}, "pstate": {"username": "alice"}}, "alice"),
("nested_path", {"msg": {"default_path": "/user/name"}, "pstate": {"user": {"name": "alice"}}}, "alice"),
("path_does_not_exist", {"msg": {"default_path": "/user/username"}, "pstate": {"username": "alice"}}, ""),
("non_string_value", {"msg": {"default_path": "/user/id"}, "pstate": {"user": {"id": 123}}}, "123"),
("no_default_path", {"msg": {}, "pstate": {"username": "alice"}}, ""),
]
for name, request, expected in test_cases:
with self.subTest(name=name):
self.assertEqual(self.auth_client._get_default_value(request), expected)
def test_patch_state(self):
test_cases = [
(
"add_op",
{"msg": {"patch": [{"op": "add", "path": "/username", "value": "alice"}]}, "pstate": {}},
{"username": "alice"},
True,
),
(
"replace_op",
{
"msg": {"patch": [{"op": "replace", "path": "/username", "value": "rods"}]},
"pstate": {"username": "alice"},
},
{"username": "rods"},
True,
),
(
"remove_op",
{"msg": {"patch": [{"op": "remove", "path": "/username"}]}, "pstate": {"username": "rods"}},
{},
True,
),
(
"add_resp_fallback",
{"msg": {"patch": [{"op": "add", "path": "/username"}]}, "pstate": {}, "resp": "alice"},
{"username": "alice"},
True,
),
(
"replace_resp_fallback",
{
"msg": {"patch": [{"op": "replace", "path": "/username"}]},
"pstate": {"username": "rods"},
"resp": "alice",
},
{"username": "alice"},
True,
),
(
"nested_add_operation",
{'msg': {'patch': [{'op': 'add', 'path': '/user/name', 'value': 'alice'}]}, 'pstate': {'user': {}}},
{'user': {'name': 'alice'}},
True,
),
(
"resp_fallback_empty",
{'msg': {'patch': [{'op': 'add', 'path': '/username'}]}, 'pstate': {}},
{'username': ''},
True,
),
(
"no_patch_ops",
{"msg": {}, "pstate": {"username": "alice"}, "pdirty": False},
{"username": "alice"},
False,
),
]
for name, request, expected_pstate, expected_pdirty in test_cases:
with self.subTest(name=name):
self.auth_client._patch_state(request)
self.assertEqual(request["pstate"], expected_pstate)
self.assertEqual(request["pdirty"], expected_pdirty)
def test_retrieve_entry(self):
test_cases = [
("surface_value", {"msg": {"retrieve": "/user"}, "pstate": {"user": "alice"}}, True, "alice"),
(
"nested_value",
{"msg": {"retrieve": "/user/password"}, "pstate": {"user": {"password": "rods"}}},
True,
"rods",
),
("empty_value", {"msg": {"retrieve": "/user"}, "pstate": {"user": ""}}, True, ""),
("path_does_not_exist", {"msg": {"retrieve": "/missing"}, "pstate": {"user": "alice"}}, True, ""),
("non_string_value", {'msg': {'retrieve': '/user/id'}, 'pstate': {'user': {'id': 456}}}, True, '456'),
("no_retrieve_key", {"msg": {}, "pstate": {"user": "alice"}}, False, None),
]
for name, request, expected_result, expected_resp in test_cases:
with self.subTest(name=name):
self.assertEqual(self.auth_client._retrieve_entry(request), expected_result)
if expected_result:
self.assertEqual(request["resp"], expected_resp)
@patch('irods.auth.pam_interactive._auth_api_request', return_value={"result": "ok"})
@patch('sys.stderr.write')
def test_get_input(self, mock_stderr, mock_api_request):
test_cases = [
("non_password_input", False, 'sys.stdin.readline', "rods\n", {"msg": {"prompt": "Prompt:"}}, "rods"),
(
"non_password_default",
False,
'sys.stdin.readline',
"\n",
{"msg": {"prompt": "Prompt:", "default_path": "/password"}, "pstate": {"password": "rods"}},
"rods",
),
("password_input", True, 'getpass.getpass', "rods", {"msg": {"prompt": "Password:"}}, "rods"),
(
"password_default",
True,
'getpass.getpass',
"",
{"msg": {"prompt": "Password:", "default_path": "/password"}, "pstate": {"password": "rods"}},
"rods",
),
]
for name, is_password, mock_target, user_input, request, expected_resp in test_cases:
with (
self.subTest(name=name),
patch(mock_target, return_value=user_input),
patch.object(self.auth_client, '_patch_state') as mock_patch,
):
resp = self.auth_client._get_input(request, is_password=is_password)
self.assertEqual(request["resp"], expected_resp)
self.assertEqual(resp, {"result": "ok"})
mock_patch.assert_called_once()
def test_pass_through_states(self):
with patch("irods.auth.pam_interactive._auth_api_request", return_value={"result": "ok"}):
request = {"msg": {"prompt": "Prompt:"}, "pstate": {}, "pdirty": False}
for state in [
self.auth_client.next,
self.auth_client.running,
self.auth_client.ready,
self.auth_client.response,
]:
with self.subTest(state=state.__name__):
resp = state(request)
self.assertEqual(resp, {"result": "ok"})
def test_failure_states(self):
request = {"foo": "bar"}
with patch("irods.auth.pam_interactive._logger"):
for state in [self.auth_client.error, self.auth_client.timeout, self.auth_client.not_authenticated]:
with self.subTest(state=state.__name__):
resp = state(request)
self.assertEqual(resp[__NEXT_OPERATION__], __FLOW_COMPLETE__)
self.assertEqual(self.auth_client.loggedIn, 0)
@patch("sys.stdin.readline", return_value="ABC123\n")
def test_pam_interactive_mfa_flow(self, mock_stdin):
state = {"stage": "before_mfa", "step": 0}
def mock_server(conn, req):
# Switch from the real server to the mock server when the password step is completed
if state["stage"] == "before_mfa":
resp = _auth_api_request(conn, req)
if req.get(__NEXT_OPERATION__) == PERFORM_NEXT: # Indicates the password step is complete
state["stage"] = "mfa_mock"
return resp
# MFA simulation steps
if state["step"] == 0:
return {
__NEXT_OPERATION__: PERFORM_WAITING,
"pstate": {"Password: ": self.password, "verification_code": ""},
"msg": {
"prompt": "Verification Code: ",
"default_path": "/verification_code",
"patch": [{"op": "add", "path": "/verification_code"}],
},
"pdirty": True,
}
elif state["step"] == 1:
return {
__NEXT_OPERATION__: PERFORM_NEXT,
"pstate": {"Password: ": self.password, "verification_code": ""},
"pdirty": True,
}
elif state["step"] == 2:
return {
__NEXT_OPERATION__: PERFORM_AUTHENTICATED,
"pstate": {"Password: ": self.password, "verification_code": "ABC123"},
"pdirty": True,
"request_result": "temp_token",
}
state["step"] += 1
with (
patch("irods.auth.pam_interactive._auth_api_request", side_effect=mock_server),
patch("getpass.getpass", return_value=self.password),
patch("irods.auth.pam_interactive._authenticate_native") as mock_native,
):
self.sess = make_session(
test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive"
)
self.sess.server_version # Trigger auth flow
mock_native.assert_called_once()
if __name__ == "__main__":
unittest.main()
#!/usr/bin/env python
import numbers
import os
import posix
import socket
import shutil
from subprocess import Popen, PIPE
import sys
IRODS_SSL_DIR = "/etc/irods/ssl"
SERVER_CERT_HOSTNAME = None
ext = ""
keep_old = False
def create_server_cert(process_output=sys.stdout, irods_key_path="irods.key", hostname=SERVER_CERT_HOSTNAME):
p = Popen(
"openssl req -new -x509 -key '{irods_key_path}' -out irods.crt{ext} -days 365 <<EOF{_sep_}"
"US{_sep_}North Carolina{_sep_}Chapel Hill{_sep_}UNC{_sep_}RENCI{_sep_}"
"{host}{_sep_}anon@mail.com{_sep_}EOF\n"
"".format(ext=ext, host=(hostname if hostname else socket.gethostname()), _sep_="\n", **locals()),
shell=True,
stdout=process_output,
stderr=process_output,
)
p.wait()
return p.returncode
def create_ssl_dir(irods_key_path="irods.key", ssl_dir="", use_strong_primes_for_dh_generation=True):
ssl_dir = ssl_dir or IRODS_SSL_DIR
save_cwd = os.getcwd()
silent_run = {"shell": True, "stderr": PIPE, "stdout": PIPE}
try:
if not (os.path.exists(ssl_dir)):
os.mkdir(ssl_dir)
os.chdir(ssl_dir)
if not keep_old:
Popen(
"openssl genrsa -out '{irods_key_path}' 2048 && chmod 600 '{irods_key_path}'".format(**locals()),
**silent_run,
).communicate()
with open("/dev/null", "wb") as dev_null:
if 0 == create_server_cert(process_output=dev_null, irods_key_path=irods_key_path):
if not keep_old:
# https://www.openssl.org/docs/man1.0.2/man1/dhparam.html#:~:text=DH%20parameter%20generation%20with%20the,that%20may%20be%20possible%20otherwise.
if use_strong_primes_for_dh_generation:
dhparam_generation_command = "openssl dhparam -2 -out dhparams.pem 2048"
else:
dhparam_generation_command = "openssl dhparam -dsaparam -out dhparams.pem 4096"
print("cmd=", dhparam_generation_command)
Popen(dhparam_generation_command, **silent_run).communicate()
return os.listdir(".")
finally:
os.chdir(save_cwd)
def test(options, args=()):
if args:
print("warning: non-option args are ignored", file=sys.stderr)
force = "-f" in options
affirm = "n" if (os.path.exists(IRODS_SSL_DIR) and not force) else "y"
if affirm == "n" and posix.isatty(sys.stdin.fileno()):
try:
input_ = raw_input
except NameError:
input_ = input
affirm = input_("This will overwrite directory '{}'. Proceed(Y/N)? ".format(IRODS_SSL_DIR))
if affirm[:1].lower() == "y":
if not keep_old:
shutil.rmtree(IRODS_SSL_DIR, ignore_errors=True)
dh_strong_primes = "-q" not in options
wait_warning = " This may take a while." if dh_strong_primes else ""
print(
"Generating new '{}'.{}".format(IRODS_SSL_DIR, wait_warning),
file=sys.stderr,
)
ssl_dir_files = create_ssl_dir(use_strong_primes_for_dh_generation=dh_strong_primes)
print("ssl_dir_files=", ssl_dir_files, file=sys.stderr)
def usage(exit_code=None):
print(
"""Usage: {sys.argv[0]} [-f] [-h <hostname>] [-k] [-q] [-x <extension>]
-f Force replacement of the existing SSL directory (/etc/irods/ssl) with a new one, containing newly generated files.
-h In the generated certificate, use the given hostname rather than the value returned from socket.gethostname()
-k (Keep old secrets files.) Do not generate new key file or dhparams.pem file.
-q For testing; do a quick generation of a dhparams.pem file rather than waiting on system entropy to make it more secure.
-x Optional extra extension for appending to end of the filename for the generated certificate.
--help Print this help.
Any invalid option prints this help.
""".format(**globals()),
file=sys.stderr,
)
if isinstance(exit_code, numbers.Integral):
exit(exit_code)
if __name__ == "__main__":
import getopt
try:
opt, arg_list = getopt.getopt(sys.argv[1:], "x:fh:kq", ["help"])
except getopt.GetoptError:
usage(exit_code=1)
opt_lookup = dict(opt)
if "--help" in opt_lookup:
usage(exit_code=0)
ext = opt_lookup.get("-x", "")
if ext:
ext = "." + ext.lstrip(".")
keep_old = opt_lookup.get("-k") is not None
SERVER_CERT_HOSTNAME = opt_lookup.get("-h")
test(opt_lookup, arg_list)
[project]
name = "python-irodsclient"
authors = [
{name = "iRODS Consortium", email = "support@irods.org"},
]
description = "A Python API for iRODS"
readme = {file = "README.md", content-type = "text/markdown"}
requires-python = ">= 3.9"
keywords = ["irods"]
license = "BSD-3-Clause"
license-files = ["LICENSE.txt"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: POSIX :: Linux",
]
dependencies = [
"PrettyTable>=0.7.2",
"defusedxml",
"jsonpointer",
"jsonpatch",
]
dynamic = ["version"]
[project.urls]
Repository = "https://github.com/irods/python-irodsclient"
Issues = "https://github.com/irods/python-irodsclient/issues"
[project.optional-dependencies]
tests = [
"unittest-xml-reporting", # for xmlrunner
"types-defusedxml", # for type checking
"progressbar", # for type checking
"types-tqdm", # for type checking
]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
py-modules = ["irods"]
script-files = ["irods/prc_write_irodsA.py"]
include-package-data = true
[tool.setuptools.packages.find]
exclude = ["test_harness*", "dist*"]
[tool.ruff]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
# Ensure this matches tool.ruff.lint.pycodestyle.max-doc-length
line-length = 120
indent-width = 4
[tool.ruff.format]
quote-style = "preserve"
indent-style = "space"
line-ending = "auto"
# Enable reformatting of code snippets in docstrings.
docstring-code-format = true
# Respect magic trailing commas.
skip-magic-trailing-comma = false
[tool.ruff.lint]
# Enable preview rules, but require them to be explicitly enabled
preview = true
explicit-preview-rules = true
# Enable rules
select = [
# eradicate
"ERA",
# flake8-2020
"YTT",
# flake8-annotations, uncomment if we ever add type annotations
#"ANN",
# flake8-executable
"EXE",
# flake8-async
"ASYNC",
# flake8-blind-except
"BLE",
# flake8-bugbear
"B",
# flake8-builtins
"A",
# flake8-comprehensions
"C4",
# flake8-datetimez
"DTZ",
# flake8-executable
"EXE",
# flake8-future-annotations
"FA",
# flake8-implicit-str-concat
"ISC",
# flake8-import-conventions
"ICN",
# flake8-logging
"LOG",
# flake8-logging-format
"G",
# flake8-no-pep420
"INP",
# flake8-pie
"PIE",
# flake8-pyi
"PYI",
# flake8-raise
"RSE",
# flake8-return
"RET",
# flake8-self
"SLF",
# flake8-simplify
"SIM",
# flake8-slots
"SLOT",
# flake8-tidy-imports
"TID",
# flake8-todos
"TD",
# flake8-type-checking
"TC",
# flake8-unused-arguments
"ARG",
# flake8-use-pathlib
"PTH",
# flynt
"FLY",
# isort
"I",
# pep8-naming
"N",
# Perflint
"PERF",
# pycodestyle
"E", "W",
# pydoclint
"DOC",
# pydocstyle, flake8-docstrings
"D",
# Pyflakes
"F",
# pygrep-hooks
"PGH",
# Pylint
"PL",
# pyupgrade
"UP",
# refurb
"FURB",
# Ruff-specific rules
"RUF",
# tryceratops
"TRY",
## flake8-bandit
## We want a subset and it's easier to handle some here than in ignore
# S1??: general tests
"S1",
# S2??: misconfiguration
"S2",
# S301: suspicious pickle usage
"S301",
# S306: usage of insecure and deprecated mktemp
"S306",
# S307: usage of insecure eval
"S307",
# S311: usage of bad rand
"S311",
# S313-S319: usage of insecure XML parsing
"S313", "S314", "S315", "S316", "S317", "S318", "S319",
# S5??: cryptography
"S5",
# shell usage
"S602", "S604", "S605",
# S612:
## flake8-commas
## We only want one rule from this one
# COM818: trailing comman on bare tuple prohibited
"COM818",
## flake8-quotes
## We don't want most rules from this one, as quotes are mostly handled by the formatter
# Q004: Unnecessary escape on inner quote character
"Q004",
## pydocstyle
## Certain rules are disabled by tool.ruff.lint.pydocstyle.convention, but we still want some of them
# D213: Multi-line docstring summary should start at the second line
"D213",
# D214: Overindented section
"D214",
# D215: Overindented section underline
"D215",
# D404: First word of the docstring should not be "This"
"D404",
# D405: Section name should be properly capitalized
"D405",
# D406: Missing newline after section name
"D406",
# D409: Mismatched section underline length
"D409",
# D416: Missing colon after section name
"D416",
# D417: Missing parameter documentation
"D417",
### preview rules
## pydoclint
# DOC102: uocumented parameter missing from signature
"DOC102",
# DOC201: undocumented return value
"DOC201",
# DOC202: docstring has returns section, but function/method does not return anything
"DOC202",
# DOC402: undocumented yield value
"DOC402",
# DOC403: docstring has yields section, but function/method does not yield anything
"DOC403",
# DOC501: undocumented raised exception
"DOC501",
]
# Disable rules
ignore = [
## flake8-datetimez
# DTZ003: datetime.datetime.utcnow() used
"DTZ003",
## flake8-tidy-imports
# TID252: Prefer absolute imports over relative imports
# This can introduce difficulties
"TID252",
## flake8-todos
# TD002: Missing author in TODO
"TD002",
## pycodestyle
# E402: Module level import not at top of file
# We often need to do some checking before importing stuff
"E402",
# Indentation/alignment/spacing stuff
# E128: continuation line under-indented for visual indent
# Don't hate me cuz I'm pretty.
# Uncomment if ruff ever supports this
#"E128",
# Most indentation handled by formatter
"E101", # indentation contains mixed spaces and tabs
"E111", # wrong indentation multiple
"E114", # wrong indentation multiple (for comments)
"E117", # over-indentation
"W191", # indentation includes tabs
## pydoclint
# DOC502: docstring specifies potential exception that is not actually raised in the fucntion/method body
# Does not account for exceptions from other sources
"DOC502",
## pylint
# PLC0415: import should be at top-level
# plenty of reasons to put it in a block
"PLC0415",
# PLC1901: comparison to empty string
# Does not account for types
"PLC1901",
# PLE0116: continue in finally block
# Supported since Python 3.8
"PLE0116",
# Design complexity stuff
# These judgements should always be made by humans
#"PLC0302", # too many lines in module (uncomment if ruff ever supports this)
#"PLR0901", # too many class ancestors (uncomment if ruff ever supports this)
#"PLR0902", # too many instance attributes (uncomment if ruff ever supports this)
#"PLR0903", # too few public methods (uncomment if ruff ever supports this)
"PLR0904", # too many public methods
"PLR0911", # too many return statements
"PLR0912", # too many branches
"PLR0913", # too many arguments
"PLR0914", # too many locals
"PLR0915", # too many statements
"PLR0916", # too many boolean expressions in a single statement
"PLR0917", # too many positional arguments
"PLR1702", # too many nested blocks
## tryceratops
# TRY003: Avoid specifying long messages outside the exception class
# Way, way too many false positives
"TRY003",
]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Don't whine about codes in noqa that ruff doesn't (yet) support
external = [
# openstack's hacking flake8 plugin
"V",
# flake8-multiline-containers
"JS",
# flake8-typing-imports
"TYP"
]
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Allow suggesting `from __future__ import annotations`
future-annotations = true
# TODO and TODO-like tags to recognize
task-tags = [
'FIXME',
'TODO',
'HACK',
'KLUDGE',
'BUG',
'OPTIMIZE',
'XXX',
'JANK',
'AWFUL',
'BAD',
'CRIME',
'CRIMES',
]
# Recognize `typing_extensions` imports
typing-extensions = true
# Speciy additional typing extension modules
typing-modules = []
[tool.ruff.lint.per-file-ignores]
"setup.py" = [
# pydocstyle, flake8-docstrings
# Ignore missing module docstring in setup.py
"D100",
]
"irods/test/*_test.py" = [
# flake8-bandit
# S106: Possible hardcoded password assigned to argument
# Ignore passwords in tests
"S106",
# pydocstyle, flake8-docstrings
# Ignore missing docstrings in tests
"D1",
]
[tool.ruff.lint.flake8-annotations]
# Suppress ANN401 for dynamically typed *args and **kwargs arguments.
allow-star-arg-any = true
# Allow omission of __init__ return type hint if at least one argument is annotated
#mypy-init-return = true
# Suppress violations for dummy argument variables
#suppress-dummy-args = true
[tool.ruff.lint.flake8-bugbear]
# function calls to allow as default arguments
extend-immutable-calls = []
[tool.ruff.lint.flake8-builtins]
#allowed-modules = []
#ignorelist = []
[lint.flake8-implicit-str-concat]
# Allow multiline implicit string concatenation
allow-multiline = true
[tool.ruff.lint.flake8-import-conventions]
# Disallow using the `from ... import ...` syntax for individual modules
banned-from = []
[tool.ruff.lint.flake8-import-conventions.extend-aliases]
# To enforce an aliasing convention, follow this pattern:
#"module1" = "alias1"
#"module2" = "alias2"
[tool.ruff.lint.flake8-import-conventions.banned-aliases]
# To disallow specific aliases, follow this pattern:
#"module1" = ["alias1", "alias2"]
#"module2" = ["alias3", "alias4"]
[tool.ruff.lint.flake8-self]
# names to ignore when flagging private member access
extend-ignore-names = ["test_*"]
[tool.ruff.lint.flake8-tidy-imports]
# Disallow specific module-level imports (allow only in functions and blocks)
banned-module-level-imports = []
[tool.ruff.lint.flake8-tidy-imports.banned-api]
# Disallow specific modules from being imported at all
"optparse".msg = "Use argparse instead of optparse"
"getopt".msg = "Use argparse instead of getopt"
"distutils".msg = "distutils has been removed from the stdlib"
"imp".msg = "imp has been removed from the stdlib"
"pycrypto".msg = "pycrypto is no longer maintained; use pyca/cryptography instead"
[tool.ruff.lint.flake8-type-checking]
# Exempt specific modules from needing to be moved into type-checking blocks
exempt-modules = ["typing"]
# Exempt classes that list any of the enumerated classes as a base class from needing to be moved
# into type-checking blocks
runtime-evaluated-base-classes = []
# Exempt classes and functions decorated with any of the enumerated decorators from being moved
# into type-checking blocks
runtime-evaluated-decorators = []
# Quote type annotations if doing so would allow an import to be moved into a type-checking block
#quote-annotations = true
[tool.ruff.lint.flake8-unused-arguments]
# Do not ignore unused *args/**kwargs
ignore-variadic-names = false
[tool.ruff.lint.isort]
# Order imports by type
order-by-type = true
# ordering of relative imports
relative-imports-order = "closest-to-furthest"
# force specific imports to the top of their section
force-to-top = []
# Combine aliased imports
combine-as-imports = true
# Combine aliased imports as multi-line compount imports
force-wrap-aliases = true
# modules to separate into auxiliary block(s), in the order specified
forced-separate = []
# modules to consider as part of the stdlib
extra-standard-library = []
# modules to consider as being third-party
known-third-party = [
# distro_distil is first-party, but is an iRODS-independent package
"distro_distil",
]
# modules to consider as first-party
known-first-party = []
# modules to consider as being a local folder
known-local-folder = []
# ensure certain imports are in all files
required-imports = []
# single-line rule exceptions
single-line-exclusions = []
# Do not fold mult-line imports if there is a trailing comma
split-on-trailing-comma = true
# tokesn to always recognize as CONSTANTs
constants = [
#"GSI_AUTH_SCHEME",
#"PAM_AUTH_SCHEME",
#"DEFAULT_CONFIG_PATH"
]
# tokens to always recognise as variables
variables = []
[tool.ruff.lint.pep8-naming]
# treat methos with these decorators as class methods
classmethod-decorators = []
# treat methos with these decorators as static methods
staticmethod-decorators = []
# names to ignore when considering naming violations
extend-ignore-names = []
[tool.ruff.lint.pycodestyle]
# Ensure this matches toll.ruff.line-length
#max-doc-length = 120
[tool.ruff.lint.pydocstyle]
# Adhere to PEP257
convention = "pep257"
# Ignore docstrings for functions/methods with these fully-qualified decorators
ignore-decorators = ["typing.overload"]
# Do not ignore undocumented *args/**kwargs
ignore-var-parameters = false
# Treat methods with these decorators as properties
property-decorators = []
[tool.ruff.lint.pyflakes]
# functions/classes to consider generic
extend-generics = []
[tool.ruff.lint.pylint]
# dunder method names to allow
allow-dunder-method-names = []
# types to ignore when flagging magic values
allow-magic-value-types = [
"str",
"bytes",
#"complex",
#"float",
#"int",
]
#!/home/tgr/repos/python-irodsclient/venv313/bin/python3.13
import getopt
import textwrap
import sys
from typing import Callable, Dict
from irods.auth.pam_password import _get_pam_password_from_stdin as get_password
from irods.client_init import write_pam_irodsA_file, write_native_irodsA_file
if __name__ == "__main__":
extra_help = textwrap.dedent(
"""
This Python module also functions as a script to produce a "secrets" (i.e. encoded password) file.
Similar to iinit in this capacity, if the environment - and where applicable, the PAM
configuration for both system and user - is already set up in every other regard, this program
will generate the secrets file with appropriate permissions and in the normal location, usually:
~/.irods/.irodsA
The user will be interactively prompted to enter their cleartext password.
"""
)
vector : Dict[str, Callable] = {"pam_password": write_pam_irodsA_file, "native": write_native_irodsA_file}
opts, args = getopt.getopt(sys.argv[1:], "hi:", ["ttl=", "help"])
optD = dict(opts)
help_selected = {*optD} & {"-h", "--help"}
if len(args) != 1 or help_selected:
print(
"{}\nUsage: {} [-i STREAM| -h | --help | --ttl HOURS] AUTH_SCHEME".format(
extra_help, sys.argv[0]
)
)
print(" Choices for AUTH_SCHEME are:")
for x in vector:
print(" {}".format(x))
print(
" STREAM is the name of a file containing a password. Alternatively, a hyphen('-') is used to\n"
" indicate that the password may be read from stdin."
)
sys.exit(0 if help_selected else 1)
scheme = args[0]
if scheme in vector:
options = {}
inp_stream = optD.get("-i", None)
if "--ttl" in optD:
options["ttl"] = optD["--ttl"]
if inp_stream is None or inp_stream == "-":
pw = get_password(sys.stdin,
prompt=f"Enter current password for scheme {scheme!r}: ",)
else:
pw = get_password(open(inp_stream, "r", encoding='utf-8'),
prompt=f"Enter current password for scheme {scheme!r}: ",)
vector[scheme](pw, **options)
else:
print("did not recognize authentication scheme argument", file=sys.stderr)
+35
-0

@@ -14,2 +14,37 @@ # Changelog

## [v3.3.0] - 2026-03-18
This release includes improvements for listing tickets, clean cancellation of parallel transfers, storage of binary data in AVUs, and more.
### Changed
- Migrate to pyproject.toml (#774, #783).
### Removed
- Remove Jenkins test framework (#778).
### Deprecated
- Deprecate `IRODS_VERSION` (#698).
### Fixed
- Fix ability to store arbitrary binary data in an AVU (#707).
- Add `__slots__` member to prevent configuration misfires (#708).
- Preserve state when chaining calls on metadata manager (#709).
- Fix segfault and hung threads when signals abort a parallel transfer (#722).
- Fix handling of username in GeneralAdmin API for proper removal of remote user home collection (#763).
- Use named loggers (#771).
### Added
- Add convenience functions for listing tickets (#120).
- Add automated testing via GitHub Actions (#502, #697).
- Add `_IRODS_VERSION` (#698).
- Add facilities for stopping parallel transfers in a clean manner (#722).
- Add configuration and GitHub Action workflows for code formatting and linting (#726).
- Allow MetadataManager options to be accessible as attributes (#795).
- Allow ticket instances to be populated using information from catalog queries (#801).
## [v3.2.0] - 2025-08-27

@@ -16,0 +51,0 @@

+2
-6

@@ -33,5 +33,3 @@ import sys

default_irods_authentication_file = os.path.expanduser("~/.irods/.irodsA")
return os.environ.get(
"IRODS_AUTHENTICATION_FILE", default_irods_authentication_file
)
return os.environ.get("IRODS_AUTHENTICATION_FILE", default_irods_authentication_file)

@@ -113,4 +111,2 @@

# and thus no settings file is auto-loaded.
client_configuration.autoload(
_file_to_load=os.environ.get(settings_path_environment_variable)
)
client_configuration.autoload(_file_to_load=os.environ.get(settings_path_environment_variable))

@@ -23,3 +23,2 @@ import collections

class iRODSAccess(metaclass=_Access_LookupMeta):
@classmethod

@@ -78,5 +77,3 @@ def to_int(cls, key):

strings = collections.OrderedDict(
(number, string) for string, number in codes.items()
)
strings = collections.OrderedDict((number, string) for string, number in codes.items())

@@ -108,5 +105,3 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None):

def __hash__(self):
return hash(
(self.access_name, iRODSPath(self.path), self.user_name, self.user_zone)
)
return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone))

@@ -122,7 +117,3 @@ def copy(self, decanonicalize=False):

}.get(self.access_name)
other.access_name = (
replacement_string
if replacement_string is not None
else self.access_name
)
other.access_name = replacement_string if replacement_string is not None else self.access_name
return other

@@ -133,5 +124,3 @@

access_name = self.access_name.replace(" ", "_")
user_type_hint = (
"({user_type})" if object_dict.get("user_type") is not None else ""
).format(**object_dict)
user_type_hint = ("({user_type})" if object_dict.get("user_type") is not None else "").format(**object_dict)
return f"<iRODSAccess {access_name} {self.path} {self.user_name}{user_type_hint} {self.user_zone}>"

@@ -146,4 +135,2 @@

)
strings = collections.OrderedDict(
(number, string) for string, number in codes.items()
)
strings = collections.OrderedDict((number, string) for string, number in codes.items())

@@ -5,3 +5,2 @@ from irods import derived_auth_filename

class iRODSAccount:
@property

@@ -23,3 +22,3 @@ def derived_auth_file(self):

env_file="",
**kwargs
**kwargs,
):

@@ -26,0 +25,0 @@

@@ -20,2 +20,5 @@ import importlib

_logger = logging.getLogger(__name__)
class AuthStorage:

@@ -145,5 +148,3 @@ """A class that facilitates flexible means of password storage.

message_body = JSON_Message(data, conn.server_version)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["AUTHENTICATION_APN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["AUTHENTICATION_APN"])
conn.send(message)

@@ -164,3 +165,2 @@ response = conn.recv()

class authentication_base:
def __init__(self, connection, scheme):

@@ -172,3 +172,3 @@ self.conn = connection

def call(self, next_operation, request):
logging.debug("next operation = %r", next_operation)
_logger.debug("next operation = %r", next_operation)
old_func = func = next_operation

@@ -182,8 +182,6 @@ # One level of indirection should be sufficient to get a callable method.

resp = func(request)
logging.debug("resp = %r", resp)
_logger.debug("resp = %r", resp)
return resp
def authenticate_client(
self, next_operation="auth_client_start", initial_request=()
):
def authenticate_client(self, next_operation="auth_client_start", initial_request=()):
if not isinstance(initial_request, dict):

@@ -201,11 +199,7 @@ initial_request = dict(initial_request)

if next_operation is None:
raise ClientAuthError(
"next_operation key missing; cannot determine next operation"
)
raise ClientAuthError("next_operation key missing; cannot determine next operation")
if next_operation in (__FLOW_COMPLETE__, ""):
raise ClientAuthError(
f"authentication flow stopped without success: scheme = {self.scheme}"
)
raise ClientAuthError(f"authentication flow stopped without success: scheme = {self.scheme}")
to_send = resp
logging.debug("fully authenticated")
_logger.debug("fully authenticated")

@@ -64,5 +64,3 @@ import base64

# user_name and zone_name keys injected by authenticate_client() method
resp[__NEXT_OPERATION__] = (
self.AUTH_CLIENT_AUTH_REQUEST
) # native_auth_client_request
resp[__NEXT_OPERATION__] = self.AUTH_CLIENT_AUTH_REQUEST # native_auth_client_request
return resp

@@ -80,5 +78,3 @@

def native_auth_establish_context(self, request):
throw_if_request_message_is_missing_key(
request, ["user_name", "zone_name", "request_result"]
)
throw_if_request_message_is_missing_key(request, ["user_name", "zone_name", "request_result"])
request = request.copy()

@@ -96,9 +92,5 @@

challenge = request["request_result"].encode("utf-8")
self.conn._client_signature = "".join(
"{:02x}".format(c) for c in challenge[:16]
)
self.conn._client_signature = "".join("{:02x}".format(c) for c in challenge[:16])
padded_pwd = struct.pack(
"%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip()
)
padded_pwd = struct.pack("%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip())

@@ -119,5 +111,3 @@ m = hashlib.md5()

def native_auth_client_response(self, request):
throw_if_request_message_is_missing_key(
request, ["user_name", "zone_name", "digest"]
)
throw_if_request_message_is_missing_key(request, ["user_name", "zone_name", "digest"])

@@ -124,0 +114,0 @@ server_req = request.copy()

@@ -10,3 +10,3 @@ from . import (

STORE_PASSWORD_IN_MEMORY,
CLIENT_GET_REQUEST_RESULT
CLIENT_GET_REQUEST_RESULT,
)

@@ -21,3 +21,3 @@ from .native import _authenticate_native

# Constants defining the states and operations for the pam_interactive authentication flow
# Constants defining the states and operations for the pam_interactive authentication flow
AUTH_CLIENT_AUTH_REQUEST = "pam_auth_client_request"

@@ -43,2 +43,3 @@ AUTH_CLIENT_AUTH_RESPONSE = "pam_auth_response"

def login(conn, **extra_opt):

@@ -51,6 +52,5 @@ """The entry point for the pam_interactive authentication scheme."""

auth_client_object = _pam_interactive_ClientAuthState(conn, depot, scheme=PAM_INTERACTIVE_SCHEME)
auth_client_object.authenticate_client(
initial_request=extra_opt
)
auth_client_object.authenticate_client(initial_request=extra_opt)
class _pam_interactive_ClientAuthState(authentication_base):

@@ -71,3 +71,3 @@ def __init__(self, conn, depot, *_, **_kw):

# The plugin is built on the authentication framework described here:
# https://github.com/irods-contrib/irods_working_group_authentication/tree/e83e84df8ea4a732e5de894fb28aae281c3b3d29/development
# https://github.com/irods-contrib/irods_working_group_authentication/tree/e83e84df8ea4a732e5de894fb28aae281c3b3d29/development

@@ -131,3 +131,3 @@ resp["pstate"] = resp.get("pstate", {})

for op in patch_ops:
if op.get("op") in ["add", "replace"] and "value" not in op:
if op.get("op") in ["add", "replace"] and "value" not in op:
op["value"] = resp

@@ -275,2 +275,2 @@

def not_authenticated(self, request):
return self._auth_failure(request, "Authentication failed possibly due to incorrect credentials.")
return self._auth_failure(request, "Authentication failed possibly due to incorrect credentials.")

@@ -56,5 +56,3 @@ import getpass

_pam_password_ClientAuthState(conn, scheme=_scheme).authenticate_client(
initial_request=req
)
_pam_password_ClientAuthState(conn, scheme=_scheme).authenticate_client(initial_request=req)

@@ -64,11 +62,7 @@ _logger.debug("----------- %s (end)", _scheme)

def _get_pam_password_from_stdin(
file_like_object=None, prompt="Enter your current PAM password: "
):
def _get_pam_password_from_stdin(file_like_object=None, prompt="Enter your current PAM password: "):
try:
if file_like_object:
if not getattr(file_like_object, "readline", None):
msg = (
"The file_like_object, if provided, must have a 'readline' method."
)
msg = "The file_like_object, if provided, must have a 'readline' method."
raise RuntimeError(msg)

@@ -89,3 +83,2 @@ sys.stdin = file_like_object

class _pam_password_ClientAuthState(authentication_base):
# Client define

@@ -106,5 +99,3 @@ AUTH_CLIENT_AUTH_REQUEST = "pam_password_auth_client_request"

# to the caller upon request.
self._list_for_request_result_return = request.pop(
CLIENT_GET_REQUEST_RESULT, False
)
self._list_for_request_result_return = request.pop(CLIENT_GET_REQUEST_RESULT, False)

@@ -128,5 +119,3 @@ ensure_ssl = request.pop(ENSURE_SSL_IS_ACTIVE, None)

# Like with the C++ plugin, we offer the user a chance to enter a password.
resp[AUTH_PASSWORD_KEY] = _get_pam_password_from_stdin(
file_like_object=password_input_obj
)
resp[AUTH_PASSWORD_KEY] = _get_pam_password_from_stdin(file_like_object=password_input_obj)
else:

@@ -133,0 +122,0 @@ # Password from .irodsA in environment.

@@ -39,7 +39,3 @@ import ast

cls = type.__new__(meta, name, bases, attrs)
cls.writeable_properties = tuple(
k
for k, v in attrs.items()
if isinstance(v, property) and v.fset is not None
)
cls.writeable_properties = tuple(k for k, v in attrs.items() if isinstance(v, property) and v.fset is not None)
return cls

@@ -49,2 +45,4 @@

class ConnectionsProperties(iRODSConfiguration, metaclass=iRODSConfigAliasMetaclass):
__slots__ = ()
@property

@@ -62,8 +60,16 @@ def xml_parser_default(self):

connections = ConnectionsProperties()
class ConfigurationError(BaseException): pass
class ConfigurationValueError(ValueError,ConfigurationError): pass
class ConfigurationError(BaseException):
pass
class ConfigurationValueError(ValueError, ConfigurationError):
pass
class Genquery1_Properties(iRODSConfiguration, metaclass=iRODSConfigAliasMetaclass):
__slots__ = ()

@@ -73,2 +79,3 @@ @property

import irods.query
return irods.query.IRODS_QUERY_LIMIT

@@ -79,9 +86,13 @@

import irods.query
requested = int(target_value)
if requested <= 0:
raise ConfigurationValueError(f'Error setting IRODS_QUERY_LIMIT to [{requested}]. Use positive values only.')
raise ConfigurationValueError(
f'Error setting IRODS_QUERY_LIMIT to [{requested}]. Use positive values only.'
)
irods.query.IRODS_QUERY_LIMIT = requested
genquery1 = Genquery1_Properties()

@@ -148,5 +159,3 @@

def __init__(self):
self.time_to_live_in_hours = (
0 # -> We default to the server's TTL preference.
)
self.time_to_live_in_hours = 0 # -> We default to the server's TTL preference.
self.password_for_auto_renew = ""

@@ -178,7 +187,3 @@ self.store_password_to_environment = False

if isinstance(root, types.ModuleType):
return [
((i, v) + flag(False))
for i, v in vars(root).items()
if isinstance(v, iRODSConfiguration)
]
return [((i, v) + flag(False)) for i, v in vars(root).items() if isinstance(v, iRODSConfiguration)]
if isinstance(root, iRODSConfiguration):

@@ -197,5 +202,3 @@ return [(i, getattr(root, i)) + flag(True) for i in _config_names(root)]

# yield from _var_items_as_generator(root = sub_node, dotted = dn)
for _dotted, _root, _is_config in _var_items_as_generator(
root=sub_node, dotted=dn
):
for _dotted, _root, _is_config in _var_items_as_generator(root=sub_node, dotted=dn):
yield _dotted, _root, _is_config

@@ -286,5 +289,3 @@

def _load_config_line(
root, setting, value, return_old=None, eval_func=ast.literal_eval
):
def _load_config_line(root, setting, value, return_old=None, eval_func=ast.literal_eval):
"""Low-level utility function for loading a line of settings, with the option to return the old (displaced) value.

@@ -319,7 +320,3 @@

# If we get this far, there's a problem loading the configuration setting. Raise an exception or log it.
error_message = (
"Bad setting: root = {root!r}, setting = {setting!r}, value = {value!r}".format(
**locals()
)
)
error_message = "Bad setting: root = {root!r}, setting = {setting!r}, value = {value!r}".format(**locals())
if loadexc:

@@ -459,7 +456,3 @@ error_message += " [{loadexc!r}]".format(**locals())

def preserve_defaults():
default_config_dict.update(
(k, copy.deepcopy(v))
for k, v in globals().items()
if isinstance(v, iRODSConfiguration)
)
default_config_dict.update((k, copy.deepcopy(v)) for k, v in globals().items() if isinstance(v, iRODSConfiguration))

@@ -483,5 +476,3 @@

return {
_tuple.dotted: "__".join(
["PYTHON_IRODSCLIENT_CONFIG"] + uppercase_and_dot_split(_tuple.dotted)
)
_tuple.dotted: "__".join(["PYTHON_IRODSCLIENT_CONFIG"] + uppercase_and_dot_split(_tuple.dotted))
for _tuple in _var_item_tuples_as_generator()

@@ -488,0 +479,0 @@ if _tuple.is_config

@@ -34,5 +34,3 @@ #!/usr/bin/env python3

if not overwrite and os.path.exists(auth_file):
raise irodsA_already_exists(
f"Overwriting not enabled and {auth_file} already exists."
)
raise irodsA_already_exists(f"Overwriting not enabled and {auth_file} already exists.")
with _open_file_for_protected_contents(auth_file, "w") as irodsA:

@@ -65,9 +63,4 @@ irodsA.write(obf.encode(encode_input))

ses = kw.pop("_session", None) or h.make_session(**kw)
if (
ses._server_version(iRODSSession.GET_SERVER_VERSION_WITHOUT_AUTH) < (4, 3)
or cfg.legacy_auth.force_legacy_auth
):
return write_pam_credentials_to_secrets_file(
password, overwrite=overwrite, ttl=ttl, _session=ses
)
if ses._server_version(iRODSSession.GET_SERVER_VERSION_WITHOUT_AUTH) < (4, 3) or cfg.legacy_auth.force_legacy_auth:
return write_pam_credentials_to_secrets_file(password, overwrite=overwrite, ttl=ttl, _session=ses)

@@ -79,15 +72,7 @@ auth_file = ses.pool.account.derived_auth_file

if ttl:
ses.set_auth_option_for_scheme(
"pam_password", irods.auth.pam_password.AUTH_TTL_KEY, ttl
)
ses.set_auth_option_for_scheme(
"pam_password", irods.auth.FORCE_PASSWORD_PROMPT, io.StringIO(password)
)
ses.set_auth_option_for_scheme(
"pam_password", irods.auth.STORE_PASSWORD_IN_MEMORY, True
)
ses.set_auth_option_for_scheme("pam_password", irods.auth.pam_password.AUTH_TTL_KEY, ttl)
ses.set_auth_option_for_scheme("pam_password", irods.auth.FORCE_PASSWORD_PROMPT, io.StringIO(password))
ses.set_auth_option_for_scheme("pam_password", irods.auth.STORE_PASSWORD_IN_MEMORY, True)
L = []
ses.set_auth_option_for_scheme(
"pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, L
)
ses.set_auth_option_for_scheme("pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, L)
with ses.pool.get_connection() as _:

@@ -109,9 +94,7 @@ _write_encoded_auth_value(auth_file, L[0], overwrite)

to_encode = []
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),
dict(setting="legacy_auth.pam.time_to_live_in_hours", value=ttl),
]
):
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),
dict(setting="legacy_auth.pam.time_to_live_in_hours", value=ttl),
]):
to_encode = s.pam_pw_negotiated

@@ -123,2 +106,3 @@ if not to_encode:

def write_pam_interactive_irodsA_file(overwrite=True, ttl="", **kw):

@@ -135,21 +119,13 @@ """Write credentials to an .irodsA file for PAM interactive authentication."""

ses.set_auth_option_for_scheme(
"pam_interactive", irods.auth.FORCE_PASSWORD_PROMPT, True
)
ses.set_auth_option_for_scheme("pam_interactive", irods.auth.FORCE_PASSWORD_PROMPT, True)
if ttl:
ses.set_auth_option_for_scheme(
"pam_interactive", "time_to_live_in_hours", ttl
)
ses.set_auth_option_for_scheme("pam_interactive", "time_to_live_in_hours", ttl)
ses.set_auth_option_for_scheme(
"pam_interactive", irods.auth.STORE_PASSWORD_IN_MEMORY, True
)
ses.set_auth_option_for_scheme("pam_interactive", irods.auth.STORE_PASSWORD_IN_MEMORY, True)
L = []
ses.set_auth_option_for_scheme(
"pam_interactive", irods.auth.CLIENT_GET_REQUEST_RESULT, L
)
ses.set_auth_option_for_scheme("pam_interactive", irods.auth.CLIENT_GET_REQUEST_RESULT, L)
with ses.pool.get_connection() as _:
_write_encoded_auth_value(auth_file, L[0], overwrite)

@@ -17,3 +17,2 @@ import itertools

class iRODSCollection:
class AbsolutePathRequired(Exception):

@@ -49,5 +48,3 @@ """Exception raised by iRODSCollection.normalize_path.

if not self._meta:
self._meta = iRODSMetaCollection(
self.manager.sess.metadata, Collection, self.path
)
self._meta = iRODSMetaCollection(self.manager.sess.metadata, Collection, self.path)
return self._meta

@@ -57,10 +54,4 @@

def subcollections(self):
query = self.manager.sess.query(Collection).filter(
Collection.parent_name == self.path
)
return [
iRODSCollection(self.manager, row)
for row in query
if row[Collection.name] != "/"
]
query = self.manager.sess.query(Collection).filter(Collection.parent_name == self.path)
return [iRODSCollection(self.manager, row) for row in query if row[Collection.name] != "/"]

@@ -72,6 +63,3 @@ @property

grouped = itertools.groupby(results, operator.itemgetter(DataObject.id))
return [
iRODSDataObject(self.manager.sess.data_objects, self, list(replicas))
for _, replicas in grouped
]
return [iRODSDataObject(self.manager.sess.data_objects, self, list(replicas)) for _, replicas in grouped]

@@ -78,0 +66,0 @@ def remove(self, recurse=True, force=False, **options):

from datetime import datetime, timezone
from calendar import timegm
class Column_remover:

@@ -8,4 +9,4 @@ def __init__(self, column):

class QueryKey:
def __init__(self, column_type):

@@ -34,3 +35,2 @@ self.column_type = column_type

class Criterion:
def __init__(self, op, query_key, value):

@@ -47,3 +47,2 @@ self.op = op

class In(Criterion):
def __init__(self, query_key, value):

@@ -64,3 +63,2 @@ super(In, self).__init__("in", query_key, value)

class Like(Criterion):
def __init__(self, query_key, value):

@@ -71,3 +69,2 @@ super(Like, self).__init__("like", query_key, value)

class NotLike(Criterion):
def __init__(self, query_key, value):

@@ -78,3 +75,2 @@ super(NotLike, self).__init__("not like", query_key, value)

class Between(Criterion):
def __init__(self, query_key, value):

@@ -93,3 +89,2 @@ super(Between, self).__init__("between", query_key, value)

class Column(QueryKey):
def __init__(self, column_type, icat_key, icat_id, min_version=(0, 0, 0)):

@@ -124,4 +119,4 @@ self.icat_key = icat_key

class Keyword(QueryKey):
def __init__(self, column_type, icat_key):

@@ -134,3 +129,2 @@ self.icat_key = icat_key

class ColumnType:
@staticmethod

@@ -146,3 +140,2 @@ def to_python(string):

class Integer(ColumnType):
@staticmethod

@@ -158,3 +151,2 @@ def to_python(string):

class String(ColumnType):
@staticmethod

@@ -176,3 +168,2 @@ def to_python(string):

class DateTime(ColumnType):
@staticmethod

@@ -179,0 +170,0 @@ def to_python(string):

@@ -73,3 +73,2 @@ import socket

class Connection:
DISALLOWING_PAM_PLAINTEXT = True

@@ -103,6 +102,3 @@

if (
self.server_version >= (4, 3, 0)
and not cfg.legacy_auth.force_legacy_auth
):
if self.server_version >= (4, 3, 0) and not cfg.legacy_auth.force_legacy_auth:
import irods.auth

@@ -139,6 +135,3 @@

def server_version(self):
detected = tuple(
int(x)
for x in self._server_version.relVersion.replace("rods", "").split(".")
)
detected = tuple(int(x) for x in self._server_version.relVersion.replace("rods", "").split("."))
return safe_eval(os.environ.get("IRODS_SERVER_VERSION", "()")) or detected

@@ -192,8 +185,3 @@

try:
err_msg = (
iRODSMessage(msg=msg.error)
.get_main_message(Error)
.RErrMsg_PI[0]
.msg
)
err_msg = iRODSMessage(msg=msg.error).get_main_message(Error).RErrMsg_PI[0].msg
except TypeError:

@@ -240,11 +228,5 @@ err_msg = None

CApath = getattr(irods_account, "ssl_ca_certificate_path", None)
verify = (
ssl.CERT_NONE
if ((None is CAfile is CApath) or verify_server == "none")
else ssl.CERT_REQUIRED
)
verify = ssl.CERT_NONE if ((None is CAfile is CApath) or verify_server == "none") else ssl.CERT_REQUIRED
# See https://stackoverflow.com/questions/30461969/disable-default-certificate-verification-in-python-2-7-9/49040695#49040695
ctx = ssl.create_default_context(
ssl.Purpose.SERVER_AUTH, cafile=CAfile, capath=CApath
)
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=CAfile, capath=CApath)
# Note: check_hostname must be assigned prior to verify_mode property or Python library complains!

@@ -269,5 +251,3 @@ ctx.check_hostname = verify_server == "hostname" and verify != ssl.CERT_NONE

# Wrap socket with context
wrapped_socket = context.wrap_socket(
self.socket, server_hostname=(host if context.check_hostname else None)
)
wrapped_socket = context.wrap_socket(self.socket, server_hostname=(host if context.check_hostname else None))

@@ -281,5 +261,3 @@ # Initial SSL handshake

# Send header-only message with client side encryption settings
packed_header = iRODSMessage.pack_header(
algo, key_size, salt_size, hash_rounds, 0
)
packed_header = iRODSMessage.pack_header(algo, key_size, salt_size, hash_rounds, 0)
wrapped_socket.sendall(packed_header)

@@ -302,6 +280,3 @@

except socket.error:
raise NetworkException(
"Could not connect to specified host and port: "
+ "{}:{}".format(*address)
)
raise NetworkException("Could not connect to specified host and port: " + "{}:{}".format(*address))

@@ -318,3 +293,2 @@ self.socket = s

if not self.requires_cs_negotiation():
if len(main_message.option) >= LONG_NAME_LEN:

@@ -353,5 +327,3 @@ message = "Application name too long."

# Perform the negotiation
neg_result, status = perform_negotiation(
client_policy=client_policy, server_policy=server_policy
)
neg_result, status = perform_negotiation(client_policy=client_policy, server_policy=server_policy)

@@ -368,7 +340,3 @@ # Send negotiation result to server

self.disconnect()
raise NetworkException(
"Client-Server negotiation failure: {},{}".format(
client_policy, server_policy
)
)
raise NetworkException("Client-Server negotiation failure: {},{}".format(client_policy, server_policy))

@@ -391,7 +359,3 @@ # Server responds with version

try:
if (
self.socket
and getattr(self, "_disconnected", False) == False
and self.socket.fileno() != -1
):
if self.socket and getattr(self, "_disconnected", False) == False and self.socket.fileno() != -1:
disconnect_msg = iRODSMessage(msg_type="RODS_DISCONNECT")

@@ -443,5 +407,3 @@ self.send(disconnect_msg)

# CLIENT CONTEXT
self.client_ctx = gssapi.SecurityContext(
name=server_name, mech=gsi_mech, flags=[2, 4], usage="initiate"
)
self.client_ctx = gssapi.SecurityContext(name=server_name, mech=gsi_mech, flags=[2, 4], usage="initiate")

@@ -485,3 +447,2 @@ def send_gsi_token(self, server_token=None):

while not (self.client_ctx.complete):
server_token = self.receive_gsi_token()

@@ -503,5 +464,3 @@

# https://github.com/irods/irods/blob/master/lib/api/include/apiNumber.h#L158
auth_req = iRODSMessage(
msg_type="RODS_API_REQ", msg=message_body, int_info=1201
)
auth_req = iRODSMessage(msg_type="RODS_API_REQ", msg=message_body, int_info=1201)
self.send(auth_req)

@@ -553,12 +512,6 @@ # Getting the challenge message

inline_password = (
self.account.authentication_scheme
== self.account._original_authentication_scheme
)
inline_password = self.account.authentication_scheme == self.account._original_authentication_scheme
time_to_live_in_hours = cfg.legacy_auth.pam.time_to_live_in_hours
new_pam_password = self.account.password
if (
not inline_password
and cfg.legacy_auth.pam.password_for_auto_renew is not None
):
if not inline_password and cfg.legacy_auth.pam.password_for_auto_renew is not None:
# Login using PAM password from .irodsA

@@ -583,5 +536,3 @@ try:

KVP_ESCAPED_CHARS = r"\;="
kvp_escape = lambda s: "".join(
(rf"\{c}" if c in KVP_ESCAPED_CHARS else c) for c in s
)
kvp_escape = lambda s: "".join((rf"\{c}" if c in KVP_ESCAPED_CHARS else c) for c in s)

@@ -603,5 +554,3 @@ # Generate a new PAM password.

use_dedicated_pam_api = cfg.legacy_auth.pam.force_use_of_dedicated_pam_api or (
len(ctx) >= MAX_NAME_LEN
)
use_dedicated_pam_api = cfg.legacy_auth.pam.force_use_of_dedicated_pam_api or (len(ctx) >= MAX_NAME_LEN)

@@ -617,9 +566,5 @@ if use_dedicated_pam_api:

api_name = (
"PAM_AUTH_REQUEST_AN" if use_dedicated_pam_api else "AUTH_PLUG_REQ_AN"
)
api_name = "PAM_AUTH_REQUEST_AN" if use_dedicated_pam_api else "AUTH_PLUG_REQ_AN"
auth_req = iRODSMessage(
msg_type="RODS_API_REQ", msg=message_body, int_info=api_number[api_name]
)
auth_req = iRODSMessage(msg_type="RODS_API_REQ", msg=message_body, int_info=api_number[api_name])

@@ -635,5 +580,3 @@ self.send(auth_req)

Pam_Response_Class = (
PamAuthRequestOut if use_dedicated_pam_api else AuthPluginOut
)
Pam_Response_Class = PamAuthRequestOut if use_dedicated_pam_api else AuthPluginOut

@@ -676,5 +619,3 @@ auth_out = output_message.get_main_message(Pam_Response_Class)

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_READ_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_READ_AN"])

@@ -711,5 +652,3 @@ logger.debug(desc)

challenge = challenge.strip()
padded_pwd = struct.pack(
"%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip()
)
padded_pwd = struct.pack("%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip())

@@ -765,5 +704,3 @@ m = hashlib.md5()

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_LSEEK_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_LSEEK_AN"])

@@ -785,5 +722,3 @@ self.send(message)

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CLOSE_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CLOSE_AN"])

@@ -794,5 +729,3 @@ self.send(message)

def temp_password(self):
request = iRODSMessage(
"RODS_API_REQ", msg=None, int_info=api_number["GET_TEMP_PASSWORD_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=None, int_info=api_number["GET_TEMP_PASSWORD_AN"])

@@ -799,0 +732,0 @@ # Send and receive request

@@ -31,3 +31,2 @@ import io

class iRODSReplica:
def __init__(self, number, status, resource_name, path, resc_hier, **kwargs):

@@ -43,9 +42,6 @@ self.number = number

def __repr__(self):
return "<{}.{} {}>".format(
self.__class__.__module__, self.__class__.__name__, self.resource_name
)
return "<{}.{} {}>".format(self.__class__.__module__, self.__class__.__name__, self.resource_name)
class iRODSDataObject:
def __init__(self, manager, parent=None, results=None):

@@ -67,17 +63,21 @@ self.manager = manager

replica_args = [(
(r[DataObject.replica_number],
r[DataObject.replica_status],
r[DataObject.resource_name],
r[DataObject.path],
r[DataObject.resc_hier],
)
,dict(
checksum=r[DataObject.checksum],
size=r[DataObject.size],
comments=r[DataObject.comments],
create_time=r[DataObject.create_time],
modify_time=r[DataObject.modify_time],
replica_args = [
(
(
r[DataObject.replica_number],
r[DataObject.replica_status],
r[DataObject.resource_name],
r[DataObject.path],
r[DataObject.resc_hier],
),
dict(
checksum=r[DataObject.checksum],
size=r[DataObject.size],
comments=r[DataObject.comments],
create_time=r[DataObject.create_time],
modify_time=r[DataObject.modify_time],
),
)
) for r in replicas]
for r in replicas
]

@@ -87,5 +87,5 @@ # Adjust for adding access_time in the iRODS 5 case.

if self.manager.sess.server_version >= (5,):
for n,r in enumerate(replicas):
for n, r in enumerate(replicas):
replica_args[n][1]['access_time'] = r[DataObject.access_time]
self.replicas = [iRODSReplica(*a,**k) for a,k in replica_args]
self.replicas = [iRODSReplica(*a, **k) for a, k in replica_args]

@@ -100,11 +100,7 @@ self._meta = None

if not self._meta:
self._meta = iRODSMetaCollection(
self.manager.sess.metadata, DataObject, self.path
)
self._meta = iRODSMetaCollection(self.manager.sess.metadata, DataObject, self.path)
return self._meta
def open(self, mode="r", finalize_on_close=True, **options):
return self.manager.open(
self.path, mode, finalize_on_close=finalize_on_close, **options
)
return self.manager.open(self.path, mode, finalize_on_close=finalize_on_close, **options)

@@ -161,5 +157,3 @@ def chksum(self, **options):

def replica_access_info(self):
message_body = JSON_Message(
{"fd": self.desc}, server_version=self.conn.server_version
)
message_body = JSON_Message({"fd": self.desc}, server_version=self.conn.server_version)
message = iRODSMessage(

@@ -183,14 +177,8 @@ "RODS_API_REQ",

replica_token = dobj_info.get("replica_token", "")
resc_hier = (dobj_info.get("data_object_info") or {}).get(
"resource_hierarchy", ""
)
resc_hier = (dobj_info.get("data_object_info") or {}).get("resource_hierarchy", "")
return (replica_token, resc_hier)
def _close_replica(self):
server_version = ast.literal_eval(
os.environ.get("IRODS_VERSION_OVERRIDE", "()")
)
if (
server_version or self.conn.server_version
) < IRODS_SERVER_WITH_CLOSE_REPLICA_API:
server_version = ast.literal_eval(os.environ.get("IRODS_VERSION_OVERRIDE", "()"))
if (server_version or self.conn.server_version) < IRODS_SERVER_WITH_CLOSE_REPLICA_API:
return False

@@ -197,0 +185,0 @@ message_body = JSON_Message(

@@ -92,5 +92,3 @@ # if you're copying these from the docs, you might find the following regex helpful:

codes: "Dict[int, iRODSException]" = {}
positive_code_error_message = (
"For {name}, a positive code of {attrs[code]} was declared."
)
positive_code_error_message = "For {name}, a positive code of {attrs[code]} was declared."

@@ -100,5 +98,3 @@ def __init__(self, name, bases, attrs):

if attrs["code"] > 0:
print(
self.positive_code_error_message.format(**locals()), file=sys.stderr
)
print(self.positive_code_error_message.format(**locals()), file=sys.stderr)
exit(1)

@@ -122,5 +118,3 @@ iRODSExceptionMeta.codes[attrs["code"]] = self

try:
return self.__class__.__name__ + repr(
tuple([e, errno.errorcode[e], os.strerror(e)])
)
return self.__class__.__name__ + repr(tuple([e, errno.errorcode[e], os.strerror(e)]))
except:

@@ -130,7 +124,5 @@ # The errno code is unrecognized, so fall through to default representation.

return self.__class__.__name__ + repr(
tuple(
[
e,
]
)
tuple([
e,
])
)

@@ -182,5 +174,3 @@

negated = -abs(nominal[0])
return (
c if (negated <= -abs(THRESHOLD)) else negated
) # produce a negative for nonzero integer input
return c if (negated <= -abs(THRESHOLD)) else negated # produce a negative for nonzero integer input

@@ -200,5 +190,3 @@

else:
message = "Supplied code {the_code!r} must be integer or string".format(
**locals()
)
message = "Supplied code {the_code!r} must be integer or string".format(**locals())
raise RuntimeError(message)

@@ -213,7 +201,3 @@ finally:

cls = iRODSExceptionMeta.codes.get(rounded)
return (
cls
if not name_only
else (cls.__name__ if cls is not None else "Unknown_iRODS_error")
)
return cls if not name_only else (cls.__name__ if cls is not None else "Unknown_iRODS_error")

@@ -220,0 +204,0 @@

@@ -20,5 +20,3 @@ import json

if not self._is_supported():
raise OperationNotSupported(
"GenQuery2 is not supported by default on this iRODS version."
)
raise OperationNotSupported("GenQuery2 is not supported by default on this iRODS version.")

@@ -37,5 +35,3 @@ def execute(self, query, zone=None):

effective_zone = self.session.zone if zone is None else zone
return json.loads(
self._exec_genquery2("", effective_zone, column_mappings_flag=True)
)
return json.loads(self._exec_genquery2("", effective_zone, column_mappings_flag=True))

@@ -48,5 +44,3 @@ def _exec_genquery2(self, query, zone, sql_flag=False, column_mappings_flag=False):

msg.column_mappings = 1 if column_mappings_flag else 0
message = iRODSMessage(
"RODS_API_REQ", msg=msg, int_info=api_number["GENQUERY2_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=msg, int_info=api_number["GENQUERY2_AN"])
with self.session.pool.get_connection() as conn:

@@ -53,0 +47,0 @@ conn.send(message)

import contextlib
import os
import sys
import irods.exception as ex
from irods import env_filename_from_keyword_args
import irods.exception as ex
from irods.message import ET, XML_Parser_Type, IRODS_VERSION
from irods.message import _IRODS_VERSION, ET, XML_Parser_Type
from irods.path import iRODSPath

@@ -18,4 +19,4 @@ from irods.session import iRODSSession

class StopTestsException(Exception):
def __init__(self, *args, **kwargs):

@@ -33,5 +34,3 @@ super().__init__(*args, **kwargs)

def _get_server_version_for_test(session, curtail_length):
return session._server_version(session.GET_SERVER_VERSION_WITHOUT_AUTH)[
:curtail_length
]
return session._server_version(session.GET_SERVER_VERSION_WITHOUT_AUTH)[:curtail_length]

@@ -59,5 +58,8 @@

session = iRODSSession(irods_env_file=env_file, **kwargs)
# irods.test.helpers version of this function sets test_server_version True by default, so
# that sessions generated for the test methods will abort on connecting with a server that
# is too recent. This is a way to ensure that tests don't fail due to a server mismatch.
if test_server_version:
connected_version = _get_server_version_for_test(session, curtail_length=3)
advertised_version = IRODS_VERSION[:3]
advertised_version = _IRODS_VERSION[:3]
if connected_version > advertised_version:

@@ -106,5 +108,3 @@ msg = (

@contextlib.contextmanager
def temporarily_assign_attribute(
target, attr, value, not_set_indicator=_unlikely_value()
):
def temporarily_assign_attribute(target, attr, value, not_set_indicator=_unlikely_value()):
save = not_set_indicator

@@ -153,3 +153,3 @@ try:

# Utility class and factory function for storing the original value of variables within the given namespace.
def create_value_cache(namespace:dict):
def create_value_cache(namespace: dict):
class CachedValues:

@@ -161,4 +161,4 @@ __namespace = namespace

cached_value = cls.__namespace[name]
setattr(cls,name,property(lambda self: cached_value))
setattr(cls, name, property(lambda self: cached_value))
return CachedValues()

@@ -98,13 +98,7 @@ """From rodsKeyWdDef.hpp"""

DRYRUN_KW = "dryrun" # do a dry run #
ACL_COLLECTION_KW = (
"aclCollection" # the collection from which the ACL should be used #
)
ACL_COLLECTION_KW = "aclCollection" # the collection from which the ACL should be used #
NO_CHK_COPY_LEN_KW = "noChkCopyLen" # Don't check the len when transfering #
TICKET_KW = "ticket" # for ticket-based-access #
PURGE_CACHE_KW = (
"purgeCache" # purge the cache copy right after the operation JMC - backport 4537
)
EMPTY_BUNDLE_ONLY_KW = (
"emptyBundleOnly" # delete emptyBundleOnly # # JMC - backport 4552
)
PURGE_CACHE_KW = "purgeCache" # purge the cache copy right after the operation JMC - backport 4537
EMPTY_BUNDLE_ONLY_KW = "emptyBundleOnly" # delete emptyBundleOnly # # JMC - backport 4552
GET_RESOURCE_INFO_OP_TYPE_KW = "getResourceInfoOpType"

@@ -114,8 +108,4 @@

# JMC - backport 4599
LOCK_TYPE_KW = (
"lockType" # valid values are READ_LOCK_TYPE, WRITE_LOCK_TYPE and UNLOCK_TYPE #
)
LOCK_CMD_KW = (
"lockCmd" # valid values are SET_LOCK_WAIT_CMD, SET_LOCK_CMD and GET_LOCK_CMD #
)
LOCK_TYPE_KW = "lockType" # valid values are READ_LOCK_TYPE, WRITE_LOCK_TYPE and UNLOCK_TYPE #
LOCK_CMD_KW = "lockCmd" # valid values are SET_LOCK_WAIT_CMD, SET_LOCK_CMD and GET_LOCK_CMD #
LOCK_FD_KW = "lockFd" # Lock file desc for unlock #

@@ -127,5 +117,3 @@ MAX_SUB_FILE_KW = "maxSubFile" # max number of files for tar file bundles #

# =-=-=-=-=-=-=-
MAX_SUB_FILE_KW = (
"maxSubFile" # max number of files for tar file bundles # # JMC - backport 4771
)
MAX_SUB_FILE_KW = "maxSubFile" # max number of files for tar file bundles # # JMC - backport 4771

@@ -132,0 +120,0 @@ # OBJ_PATH_KW already defined #

class Manager:
__server_version = ()

@@ -4,0 +3,0 @@

@@ -7,9 +7,5 @@ from irods.api_number import api_number

with session.pool.get_connection() as conn:
message_body = JSON_Message(
{"logical_path": path, "options": options}, conn.server_version
)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["TOUCH_APN"]
)
message_body = JSON_Message({"logical_path": path, "options": options}, conn.server_version)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["TOUCH_APN"])
conn.send(message)
response = conn.recv()

@@ -34,25 +34,11 @@ from os.path import basename, dirname

raise
cond = (
()
if not ids
else (
(In(User.id, list(map(int, ids))),)
if len(ids) > 1
else (User.id == int(ids[0]),)
)
)
return [
iRODSUser(session.users, i)
for i in session.query(User.id, User.name, User.type, User.zone).filter(*cond)
]
cond = () if not ids else ((In(User.id, list(map(int, ids))),) if len(ids) > 1 else (User.id == int(ids[0]),))
return [iRODSUser(session.users, i) for i in session.query(User.id, User.name, User.type, User.zone).filter(*cond)]
class AccessManager(Manager):
def get(self, target, report_raw_acls=True, **kw):
if report_raw_acls:
return self.__get_raw(
target, **kw
) # prefer a behavior consistent with 'ils -A`
return self.__get_raw(target, **kw) # prefer a behavior consistent with 'ils -A`

@@ -74,7 +60,3 @@ # different query whether target is an object or a collection

results = (
self.sess.query(user_type.name, user_type.zone, access_type.name)
.filter(*conditions)
._all()
)
results = self.sess.query(user_type.name, user_type.zone, access_type.name).filter(*conditions)._all()

@@ -96,5 +78,3 @@ def get_usertype(row):

def coll_access_query(self, path):
return self.sess.query(Collection, CollectionAccess).filter(
Collection.name == path
)
return self.sess.query(Collection, CollectionAccess).filter(Collection.name == path)

@@ -104,5 +84,3 @@ def data_access_query(self, path):

dn = irods_basename(path)
return self.sess.query(DataObject, DataAccess).filter(
Collection.name == cn, DataObject.name == dn
)
return self.sess.query(DataObject, DataAccess).filter(Collection.name == cn, DataObject.name == dn)

@@ -138,5 +116,3 @@ def __get_raw(self, target, **kw):

extant_ids = set(u[User.id] for u in self.sess.query(User))
rows = [
r for r in query_func(target.path) if r[access_column.user_id] in extant_ids
]
rows = [r for r in query_func(target.path) if r[access_column.user_id] in extant_ids]
userids = set(r[access_column.user_id] for r in rows)

@@ -160,14 +136,12 @@

acls = list(
{
iRODSAccess(
r[access_column.name],
target.path,
user_lookup[r[access_column.user_id]].name,
user_lookup[r[access_column.user_id]].zone,
user_lookup[r[access_column.user_id]].type,
)
for r in rows
}
)
acls = list({
iRODSAccess(
r[access_column.name],
target.path,
user_lookup[r[access_column.user_id]].name,
user_lookup[r[access_column.user_id]].zone,
user_lookup[r[access_column.user_id]].type,
)
for r in rows
})
return acls

@@ -174,0 +148,0 @@

@@ -19,3 +19,2 @@ from irods.models import Collection, DataObject

class CollectionManager(Manager):
def get(self, path):

@@ -42,8 +41,4 @@ path = iRODSCollection.normalize_path(path)

message_body = CollectionRequest(
collName=path, KeyValPair_PI=StringStringMap(options)
)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["COLL_CREATE_AN"]
)
message_body = CollectionRequest(collName=path, KeyValPair_PI=StringStringMap(options))
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["COLL_CREATE_AN"])
with self.sess.pool.get_connection() as conn:

@@ -71,5 +66,3 @@ conn.send(message)

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["RM_COLL_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["RM_COLL_AN"])
with self.sess.pool.get_connection() as conn:

@@ -126,5 +119,3 @@ conn.send(message)

message_body = ObjCopyRequest(srcDataObjInp_PI=src, destDataObjInp_PI=dest)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_RENAME_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_RENAME_AN"])

@@ -149,5 +140,3 @@ with self.sess.pool.get_connection() as conn:

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["PHY_PATH_REG_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["PHY_PATH_REG_AN"])

@@ -154,0 +143,0 @@ with self.sess.pool.get_connection() as conn:

@@ -69,5 +69,3 @@ import ast

_update_types[:] = list(
(k, v)
for k, v in collections.OrderedDict([(type_, factory_)] + _update_types).items()
if v is not None
(k, v) for k, v in collections.OrderedDict([(type_, factory_)] + _update_types).items() if v is not None
)

@@ -107,5 +105,3 @@

else:
logging_function(
"Could not derive an update function for: %r", object_
)
logging_function("Could not derive an update function for: %r", object_)
continue

@@ -129,3 +125,2 @@

class ManagedBufferedRandom(io.BufferedRandom):
def __init__(self, *a, **kwd):

@@ -136,9 +131,13 @@ # Help ensure proper teardown sequence by storing a reference to the session,

super(ManagedBufferedRandom, self).__init__(*a, **kwd)
self.do_close = True
import irods.session
with irods.session._fds_lock:
irods.session._fds[self] = None
if irods.session._fds is not None:
irods.session._fds[self] = None
def __del__(self):
if not self.closed:
if self.do_close and not self.closed:
self.close()

@@ -150,5 +149,3 @@ call___del__if_exists(super(ManagedBufferedRandom, self))

DEFAULT_NUMBER_OF_THREADS = (
0 # Defaults for reasonable number of threads -- optimized to be
)
DEFAULT_NUMBER_OF_THREADS = 0 # Defaults for reasonable number of threads -- optimized to be
# performant but allow no more worker threads than available CPUs.

@@ -171,3 +168,2 @@

class DataObjectManager(Manager):
READ_BUFFER_SIZE = 1024 * io.DEFAULT_BUFFER_SIZE

@@ -225,5 +221,3 @@ WRITE_BUFFER_SIZE = 1024 * io.DEFAULT_BUFFER_SIZE

return obj_sz > MAXIMUM_SINGLE_THREADED_TRANSFER_SIZE
message = "obj_sz of {obj_sz!r} is neither an integer nor a seekable object".format(
**locals()
)
message = "obj_sz of {obj_sz!r} is neither an integer nor a seekable object".format(**locals())
raise RuntimeError(message)

@@ -249,17 +243,19 @@ finally:

data_open_returned_values_ = {}
with self.open(
obj, "r", returned_values=data_open_returned_values_, **options
) as o:
if self.should_parallelize_transfer(
num_threads, o, open_options=options.items()
):
if not self.parallel_get(
(obj, o),
local_file,
num_threads=num_threads,
target_resource_name=options.get(kw.RESC_NAME_KW, ""),
data_open_returned_values=data_open_returned_values_,
updatables=updatables,
):
raise RuntimeError("parallel get failed")
with self.open(obj, "r", returned_values=data_open_returned_values_, **options) as o:
if self.should_parallelize_transfer(num_threads, o, open_options=options.items()):
error = RuntimeError("parallel get failed")
try:
if not self.parallel_get(
(obj, o),
local_file,
num_threads=num_threads,
target_resource_name=options.get(kw.RESC_NAME_KW, ""),
data_open_returned_values=data_open_returned_values_,
updatables=updatables,
):
raise error
except ex.iRODSException as e:
raise e
except BaseException as e:
raise error from e
else:

@@ -271,10 +267,3 @@ with open(local_file, "wb") as f:

def get(
self,
path,
local_path=None,
num_threads=DEFAULT_NUMBER_OF_THREADS,
updatables=(),
**options
):
def get(self, path, local_path=None, num_threads=DEFAULT_NUMBER_OF_THREADS, updatables=(), **options):
"""

@@ -290,12 +279,7 @@ Get a reference to the data object at the specified `path'.

if local_path:
self._download(
path,
local_path,
num_threads=num_threads,
updatables=updatables,
**options
)
self._download(path, local_path, num_threads=num_threads, updatables=updatables, **options)
query = (
self.sess.query(DataObject)
self.sess
.query(DataObject)
.filter(DataObject.name == irods_basename(path))

@@ -307,5 +291,3 @@ .filter(DataObject.collection_id == parent.id)

if self.sess.ticket__:
query = query.filter(
Collection.id != 0
) # a no-op, but necessary because CAT_SQL_ERR results if the ticket
query = query.filter(Collection.id != 0) # a no-op, but necessary because CAT_SQL_ERR results if the ticket
# is for a DataObject and we don't explicitly join to Collection

@@ -343,13 +325,9 @@

updatables=(),
**options
**options,
):
# Decide if a put option should be used and modify options accordingly.
self._resolve_force_put_option(
options, default_setting=client_config.data_objects.force_put_by_default
)
self._resolve_force_put_option(options, default_setting=client_config.data_objects.force_put_by_default)
if self.sess.collections.exists(irods_path):
obj = iRODSCollection.normalize_path(
irods_path, os.path.basename(local_path)
)
obj = iRODSCollection.normalize_path(irods_path, os.path.basename(local_path))
else:

@@ -363,18 +341,21 @@ obj = irods_path

sizelist = []
if self.should_parallelize_transfer(
num_threads, f, measured_obj_size=sizelist, open_options=options
):
if self.should_parallelize_transfer(num_threads, f, measured_obj_size=sizelist, open_options=options):
o = deferred_call(self.open, (obj, "w"), options)
f.close()
if not self.parallel_put(
local_path,
(obj, o),
total_bytes=sizelist[0],
num_threads=num_threads,
target_resource_name=options.get(kw.RESC_NAME_KW, "")
or options.get(kw.DEST_RESC_NAME_KW, ""),
open_options=options,
updatables=updatables,
):
raise RuntimeError("parallel put failed")
error = RuntimeError("parallel put failed")
try:
if not self.parallel_put(
local_path,
(obj, o),
total_bytes=sizelist[0],
num_threads=num_threads,
target_resource_name=options.get(kw.RESC_NAME_KW, "") or options.get(kw.DEST_RESC_NAME_KW, ""),
open_options=options,
updatables=updatables,
):
raise error
except ex.iRODSException as e:
raise e
except BaseException as e:
raise error from e
else:

@@ -407,5 +388,3 @@ with self.open(obj, "w", **options) as o:

message_body = DataObjChksumRequest(path, **options)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CHKSUM_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CHKSUM_AN"])
checksum = ""

@@ -421,8 +400,6 @@ msg_retn = []

response = msg_retn[0]
logging.warning("Exception checksumming data object %r - %r", path, exc)
logger.warning("Exception checksumming data object %r - %r", path, exc)
if "response" in locals():
try:
results = response.get_main_message(
DataObjChksumResponse, r_error=r_error_stack
)
results = response.get_main_message(DataObjChksumResponse, r_error=r_error_stack)
checksum = results.myStr.strip()

@@ -508,7 +485,3 @@ if checksum[0] in (

def create(
self,
path,
resource=None,
force=client_config.getter("data_objects", "force_create_by_default"),
**options
self, path, resource=None, force=client_config.getter("data_objects", "force_create_by_default"), **options
):

@@ -546,5 +519,3 @@ """

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CREATE_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CREATE_AN"])

@@ -564,9 +535,7 @@ with self.sess.pool.get_connection() as conn:

_RESC_flags_for_open = frozenset(
(
kw.RESC_NAME_KW,
kw.DEST_RESC_NAME_KW, # may be deprecated in the future
kw.RESC_HIER_STR_KW,
)
)
_RESC_flags_for_open = frozenset((
kw.RESC_NAME_KW,
kw.DEST_RESC_NAME_KW, # may be deprecated in the future
kw.RESC_HIER_STR_KW,
))

@@ -585,3 +554,3 @@ def open(

allow_redirect=client_config.getter("data_objects", "allow_redirect"),
**options
**options,
):

@@ -681,11 +650,7 @@ _raw_fd_holder = options.get("_raw_fd_holder", [])

# Perform DATA_OBJ_OPEN call
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_OPEN_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_OPEN_AN"])
conn.send(message)
desc = conn.recv().int_info
raw = iRODSDataObjectFileRaw(
conn, desc, finalize_on_close=finalize_on_close, **options
)
raw = iRODSDataObjectFileRaw(conn, desc, finalize_on_close=finalize_on_close, **options)
raw.session = directed_sess

@@ -715,5 +680,3 @@

if self.sess.server_version < required_server_version:
raise ex.NotImplementedInIRODSServer(
"replica_truncate", required_server_version
)
raise ex.NotImplementedInIRODSServer("replica_truncate", required_server_version)

@@ -730,5 +693,3 @@ message_body = FileOpenRequest(

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["REPLICA_TRUNCATE_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["REPLICA_TRUNCATE_AN"])

@@ -759,5 +720,3 @@ with self.sess.pool.get_connection() as conn:

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_TRIM_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_TRIM_AN"])

@@ -787,5 +746,3 @@ with self.sess.pool.get_connection() as conn:

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_UNLINK_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_UNLINK_AN"])

@@ -843,5 +800,3 @@ with self.sess.pool.get_connection() as conn:

message_body = ObjCopyRequest(srcDataObjInp_PI=src, destDataObjInp_PI=dest)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_RENAME_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_RENAME_AN"])

@@ -882,5 +837,3 @@ with self.sess.pool.get_connection() as conn:

message_body = ObjCopyRequest(srcDataObjInp_PI=src, destDataObjInp_PI=dest)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_COPY_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_COPY_AN"])

@@ -930,5 +883,3 @@ with self.sess.pool.get_connection() as conn:

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_REPL_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_REPL_AN"])

@@ -952,5 +903,3 @@ with self.sess.pool.get_connection() as conn:

)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["PHY_PATH_REG_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["PHY_PATH_REG_AN"])

@@ -962,7 +911,3 @@ with self.sess.pool.get_connection() as conn:

def modDataObjMeta(self, data_obj_info, meta_dict, **options):
if (
"rescHier" not in data_obj_info
and "rescName" not in data_obj_info
and "replNum" not in data_obj_info
):
if "rescHier" not in data_obj_info and "rescName" not in data_obj_info and "replNum" not in data_obj_info:
meta_dict["all"] = ""

@@ -1011,3 +956,3 @@

if 'dataAccessTime' in DataObjInfo_class.__dict__:
fields["dataAccessTime"]=""
fields["dataAccessTime"] = ""

@@ -1014,0 +959,0 @@ message_body = ModDataObjMeta_for_session(self.sess)(

@@ -30,9 +30,17 @@ import logging

# This was necessarily made separate from the MetadataManager definition
# in order to avoid infinite recursion in iRODSMetaCollection.__getattr__
_MetadataManager_opts_initializer = {'admin': False, 'timestamps': False, 'iRODSMeta_type': iRODSMeta, 'reload': True}
class MetadataManager(Manager):
def __init__(self, *_):
self._opts = _MetadataManager_opts_initializer.copy()
super().__init__(*_)
@property
def use_timestamps(self):
return getattr(self, "_use_ts", False)
return self._opts['timestamps']
__kw : Dict[str, Any] = {} # default (empty) keywords
__kw: Dict[str, Any] = {} # default (empty) keywords

@@ -44,8 +52,19 @@ def _updated_keywords(self, opts):

def __call__(self, admin=False, timestamps=False, **irods_kw_opt):
if admin:
irods_kw_opt.update([(kw.ADMIN_KW, "")])
def get_api_keywords(self):
return self.__kw.copy()
def __call__(self, **flags):
# Make a new shallow copy of the manager object, but update options from parameter list.
new_self = copy.copy(self)
new_self._use_ts = timestamps
new_self.__kw = irods_kw_opt
new_self._opts = copy.copy(self._opts)
# Update the flags that do bookkeeping in the returned(new) manager object.
new_self._opts.update((key, val) for key, val in flags.items() if val is not None)
# Update the ADMIN_KW flag in the returned(new) object.
if new_self._opts.get('admin'):
self.__kw[kw.ADMIN_KW] = ""
else:
self.__kw.pop(kw.ADMIN_KW, None)
return new_self

@@ -72,2 +91,5 @@

def get(self, model_cls, path):
if not path:
# Short circuit. This should be of the same type as the object returned at the function's end.
return []
resource_type = self._model_class_to_resource_type(model_cls)

@@ -102,3 +124,3 @@ model = {

return [
iRODSMeta(
self._opts['iRODSMeta_type'](None, None, None)._from_column_triple(
row[model.name], row[model.value], row[model.units], **meta_opts(row)

@@ -113,13 +135,5 @@ )

message_body = MetadataRequest(
"add",
"-" + resource_type,
path,
meta.name,
meta.value,
meta.units,
**self._updated_keywords(opts)
"add", "-" + resource_type, path, *meta._to_column_triple(), **self._updated_keywords(opts)
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"])
with self.sess.pool.get_connection() as conn:

@@ -133,13 +147,5 @@ conn.send(request)

message_body = MetadataRequest(
"rm",
"-" + resource_type,
path,
meta.name,
meta.value,
meta.units,
**self._updated_keywords(opts)
"rm", "-" + resource_type, path, *meta._to_column_triple(), **self._updated_keywords(opts)
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"])
with self.sess.pool.get_connection() as conn:

@@ -154,12 +160,5 @@ conn.send(request)

message_body = MetadataRequest(
"cp",
"-" + src_resource_type,
"-" + dest_resource_type,
src,
dest,
**self._updated_keywords(opts)
"cp", "-" + src_resource_type, "-" + dest_resource_type, src, dest, **self._updated_keywords(opts)
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"])

@@ -174,13 +173,5 @@ with self.sess.pool.get_connection() as conn:

message_body = MetadataRequest(
"set",
"-" + resource_type,
path,
meta.name,
meta.value,
meta.units,
**self._updated_keywords(opts)
"set", "-" + resource_type, path, *meta._to_column_triple(), **self._updated_keywords(opts)
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"])
with self.sess.pool.get_connection() as conn:

@@ -204,5 +195,3 @@ conn.send(request)

if not all(isinstance(op, AVUOperation) for op in avu_ops):
raise InvalidAtomicAVURequest(
"avu_ops must contain 1 or more AVUOperations"
)
raise InvalidAtomicAVURequest("avu_ops must contain 1 or more AVUOperations")
request = {

@@ -209,0 +198,0 @@ "admin_mode": True if kw.ADMIN_KW in self.__kw.keys() else False,

@@ -14,9 +14,6 @@ from irods.models import Resource

class ResourceManager(Manager):
@staticmethod
def serialize(context):
if isinstance(context, dict):
return ";".join(
"{}={}".format(key, value) for (key, value) in list(context.items())
)
return ";".join("{}={}".format(key, value) for (key, value) in list(context.items()))
return context

@@ -92,5 +89,3 @@

message_body = GeneralAdminRequest("rm", "resource", name, mode)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])
with self.sess.pool.get_connection() as conn:

@@ -106,5 +101,3 @@ conn.send(request)

with self.sess.pool.get_connection() as conn:
message_body = GeneralAdminRequest(
"modify", "resource", name, attribute, self.serialize(value)
)
message_body = GeneralAdminRequest("modify", "resource", name, attribute, self.serialize(value))

@@ -130,5 +123,3 @@ request = iRODSMessage(

message_body = GeneralAdminRequest(
"add", "childtoresc", parent, child, context
)
message_body = GeneralAdminRequest("add", "childtoresc", parent, child, context)

@@ -135,0 +126,0 @@ request = iRODSMessage(

@@ -30,3 +30,2 @@ import logging

class UserManager(Manager):
def _get_session(self):

@@ -40,12 +39,39 @@ return self.sess

def set_quota(self, user_name, amount, resource="total"):
return _do_GeneralAdminRequest(
self._get_session, "set-quota", "user", user_name, resource, str(amount)
)
return _do_GeneralAdminRequest(self._get_session, "set-quota", "user", user_name, resource, str(amount))
def remove_quota(self, user_name, resource="total"):
return _do_GeneralAdminRequest(
self._get_session, "set-quota", "user", user_name, resource, "0"
)
return _do_GeneralAdminRequest(self._get_session, "set-quota", "user", user_name, resource, "0")
@staticmethod
def _parse_user_and_zone(user_param, zone_param):
"""
Parse out user and zone components from the arguments given.
Args:
user_param: either a simple user name, or a combination of both
user and zone names joined with "#".
zone_param: a simple zone name,
Returns:
The resulting parsed user and zone.
Raises:
RuntimeError: in the case of formatting errors or conflicting zone names.
"""
if '#' in user_param:
u_parsed_user, u_parsed_zone = user_param.split('#', 1)
if not u_parsed_zone:
raise RuntimeError("The compound user#zone specification may not contain a zero-length zone")
if '#' in u_parsed_zone:
raise RuntimeError(f"{u_parsed_zone = } is wrongly formatted")
if zone_param and (u_parsed_zone != zone_param):
raise RuntimeError(
f"Two nonzero-length zone names ({u_parsed_zone}, {zone_param}) were given, but they do not agree."
)
return u_parsed_user, u_parsed_zone
return user_param, zone_param
def get(self, user_name, user_zone=""):
user_name, user_zone = self._parse_user_and_zone(user_name, user_zone)
if not user_zone:

@@ -62,11 +88,11 @@ user_zone = self.sess.zone

def create_remote(self, user_name:str, user_zone:str):
def create_remote(self, user_name: str, user_zone: str):
"""
Create an entry in the local catalog for a remote user. The user_type will be 'rodsuser'.
"""
if user_zone in (self.sess.zone,""):
if user_zone in (self.sess.zone, ""):
raise ValueError(f"Parameter [{user_zone = }] must be a remote zone.")
return self.create_with_password(user_name, password='', user_zone=user_zone)
def create_with_password(self, user_name:str, password:str, user_zone:str=""):
def create_with_password(self, user_name: str, password: str, user_zone: str = ""):
"""This method can be used by a groupadmin to initialize the password field while creating the new user.

@@ -85,13 +111,5 @@ (This is necessary since group administrators may not change the password of an existing user.)

user_name + ("" if not user_zone else f"#{user_zone}"),
(
""
if not password
else obf.obfuscate_new_password_with_key(
password, self.sess.pool.account.password
)
),
("" if not password else obf.obfuscate_new_password_with_key(password, self.sess.pool.account.password)),
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["USER_ADMIN_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["USER_ADMIN_AN"])
with self.sess.pool.get_connection() as conn:

@@ -107,7 +125,3 @@ conn.send(request)

"user",
(
user_name
if not user_zone or user_zone == self.sess.zone
else f"{user_name}#{user_zone}"
),
(user_name if not user_zone or user_zone == self.sess.zone else f"{user_name}#{user_zone}"),
user_type,

@@ -117,5 +131,3 @@ user_zone,

)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])
with self.sess.pool.get_connection() as conn:

@@ -130,15 +142,14 @@ conn.send(request)

_object = self.get(user_name, user_zone)
if _object.type == "rodsgroup": # noqa: SIM108
uz_args = (f"{_object.name}",)
else:
uz_args = (f"{_object.name}#{_object.zone}",)
message_body = GeneralAdminRequest(
"rm",
(
"user"
if (_object.type != "rodsgroup" or self.sess.server_version < (4, 3, 2))
else "group"
),
user_name,
user_zone,
("user" if (_object.type != "rodsgroup" or self.sess.server_version < (4, 3, 2)) else "group"),
*uz_args,
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])
with self.sess.pool.get_connection() as conn:

@@ -151,5 +162,3 @@ conn.send(request)

with self.sess.pool.get_connection() as conn:
message_body = GetTempPasswordForOtherRequest(
targetUser=user_name, unused=None
)
message_body = GetTempPasswordForOtherRequest(targetUser=user_name, unused=None)
request = iRODSMessage(

@@ -191,5 +200,3 @@ "RODS_API_REQ",

def modify_password(
self, old_value, new_value, modify_irods_authentication_file=False
):
def modify_password(self, old_value, new_value, modify_irods_authentication_file=False):
"""

@@ -205,3 +212,2 @@ Change the password for the current user (in the manner of `ipasswd').

with self.sess.pool.get_connection() as conn:
if (

@@ -214,12 +220,6 @@ old_value != self.sess.pool.account.password

hash_new_value = obf.obfuscate_new_password(
new_value, old_value, conn.client_signature
)
hash_new_value = obf.obfuscate_new_password(new_value, old_value, conn.client_signature)
message_body = UserAdminRequest(
"userpw", self.sess.username, "password", hash_new_value
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["USER_ADMIN_AN"]
)
message_body = UserAdminRequest("userpw", self.sess.username, "password", hash_new_value)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["USER_ADMIN_AN"])

@@ -260,9 +260,6 @@ conn.send(request)

with self.sess.pool.get_connection() as conn:
# if modifying password, new value needs obfuscating
if option == "password":
current_password = self.sess.pool.account.password
new_value = obf.obfuscate_new_password(
new_value, current_password, conn.client_signature
)
new_value = obf.obfuscate_new_password(new_value, current_password, conn.client_signature)

@@ -288,4 +285,5 @@ message_body = GeneralAdminRequest(

CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD = (4,3,4)
CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD = (4, 3, 4)
def get__group_create__user_type__default(session):

@@ -296,4 +294,4 @@ if session.server_version < CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD:

class GroupManager(UserManager):
def get(self, name, user_zone=""):

@@ -318,6 +316,3 @@ query = self.sess.query(Group).filter(Group.name == name)

sess = self.sess
if group_admin_flag or (
group_admin_flag is not False
and sess.users.get(sess.username).type == "groupadmin"
):
if group_admin_flag or (group_admin_flag is not False and sess.users.get(sess.username).type == "groupadmin"):
return (UserAdminRequest, "USER_ADMIN_AN")

@@ -337,9 +332,9 @@ return (GeneralAdminRequest, "GENERAL_ADMIN_AN")

Input parameters:
-----------------
name: This is the name to be given to the new group.
user_type: (deprecated parameter) This parameter should remain unused and will effectively resolve as "rodsgroup".
user_zone: (deprecated parameter) In iRODS 4.3+, do not use this parameter as groups may not be made for a remote zone.
auth_type: (deprecated parameter) This parameter should be left to default to "" as groups do not need authentication.
group_admin: If left to its default value of None, seamlessly allows a groupadmin to create new groups.
Input parameters:
-----------------
name: This is the name to be given to the new group.
user_type: (deprecated parameter) This parameter should remain unused and will effectively resolve as "rodsgroup".
user_zone: (deprecated parameter) In iRODS 4.3+, do not use this parameter as groups may not be made for a remote zone.
auth_type: (deprecated parameter) This parameter should be left to default to "" as groups do not need authentication.
group_admin: If left to its default value of None, seamlessly allows a groupadmin to create new groups.
"""

@@ -365,7 +360,12 @@

"add",
("user" if self.sess.server_version < CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD else "group"),
(
"user"
if self.sess.server_version < CREATE_GROUP__USER_TYPE__DEFAULT__API_CHANGE__VERSION_THRESHOLD
else "group"
),
name,
user_type,
"",
"")
"",
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_to_use)

@@ -379,18 +379,10 @@ with self.sess.pool.get_connection() as conn:

def getmembers(self, name):
results = self.sess.query(User).filter(
User.type != "rodsgroup", Group.name == name
)
results = self.sess.query(User).filter(User.type != "rodsgroup", Group.name == name)
return [iRODSUser(self, row) for row in results]
def addmember(
self, group_name, user_name, user_zone="", group_admin=None, **options
):
def addmember(self, group_name, user_name, user_zone="", group_admin=None, **options):
(MessageClass, api_key) = self._api_info(group_admin)
message_body = MessageClass(
"modify", "group", group_name, "add", user_name, user_zone
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number[api_key]
)
message_body = MessageClass("modify", "group", group_name, "add", user_name, user_zone)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number[api_key])
with self.sess.pool.get_connection() as conn:

@@ -401,13 +393,7 @@ conn.send(request)

def removemember(
self, group_name, user_name, user_zone="", group_admin=None, **options
):
def removemember(self, group_name, user_name, user_zone="", group_admin=None, **options):
(MessageClass, api_key) = self._api_info(group_admin)
message_body = MessageClass(
"modify", "group", group_name, "remove", user_name, user_zone
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number[api_key]
)
message_body = MessageClass("modify", "group", group_name, "remove", user_name, user_zone)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number[api_key])
with self.sess.pool.get_connection() as conn:

@@ -422,8 +408,4 @@ conn.send(request)

def set_quota(self, group_name, amount, resource="total"):
message_body = GeneralAdminRequest(
"set-quota", "group", group_name, resource, str(amount)
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
message_body = GeneralAdminRequest("set-quota", "group", group_name, resource, str(amount))
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])
with self.sess.pool.get_connection() as conn:

@@ -430,0 +412,0 @@ conn.send(request)

@@ -14,3 +14,2 @@ import logging

class ZoneManager(Manager):
def get(self, zone_name):

@@ -32,5 +31,3 @@ query = self.sess.query(Zone).filter(Zone.name == zone_name)

)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])
with self.sess.pool.get_connection() as conn:

@@ -44,5 +41,3 @@ conn.send(request)

message_body = GeneralAdminRequest("rm", "zone", zone_name)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])
with self.sess.pool.get_connection() as conn:

@@ -49,0 +44,0 @@ conn.send(request)

"""Define objects related to communication with iRODS server API endpoints."""
import struct
import ast
import json
import logging
import os
import socket
import json
import irods.exception as ex
import struct
import threading
import xml.etree.ElementTree as ET_xml
from collections import namedtuple
from typing import Optional
import xml.etree.ElementTree as ET_xml
from warnings import warn
import defusedxml.ElementTree as ET_secure_xml
import irods.exception as ex
from . import quasixml as ET_quasi_xml
from ..api_number import api_number
from collections import namedtuple
import os
import ast
import threading
from .message import Message
from .property_types import (
ArrayProperty,
BinaryProperty,
StringProperty,
IntegerProperty,
LongProperty,
ArrayProperty,
StringProperty,
SubmessageProperty,
)
from ..api_number import api_number

@@ -55,5 +59,3 @@

# choices -- to their corresponding names as strings (e.g. XML_Parser_Type.STANDARD_XML is mapped to 'STANDARD_XML'):
PARSER_TYPE_STRINGS = {
v: k for k, v in XML_Parser_Type.__members__.items() if v.value != 0
}
PARSER_TYPE_STRINGS = {v: k for k, v in XML_Parser_Type.__members__.items() if v.value != 0}

@@ -80,8 +82,4 @@ # We maintain values on a per-thread basis of:

_Quasi_Xml_Server_Version = _qxml_server_version(
"PYTHON_IRODSCLIENT_QUASI_XML_SERVER_VERSION"
)
if (
_Quasi_Xml_Server_Version is None
): # unspecified in environment yields empty tuple ()
_Quasi_Xml_Server_Version = _qxml_server_version("PYTHON_IRODSCLIENT_QUASI_XML_SERVER_VERSION")
if _Quasi_Xml_Server_Version is None: # unspecified in environment yields empty tuple ()
raise BadXMLSpec("Must properly specify a server version to use QUASI_XML")

@@ -91,5 +89,3 @@

_default_XML_env = os.environ.get(
"PYTHON_IRODSCLIENT_DEFAULT_XML", globals().get("_default_XML")
)
_default_XML_env = os.environ.get("PYTHON_IRODSCLIENT_DEFAULT_XML", globals().get("_default_XML"))

@@ -172,5 +168,3 @@ if not _default_XML_env:

_thrlocal.xml_type = (
default_XML_parser()
if xml_type in (None, XML_Parser_Type(0))
else XML_Parser_Type(xml_type)
default_XML_parser() if xml_type in (None, XML_Parser_Type(0)) else XML_Parser_Type(xml_type)
)

@@ -188,4 +182,18 @@ if isinstance(server_version, _TUPLE_LIKE_TYPES):

IRODS_VERSION = (5, 0, 1, "d")
# The symbol _IRODS_VERSION is for internal use in testing only. It indicates the current
# server version for which PRC has maintained compatibility. Attempting the unit tests with
# more recent servers will fail by design.
_IRODS_VERSION = (5, 0, 2, "d")
# This is the older, now deprecated, version of the above symbol.
_deprecated_names = {"IRODS_VERSION": _IRODS_VERSION}
def __getattr__(name):
if name in _deprecated_names:
warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2)
return _deprecated_names[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
UNICODE = str

@@ -247,5 +255,3 @@

retbuf_size = len(retbuf) if retbuf is not None else 0
msg = "Read {} bytes from socket instead of expected {} bytes".format(
retbuf_size, size
)
msg = "Read {} bytes from socket instead of expected {} bytes".format(retbuf_size, size)
raise socket.error(msg)

@@ -293,3 +299,2 @@

class iRODSMessage:
class ResponseNotParseable(Exception):

@@ -432,5 +437,3 @@ """

# pack header
packed_header = self.pack_header(
self.msg_type, len(main_msg), len(self.error), len(self.bs), self.int_info
)
packed_header = self.pack_header(self.msg_type, len(main_msg), len(self.error), len(self.bs), self.int_info)

@@ -441,5 +444,3 @@ return packed_header + main_msg + self.error + self.bs

msg = cls()
logger.debug(
"Attempt to parse server response [%r] as class [%r].", self.msg, cls
)
logger.debug("Attempt to parse server response [%r] as class [%r].", self.msg, cls)
if self.error and isinstance(r_error, RErrorStack):

@@ -452,7 +453,3 @@ r_error.fill(iRODSMessage(msg=self.error).get_main_message(Error))

# through as usual for express reporting by instances of irods.connection.Connection .
message = (
"Server response was {self.msg} while parsing as [{cls}]".format(
**locals()
)
)
message = "Server response was {self.msg} while parsing as [{cls}]".format(**locals())
raise self.ResponseNotParseable(message)

@@ -486,4 +483,4 @@ msg.unpack(ET().fromstring(self.msg))

self.clientUser, self.clientRcatZone = client_user
self.relVersion = "rods{}.{}.{}".format(*IRODS_VERSION)
self.apiVersion = "{3}".format(*IRODS_VERSION)
self.relVersion = "rods{}.{}.{}".format(*_IRODS_VERSION)
self.apiVersion = "{3}".format(*_IRODS_VERSION)
self.option = application_name

@@ -814,5 +811,3 @@

elif i < 5 and not (arg):
error = Bad_AVU_Field(
"AVU %s (%r) is zero-length." % (field_name(i), arg)
)
error = Bad_AVU_Field("AVU %s (%r) is zero-length." % (field_name(i), arg))
if error is not None:

@@ -881,5 +876,4 @@ raise error

class _admin_request_base(Message):
_name: Optional[str] = None
_name : Optional[str] = None
def __init__(self, *args):

@@ -920,5 +914,3 @@ if self.__class__._name is None:

message_body = GeneralAdminRequest(*args)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])
with sess.pool.get_connection() as conn:

@@ -1045,8 +1037,5 @@ conn.send(request)

if name == "type":
# unpack struct accordingly
message_class = globals()[unpacked_value]
self._values["inOutStruct"] = SubmessageProperty(message_class).unpack(
root.findall(unpacked_value)
)
self._values["inOutStruct"] = SubmessageProperty(message_class).unpack(root.findall(unpacked_value))

@@ -1181,2 +1170,3 @@

# -- A tuple-descended class which facilitates filling in a

@@ -1200,10 +1190,3 @@ # quasi-RError stack from a JSON formatted list.

if isinstance(Err, (tuple, list)):
self[:] = [
RError(
_Server_Status_Message(
msg=elem["message"], status=elem["error_code"]
)
)
for elem in Err
]
self[:] = [RError(_Server_Status_Message(msg=elem["message"], status=elem["error_code"])) for elem in Err]
return

@@ -1257,7 +1240,4 @@

"""Show both the message and iRODS error type (both integer and human-readable)."""
return (
"{self.__class__.__name__}"
"<message = {self.message!r}, status = {self.status} {self.status_str}>".format(
**locals()
)
return "{self.__class__.__name__}<message = {self.message!r}, status = {self.status} {self.status_str}>".format(
**locals()
)

@@ -1285,6 +1265,4 @@

def empty_gen_query_out(cols):
sql_results = [
GenQueryResponseColumn(attriInx=col.icat_id, value=[]) for col in cols
]
sql_results = [GenQueryResponseColumn(attriInx=col.icat_id, value=[]) for col in cols]
gqo = GenQueryResponse(rowCnt=0, attriCnt=len(cols), SqlResult_PI=sql_results)
return gqo

@@ -6,3 +6,2 @@ # http://askawizard.blogspot.com/2008/10/ordered-properties-python-saga-part-5.html

class MessageMetaclass(OrderedMetaclass):
def __init__(self, name, bases, attys):

@@ -15,3 +14,2 @@ super(MessageMetaclass, self).__init__(name, bases, attys)

class Message(OrderedClass, metaclass=MessageMetaclass):
def __init__(self, *args, **kwargs):

@@ -18,0 +16,0 @@ super(Message, self).__init__()

@@ -7,4 +7,4 @@ # Ordered property classes stolen from Kris Kowal of Ask a Wizard

class OrderedProperty:
def __init__(self, *args, **kws):

@@ -16,3 +16,2 @@ self._creation_counter = next_counter()

class OrderedMetaclass(type):
def __init__(self, name, bases, attys):

@@ -26,4 +25,3 @@ super(OrderedMetaclass, self).__init__(name, bases, attys)

for name, value in base.__dict__.items()
if isinstance(value, OrderedProperty)
or isinstance(value, OrderedMetaclass)
if isinstance(value, OrderedProperty) or isinstance(value, OrderedMetaclass)
),

@@ -30,0 +28,0 @@ key=lambda property: property[1]._creation_counter,

@@ -8,3 +8,2 @@ from base64 import b64encode, b64decode

class MessageProperty(OrderedProperty):
def __get__(self, obj, cls):

@@ -38,3 +37,2 @@ return obj._values[self.name]

class IntegerProperty(MessageProperty):
def format(self, value):

@@ -48,3 +46,2 @@ return str(value)

class LongProperty(MessageProperty):
def format(self, value):

@@ -58,3 +55,2 @@ return str(value)

class BinaryProperty(MessageProperty):
def __init__(self, length=None):

@@ -76,3 +72,2 @@ self.length = length

class StringProperty(MessageProperty):
def __init__(self, length=None):

@@ -100,3 +95,2 @@ self.length = length

class ArrayProperty(MessageProperty):
def __init__(self, prop):

@@ -115,3 +109,2 @@ self.prop = prop

class SubmessageProperty(MessageProperty):
def __init__(self, message_cls=None):

@@ -118,0 +111,0 @@ self.message_cls = message_cls

@@ -46,9 +46,5 @@ # A parser for the iRODS XML-like protocol.

if type(self.body) is list:
return "<{}>{}</{}>".format(
self.name, "".join(map(str, self.body)), self.name
)
return "<{}>{}</{}>".format(self.name, "".join(map(str, self.body)), self.name)
else:
return "<{}>{}</{}>".format(
self.name, encode_entities(self.body), self.name
)
return "<{}>{}</{}>".format(self.name, encode_entities(self.body), self.name)

@@ -138,5 +134,3 @@ def __repr__(self):

if type(topen) is not TokenTagOpen:
raise QuasiXmlParseError(
"protocol error: data does not start with open tag"
)
raise QuasiXmlParseError("protocol error: data does not start with open tag")

@@ -156,10 +150,6 @@ children = []

raise QuasiXmlParseError(
"protocol error: close tag <{}> does not match opening tag <{}>".format(
t.text, topen.text
)
"protocol error: close tag <{}> does not match opening tag <{}>".format(t.text, topen.text)
)
elif cdata is not None and len(children):
raise QuasiXmlParseError(
"protocol error: mixed cdata and child elements"
)
raise QuasiXmlParseError("protocol error: mixed cdata and child elements")
return (

@@ -166,0 +156,0 @@ Element(

@@ -0,12 +1,47 @@

import base64
import copy
class iRODSMeta:
def _to_column_triple(self):
return (self.name, self.forward_translate(self.value)) + (
('',) if not self.units else (self.forward_translate(self.units),)
)
def _from_column_triple(self, name, value, units, **kw):
self.__low_level_init(
name, self.reverse_translate(value), units=None if not units else self.reverse_translate(units), **kw
)
return self
reverse_translate = forward_translate = staticmethod(lambda _: _)
INIT_KW_ARGS = ['units', 'avu_id', 'create_time', 'modify_time']
def __init__(
self, name, value, units=None, avu_id=None, create_time=None, modify_time=None
self,
name,
value,
/,
units=None,
*,
avu_id=None,
create_time=None,
modify_time=None,
):
self.avu_id = avu_id
# Defer initialization for iRODSMeta(attribute,value,...) if neither attribute nor value is True under
# a 'bool' transformation. In so doing we streamline initialization for iRODSMeta (and any subclasses)
# for alternatively populating via _from_column_triple(...).
# This is the pathway for allowing user-defined encodings of the iRODSMeta (byte-)string AVU components.
if name or value:
# Note: calling locals() inside the dict comprehension would not access variables in this frame.
local_vars = locals()
kw = {name: local_vars.get(name) for name in self.INIT_KW_ARGS}
self.__low_level_init(name, value, **kw)
def __low_level_init(self, name, value, **kw):
self.name = name
self.value = value
self.units = units
self.create_time = create_time
self.modify_time = modify_time
for attr in self.INIT_KW_ARGS:
setattr(self, attr, kw.get(attr))

@@ -23,5 +58,20 @@ def __eq__(self, other):

def __repr__(self):
return "<iRODSMeta {avu_id} {name} {value} {units}>".format(**vars(self))
return f"<{self.__class__.__name__} {self.avu_id} {self.name} {self.value} {self.units}>"
def __hash__(self):
return hash(tuple(self))
class iRODSBinOrStringMeta(iRODSMeta):
@staticmethod
def reverse_translate(value):
"""Translate an AVU field from its iRODS object-database form into the client representation of that field."""
return value if value[0] != '\\' else base64.decodebytes(value[1:].encode('utf8'))
@staticmethod
def forward_translate(value):
"""Translate an AVU field from the form it takes in the client, into an iRODS object-database compatible form."""
return b'\\' + base64.encodebytes(value).strip() if isinstance(value, (bytes, bytearray)) else value
class BadAVUOperationKeyword(Exception):

@@ -36,3 +86,2 @@ pass

class AVUOperation(dict):
@property

@@ -58,5 +107,4 @@ def operation(self):

if not isinstance(avu_param, iRODSMeta):
error_msg = (
"Nonconforming avu {!r} of type {}; must be an iRODSMeta."
"".format(avu_param, type(avu_param).__name__)
error_msg = "Nonconforming avu {!r} of type {}; must be an iRODSMeta.".format(
avu_param, type(avu_param).__name__
)

@@ -67,7 +115,3 @@ raise BadAVUOperationValue(error_msg)

if operation not in ("add", "remove"):
error_msg = (
"Nonconforming operation {!r}; must be 'add' or 'remove'.".format(
operation
)
)
error_msg = "Nonconforming operation {!r}; must be 'add' or 'remove'.".format(operation)
raise BadAVUOperationValue(error_msg)

@@ -84,5 +128,3 @@

if kw:
raise BadAVUOperationKeyword(
"""Nonconforming keyword (s) {}.""".format(list(kw.keys()))
)
raise BadAVUOperationKeyword("""Nonconforming keyword (s) {}.""".format(list(kw.keys())))
for atr in ("operation", "avu"):

@@ -92,10 +134,70 @@ setattr(self, atr, locals()[atr])

import copy
class iRODSMetaCollection:
def __setattr__(self, name, value):
"""
Override __setattr__.
Protect the virtual, read-only attributes such as 'admin', 'timestamps', etc.,
from being written or created as concrete attributes, which would interfere with
__getattr__'s intended operation for these cases.
class iRODSMetaCollection:
Args:
name: the name of the attribute to be written.
value: the value to be written to the attribute.
def __call__(self, admin=False, timestamps=False, **opts):
Raises:
AttributeError: on any attempt to write to these special attributes.
"""
from irods.manager.metadata_manager import _MetadataManager_opts_initializer
if name in _MetadataManager_opts_initializer:
msg = (
f"""The "{name}" attribute is a special one, settable only via a """
f"""call on the object. For example: admin_view = data_obj.metadata({name}=<value>)"""
)
raise AttributeError(msg)
super().__setattr__(name, value)
def __getattr__(self, name):
"""
Override __getattr__.
Expose certain settable flags (e.g. "admin", "timestamps") as virtual, read-only
"attributes." The names of these special attributes appear as the keys of the
_MetadataManager_opts_initializer dictionary.
Args:
name: the name of the attribute to be fetched.
Returns:
the value of the named attribute.
Raises:
AttributeError: because this is the protocol for deferring to __getattr__'s
default behavior for the case in which none of the special attribute keys are
a match for 'name'.
"""
from irods.manager.metadata_manager import _MetadataManager_opts_initializer
# Separating _MetadataManager_opts_initializer from the MetadataManager class
# prevents the possibility of arbitrary access by copy.copy() to parts of
# our object's state before they have been initialized, as it is known to do
# by calling hasattr on the "__setstate__" attribute. The result of such
# unfettered access is infinite recursion. See:
# https://nedbatchelder.com/blog/201010/surprising_getattr_recursion
if name in _MetadataManager_opts_initializer:
return self._manager._opts[name] # noqa: SLF001
raise AttributeError
def __call__(self, **opts):
"""
Optional parameters in **opts are:
admin (default: False): apply ADMIN_KW to future metadata operations.
timestamps (default: False): attach (ctime,mtime) timestamp attributes to AVUs received from iRODS.
"""
x = copy.copy(self)
x._manager = (x._manager)(admin, timestamps, **opts)
x._manager = (x._manager)(**opts)
x._reset_metadata()

@@ -111,3 +213,7 @@ return x

def _reset_metadata(self):
self._meta = self._manager.get(self._model_cls, self._path)
m = self._manager
if not hasattr(self, "_meta"):
self._meta = m.get(None, "")
if m._opts.setdefault('reload', True):
self._meta = m.get(self._model_cls, self._path)

@@ -139,3 +245,3 @@ def get_all(self, key):

raise ValueError("Must specify an iRODSMeta object or key, value, units)")
return args[0] if len(args) == 1 else iRODSMeta(*args)
return args[0] if len(args) == 1 else self._manager._opts['iRODSMeta_type'](*args)

@@ -142,0 +248,0 @@ def apply_atomic_operations(self, *avu_ops):

@@ -6,4 +6,4 @@ from typing import Dict, List, Tuple

class ModelBase(type):
column_items : List[Tuple[int, Column]] = []
column_dict : Dict[int, Column] = {}
column_items: List[Tuple[int, Column]] = []
column_dict: Dict[int, Column] = {}

@@ -247,5 +247,4 @@ @classmethod

## For now, use of these columns raises CAT_SQL_ERR in both PRC and iquest: (irods/irods#5929)
# create_time = Column(String, 'TICKET_CREATE_TIME', 2209)
# modify_time = Column(String, 'TICKET_MODIFY_TIME', 2210)
create_time = Column(DateTime, 'TICKET_CREATE_TIME', 2209, min_version=(4, 3, 0))
modify_time = Column(DateTime, 'TICKET_MODIFY_TIME', 2210, min_version=(4, 3, 0))

@@ -252,0 +251,0 @@ class DataObject(Model):

@@ -12,3 +12,4 @@ #!/usr/bin/env python

import multiprocessing
from typing import List, Union
from typing import List, Union, Any
import weakref

@@ -20,3 +21,57 @@ from irods.data_object import iRODSDataObject

paths_active: weakref.WeakValueDictionary[str, "AsyncNotify"] = weakref.WeakValueDictionary()
transfer_managers: weakref.WeakKeyDictionary["_Multipart_close_manager", Any] = weakref.WeakKeyDictionary()
class FILTER_FUNCTIONS:
"""The members of this class are free functions designed to be passed to
the "filter_function" parameter of the abort_parallel_transfers function.
"""
foreground = staticmethod(lambda item: isinstance(item[1], tuple))
background = staticmethod(lambda item: not isinstance(item[1], tuple))
def abort_parallel_transfers(dry_run=False, filter_function=None, transform=weakref.WeakKeyDictionary):
"""
If no explicit arguments are given, all ongoing parallel puts and gets are
cancelled as soon as possible. The corresponding threads are signalled to
exit by calling the quit() method on their corresponding transfer-manager
objects.
Setting dry_run=True results in no such cancellation being performed,
although a dict object will be computed for the return value containing,
as its keys, the transfer-manager objects that would have been so affected.
filter_function is usually left to its default value of None. By applying
a member of the FILTER_FUNCTIONS class, the caller may specify which
transfer-managers are affected and/or reflected in the return value:
FILTER_FUNCTIONS.foreground for example limits its scope to instances
of session.data_object.put() and session.data_object.get() that are
running synchronously. These calls block within the thread(s) that
are calling them.
FILTER_FUNCTIONS.background limits its scope to transfers started by
calls to io_main() which used Oper.NONBLOCKING to spawn the put or
get operation in the background.
transform defaults to a dictionary type with weak keys, since
allowing strong references to transfer-manager objects may artificially
increase lifetimes of threads and other objects unnecessarily and
complicate troubleshooting by altering library behavior. Consider using
transform=len if all that is desired is to check how many parallel
transfers exist in total, at the time.
"""
mgrs = dict(filter(filter_function, transfer_managers.items()))
if not dry_run:
for mgr, item in mgrs.items():
if isinstance(item, tuple):
quit_func, args = item[:2]
quit_func(*args)
else:
mgr.quit()
return transform(mgrs)
logger = logging.getLogger(__name__)

@@ -74,10 +129,6 @@ _nullh = logging.NullHandler()

if not callable(callback):
raise BadCallbackTarget(
'"callback" must be a callable accepting at least 1 argument'
)
raise BadCallbackTarget('"callback" must be a callable accepting at least 1 argument')
self.done_callback = callback
def __init__(
self, futuresList, callback=None, progress_Queue=None, total=None, keep_=()
):
def __init__(self, futuresList, callback=None, progress_Queue=None, total=None, keep_=()):
"""AsyncNotify initialization (used internally to the io.parallel library).

@@ -97,5 +148,7 @@ The casual user will only be concerned with the callback parameter, called when all threads

else:
self.__invoke_done_callback()
self.__invoke_futures_done_logic()
return
self.progress = [0, 0]
if (progress_Queue) and (total is not None):

@@ -118,5 +171,3 @@ self.progress[1] = total

self._progress_fn = _progress
self._progress_thread = threading.Thread(
target=self._progress_fn, args=(progress_Queue, self)
)
self._progress_thread = threading.Thread(target=self._progress_fn, args=(progress_Queue, self), daemon=True)
self._progress_thread.start()

@@ -154,5 +205,3 @@

def __call__(
self, future
): # Our instance is called by each future (individual file part) when done.
def __call__(self, future): # Our instance is called by each future (individual file part) when done.
# When all futures are done, we invoke the configured callback.

@@ -162,7 +211,8 @@ with self._lock:

if len(self._futures) == len(self._futures_done):
self.__invoke_done_callback()
# If a future returns None rather than an integer byte count, it has aborted the transfer.
self.__invoke_futures_done_logic(skip_user_callback=(None in self._futures_done.values()))
def __invoke_done_callback(self):
def __invoke_futures_done_logic(self, skip_user_callback=False):
try:
if callable(self.done_callback):
if not skip_user_callback and callable(self.done_callback):
self.done_callback(self)

@@ -250,2 +300,8 @@ finally:

while True and bytecount < length:
if mgr._quit:
# Indicate by the return value that we are aborting (this part of) the data transfer.
# In the great majority of cases, this should be seen by the application as an overall
# abort of the PUT or GET of the requested object.
bytecount = None
break
buf = src.read(min(COPY_BUF_SIZE, length - bytecount))

@@ -285,3 +341,4 @@ buf_len = len(buf)

def __init__(self, initial_io_, exit_barrier_):
def __init__(self, initial_io_, exit_barrier_, executor=None):
self._quit = False
self.exit_barrier = exit_barrier_

@@ -291,3 +348,30 @@ self.initial_io = initial_io_

self.aux = []
self.futures = set()
self.executor = executor
def add_future(self, future):
self.futures.add(future)
@property
def active_futures(self):
return tuple(_ for _ in self.futures if not _.done())
def shutdown(self):
if self.executor:
self.executor.shutdown(cancel_futures=True)
def quit(self):
from irods.session import _exclude_fds_from_auto_close
_exclude_fds_from_auto_close(self.aux + [self.initial_io])
if not self._quit:
self._quit = True
# Disable barrier and abort threads.
self.exit_barrier.abort()
self.shutdown()
return self.active_futures
def __contains__(self, Io):

@@ -317,4 +401,8 @@ with self.__lock:

is_initial = False
self.exit_barrier.wait()
if is_initial:
broken = False
try:
self.exit_barrier.wait()
except threading.BrokenBarrierError:
broken = True
if is_initial and not (broken or self._quit):
self.finalize()

@@ -351,9 +439,5 @@

return (
_copy_part(
file_, objHandle, length, queueObject, thread_debug_id, mgr_, updatables
)
_copy_part(file_, objHandle, length, queueObject, thread_debug_id, mgr_, updatables)
if Operation.isPut()
else _copy_part(
objHandle, file_, length, queueObject, thread_debug_id, mgr_, updatables
)
else _copy_part(objHandle, file_, length, queueObject, thread_debug_id, mgr_, updatables)
)

@@ -363,11 +447,3 @@

def _io_multipart_threaded(
operation_,
dataObj_and_IO,
replica_token,
hier_str,
session,
fname,
total_size,
num_threads,
**extra_options
operation_, dataObj_and_IO, replica_token, hier_str, session, fname, total_size, num_threads, **extra_options
):

@@ -391,10 +467,5 @@ """Called by _io_main.

ranges = [
bytes_range_for_thread(i, num_threads, total_size, bytes_per_thread)
for i in range(num_threads)
]
ranges = [bytes_range_for_thread(i, num_threads, total_size, bytes_per_thread) for i in range(num_threads)]
logger.info(
"num_threads = %s ; bytes_per_thread = %s", num_threads, bytes_per_thread
)
logger.info("num_threads = %s ; bytes_per_thread = %s", num_threads, bytes_per_thread)

@@ -410,7 +481,5 @@ queueLength = extra_options.get("queueLength", 0)

num_threads = min(num_threads, len(ranges))
mgr = _Multipart_close_manager(Io, Barrier(num_threads))
mgr = _Multipart_close_manager(Io, Barrier(num_threads), executor)
counter = 1
gen_file_handle = lambda: open(
fname, Operation.disk_file_mode(initial_open=(counter == 1))
)
gen_file_handle = lambda: open(fname, Operation.disk_file_mode(initial_open=(counter == 1)))
File = gen_file_handle()

@@ -423,46 +492,75 @@

for byte_range in ranges:
if Io is None:
Io = session.data_objects.open(
Data_object.path,
Operation.data_object_mode(initial_open=False),
create=False,
finalize_on_close=False,
allow_redirect=False,
**{
kw.NUM_THREADS_KW: str(num_threads),
kw.DATA_SIZE_KW: str(total_size),
kw.RESC_HIER_STR_KW: hier_str,
kw.REPLICA_TOKEN_KW: replica_token,
}
)
mgr.add_io(Io)
logger.debug("target_host = %s", Io.raw.session.pool.account.host)
if File is None:
File = gen_file_handle()
futures.append(
executor.submit(
_io_part,
Io,
byte_range,
File,
Operation,
mgr,
thread_debug_id=str(counter),
**thread_opts
)
)
counter += 1
Io = File = None
transfer_managers[mgr] = (_quit_current_transfer, [id(mgr)])
if Operation.isNonBlocking():
if queueLength:
return futures, queueObject, mgr
try:
thread_setup_error = None
for byte_range in ranges:
if Io is None:
Io = session.data_objects.open(
Data_object.path,
Operation.data_object_mode(initial_open=False),
create=False,
finalize_on_close=False,
allow_redirect=False,
**{
kw.NUM_THREADS_KW: str(num_threads),
kw.DATA_SIZE_KW: str(total_size),
kw.RESC_HIER_STR_KW: hier_str,
kw.REPLICA_TOKEN_KW: replica_token,
},
)
mgr.add_io(Io)
logger.debug("target_host = %s", Io.raw.session.pool.account.host)
if File is None:
File = gen_file_handle()
try:
f = None
futures.append(
f := executor.submit(
_io_part, Io, byte_range, File, Operation, mgr, thread_debug_id=str(counter), **thread_opts
)
)
except RuntimeError as error:
# Executor was probably shut down before parallel transfer could be initiated.
thread_setup_error = error
break
else:
mgr.add_future(f)
counter += 1
Io = File = None
if thread_setup_error:
raise thread_setup_error
bytes_transferred = 0
if Operation.isNonBlocking():
transfer_managers[mgr] = None
return (futures, mgr, queueObject)
else:
return futures
else:
bytecounts = [f.result() for f in futures]
return sum(bytecounts), total_size
# Enable user attempts to cancel the current synchronous transfer.
# At any given time, only one transfer manager key should map to a tuple object T.
# You should be able to quit all threads of the current transfer by calling T[0](*T[1]).
bytecounts = [future.result() for future in futures]
# If, rather than an integer byte-count, the "None" object was included as one of futures' return values, this
# is an indication that the PUT or GET operation should be marked as aborted, i.e. no bytes transferred.
if None not in bytecounts:
bytes_transferred = sum(bytecounts)
return (bytes_transferred, total_size)
except BaseException as e:
if isinstance(e, (SystemExit, KeyboardInterrupt, RuntimeError)):
mgr.quit()
raise
def _quit_current_transfer(obj_id):
l = [_ for _ in transfer_managers if id(_) == obj_id]
if l:
l[0].quit()
def io_main(session, Data, opr_, fname, R="", **kwopt):

@@ -520,3 +618,3 @@ """

returned_values=output_values,
**open_options
**open_options,
)

@@ -570,3 +668,3 @@ else:

num_threads=num_threads,
**{k: v for k, v in kwopt.items() if k in pass_thru_options}
**{k: v for k, v in kwopt.items() if k in pass_thru_options},
)

@@ -579,15 +677,14 @@

if Operation.isNonBlocking():
(futures, mgr, chunk_notify_queue) = retval
if queueLength > 0:
(futures, chunk_notify_queue, mgr) = retval
else:
futures = retval
chunk_notify_queue = total_bytes = None
# For convenience, this information can help determine which data object mgr is tracking.
transfer_managers[mgr] = Data.path
return AsyncNotify(
paths_active[Data.path] = async_notify = AsyncNotify(
futures, # individual futures, one per transfer thread
progress_Queue=chunk_notify_queue, # for notifying the progress indicator thread
total=total_bytes, # total number of bytes for parallel transfer
keep_={"mgr": mgr},
) # an open raw i/o object needing to be persisted, if any
keep_={"mgr": mgr}, # objects needing to be persisted while futures are pending
)
return async_notify
else:

@@ -599,3 +696,2 @@ (_bytes_transferred, _bytes_total) = retval

if __name__ == "__main__":
import getopt

@@ -620,5 +716,3 @@ import atexit

env_file = os.path.expanduser("~/.irods/irods_environment.json")
ssl_context = ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH, cafile=None, capath=None, cadata=None
)
ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=None, capath=None, cadata=None)
ssl_settings = {"ssl_context": ssl_context}

@@ -633,5 +727,3 @@ sess = iRODSSession(irods_env_file=env_file, **ssl_settings)

logFilename = opts.pop(
"-L", None
) # '' for console, non-empty for filesystem destination
logFilename = opts.pop("-L", None) # '' for console, non-empty for filesystem destination
logLevel = logging.INFO if logFilename is None else logging.DEBUG

@@ -663,13 +755,9 @@ logFilename = logFilename or opts.pop("-l", None)

print("waiting on completion...", file=sys.stderr)
ret.set_transfer_done_callback(
lambda r: print("Async transfer done for:", r, file=sys.stderr)
)
done = ret.wait_until_transfer_done(
timeout=10.0
) # - or do other useful work here
ret.set_transfer_done_callback(lambda r: print("Async transfer done for:", r, file=sys.stderr))
done = ret.wait_until_transfer_done(timeout=10.0) # - or do other useful work here
if done:
bytes_transferred = sum(ret.futures_done.values())
bytes_transferred_total = sum(ret.futures_done.values())
print(
"Asynch transfer complete. Total bytes transferred:",
bytes_transferred,
bytes_transferred_total,
file=sys.stderr,

@@ -676,0 +764,0 @@ )

@@ -248,5 +248,3 @@ import hashlib

# insert the seq_index (which is NOT encoded):
encoded_string = chr(seq_index + ord("e")).join(
[encoded_string[:6], encoded_string[6:]]
)
encoded_string = chr(seq_index + ord("e")).join([encoded_string[:6], encoded_string[6:]])

@@ -309,5 +307,3 @@ # aaaaand, append a null character. because we want to print

# the index of the target character in wheel
wheel_index = (
wheel.index(c) - encoder_ring[encoder_ring_index % 61] - chain
) % len(wheel)
wheel_index = (wheel.index(c) - encoder_ring[encoder_ring_index % 61] - chain) % len(wheel)
unscrambled_string += wheel[wheel_index]

@@ -346,5 +342,3 @@ if block_chaining:

# the index of the target character in wheel
wheel_index = (
wheel.index(c) + encoder_ring[encoder_ring_index % 61] + chain
) % len(wheel)
wheel_index = (wheel.index(c) + encoder_ring[encoder_ring_index % 61] + chain) % len(wheel)
scrambled_string += wheel[wheel_index]

@@ -363,5 +357,3 @@ if block_chaining:

to_scramble = (
random.SystemRandom().choice(string.printable) + v2_prefix[1:10] + s[:150]
)
to_scramble = random.SystemRandom().choice(string.printable) + v2_prefix[1:10] + s[:150]

@@ -382,5 +374,3 @@ key = first_key[:90] + second_key[:100]

return scramble(
to_scramble, key=hashed_key, scramble_prefix="", block_chaining=True
)
return scramble(to_scramble, key=hashed_key, scramble_prefix="", block_chaining=True)

@@ -387,0 +377,0 @@

@@ -10,2 +10,5 @@ """A module providing tools for path normalization and manipulation."""

_logger = logging.getLogger(__name__)
class iRODSPath(str):

@@ -53,3 +56,3 @@ """A subclass of the Python string that normalizes iRODS logical paths."""

if kw:
logging.warning("These iRODSPath options have no effect: %r", kw.items())
_logger.warning("These iRODSPath options have no effect: %r", kw.items())
normalized = _normalize_iRODS_logical_path(elem_list, absolute_)

@@ -56,0 +59,0 @@ obj = str.__new__(cls, normalized)

@@ -39,6 +39,3 @@ import contextlib

class Pool:
def __init__(
self, account, application_name="", connection_refresh_time=-1, session=None
):
def __init__(self, account, application_name="", connection_refresh_time=-1, session=None):
"""

@@ -57,7 +54,3 @@ Pool( account , application_name='' )

self.connection_timeout = DEFAULT_CONNECTION_TIMEOUT
self.application_name = (
os.environ.get("spOption", "")
or application_name
or DEFAULT_APPLICATION_NAME
)
self.application_name = os.environ.get("spOption", "") or application_name or DEFAULT_APPLICATION_NAME
self._need_auth = True

@@ -103,4 +96,3 @@

self.refresh_connection
and (curr_time - conn.create_time).total_seconds()
> self.connection_refresh_time
and (curr_time - conn.create_time).total_seconds() > self.connection_refresh_time
):

@@ -122,5 +114,3 @@ logger.debug(

new_conn = True
logger.debug(
f"No connection found in idle set. Created a new connection with id: {id(conn)}"
)
logger.debug(f"No connection found in idle set. Created a new connection with id: {id(conn)}")

@@ -127,0 +117,0 @@ self.active.add(conn)

@@ -25,3 +25,3 @@ #!/usr/bin/env python3

vector : Dict[str, Callable] = {"pam_password": write_pam_irodsA_file, "native": write_native_irodsA_file}
vector: Dict[str, Callable] = {"pam_password": write_pam_irodsA_file, "native": write_native_irodsA_file}
opts, args = getopt.getopt(sys.argv[1:], "hi:", ["ttl=", "help"])

@@ -31,7 +31,3 @@ optD = dict(opts)

if len(args) != 1 or help_selected:
print(
"{}\nUsage: {} [-i STREAM| -h | --help | --ttl HOURS] AUTH_SCHEME".format(
extra_help, sys.argv[0]
)
)
print("{}\nUsage: {} [-i STREAM| -h | --help | --ttl HOURS] AUTH_SCHEME".format(extra_help, sys.argv[0]))
print(" Choices for AUTH_SCHEME are:")

@@ -52,9 +48,13 @@ for x in vector:

if inp_stream is None or inp_stream == "-":
pw = get_password(sys.stdin,
prompt=f"Enter current password for scheme {scheme!r}: ",)
pw = get_password(
sys.stdin,
prompt=f"Enter current password for scheme {scheme!r}: ",
)
else:
pw = get_password(open(inp_stream, "r", encoding='utf-8'),
prompt=f"Enter current password for scheme {scheme!r}: ",)
pw = get_password(
open(inp_stream, "r", encoding='utf-8'),
prompt=f"Enter current password for scheme {scheme!r}: ",
)
vector[scheme](pw, **options)
else:
print("did not recognize authentication scheme argument", file=sys.stderr)

@@ -39,4 +39,4 @@ from collections import OrderedDict

class Query:
def __init__(self, sess, *args, **kwargs):

@@ -95,12 +95,5 @@ self.sess = sess

elif type(criterion.value) == list:
criterion.value = [
str.upper(c) if type(c) == str else c for c in criterion.value
]
criterion.value = [str.upper(c) if type(c) == str else c for c in criterion.value]
elif type(criterion.value) == tuple:
criterion.value = tuple(
[
str.upper(c) if type(c) == str else c
for c in list(criterion.value)
]
)
criterion.value = tuple([str.upper(c) if type(c) == str else c for c in list(criterion.value)])
new_q.criteria.append(criterion)

@@ -166,5 +159,3 @@

def _select_message(self):
dct = OrderedDict(
[(column.icat_id, value) for (column, value) in self.columns.items()]
)
dct = OrderedDict([(column.icat_id, value) for (column, value) in self.columns.items()])
return IntegerIntegerMap(dct)

@@ -175,25 +166,21 @@

def _conds_message(self):
dct = _OrderedMultiMapping(
[
(
criterion.query_key.icat_id,
criterion.op + " " + criterion.irods_value,
)
for criterion in self.criteria
if isinstance(criterion.query_key, Column)
]
)
dct = _OrderedMultiMapping([
(
criterion.query_key.icat_id,
criterion.op + " " + criterion.irods_value,
)
for criterion in self.criteria
if isinstance(criterion.query_key, Column)
])
return IntegerStringMap(dct)
def _kw_message(self):
dct = dict(
[
(
criterion.query_key.icat_key,
criterion.op + " " + criterion.irods_value,
)
for criterion in self.criteria
if isinstance(criterion.query_key, Keyword)
]
)
dct = dict([
(
criterion.query_key.icat_key,
criterion.op + " " + criterion.irods_value,
)
for criterion in self.criteria
if isinstance(criterion.query_key, Keyword)
])
for key in self._keywords:

@@ -218,7 +205,4 @@ dct[key] = self._keywords[key]

with self.sess.pool.get_connection() as conn:
message_body = self._message()
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GEN_QUERY_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GEN_QUERY_AN"])

@@ -261,5 +245,3 @@ conn.send(message)

try:
result_set = self.continue_index(
result_set.continue_index
).execute()
result_set = self.continue_index(result_set.continue_index).execute()
yield result_set

@@ -306,3 +288,2 @@ except CAT_NO_ROWS_FOUND:

class SpecificQuery:
def __init__(self, sess, sql=None, alias=None, columns=None, args=None):

@@ -323,8 +304,4 @@ if not sql and not alias:

message_body = GeneralAdminRequest(
"add", "specificQuery", self._sql, self._alias
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
message_body = GeneralAdminRequest("add", "specificQuery", self._sql, self._alias)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])

@@ -340,5 +317,3 @@ with self.session.pool.get_connection() as conn:

message_body = GeneralAdminRequest("rm", "specificQuery", target)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])

@@ -367,8 +342,6 @@ with self.session.pool.get_connection() as conn:

KeyValPair_PI=conditions,
**sql_args
**sql_args,
)
request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["SPECIFIC_QUERY_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["SPECIFIC_QUERY_AN"])

@@ -405,3 +378,4 @@ with self.session.pool.get_connection() as conn:

import irods.helpers as helpers
cached_values = helpers.create_value_cache(globals())
cached_values.make_entry('IRODS_QUERY_LIMIT')

@@ -6,3 +6,2 @@ from irods.models import Resource

class iRODSResource:
def __init__(self, manager, result=None):

@@ -57,7 +56,3 @@ self._hierarchy_string = ""

else:
self._parent_id = (
sess.query(Resource)
.filter(Resource.name == self.parent)
.one()[Resource.id]
)
self._parent_id = sess.query(Resource).filter(Resource.name == self.parent).one()[Resource.id]
return int(self._parent_id)

@@ -74,7 +69,3 @@

else:
self._parent_name = (
sess.query(Resource)
.filter(Resource.id == self.parent)
.one()[Resource.name]
)
self._parent_name = sess.query(Resource).filter(Resource.id == self.parent).one()[Resource.name]
return self._parent_name

@@ -87,5 +78,3 @@

if self._hierarchy_string == "":
self._hierarchy_string = ";".join(
r.name for r in self.hierarchy_as_list_of_resource_objects()
)
self._hierarchy_string = ";".join(r.name for r in self.hierarchy_as_list_of_resource_objects())
return self._hierarchy_string

@@ -108,5 +97,3 @@

if not self._meta:
self._meta = iRODSMetaCollection(
self.manager.sess.metadata, Resource, self.name
)
self._meta = iRODSMetaCollection(self.manager.sess.metadata, Resource, self.name)
return self._meta

@@ -137,5 +124,3 @@

# query for children and cache results
query = session.query(Resource).filter(
Resource.parent == "{}".format(parent)
)
query = session.query(Resource).filter(Resource.parent == "{}".format(parent))
self._children = [self.__class__(self.manager, res) for res in query]

@@ -142,0 +127,0 @@

@@ -6,3 +6,2 @@ from prettytable import PrettyTable

class ResultSet:
def __init__(self, raw):

@@ -40,11 +39,7 @@ self.length = raw.rowCnt

_get_column_values = lambda self, index: [
(col, col.value[index]) for col in self.cols
]
_get_column_values = lambda self, index: [(col, col.value[index]) for col in self.cols]
def _format_row(self, index):
values = self._get_column_values(index)
return dict(
[self._format_attribute(col.attriInx, value) for col, value in values]
)
return dict([self._format_attribute(col.attriInx, value) for col, value in values])

@@ -72,3 +67,2 @@ def __getitem__(self, index):

class SpecificQueryResultSet(ResultSet):
def __init__(self, raw, columns=None):

@@ -75,0 +69,0 @@ self._query_columns = columns

@@ -61,7 +61,3 @@ from irods.message import (

else:
self.body = (
"@external\n" + body
if irods_3_literal_style
else "@external rule { " + body + " }"
)
self.body = "@external\n" + body if irods_3_literal_style else "@external rule { " + body + " }"

@@ -114,11 +110,5 @@ # overwrite params and output if received arguments

with (
io_open(rule_file, encoding=encoding)
if isinstance(rule_file, str)
else rule_file
) as f:
with io_open(rule_file, encoding=encoding) if isinstance(rule_file, str) else rule_file as f:
# parse rule file line-by-line
for line in f:
# convert input line to Unicode if necessary

@@ -130,3 +120,2 @@ if isinstance(line, bytes):

if line.strip().lower().startswith("input"):
input_header, input_line = line.split(None, 1)

@@ -174,17 +163,9 @@

inOutStruct = STR_PI(myStr=value)
param_array.append(
MsParam(label=label, type="STR_PI", inOutStruct=inOutStruct)
)
param_array.append(MsParam(label=label, type="STR_PI", inOutStruct=inOutStruct))
inpParamArray = MsParamArray(
paramLen=len(param_array), oprType=0, MsParam_PI=param_array
)
inpParamArray = MsParamArray(paramLen=len(param_array), oprType=0, MsParam_PI=param_array)
# rule body
addr = RodsHostAddress(hostAddr="", rodsZone="", port=0, dummyInt=0)
condInput = StringStringMap(
{}
if self.instance_name is None
else {"instance_name": self.instance_name}
)
condInput = StringStringMap({} if self.instance_name is None else {"instance_name": self.instance_name})
message_body = RuleExecutionRequest(

@@ -198,19 +179,11 @@ myRule=self.body,

request = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["EXEC_MY_RULE_AN"]
)
request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["EXEC_MY_RULE_AN"])
with self.session.pool.get_connection() as conn:
conn.send(request)
response = conn.recv(
acceptable_errors=acceptable_errors, return_message=return_message
)
response = conn.recv(acceptable_errors=acceptable_errors, return_message=return_message)
try:
out_param_array = response.get_main_message(
MsParamArray, r_error=r_error
)
out_param_array = response.get_main_message(MsParamArray, r_error=r_error)
except iRODSMessage.ResponseNotParseable:
return (
MsParamArray()
) # Ergo, no useful return value - but the RError stack will be accessible
return MsParamArray() # Ergo, no useful return value - but the RError stack will be accessible
finally:

@@ -217,0 +190,0 @@ if session_cleanup:

@@ -5,2 +5,3 @@ import ast

import errno
from io import BufferedRandom
import json

@@ -11,3 +12,5 @@ import logging

import threading
from typing import Iterable, Any, Optional
import weakref
import irods.auth

@@ -34,3 +37,3 @@ from irods.query import Query

_fds = None
_fds: Optional[dict[BufferedRandom, Any]] = None
_fds_lock = threading.Lock()

@@ -41,2 +44,14 @@ _sessions = None

def _exclude_fds_from_auto_close(descriptors: Iterable):
"""Remove all descriptors from consideration for auto_close."""
from irods.manager.data_object_manager import ManagedBufferedRandom
with _fds_lock:
fds: dict[BufferedRandom, Any] = _fds or {}
for fd in descriptors:
fds.pop(fd, None)
if isinstance(fd, ManagedBufferedRandom):
fd.do_close = False
def _cleanup_remaining_sessions():

@@ -53,5 +68,3 @@ for fd in list((_fds or {}).keys()):

with _sessions_lock:
at_client_exit._register(
at_client_exit.LibraryCleanupStage.DURING, _cleanup_remaining_sessions
)
at_client_exit._register(at_client_exit.LibraryCleanupStage.DURING, _cleanup_remaining_sessions)

@@ -80,3 +93,2 @@

class iRODSSession:
def library_features(self):

@@ -86,5 +98,3 @@ irods_version_needed = (4, 3, 1)

raise NotImplementedInIRODSServer("library_features", irods_version_needed)
message = iRODSMessage(
"RODS_API_REQ", int_info=api_number["GET_LIBRARY_FEATURES_AN"]
)
message = iRODSMessage("RODS_API_REQ", int_info=api_number["GET_LIBRARY_FEATURES_AN"])
with self.pool.get_connection() as conn:

@@ -111,5 +121,3 @@ conn.send(message)

except AttributeError:
self.__access = (
_iRODSAccess_pre_4_3_0 if self.server_version < (4, 3) else iRODSAccess
)
self.__access = _iRODSAccess_pre_4_3_0 if self.server_version < (4, 3) else iRODSAccess
return self.__access

@@ -124,5 +132,3 @@

self._cached_connection_timeout = None
self.connection_timeout = kwargs.pop(
"connection_timeout", DEFAULT_CONNECTION_TIMEOUT
)
self.connection_timeout = kwargs.pop("connection_timeout", DEFAULT_CONNECTION_TIMEOUT)
self.__configured = None

@@ -145,7 +151,3 @@ if configure:

self.auth_options_by_scheme = {
"pam_password": {
irods.auth.CLIENT_GET_REQUEST_RESULT: (lambda sess, conn: [])
}
}
self.auth_options_by_scheme = {"pam_password": {irods.auth.CLIENT_GET_REQUEST_RESULT: (lambda sess, conn: [])}}

@@ -275,7 +277,3 @@ if auto_cleanup:

if missing_file_path:
error_args += [
"Authentication file not found at {!r}".format(
missing_file_path[0]
)
]
error_args += ["Authentication file not found at {!r}".format(missing_file_path[0])]
raise NonAnonymousLoginWithoutPassword(*error_args)

@@ -292,7 +290,3 @@

connection_refresh_time = self.get_connection_refresh_time(**kwargs)
logger.debug(
"In iRODSSession's configure(). connection_refresh_time set to {}".format(
connection_refresh_time
)
)
logger.debug("In iRODSSession's configure(). connection_refresh_time set to {}".format(connection_refresh_time))
self.pool = Pool(

@@ -354,5 +348,3 @@ account,

GET_SERVER_VERSION_WITHOUT_AUTH = staticmethod(
lambda s: s.server_version_without_auth()
)
GET_SERVER_VERSION_WITHOUT_AUTH = staticmethod(lambda s: s.server_version_without_auth())

@@ -402,9 +394,4 @@ def _server_version(self, version_func=None):

self.pool.account.store_pw = box = []
if (
self.server_version_without_auth() >= (4, 3)
and not client_config.legacy_auth.force_legacy_auth
):
old_setting = self.set_auth_option_for_scheme(
"pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, box
)
if self.server_version_without_auth() >= (4, 3) and not client_config.legacy_auth.force_legacy_auth:
old_setting = self.set_auth_option_for_scheme("pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, box)
conn = self.pool.get_connection()

@@ -417,5 +404,3 @@ pw = getattr(self.pool.account, "store_pw", [])

if old_setting is not _dummy:
self.set_auth_option_for_scheme(
"pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, old_setting
)
self.set_auth_option_for_scheme("pam_password", irods.auth.CLIENT_GET_REQUEST_RESULT, old_setting)

@@ -437,5 +422,3 @@ @property

if seconds == 0:
exc = ValueError(
"Setting an iRODS connection_timeout to 0 seconds would make it non-blocking."
)
exc = ValueError("Setting an iRODS connection_timeout to 0 seconds would make it non-blocking.")
raise exc

@@ -459,5 +442,3 @@ elif isinstance(seconds, Number):

else:
exc = ValueError(
"The iRODS connection_timeout must be assigned a positive int, positive float, or None."
)
exc = ValueError("The iRODS connection_timeout must be assigned a positive int, positive float, or None.")
raise exc

@@ -531,5 +512,3 @@ self._cached_connection_timeout = seconds

env_file_map = self.get_irods_env(env_file)
connection_refresh_time = int(
env_file_map.get("irods_connection_refresh_time", -1)
)
connection_refresh_time = int(env_file_map.get("irods_connection_refresh_time", -1))
if connection_refresh_time < 1:

@@ -536,0 +515,0 @@ # Negative values are not allowed.

@@ -19,3 +19,2 @@ #! /usr/bin/env python

class TestAccess(unittest.TestCase):
def setUp(self):

@@ -25,5 +24,3 @@ self.sess = helpers.make_session()

# Create test collection
self.coll_path = "/{}/home/{}/test_dir".format(
self.sess.zone, self.sess.username
)
self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username)
self.coll = helpers.make_collection(self.sess, self.coll_path)

@@ -86,5 +83,3 @@ VERSION_DEPENDENT_STRINGS = (

repr(acl),
"<iRODSAccess own {path} {name}({type}) {zone}>".format(
path=path, **vars(user)
),
"<iRODSAccess own {path} {name}({type}) {zone}>".format(path=path, **vars(user)),
)

@@ -113,5 +108,3 @@

user = self.sess.users.create("bob", "rodsuser")
data = self.sess.data_objects.create(
"{}/obj_422".format(helpers.home_collection(self.sess))
)
data = self.sess.data_objects.create("{}/obj_422".format(helpers.home_collection(self.sess)))
permission_strings = self.sess.available_permissions.keys()

@@ -165,16 +158,10 @@ for perm in permission_strings:

c[Collection.id]
for c in self.sess.query(Collection.id).filter(
Like(Collection.name, deepcoll.path + "%")
)
for c in self.sess.query(Collection.id).filter(Like(Collection.name, deepcoll.path + "%"))
]
D_rods = list(
self.sess.query(Collection.name, DataObject.name).filter(
In(DataObject.collection_id, coll_IDs)
)
self.sess.query(Collection.name, DataObject.name).filter(In(DataObject.collection_id, coll_IDs))
)
self.assertEqual(
len(D_rods), OBJ_PER_LVL * DEPTH + 1
) # counts the 'older' objects plus one new object
self.assertEqual(len(D_rods), OBJ_PER_LVL * DEPTH + 1) # counts the 'older' objects plus one new object

@@ -188,9 +175,4 @@ with iRODSSession(

) as bob:
D = list(bob.query(Collection.name, DataObject.name).filter(In(DataObject.collection_id, coll_IDs)))
D = list(
bob.query(Collection.name, DataObject.name).filter(
In(DataObject.collection_id, coll_IDs)
)
)
# - bob should only see the new data object, but none existing before ACLs were applied

@@ -208,6 +190,4 @@

C = list(bob.query(Collection).filter(In(Collection.id, coll_IDs)))
self.assertEqual(len(C), 2) # query should return only the top-level and newly created collections
self.assertEqual(
len(C), 2
) # query should return only the top-level and newly created collections
self.assertEqual(
sorted([c[Collection.name] for c in C]),

@@ -228,5 +208,3 @@ sorted([new_collection.path, deepcoll.path]),

test_coll_path = self.coll_path + "/test"
deepcoll = helpers.make_deep_collection(
self.sess, test_coll_path, depth=DEPTH, objects_per_level=2
)
deepcoll = helpers.make_deep_collection(self.sess, test_coll_path, depth=DEPTH, objects_per_level=2)
acl1 = iRODSAccess("inherit", deepcoll.path)

@@ -236,5 +214,3 @@ self.sess.acls.set(acl1, recursive=recursionTruth)

iRODSCollection(self.sess.collections, _)
for _ in self.sess.query(Collection).filter(
Like(Collection.name, deepcoll.path + "/%")
)
for _ in self.sess.query(Collection).filter(Like(Collection.name, deepcoll.path + "/%"))
)

@@ -247,5 +223,3 @@

# assert lower level collections affected only for case when recursive = True
subcoll_truths = [
(_.inheritance == recursionTruth) for _ in test_subcolls
]
subcoll_truths = [(_.inheritance == recursionTruth) for _ in test_subcolls]
self.assertEqual(len(subcoll_truths), DEPTH - 1)

@@ -356,4 +330,3 @@ self.assertTrue(all(subcoll_truths))

for u in [
iRODSUser(self.sess.users, row)
for row in self.sess.query(User).filter(In(User.id, ids_for_delete))
iRODSUser(self.sess.users, row) for row in self.sess.query(User).filter(In(User.id, ids_for_delete))
]:

@@ -384,7 +357,4 @@ u.remove()

for obj in self.coll_395, self.data:
# Add ACLs
for access in iRODSAccess("read", obj.path, "team"), iRODSAccess(
"write", obj.path, "alice"
):
for access in iRODSAccess("read", obj.path, "team"), iRODSAccess("write", obj.path, "alice"):
ses.acls.set(access)

@@ -397,10 +367,3 @@

1,
len(
[
ac
for ac in ACLs
if (ac.access_name, ac.user_name)
== (self.mapping["write"], "alice")
]
),
len([ac for ac in ACLs if (ac.access_name, ac.user_name) == (self.mapping["write"], "alice")]),
)

@@ -425,5 +388,3 @@

) as bob:
self.assertTrue(
bob.data_objects.open(self.data.path, "r").read(), b"contents-395"
)
self.assertTrue(bob.data_objects.open(self.data.path, "r").read(), b"contents-395")

@@ -474,5 +435,3 @@ finally:

user_name = "testuser"
collection_path = "/".join(
[helpers.home_collection(self.sess), "give_read_access_to_this"]
)
collection_path = "/".join([helpers.home_collection(self.sess), "give_read_access_to_this"])

@@ -499,5 +458,3 @@ try:

user_name = "testuser"
data_object_path = "/".join(
[helpers.home_collection(self.sess), "give_read_access_to_this"]
)
data_object_path = "/".join([helpers.home_collection(self.sess), "give_read_access_to_this"])

@@ -524,5 +481,3 @@ try:

user_name = "testuser"
data_object_path = "/".join(
[helpers.home_collection(self.sess), "give_read_access_to_this"]
)
data_object_path = "/".join([helpers.home_collection(self.sess), "give_read_access_to_this"])

@@ -553,7 +508,3 @@ try:

# TODO(#480): We cannot use the unittest.assertRaises context manager as this was introduced in python 3.1.
assertCall = getattr(self, "assertRaisesRegex", None)
if assertCall is None:
assertCall = self.assertRaisesRegexp
assertCall(
self.assertRaisesRegex(
TypeError,

@@ -560,0 +511,0 @@ "'path' parameter must be of type 'str', 'irods.collection.iRODSCollection', "

@@ -7,12 +7,14 @@ #! /usr/bin/env python

import unittest
from irods.models import User, Group
import irods.keywords as kw
from irods.column import Like
from irods.exception import (
SYS_NO_API_PRIV,
ResourceDoesNotExist,
UserDoesNotExist,
ResourceDoesNotExist,
SYS_NO_API_PRIV,
)
from irods.models import Collection, Group, User
from irods.resource import iRODSResource
from irods.session import iRODSSession
from irods.resource import iRODSResource
import irods.test.helpers as helpers
import irods.keywords as kw
from irods.test import helpers

@@ -45,5 +47,3 @@

self.assertEqual(user.zone, self.sess.zone)
self.assertEqual(
repr(user), "<iRODSUser {id} {name} {type} {zone}>".format(**vars(user))
)
self.assertEqual(repr(user), "<iRODSUser {id} {name} {type} {zone}>".format(**vars(user)))

@@ -63,5 +63,3 @@ # delete user

# create user
user = self.sess.users.create(
self.new_user_name, self.new_user_type, self.sess.zone
)
user = self.sess.users.create(self.new_user_name, self.new_user_type, self.sess.zone)

@@ -83,3 +81,3 @@ # assertions

gpadmin_user = self.sess.users.create(self.new_user_name, "groupadmin")
gpadmin_user.modify("password", gpadmin_password:='my-gpadmin-passw')
gpadmin_user.modify("password", gpadmin_password := 'my-gpadmin-passw')
remote_zone = self.sess.zones.create('other_zone', 'remote')

@@ -93,18 +91,18 @@ with iRODSSession(

) as gpadmin:
for expected_exception,args,kwargs in (
(None, ("newUser","newPassword"), {}), # no zone supplied
(None, ("newUser","newPassword"), {"user_zone":gpadmin.zone}), # local zone supplied
(ValueError, ("newUser","newPassword"), {"user_zone":remote_zone.name}), # remote zone supplied
(ValueError, ("newUser","newPassword"), {"user_zone":"fictionZone"}), # nonexistent zone supplied
(ValueError, ("newUser#tempZone","newPassword"), {}),
(ValueError, (f"newUser#{remote_zone.name}","newPassword"), {}),
(ValueError, ("newUser#fictionZone","newPassword"), {}),
(ValueError, (f"newUser#{gpadmin.zone}","newPassword"), {"user_zone":gpadmin.zone}),
(ValueError, (f"newUser#{gpadmin.zone}","newPassword"), {"user_zone":remote_zone.name}),
(ValueError, (f"newUser#{gpadmin.zone}","newPassword"), {"user_zone":"fictionZone"}),
for expected_exception, args, kwargs in (
(None, ("newUser", "newPassword"), {}), # no zone supplied
(None, ("newUser", "newPassword"), {"user_zone": gpadmin.zone}), # local zone supplied
(ValueError, ("newUser", "newPassword"), {"user_zone": remote_zone.name}), # remote zone supplied
(ValueError, ("newUser", "newPassword"), {"user_zone": "fictionZone"}), # nonexistent zone supplied
(ValueError, ("newUser#tempZone", "newPassword"), {}),
(ValueError, (f"newUser#{remote_zone.name}", "newPassword"), {}),
(ValueError, ("newUser#fictionZone", "newPassword"), {}),
(ValueError, (f"newUser#{gpadmin.zone}", "newPassword"), {"user_zone": gpadmin.zone}),
(ValueError, (f"newUser#{gpadmin.zone}", "newPassword"), {"user_zone": remote_zone.name}),
(ValueError, (f"newUser#{gpadmin.zone}", "newPassword"), {"user_zone": "fictionZone"}),
):
with self.subTest(args = args, kwargs = kwargs):
with self.subTest(args=args, kwargs=kwargs):
user = None
try:
test_function = lambda:gpadmin.users.create_with_password(*args, **kwargs)
test_function = lambda: gpadmin.users.create_with_password(*args, **kwargs)
if expected_exception:

@@ -115,6 +113,10 @@ with self.assertRaises(expected_exception):

user = test_function()
self.assertEqual(user is None, expected_exception is not None, "In case of error, and only then, user should not exist.")
self.assertEqual(
user is None,
expected_exception is not None,
"In case of error, and only then, user should not exist.",
)
finally:
if user:
self.sess.users.get(user.name,user.zone).remove()
self.sess.users.get(user.name, user.zone).remove()
finally:

@@ -129,3 +131,3 @@ if remote_zone:

gpadmin_user = self.sess.users.create(self.new_user_name, "groupadmin")
gpadmin_user.modify("password", gpadmin_password:='my-gpadmin-passw')
gpadmin_user.modify("password", gpadmin_password := 'my-gpadmin-passw')
try:

@@ -150,6 +152,9 @@ remote_zone = self.sess.zones.create('other_zone', 'remote')

# Add the remote test user to the group we created, then assert membership.
test_group.addmember(test_user.name, user_zone = remote_zone.name)
test_group.addmember(test_user.name, user_zone=remote_zone.name)
self.assertIn(
(test_user.name, test_user.zone),
[(_[User.name],_[User.zone]) for _ in self.sess.query(User,Group).filter(Group.name == test_group.name)]
[
(_[User.name], _[User.zone])
for _ in self.sess.query(User, Group).filter(Group.name == test_group.name)
],
)

@@ -159,3 +164,3 @@ finally:

# Iterate through members from group (with groupadmin as the last), removing each one.
for member in sorted(test_group.members, key=lambda _:_.name == gpadmin_user.name):
for member in sorted(test_group.members, key=lambda _: _.name == gpadmin_user.name):
test_group.removemember(member.name, user_zone=member.zone)

@@ -246,5 +251,3 @@ # Use rodadmin-enabled session to remove the group

# change type to rodsadmin
self.sess.users.modify(
"{}#{}".format(self.new_user_name, self.sess.zone), "type", "rodsadmin"
)
self.sess.users.modify("{}#{}".format(self.new_user_name, self.sess.zone), "type", "rodsadmin")

@@ -391,5 +394,3 @@ # check type again

resc_path = "/nobucket"
s3 = session.resources.create(
resc_name, resc_type, resc_host, resc_path, context
)
s3 = session.resources.create(resc_name, resc_type, resc_host, resc_path, context)

@@ -432,5 +433,3 @@ # verify context fields

# make new resource
self.sess.resources.create(
resc_name, resc_type, resc_host, resc_path, resource_class=resc_class
)
self.sess.resources.create(resc_name, resc_type, resc_host, resc_path, resource_class=resc_class)

@@ -497,3 +496,2 @@ # try invalid params

) as session:
# do something that connects to the server

@@ -547,3 +545,31 @@ session.users.get(username)

def test_deleting_remote_user_including_home_collection_and_trash_artifact__issue_763(self):
# Test and confirm that, when passing user and zone parameters separately in calls to
# remove remote users, that both /tempZone/home/user#zone and /tempZone/trash/home/user#zone
# are deleted.
remote_zone = remote_user = None
try:
remote_zone = (sess := self.sess).zones.create('other_zone', 'remote')
remote_user = sess.users.create(user_name='myuser', user_type='rodsuser', user_zone=remote_zone.name)
def get_collection_artifacts():
return list(
sess.query(Collection).filter(Like(Collection.name, f'%/{remote_user.name}#{remote_zone.name}'))
)
# Two collection artifacts should be present, with names:
# /<local_zone>/home/remote_user#remote_zone
# /<local_zone>/trash/home/remote_user#remote_zone
self.assertEqual(len(get_collection_artifacts()), 2)
remote_user.remove()
# The above-mentioned artifacts should have been deleted along with the remote user.
self.assertEqual(len(get_collection_artifacts()), 0)
finally:
if remote_zone:
remote_zone.remove()
if __name__ == "__main__":

@@ -550,0 +576,0 @@ # let the tests find the parent irods lib

@@ -13,7 +13,4 @@ import os

class TestCleanupFunctions(unittest.TestCase):
def test_execution_of_client_exit_functions_at_proper_time__issue_614(self):
helper_script = os.path.join(
test_modules.__path__[0], "test_client_exit_functions.py"
)
helper_script = os.path.join(test_modules.__path__[0], "test_client_exit_functions.py")

@@ -28,5 +25,3 @@ # Note: The enum.Enum subclass's __members__ is an ordered dictionary, i.e. key order is preserved:

p = subprocess.Popen(
[sys.executable, helper_script, *args], stdout=subprocess.PIPE
)
p = subprocess.Popen([sys.executable, helper_script, *args], stdout=subprocess.PIPE)
script_output = p.communicate()[0].decode().strip()

@@ -67,6 +62,4 @@ from irods.test.modules.test_client_exit_functions import (

stdout_content, stderr_content = process.communicate()
self.assertEqual(len(list(re.finditer(rb"ZeroDivisionError.*\n", stderr_content))), 4)
self.assertEqual(
len(list(re.finditer(rb"ZeroDivisionError.*\n", stderr_content))), 4
)
self.assertEqual(
b",".join([m.group() for m in re.finditer(rb"(\w+)", stdout_content)]),

@@ -73,0 +66,0 @@ b"before,during,after2,after1",

@@ -11,3 +11,2 @@ #!/usr/bin/env python

class TestClientHints(unittest.TestCase):
def setUp(self):

@@ -14,0 +13,0 @@ self.sess = helpers.make_session()

@@ -22,3 +22,2 @@ #! /usr/bin/env python

class TestCollection(unittest.TestCase):
class WrongUserType(RuntimeError):

@@ -31,5 +30,3 @@ pass

if adm.users.get(adm.username).type != "rodsadmin":
raise cls.WrongUserType(
"Must be an iRODS admin to run tests in class {0.__name__}".format(cls)
)
raise cls.WrongUserType("Must be an iRODS admin to run tests in class {0.__name__}".format(cls))
cls.logins = helpers.iRODSUserLogins(adm)

@@ -49,5 +46,3 @@ cls.logins.create_user(RODSUSER, "abc123")

self.test_coll_path = "/{}/home/{}/test_dir".format(
self.sess.zone, self.sess.username
)
self.test_coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username)
self.test_coll = self.sess.collections.create(self.test_coll_path)

@@ -299,5 +294,3 @@

# confirm object count in collection
query = (
self.sess.query().count(DataObject.id).filter(Collection.name == coll_path)
)
query = self.sess.query().count(DataObject.id).filter(Collection.name == coll_path)
obj_count = next(query.get_results())[DataObject.id]

@@ -335,5 +328,3 @@ self.assertEqual(file_count, int(obj_count))

# confirm object count in collection
query = (
self.sess.query().count(DataObject.id).filter(Collection.name == coll_path)
)
query = self.sess.query().count(DataObject.id).filter(Collection.name == coll_path)
obj_count = next(query.get_results())[DataObject.id]

@@ -358,5 +349,3 @@ self.assertEqual(file_count, int(obj_count))

Home = helpers.home_collection(self.sess)
subcoll, dataobj = [
unique_name(my_function_name(), time.time()) for x in range(2)
]
subcoll, dataobj = [unique_name(my_function_name(), time.time()) for x in range(2)]
subcoll_fullpath = "{}/{}".format(Home, subcoll)

@@ -377,5 +366,3 @@ subcoll_unnormalized = subcoll_fullpath + "/"

self.assertEqual(
self.sess.query(DataObject)
.filter(DataObject.name == dataobj)
.one()[DataObject.collection_id],
self.sess.query(DataObject).filter(DataObject.name == dataobj).one()[DataObject.collection_id],
c1.id,

@@ -427,11 +414,7 @@ )

new_mtime = 1400000000
user_session.collections.touch(
home_collection_path, seconds_since_epoch=new_mtime
)
user_session.collections.touch(home_collection_path, seconds_since_epoch=new_mtime)
# Compare mtimes for correctness.
collection = user_session.collections.get(home_collection_path)
self.assertEqual(
datetime.fromtimestamp(new_mtime, timezone.utc), collection.modify_time
)
self.assertEqual(datetime.fromtimestamp(new_mtime, timezone.utc), collection.modify_time)
self.assertGreater(old_mtime, collection.modify_time)

@@ -465,4 +448,6 @@

# Create a data object.
data_object_path = "{home_collection}/test_touch_operation_does_not_work_when_given_a_data_object__525.txt".format(
**locals()
data_object_path = (
"{home_collection}/test_touch_operation_does_not_work_when_given_a_data_object__525.txt".format(
**locals()
)
)

@@ -485,5 +470,3 @@ self.assertFalse(user_session.data_objects.exists(data_object_path))

home_collection = helpers.home_collection(user_session)
path = "{home_collection}/test_touch_operation_ignores_unsupported_options__525".format(
**locals()
)
path = "{home_collection}/test_touch_operation_ignores_unsupported_options__525".format(**locals())

@@ -490,0 +473,0 @@ try:

@@ -20,3 +20,2 @@ #! /usr/bin/env python

class TestConnections(unittest.TestCase):
def setUp(self):

@@ -112,5 +111,3 @@ self.sess = helpers.make_session()

local_file.write(rand)
obj = sess.data_objects.put(
local_file.name, logical_path, return_data_object=True
)
obj = sess.data_objects.put(local_file.name, logical_path, return_data_object=True)

@@ -120,5 +117,3 @@ # Set a very short socket timeout and remove all pre-existing socket connections.

sess = (
obj.manager.sess
) # Because of client-redirect it is possible that self.sess and
sess = obj.manager.sess # Because of client-redirect it is possible that self.sess and
# obj.manager.sess do not refer to the same object. In any case,

@@ -181,5 +176,3 @@ # it is the latter of the two iRODSSession objects that is

sess = helpers.make_session(connection_timeout=timeout)
self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(
sess, timeout
)
self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(sess, timeout)

@@ -190,5 +183,3 @@ def test_assigning_session_connection_timeout__issue_377(self):

sess.connection_timeout = timeout
self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(
sess, timeout
)
self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(sess, timeout)

@@ -241,5 +232,3 @@ def test_assigning_session_connection_timeout_to_invalid_values__issue_569(self):

self.assertEqual(old_timeout, sess.connection_timeout)
self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(
sess, old_timeout
)
self._assert_timeout_value_is_propagated_to_all_sockets__issue_569(sess, old_timeout)

@@ -249,10 +238,6 @@ def test_legacy_auth_used_with_force_legacy_auth_configuration__issue_499(self):

with config.loadlines(
entries=[dict(setting="legacy_auth.force_legacy_auth", value=True)]
):
with config.loadlines(entries=[dict(setting="legacy_auth.force_legacy_auth", value=True)]):
stream = io.StringIO()
logger = logging.getLogger("irods.connection")
with helpers.enableLogging(
logger, logging.StreamHandler, (stream,), level_=logging.INFO
):
with helpers.enableLogging(logger, logging.StreamHandler, (stream,), level_=logging.INFO):
with temp_setter(logger, "propagate", False):

@@ -259,0 +244,0 @@ helpers.make_session().collections.get("/")

@@ -13,3 +13,2 @@ #! /usr/bin/env python

class TestException(unittest.TestCase):
def setUp(self):

@@ -28,5 +27,3 @@ # open the session (per-test)

seed = helpers.my_function_name() + ":" + str(datetime.now())
data = (
helpers.home_collection(ses) + "/" + helpers.unique_name(seed, "data")
)
data = helpers.home_collection(ses) + "/" + helpers.unique_name(seed, "data")
exc = None

@@ -46,6 +43,4 @@ with helpers.create_simple_resc(self, vault_path="/home") as resc_name:

errno_repr = repr(errno_object)
self.assertRegexpMatches(errno_repr, r"\bErrno\b")
self.assertRegexpMatches(
errno_repr, """['"]{msg}['"]""".format(msg=os.strerror(errno.EACCES))
)
self.assertRegex(errno_repr, r"\bErrno\b")
self.assertRegex(errno_repr, """['"]{msg}['"]""".format(msg=os.strerror(errno.EACCES)))
self.assertIn(errno_repr, excep_repr)

@@ -52,0 +47,0 @@ finally:

@@ -11,3 +11,2 @@ #! /usr/bin/env python

class TestContinueQuery(unittest.TestCase):
@classmethod

@@ -52,5 +51,3 @@ def setUpClass(cls):

# Query for all files in test collection
query = self.sess.query(DataObject.name, Collection.name).filter(
Collection.name == self.coll_path
)
query = self.sess.query(DataObject.name, Collection.name).filter(Collection.name == self.coll_path)

@@ -64,5 +61,3 @@ counter = 0

# what we see
result_path = "{}/{}".format(
result[Collection.name], result[DataObject.name]
)
result_path = "{}/{}".format(result[Collection.name], result[DataObject.name])

@@ -82,8 +77,3 @@ # compare

# Query should close after getting max_rows
results = (
self.sess.query(DataObject.name, Collection.name)
.offset(offset)
.limit(max_rows)
.all()
)
results = self.sess.query(DataObject.name, Collection.name).offset(offset).limit(max_rows).all()
self.assertEqual(len(results), max_rows)

@@ -90,0 +80,0 @@

@@ -16,5 +16,3 @@ #! /usr/bin/env python

# Create test collection
self.coll_path = "/{}/home/{}/test_dir".format(
self.sess.zone, self.sess.username
)
self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username)
self.collection = self.sess.collections.create(self.coll_path)

@@ -21,0 +19,0 @@

@@ -12,3 +12,2 @@ #! /usr/bin/env python

class TestForceCreate(unittest.TestCase):
def setUp(self):

@@ -15,0 +14,0 @@ self.sess = helpers.make_session()

@@ -9,3 +9,2 @@ import os

class TestGenQuery2(unittest.TestCase):
@classmethod

@@ -17,12 +16,6 @@ def setUpClass(cls):

if cls.sess.server_version < (4, 3, 2):
raise unittest.SkipTest(
"GenQuery2 is not available by default in iRODS before v4.3.2."
)
raise unittest.SkipTest("GenQuery2 is not available by default in iRODS before v4.3.2.")
cls.coll_path_a = "/{}/home/{}/test_query2_coll_a".format(
cls.sess.zone, cls.sess.username
)
cls.coll_path_b = "/{}/home/{}/test_query2_coll_b".format(
cls.sess.zone, cls.sess.username
)
cls.coll_path_a = "/{}/home/{}/test_query2_coll_a".format(cls.sess.zone, cls.sess.username)
cls.coll_path_b = "/{}/home/{}/test_query2_coll_b".format(cls.sess.zone, cls.sess.username)
cls.sess.collections.create(cls.coll_path_a)

@@ -69,5 +62,3 @@ cls.sess.collections.create(cls.coll_path_b)

def test_select_or(self):
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}' OR COLL_NAME = '{}'".format(
self.coll_path_a, self.coll_path_b
)
query = "SELECT COLL_NAME WHERE COLL_NAME = '{}' OR COLL_NAME = '{}'".format(self.coll_path_a, self.coll_path_b)
q = self.sess.genquery2_object()

@@ -82,6 +73,4 @@ query_result = q.execute(query)

def test_select_and(self):
query = (
"SELECT COLL_NAME WHERE COLL_NAME LIKE '{}' AND COLL_NAME LIKE '{}'".format(
"%test_query2_coll%", "%query2_coll_a%"
)
query = "SELECT COLL_NAME WHERE COLL_NAME LIKE '{}' AND COLL_NAME LIKE '{}'".format(
"%test_query2_coll%", "%query2_coll_a%"
)

@@ -88,0 +77,0 @@ q = self.sess.genquery2_object()

import base64
import contextlib
import io
import datetime
import hashlib
import inspect
import io
import json

@@ -11,6 +11,6 @@ import logging

import os
import random
import re
import shutil
import socket
import random
import re
import sys

@@ -21,8 +21,9 @@ import tempfile

import irods.client_configuration as config
import irods.rule
from irods.helpers import (
home_collection,
make_session as _irods_helpers_make_session)
from irods.message import iRODSMessage, IRODS_VERSION
make_session as _irods_helpers_make_session,
)
from irods.message import iRODSMessage
from irods.password_obfuscation import encode
import irods.rule
from irods.session import iRODSSession

@@ -53,5 +54,3 @@

def create_user(
self, username, password=None, usertype="rodsuser", auto_remove=True
):
def create_user(self, username, password=None, usertype="rodsuser", auto_remove=True):
u = self.admin.users.create(username, usertype)

@@ -143,5 +142,3 @@ if password is not None:

if vault_path:
session.resources.create(
IRODS_REG_RESC, "unixfilesystem", session.host, vault_path
)
session.resources.create(IRODS_REG_RESC, "unixfilesystem", session.host, vault_path)
Reg_Resc_Name = IRODS_REG_RESC

@@ -160,5 +157,3 @@ return Reg_Resc_Name

with open(config, "w") as f1:
json.dump(
{recast(k): v for k, v in params.items() if k != "password"}, f1, indent=4
)
json.dump({recast(k): v for k, v in params.items() if k != "password"}, f1, indent=4)
auth = os.path.join(dir_, ".irodsA")

@@ -176,5 +171,3 @@ with open(auth, "w") as f2:

if _irods_helpers_make_session.__doc__ is not None:
make_session.__doc__ = re.sub(
r"(test_server_version\s*)=\s*\w+", r"\1 = True", _irods_helpers_make_session.__doc__
)
make_session.__doc__ = re.sub(r"(test_server_version\s*)=\s*\w+", r"\1 = True", _irods_helpers_make_session.__doc__)

@@ -224,5 +217,3 @@

def make_deep_collection(
session, root_path, depth=10, objects_per_level=50, object_content=None
):
def make_deep_collection(session, root_path, depth=10, objects_per_level=50, object_content=None):
# start at root path

@@ -234,12 +225,7 @@ current_coll_path = root_path

# make list of object names
obj_names = [
"obj" + str(i).zfill(len(str(objects_per_level)))
for i in range(objects_per_level)
]
obj_names = ["obj" + str(i).zfill(len(str(objects_per_level))) for i in range(objects_per_level)]
# make subcollection and objects
if d == 0:
root_coll = make_collection(
session, current_coll_path, obj_names, object_content
)
root_coll = make_collection(session, current_coll_path, obj_names, object_content)
else:

@@ -249,5 +235,3 @@ make_collection(session, current_coll_path, obj_names, object_content)

# next level down
current_coll_path = os.path.join(
current_coll_path, "subcoll" + str(d).zfill(len(str(d)))
)
current_coll_path = os.path.join(current_coll_path, "subcoll" + str(d).zfill(len(str(d))))

@@ -276,5 +260,3 @@ return root_coll

if not rescName:
rescName = "simple_resc_" + unique_name(
my_function_name() + "_simple_resc", datetime.datetime.now()
)
rescName = "simple_resc_" + unique_name(my_function_name() + "_simple_resc", datetime.datetime.now())
created = False

@@ -329,5 +311,3 @@ try:

message_body = GeneralAdminRequest("rm", "unusedAVUs", "", "", "", "")
req = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]
)
req = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"])
with session.pool.get_connection() as conn:

@@ -344,9 +324,5 @@ conn.send(req)

if _basename is None and require_that_file_exists:
err = RuntimeError(
"Attempted to back up a file which doesn't exist: %r" % (filename,)
)
err = RuntimeError("Attempted to back up a file which doesn't exist: %r" % (filename,))
raise err
with tempfile.NamedTemporaryFile(
prefix=("tmp" if not _basename else _basename)
) as f:
with tempfile.NamedTemporaryFile(prefix=("tmp" if not _basename else _basename)) as f:
try:

@@ -353,0 +329,0 @@ if _basename is not None:

@@ -11,3 +11,2 @@ #! /usr/bin/env python

class TestLibraryFeatures(unittest.TestCase):
def setUp(self):

@@ -14,0 +13,0 @@ self.sess = helpers.make_session()

@@ -26,3 +26,3 @@ #! /usr/bin/env python

import gc
from irods.test.setupssl import create_ssl_dir
from irods.test.setup_ssl import create_ssl_dir

@@ -119,5 +119,3 @@ #

old_init(self, *arg, **kw)
self.set_auth_option_for_scheme(
"pam_password", ENSURE_SSL_IS_ACTIVE, not (allow)
)
self.set_auth_option_for_scheme("pam_password", ENSURE_SSL_IS_ACTIVE, not (allow))

@@ -165,3 +163,3 @@ with irods.helpers.temporarily_assign_attribute(iRODSSession, "__init__", new_init):

2. ./setupssl.py (sets up SSL keys etc. in /etc/irods/ssl) should be run
2. ./setup_ssl.py (sets up SSL keys etc. in /etc/irods/ssl) should be run
first to create (or overwrite, if appropriate) the /etc/irods/ssl directory

@@ -184,3 +182,3 @@ and its contents.

env_save: Dict[str,Optional[str]] = {}
env_save: Dict[str, Optional[str]] = {}

@@ -214,2 +212,7 @@ @contextlib.contextmanager

port=1247,
**(
{**SERVER_ENV_SSL_SETTINGS, **CLIENT_OPTIONS_FOR_SSL}
if self.admin.server_version >= (5,)
else {}
),
)

@@ -228,5 +231,3 @@ try:

cl_env = client_env_keys_from_admin_env(TEST_RODS_USER)
if (
lookup.get("AUTH", None) is not None
): # - specify auth scheme only if given
if lookup.get("AUTH", None) is not None: # - specify auth scheme only if given
cl_env["irods_authentication_scheme"] = lookup["AUTH"]

@@ -258,5 +259,3 @@ dirbase = os.path.join(os.environ["HOME"], dirname)

if cls.admin.server_version >= (4, 3) and not cfg.legacy_auth.force_legacy_auth:
cls.PAM_SCHEME_STRING = cls.user_auth_envs[".irods.pam"]["AUTH"] = (
"pam_password"
)
cls.PAM_SCHEME_STRING = cls.user_auth_envs[".irods.pam"]["AUTH"] = "pam_password"

@@ -288,5 +287,3 @@ @classmethod

my_connect = [s for s in (session.pool.active | session.pool.idle)][0]
self.assertEqual(
bool(use_ssl), my_connect.socket.__class__ is ssl.SSLSocket
)
self.assertEqual(bool(use_ssl), my_connect.socket.__class__ is ssl.SSLSocket)

@@ -303,11 +300,8 @@ @contextlib.contextmanager

def tst0(
self, ssl_opt, auth_opt, env_opt, name=TEST_RODS_USER, make_irods_pw=False
):
def tst0(self, ssl_opt, auth_opt, env_opt, name=TEST_RODS_USER, make_irods_pw=False):
session = None
_auth_opt = auth_opt
if auth_opt in ("pam", "pam_password"):
auth_opt = self.PAM_SCHEME_STRING
with self._setup_rodsuser_and_optional_pw(
name=name, make_irods_pw=make_irods_pw
):
with self._setup_rodsuser_and_optional_pw(name=name, make_irods_pw=make_irods_pw):
self.envdirs = self.create_env_dirs()

@@ -321,10 +315,7 @@ if not self.envdirs:

if env_opt:
with self.setenv(
"IRODS_ENVIRONMENT_FILE", json_env_fullpath(auth_opt_explicit)
) as env_file, self.setenv(
"IRODS_AUTHENTICATION_FILE", secrets_fullpath(auth_opt_explicit)
with (
self.setenv("IRODS_ENVIRONMENT_FILE", json_env_fullpath(auth_opt_explicit)) as env_file,
self.setenv("IRODS_AUTHENTICATION_FILE", secrets_fullpath(auth_opt_explicit)),
):
cli_env_extras = (
{} if not (ssl_opt) else dict(CLIENT_OPTIONS_FOR_SSL)
)
cli_env_extras = {} if not (ssl_opt) else dict(CLIENT_OPTIONS_FOR_SSL)
if auth_opt:

@@ -336,5 +327,3 @@ cli_env_extras.update(irods_authentication_scheme=auth_opt)

with helpers.file_backed_up(env_file):
json_file_update(
env_file, keys_to_delete=remove, **cli_env_extras
)
json_file_update(env_file, keys_to_delete=remove, **cli_env_extras)
with pam_password_in_plaintext(nop=ssl_opt):

@@ -344,5 +333,3 @@ session = iRODSSession(irods_env_file=env_file)

out = json.load(f)
self.validate_session(
session, verbose=verbosity, ssl=ssl_opt
)
self.validate_session(session, verbose=verbosity, ssl=ssl_opt)
session.cleanup()

@@ -363,7 +350,5 @@ out["ARGS"] = "no"

),
**CLIENT_OPTIONS_FOR_SSL
**CLIENT_OPTIONS_FOR_SSL,
)
lookup = self.user_auth_envs[
".irods." + ("native" if not (_auth_opt) else _auth_opt)
]
lookup = self.user_auth_envs[".irods." + ("native" if not (_auth_opt) else _auth_opt)]
with pam_password_in_plaintext(nop=ssl_opt):

@@ -376,3 +361,3 @@ session = iRODSSession(

port=1247,
**session_options
**session_options,
)

@@ -388,9 +373,9 @@ out = session_options

"--- > ",
json.dumps(
{k: v for k, v in out.items() if k != "ssl_context"}, indent=4
),
json.dumps({k: v for k, v in out.items() if k != "ssl_context"}, indent=4),
)
print("---")
return session
if session:
session.cleanup()
return session

@@ -431,4 +416,6 @@ # == test defaulting to 'native'

def test_6(self):
if self.admin.server_version >= (5,):
self.skipTest("iRODS 5 does not permit sending the raw PAM password on an unencrypted connection.")
try:
ses = self.tst0(ssl_opt=False, auth_opt="pam", env_opt=False)
session = self.tst0(ssl_opt=False, auth_opt="pam", env_opt=False)
except PlainTextPAMPasswordError:

@@ -439,3 +426,3 @@ pass

# but for 4.2 and previous, we expect the PlainTextPAMPasswordError to be raised.
if ses.server_version_without_auth() < (4, 3):
if session.server_version_without_auth() < (4, 3):
self.fail("PlainTextPAMPasswordError should have been raised")

@@ -478,3 +465,3 @@

authentication_scheme="pam",
**ssl_settings
**ssl_settings,
)

@@ -486,3 +473,2 @@ home_coll = "/{0.zone}/home/{0.username}".format(irods_session)

class TestAnonymousUser(unittest.TestCase):
def setUp(self):

@@ -531,3 +517,2 @@ admin = self.admin = helpers.make_session()

class TestMiscellaneous(unittest.TestCase):
def test_nonanonymous_login_without_auth_file_fails__290(self):

@@ -557,3 +542,3 @@ ses = self.admin

# -- Check that we raise an appropriate exception pointing to the missing auth file path --
with self.assertRaisesRegexp(NonAnonymousLoginWithoutPassword, bob_auth):
with self.assertRaisesRegex(NonAnonymousLoginWithoutPassword, bob_auth):
with helpers.make_session(**login_options) as s:

@@ -617,5 +602,3 @@ s.users.get("bob")

admin = self.admin
with iRODSSession(
zone=admin.zone, port=admin.port, host=admin.host, user="alice"
) as alice:
with iRODSSession(zone=admin.zone, port=admin.port, host=admin.host, user="alice") as alice:
alice.collections.get(helpers.home_collection(alice))

@@ -634,5 +617,3 @@

if not os.path.exists("/etc/irods/ssl"):
self.skipTest(
"Running setupssl.py as irods user is prerequisite for this test."
)
self.skipTest("Running setup_ssl.py as irods user is prerequisite for this test.")
with helpers.make_session() as session:

@@ -651,5 +632,3 @@ if not session.host in ("localhost", socket.gethostname()):

# Elect for efficiency in DH param generation, eg. when setting up for testing.
create_ssl_dir(
ssl_dir=my_ssl_directory, use_strong_primes_for_dh_generation=False
)
create_ssl_dir(ssl_dir=my_ssl_directory, use_strong_primes_for_dh_generation=False)
settings_to_update = {

@@ -665,5 +644,3 @@ key: value.replace("/etc/irods/ssl", my_ssl_directory)

with helpers.make_session() as session:
session.collections.get(
"/{session.zone}/home/{session.username}".format(**locals())
)
session.collections.get("/{session.zone}/home/{session.username}".format(**locals()))
finally:

@@ -670,0 +647,0 @@ if my_ssl_directory:

@@ -28,3 +28,2 @@ #!/usr/bin/env python

class TestMessages(unittest.TestCase):
def test_startup_pack(self):

@@ -216,3 +215,4 @@ sup = StartupPack(("rods", "tempZone"), ("rods", "tempZone"))

if __name__ == "__main__":
unittest.main()
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import datetime
import os

@@ -8,19 +9,20 @@ import re

import time
import datetime
import unittest
import irods.exception as ex
import irods.keywords as kw
from irods.column import Like, NotLike
from irods.manager.metadata_manager import InvalidAtomicAVURequest
from irods.message import Bad_AVU_Field
from irods.meta import (
iRODSMeta,
AVUOperation,
BadAVUOperationKeyword,
BadAVUOperationValue,
BadAVUOperationKeyword,
iRODSBinOrStringMeta,
iRODSMeta,
)
from irods.models import DataObject, Collection, Resource, CollectionMeta
import irods.test.helpers as helpers
import irods.keywords as kw
from irods.models import Collection, CollectionMeta, DataObject, ModelBase, Resource
from irods.path import iRODSPath
from irods.session import iRODSSession
from irods.message import Bad_AVU_Field
from irods.models import ModelBase
from irods.column import Like, NotLike
from irods.test import helpers

@@ -68,3 +70,2 @@

for v in (value, value_encoded):
# Establish invariant of exactly 2 AVUs attached to object.

@@ -74,5 +75,3 @@ self.coll.metadata.remove_all()

self.coll.metadata["b"] = iRODSMeta("b", "<arbitrary>")
q = self.sess.query(CollectionMeta).filter(
Collection.name == self.coll_path
)
q = self.sess.query(CollectionMeta).filter(Collection.name == self.coll_path)
self.assertEqual(len(list(q)), 2)

@@ -93,9 +92,5 @@

# Test query with operators == and !=
q = self.sess.query(CollectionMeta).filter(
Collection.name == self.coll_path, CollectionMeta.value == v
)
q = self.sess.query(CollectionMeta).filter(Collection.name == self.coll_path, CollectionMeta.value == v)
self.assertEqual(len(list(q)), 1)
q = self.sess.query(CollectionMeta).filter(
Collection.name == self.coll_path, CollectionMeta.value != v
)
q = self.sess.query(CollectionMeta).filter(Collection.name == self.coll_path, CollectionMeta.value != v)
self.assertEqual(len(list(q)), 1)

@@ -124,5 +119,3 @@ metadata.append(self.coll.metadata["a"])

# test data
self.coll_path = "/{}/home/{}/test_dir".format(
self.sess.zone, self.sess.username
)
self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username)
self.obj_name = "test1"

@@ -162,5 +155,3 @@ self.obj_path = "{coll_path}/{obj_name}".format(**vars(self))

try:
obj.metadata.apply_atomic_operations(
AVUOperation(operation="add", avu=iRODSMeta("a", "b", "c"))
)
obj.metadata.apply_atomic_operations(AVUOperation(operation="add", avu=iRODSMeta("a", "b", "c")))
except ex.iRODSException as e:

@@ -173,4 +164,6 @@ resp = e.server_msg.get_json_encoded_struct()

except Exception as e:
fail_message = "apply_atomic_operations on a nonexistent object raised an unexpected exception {e!r}".format(
**locals()
fail_message = (
"apply_atomic_operations on a nonexistent object raised an unexpected exception {e!r}".format(
**locals()
)
)

@@ -203,12 +196,5 @@ else:

for n, obj in enumerate((group, user, self.coll, self.obj)):
avus = [
iRODSMeta("some_attribute", str(i), "some_units")
for i in range(n * 100, (n + 1) * 100)
]
obj.metadata.apply_atomic_operations(
*[AVUOperation(operation="add", avu=avu_) for avu_ in avus]
)
obj.metadata.apply_atomic_operations(
*[AVUOperation(operation="remove", avu=avu_) for avu_ in avus]
)
avus = [iRODSMeta("some_attribute", str(i), "some_units") for i in range(n * 100, (n + 1) * 100)]
obj.metadata.apply_atomic_operations(*[AVUOperation(operation="add", avu=avu_) for avu_ in avus])
obj.metadata.apply_atomic_operations(*[AVUOperation(operation="remove", avu=avu_) for avu_ in avus])

@@ -230,8 +216,4 @@ def test_atomic_metadata_operation_for_resource_244(self):

resc_meta = self.sess.metadata.get(Resource, resc.name)
avus_to_tuples = lambda avu_list: sorted(
[(i.name, i.value, i.units) for i in avu_list]
)
self.assertEqual(
avus_to_tuples(resc_meta), avus_to_tuples([iRODSMeta(*resc_tuple)])
)
avus_to_tuples = lambda avu_list: sorted([(i.name, i.value, i.units) for i in avu_list])
self.assertEqual(avus_to_tuples(resc_meta), avus_to_tuples([iRODSMeta(*resc_tuple)]))

@@ -254,5 +236,3 @@ def test_atomic_metadata_operation_for_data_object_244(self):

)
meta = self.sess.metadata.get(
DataObject, self.obj_path
) # ... check integrity of change
meta = self.sess.metadata.get(DataObject, self.obj_path) # ... check integrity of change
self.assertEqual(sorted([AVU_Units_String(i) for i in meta]), ["", "units_244"])

@@ -274,5 +254,3 @@

for n, item in enumerate(avus):
obj.metadata.apply_atomic_operations(
AVUOperation(operation="add", avu=item)
)
obj.metadata.apply_atomic_operations(AVUOperation(operation="add", avu=item))
self.assertEqual(len(obj.metadata.items()), n + 1)

@@ -294,5 +272,3 @@ finally:

self.sess.resources.get(rescname).metadata.remove_all()
self.sess.metadata.set(
Resource, rescname, iRODSMeta("zero", "marginal", "cost")
)
self.sess.metadata.set(Resource, rescname, iRODSMeta("zero", "marginal", "cost"))
self.sess.metadata.add(Resource, rescname, iRODSMeta("zero", "marginal"))

@@ -310,8 +286,4 @@ self.sess.metadata.set(Resource, rescname, iRODSMeta("for", "ever", "after"))

# add metadata to test object
self.sess.metadata.add(
DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0)
)
self.sess.metadata.add(
DataObject, self.obj_path, iRODSMeta(self.attr1, self.value1, self.unit1)
)
self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0))
self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta(self.attr1, self.value1, self.unit1))

@@ -347,5 +319,3 @@ # Throw in some unicode for good measure

with self.assertRaises(ValueError):
self.sess.metadata.add(
DataObject, self.obj_path, iRODSMeta("attr_with_empty_value", "")
)
self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta("attr_with_empty_value", ""))

@@ -358,5 +328,3 @@ def test_copy_obj_meta(self):

# add metadata to test object
self.sess.metadata.add(
DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0)
)
self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0))

@@ -374,5 +342,3 @@ # copy metadata

# add metadata to test object
self.sess.metadata.add(
DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0)
)
self.sess.metadata.add(DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0))

@@ -384,5 +350,3 @@ # check that metadata is there

# remove metadata from object
self.sess.metadata.remove(
DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0)
)
self.sess.metadata.remove(DataObject, self.obj_path, iRODSMeta(self.attr0, self.value0))

@@ -399,5 +363,3 @@ # check that metadata is gone

if adm.server_version <= (4, 2, 11):
self.skipTest(
"ADMIN_KW not valid for Metadata API in iRODS 4.2.11 and previous"
)
self.skipTest("ADMIN_KW not valid for Metadata API in iRODS 4.2.11 and previous")

@@ -416,5 +378,3 @@ # Create a rodsuser, and a session for that roduser.

# has the desired effect.
d = ses.data_objects.create(
"/{adm.zone}/home/{user.name}/testfile".format(**locals())
)
d = ses.data_objects.create("/{adm.zone}/home/{user.name}/testfile".format(**locals()))

@@ -452,5 +412,3 @@ d.metadata.set("a", "aa", "1")

if adm.server_version <= (4, 2, 11):
self.skipTest(
"ADMIN_KW not valid for Metadata API in iRODS 4.2.11 and previous"
)
self.skipTest("ADMIN_KW not valid for Metadata API in iRODS 4.2.11 and previous")

@@ -476,5 +434,3 @@ try:

avu_item = iRODSMeta("issue_576", "dummy_value")
data_via_admin.metadata(admin=True).apply_atomic_operations(
AVUOperation(operation="add", avu=avu_item)
)
data_via_admin.metadata(admin=True).apply_atomic_operations(AVUOperation(operation="add", avu=avu_item))
self.assertIn(avu_item, data_via_admin.metadata.items())

@@ -492,5 +448,3 @@ finally:

# add metadata to test collection
self.sess.metadata.add(
Collection, self.coll_path, iRODSMeta(self.attr0, self.value0)
)
self.sess.metadata.add(Collection, self.coll_path, iRODSMeta(self.attr0, self.value0))

@@ -505,5 +459,3 @@ # get collection metadata

# remove collection metadata
self.sess.metadata.remove(
Collection, self.coll_path, iRODSMeta(self.attr0, self.value0)
)
self.sess.metadata.remove(Collection, self.coll_path, iRODSMeta(self.attr0, self.value0))

@@ -527,5 +479,3 @@ # check that metadata is gone

# add metadata to test object
meta = self.sess.metadata.add(
DataObject, test_obj_path, iRODSMeta(attribute, value, units)
)
meta = self.sess.metadata.add(DataObject, test_obj_path, iRODSMeta(attribute, value, units))

@@ -557,5 +507,3 @@ # get metadata

# test AVUs
triplets = [
("test_attr" + str(i), "test_value", "test_units") for i in range(avu_count)
]
triplets = [("test_attr" + str(i), "test_value", "test_units") for i in range(avu_count)]

@@ -696,5 +644,3 @@ # get coll meta

data = self.sess.data_objects.create(
"{hc}/{index}_{edge_case_arg}_{method}_AZ__issue_547".format(
**locals()
)
"{hc}/{index}_{edge_case_arg}_{method}_AZ__issue_547".format(**locals())
)

@@ -712,5 +658,5 @@ to_delete.append(data)

args = ("an_attribute", 0)
with self.assertRaisesRegexp(Bad_AVU_Field, "incorrect type"):
with self.assertRaisesRegex(Bad_AVU_Field, "incorrect type"):
self.coll.metadata.set(*args)
with self.assertRaisesRegexp(Bad_AVU_Field, "incorrect type"):
with self.assertRaisesRegex(Bad_AVU_Field, "incorrect type"):
self.coll.metadata.add(*args)

@@ -720,10 +666,8 @@

args = ("an_attribute", "")
with self.assertRaisesRegexp(Bad_AVU_Field, "zero-length"):
with self.assertRaisesRegex(Bad_AVU_Field, "zero-length"):
self.coll.metadata.set(*args)
with self.assertRaisesRegexp(Bad_AVU_Field, "zero-length"):
with self.assertRaisesRegex(Bad_AVU_Field, "zero-length"):
self.coll.metadata.add(*args)
@unittest.skipUnless(
os.path.isfile(RODS_GENQUERY_INCLUDE_FILE_PATH), "need package irods-dev(el)"
)
@unittest.skipUnless(os.path.isfile(RODS_GENQUERY_INCLUDE_FILE_PATH), "need package irods-dev(el)")
def test_that_all_column_mappings_are_uniquely_and_properly_defined__issue_643(

@@ -741,22 +685,16 @@ self,

server_column_defs = sorted(
[
(match.group("column_name"), int(match.group("column_value")))
for match in (
column_definitions_regex.match(line) for line in include_lines
)
if match
]
)
server_column_defs = sorted([
(match.group("column_name"), int(match.group("column_value")))
for match in (column_definitions_regex.match(line) for line in include_lines)
if match
])
# Extract all GenQuery1 name-to-number mappings from PRC model class definitions (some omit 'COL_' prefix, so allow some flexibility there.)
prepend_col_prefix_if_needed = lambda s: (
"COL_" + s if not s.startswith("COL_") else s
)
prc_column_defs = sorted(
[
(prepend_col_prefix_if_needed(i[1].icat_key), i[1].icat_id)
for i in ModelBase.column_items
]
)
prepend_col_prefix_if_needed = lambda s: "COL_" + s if not s.startswith("COL_") else s
current_server_version = self.sess.server_version
prc_column_defs = sorted([
(prepend_col_prefix_if_needed(i[1].icat_key), i[1].icat_id)
for i in ModelBase.column_items
if current_server_version >= i[1].min_version
])

@@ -774,5 +712,5 @@ sr = set(a for a, b in set(prc_column_defs) - set(server_column_defs))

meta_units = str(time.time())
execute_my_query = lambda: self.sess.query(
CollectionMeta, Collection.name
).filter(Collection.name == test_coll.path, CollectionMeta.name == meta_key)
execute_my_query = lambda: self.sess.query(CollectionMeta, Collection.name).filter(
Collection.name == test_coll.path, CollectionMeta.name == meta_key
)
# Make sure no iRODSMeta exists under the test key.

@@ -811,14 +749,12 @@ del test_coll.metadata[meta_key]

1,
len(
[
_
for _ in (
session.query(CollectionMeta).filter(
Collection.name == hc,
CollectionMeta.name == attr_str,
CollectionMeta.value == string_value,
)
len([
_
for _ in (
session.query(CollectionMeta).filter(
Collection.name == hc,
CollectionMeta.name == attr_str,
CollectionMeta.value == string_value,
)
]
),
)
]),
)

@@ -831,3 +767,68 @@ finally:

def test_binary_avu_fields__issue_707(self):
meta_coll = self.obj.metadata(iRODSMeta_type=iRODSBinOrStringMeta)
illegal_unicode_sequence = '\u1000'.encode()[:2]
avu_name = 'issue709'
meta_coll.set(
avu_name, (value := b'value_' + illegal_unicode_sequence), (units := b'units_' + illegal_unicode_sequence)
)
self.assertEqual(meta_coll.get_one(avu_name), (avu_name, value, units))
meta_coll.add(*(new_avu := iRODSMeta(avu_name, '\u1000', '\u1001')))
relevant_avus = meta_coll.get_all(avu_name)
self.assertIn(new_avu, relevant_avus)
def test_cascading_changes_of_metadata_manager_options__issue_709(self):
d = None
try:
d = self.sess.data_objects.create(f'{self.coll.path}/issue_709_test_1')
m = d.metadata
self.assertEqual(m.admin, False)
m2 = m(admin=True)
self.assertEqual(m2.timestamps, False)
self.assertEqual(m2.admin, True)
m3 = m2(timestamps=True)
self.assertEqual(m3.timestamps, True)
self.assertEqual(m3.admin, True)
self.assertEqual(m3._manager.get_api_keywords().get(kw.ADMIN_KW), "")
m4 = m3(admin=False)
self.assertEqual(m4.admin, False)
self.assertEqual(m4._manager.get_api_keywords().get(kw.ADMIN_KW), None)
finally:
if d:
d.unlink(force=True)
def test_reload_can_be_deactivated__issue_768(self):
# Set an initial AVU
metacoll = self.obj.metadata
metacoll.set(item_1 := iRODSMeta('aa', 'bb', 'cc'))
# Initial defaults will always reload the AVU list from the server, so new AVU should be seen.
self.assertIn(item_1, metacoll.items())
# Setting reload option to False will prevent reload of object AVUs, so an AVU just set should not be seen.
metacoll_2 = metacoll(reload=False)
metacoll_2.set(item_2 := iRODSMeta('xx', 'yy', 'zz'))
items = metacoll_2.items()
self.assertIn(item_1, items)
self.assertNotIn(item_2, items)
# Restore old setting. Check that both AVUs are seen as present.
items_reloaded = metacoll_2(reload=True).items()
self.assertIn(item_1, items_reloaded)
self.assertIn(item_2, items_reloaded)
def test_prevention_of_attribute_creation__issue_795(self):
data_path = iRODSPath(self.coll_path, helpers.unique_name(datetime.datetime.now())) # noqa: DTZ005
data = self.sess.data_objects.create(data_path)
with self.assertRaises(AttributeError):
# This should cause an error since "admin" is considered as a read-only attribute; whereas
# data.metadata(admin = True) generates a cloned object but for the one change to "admin".
data.metadata.admin = True
if __name__ == "__main__":

@@ -834,0 +835,0 @@ # let the tests find the parent irods lib

@@ -45,7 +45,3 @@ # This helper module can double as a Python script, allowing us to run the below

# but by specifying a list/tuple of keys we can export only those specific locals by name.
return (
L
if not isinstance(return_locals, (tuple, list))
else [L[k] for k in return_locals]
)
return L if not isinstance(return_locals, (tuple, list)) else [L[k] for k in return_locals]

@@ -52,0 +48,0 @@

@@ -41,6 +41,4 @@ # Used in test of:

if __name__ == "__main__":
function_info = [
[*get_stage_object_and_value_from_name(name), object_printer(name)]
for name in reversed(sys.argv[1:])
[*get_stage_object_and_value_from_name(name), object_printer(name)] for name in reversed(sys.argv[1:])
]

@@ -47,0 +45,0 @@

@@ -22,5 +22,3 @@ #! /usr/bin/env python

LOCALHOST_REGEX = re.compile(
r"""^(127(\.\d+){1,3}|[0:]+1|(.*-)?localhost(\.\w+)?)$""", re.IGNORECASE
)
LOCALHOST_REGEX = re.compile(r"""^(127(\.\d+){1,3}|[0:]+1|(.*-)?localhost(\.\w+)?)$""", re.IGNORECASE)
USE_ONLY_LOCALHOST = False

@@ -30,6 +28,5 @@

class TestPool(unittest.TestCase):
config_extension = ".json"
test_extension = ""
preferred_parameters : Dict[str, Any] = {}
preferred_parameters: Dict[str, Any] = {}

@@ -283,8 +280,4 @@ @classmethod

if self.sess.host != socket.gethostname() and not LOCALHOST_REGEX.match(
self.sess.host
):
self.skipTest(
"local test only - client dot does not like the extra logging"
)
if self.sess.host != socket.gethostname() and not LOCALHOST_REGEX.match(self.sess.host):
self.skipTest("local test only - client dot does not like the extra logging")

@@ -356,5 +349,3 @@ # Set 'irods_connection_refresh_time' to '3' (in seconds) in

def test_get_connection_refresh_time_no_env_file_input_param(self):
connection_refresh_time = self.sess.get_connection_refresh_time(
first_name="Magic", last_name="Johnson"
)
connection_refresh_time = self.sess.get_connection_refresh_time(first_name="Magic", last_name="Johnson")
self.assertEqual(connection_refresh_time, -1)

@@ -398,5 +389,3 @@

)
connection_refresh_time = self.sess.get_connection_refresh_time(
irods_env_file=default_path
)
connection_refresh_time = self.sess.get_connection_refresh_time(irods_env_file=default_path)
self.assertEqual(connection_refresh_time, 3)

@@ -403,0 +392,0 @@

@@ -47,3 +47,2 @@ #! /usr/bin/env python

class TestQuery(unittest.TestCase):
Iterate_to_exhaust_statement_table = range(IRODS_STATEMENT_TABLE_SIZE + 1)

@@ -71,5 +70,3 @@

print(
"Could not remove resc {!r} due to: {} ".format(
cls.register_resc, e
),
"Could not remove resc {!r} due to: {} ".format(cls.register_resc, e),
file=sys.stderr,

@@ -82,5 +79,3 @@ )

# test data
self.coll_path = "/{}/home/{}/test_dir".format(
self.sess.zone, self.sess.username
)
self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username)
self.obj_name = "test1"

@@ -90,8 +85,4 @@ self.case_sensitive_obj_name1 = "caseSENSITIVEobject"

self.obj_path = "{coll_path}/{obj_name}".format(**vars(self))
self.case_sensitive_obj_path1 = "{coll_path}/{case_sensitive_obj_name1}".format(
**vars(self)
)
self.case_sensitive_obj_path2 = "{coll_path}/{case_sensitive_obj_name2}".format(
**vars(self)
)
self.case_sensitive_obj_path1 = "{coll_path}/{case_sensitive_obj_name1}".format(**vars(self))
self.case_sensitive_obj_path2 = "{coll_path}/{case_sensitive_obj_name2}".format(**vars(self))

@@ -139,7 +130,3 @@ # Create test collection and (empty) test object

result1 = (
self.sess.query(DataObject.name)
.filter(DataObject.name == self.case_sensitive_obj_name1)
.all()
)
result1 = self.sess.query(DataObject.name).filter(DataObject.name == self.case_sensitive_obj_name1).all()
self.assertTrue(result1.has_value(self.case_sensitive_obj_name1))

@@ -149,5 +136,3 @@ self.assertEqual(len(result1), 1)

result2 = (
self.sess.query(DataObject.name)
.filter(DataObject.name == str.lower(self.case_sensitive_obj_name1))
.all()
self.sess.query(DataObject.name).filter(DataObject.name == str.lower(self.case_sensitive_obj_name1)).all()
)

@@ -157,5 +142,3 @@ self.assertEqual(len(result2), 0)

result3 = (
self.sess.query(DataObject.name)
.filter(DataObject.name == str.upper(self.case_sensitive_obj_name1))
.all()
self.sess.query(DataObject.name).filter(DataObject.name == str.upper(self.case_sensitive_obj_name1)).all()
)

@@ -168,26 +151,15 @@ self.assertEqual(len(result3), 0)

result4 = (
self.sess.query(DataObject.name)
.filter(Like(DataObject.name, search_expression))
.all()
)
result4 = self.sess.query(DataObject.name).filter(Like(DataObject.name, search_expression)).all()
self.assertTrue(result4.has_value(self.case_sensitive_obj_name1))
self.assertEqual(len(result4), 1)
result5 = (
self.sess.query(DataObject.name)
.filter(Like(DataObject.name, str.lower(search_expression)))
.all()
)
result5 = self.sess.query(DataObject.name).filter(Like(DataObject.name, str.lower(search_expression))).all()
self.assertEqual(len(result5), 0)
result6 = (
self.sess.query(DataObject.name)
.filter(Like(DataObject.name, str.upper(search_expression)))
.all()
)
result6 = self.sess.query(DataObject.name).filter(Like(DataObject.name, str.upper(search_expression))).all()
self.assertEqual(len(result6), 0)
result7 = (
self.sess.query(DataObject.name)
self.sess
.query(DataObject.name)
.filter(Collection.name == self.coll_path)

@@ -201,3 +173,4 @@ .filter(NotLike(DataObject.name, search_expression))

result8 = (
self.sess.query(DataObject.name)
self.sess
.query(DataObject.name)
.filter(Collection.name == self.coll_path)

@@ -212,3 +185,4 @@ .filter(NotLike(DataObject.name, str.lower(search_expression)))

result9 = (
self.sess.query(DataObject.name)
self.sess
.query(DataObject.name)
.filter(Collection.name == self.coll_path)

@@ -225,3 +199,4 @@ .filter(NotLike(DataObject.name, str.upper(search_expression)))

result10 = (
self.sess.query(DataObject.name)
self.sess
.query(DataObject.name)
.filter(Collection.name == self.coll_path)

@@ -235,3 +210,4 @@ .filter(In(DataObject.name, [self.case_sensitive_obj_name1]))

result11 = (
self.sess.query(DataObject.name)
self.sess
.query(DataObject.name)
.filter(Collection.name == self.coll_path)

@@ -244,3 +220,4 @@ .filter(In(DataObject.name, [str.lower(self.case_sensitive_obj_name1)]))

result12 = (
self.sess.query(DataObject.name)
self.sess
.query(DataObject.name)
.filter(Collection.name == self.coll_path)

@@ -283,3 +260,4 @@ .filter(In(DataObject.name, [str.upper(self.case_sensitive_obj_name1)]))

result1 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(DataObject.name == self.case_sensitive_obj_name1)

@@ -293,3 +271,4 @@ .all()

result2 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(DataObject.name == str.lower(self.case_sensitive_obj_name1))

@@ -303,3 +282,4 @@ .all()

result3 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(DataObject.name == str.upper(self.case_sensitive_obj_name1))

@@ -317,3 +297,4 @@ .all()

result4 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Like(DataObject.name, search_expression))

@@ -327,3 +308,4 @@ .all()

result5 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Like(DataObject.name, str.lower(search_expression)))

@@ -337,3 +319,4 @@ .all()

result6 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Like(DataObject.name, str.upper(search_expression)))

@@ -347,3 +330,4 @@ .all()

result7 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -358,3 +342,4 @@ .filter(NotLike(DataObject.name, search_expression))

result8 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -369,3 +354,4 @@ .filter(NotLike(DataObject.name, str.lower(search_expression)))

result9 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -382,3 +368,4 @@ .filter(NotLike(DataObject.name, str.upper(search_expression)))

result10 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -393,3 +380,4 @@ .filter(In(DataObject.name, [self.case_sensitive_obj_name1]))

result11 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -404,3 +392,4 @@ .filter(In(DataObject.name, [str.lower(self.case_sensitive_obj_name1)]))

result12 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -417,3 +406,4 @@ .filter(In(DataObject.name, [str.upper(self.case_sensitive_obj_name1)]))

result13 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -436,3 +426,4 @@ .filter(

result14 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -455,3 +446,4 @@ .filter(

result15 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Collection.name == self.coll_path)

@@ -477,3 +469,4 @@ .filter(

result16 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Like(DataObject.name, search_expression))

@@ -488,3 +481,4 @@ .filter(Collection.name == self.coll_path)

result17 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Like(DataObject.name, str.lower(search_expression)))

@@ -499,3 +493,4 @@ .filter(Collection.name == str.lower(self.coll_path))

result18 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Like(DataObject.name, str.upper(search_expression)))

@@ -511,3 +506,4 @@ .filter(Collection.name == str.upper(self.coll_path))

result19 = (
self.sess.query(DataObject.name, case_sensitive=False)
self.sess
.query(DataObject.name, case_sensitive=False)
.filter(Like(DataObject.name, "THIS_SHOULD_NOT_MATCH"))

@@ -601,19 +597,11 @@ .all()

test_collection_size = 8
test_collection_path = "/{0}/home/{1}/testcoln_for_col_not_in_result".format(
self.sess.zone, self.sess.username
)
test_collection_path = "/{0}/home/{1}/testcoln_for_col_not_in_result".format(self.sess.zone, self.sess.username)
c1 = c2 = None
try:
c1 = helpers.make_test_collection(
self.sess, test_collection_path + "1", obj_count=test_collection_size
)
c2 = helpers.make_test_collection(
self.sess, test_collection_path + "2", obj_count=test_collection_size
)
d12 = [
sorted([d.id for d in c.data_objects])
for c in sorted((c1, c2), key=lambda c: c.id)
]
c1 = helpers.make_test_collection(self.sess, test_collection_path + "1", obj_count=test_collection_size)
c2 = helpers.make_test_collection(self.sess, test_collection_path + "2", obj_count=test_collection_size)
d12 = [sorted([d.id for d in c.data_objects]) for c in sorted((c1, c2), key=lambda c: c.id)]
query = (
self.sess.query(DataObject)
self.sess
.query(DataObject)
.filter(Like(Collection.name, test_collection_path + "_"))

@@ -623,7 +611,3 @@ .order_by(Collection.id)

q12 = list(map(lambda res: res[DataObject.id], query))
self.assertTrue(
d12[0] + d12[1]
== sorted(q12[:test_collection_size])
+ sorted(q12[test_collection_size:])
)
self.assertTrue(d12[0] + d12[1] == sorted(q12[:test_collection_size]) + sorted(q12[test_collection_size:]))
finally:

@@ -657,5 +641,3 @@ if c1:

for result in query:
res_str = "{} {}/{}".format(
result[Resource.name], result[Collection.name], result[DataObject.name]
)
res_str = "{} {}/{}".format(result[Resource.name], result[Collection.name], result[DataObject.name])
self.assertIn(session.zone, res_str)

@@ -683,11 +665,7 @@

for x in range(3, 9):
obj = helpers.make_object(
self.sess, file_path + "-{}".format(x)
) # with metadata
obj = helpers.make_object(self.sess, file_path + "-{}".format(x)) # with metadata
objects.append(obj)
obj.metadata.add("A_meta", "1{}".format(x))
obj.metadata.add("B_meta", "2{}".format(x))
decoys.append(
helpers.make_object(self.sess, file_path + "-dummy{}".format(x))
) # without metadata
decoys.append(helpers.make_object(self.sess, file_path + "-dummy{}".format(x))) # without metadata
self.assertTrue(len(objects) > 0)

@@ -697,3 +675,4 @@

q = (
self.sess.query(DataObject, DataObjectMeta)
self.sess
.query(DataObject, DataObjectMeta)
.filter(DataObjectMeta.name == "A_meta", DataObjectMeta.value < "20")

@@ -705,8 +684,6 @@ .filter(DataObjectMeta.name == "B_meta", DataObjectMeta.value >= "20")

# -- test no-stomp of previous filter --
self.assertTrue(
("B_meta", "28")
in [(x.name, x.value) for x in objects[-1].metadata.items()]
)
self.assertTrue(("B_meta", "28") in [(x.name, x.value) for x in objects[-1].metadata.items()])
q = (
self.sess.query(DataObject, DataObjectMeta)
self.sess
.query(DataObject, DataObjectMeta)
.filter(DataObjectMeta.name == "B_meta")

@@ -722,3 +699,4 @@ .filter(DataObjectMeta.value < "28")

q = (
self.sess.query(DataObject, DataObjectMeta)
self.sess
.query(DataObject, DataObjectMeta)
.filter(DataObjectMeta.name == "B_meta")

@@ -739,5 +717,3 @@ .filter(DataObjectMeta.value == "28")

)
testColl = helpers.make_test_collection(
self.sess, test_collection_path, obj_count=1
)
testColl = helpers.make_test_collection(self.sess, test_collection_path, obj_count=1)
testData = testColl.data_objects[0]

@@ -769,12 +745,8 @@ testResc = self.sess.resources.get("demoResc")

for suffix, tblpair in tables.items():
self.sess.query(*tblpair).filter(
tblpair[1].modify_time <= after
).filter(tblpair[1].modify_time > before).filter(
tblpair[0].id == object_IDs[suffix]
).one()
self.sess.query(*tblpair).filter(
tblpair[1].create_time <= after
).filter(tblpair[1].create_time > before).filter(
tblpair[0].id == object_IDs[suffix]
).one()
self.sess.query(*tblpair).filter(tblpair[1].modify_time <= after).filter(
tblpair[1].modify_time > before
).filter(tblpair[0].id == object_IDs[suffix]).one()
self.sess.query(*tblpair).filter(tblpair[1].create_time <= after).filter(
tblpair[1].create_time > before
).filter(tblpair[0].id == object_IDs[suffix]).one()
finally:

@@ -789,3 +761,9 @@ for obj in objects.values():

# Remove the column skips when irods/irods #8574 is resolved.
skipped_columns = {DataObject.map_id, Collection.map_id, DataObject.status, DataObject.type, DataObject.collection_id}
skipped_columns = {
DataObject.map_id,
Collection.map_id,
DataObject.status,
DataObject.type,
DataObject.collection_id,
}
collection = self.coll_path

@@ -803,10 +781,7 @@ filename = "test_multiple_AVU_joins"

q = self.sess.query(Collection, DataObject, *{-col for col in skipped_columns})
dummy_test = [
d
for d in q
if d[DataObject.name][-1:] != "8" and d[DataObject.name][-7:-1] == "-dummy"
]
dummy_test = [d for d in q if d[DataObject.name][-1:] != "8" and d[DataObject.name][-7:-1] == "-dummy"]
self.assertTrue(len(dummy_test) > 0)
q = (
q.filter(Like(DataObject.name, "%-dummy_"))
q
.filter(Like(DataObject.name, "%-dummy_"))
.filter(Collection.name == collection)

@@ -837,5 +812,3 @@ .filter(DataObject.name != (filename + "-dummy8"))

if not reg_info:
self.skipTest(
"server is non-localhost and no common path exists for object registration"
)
self.skipTest("server is non-localhost and no common path exists for object registration")
(dir_, resc_option) = reg_info

@@ -862,5 +835,3 @@

(fd, encoded_test_file) = (
tempfile.mkstemp(
dir=dir_.encode("utf-8"), prefix=filename_prefix.encode("utf-8")
)
tempfile.mkstemp(dir=dir_.encode("utf-8"), prefix=filename_prefix.encode("utf-8"))
if sys.version_info >= (3, 5)

@@ -875,12 +846,6 @@ else python34_unicode_mkstemp(dir=dir_, prefix=filename_prefix)

self.sess.data_objects.register(test_file, obj_path, **resc_option)
results = list(
self.sess.query(DataObject, Collection.name).filter(
DataObject.path == test_file
)
)
results = list(self.sess.query(DataObject, Collection.name).filter(DataObject.path == test_file))
if results:
results = results[0]
result_logical_path = os.path.join(
results[Collection.name], results[DataObject.name]
)
result_logical_path = os.path.join(results[Collection.name], results[DataObject.name])
result_physical_path = results[DataObject.path]

@@ -910,5 +875,3 @@ self.assertEqual(result_logical_path, obj_path)

if "/" not in coll_path:
coll_path = "/{}/home/{}/{}".format(
self.session.zone, self.session.username, coll_path
)
coll_path = "/{}/home/{}/{}".format(self.session.zone, self.session.username, coll_path)
self.coll_path = coll_path

@@ -927,9 +890,6 @@ self.num_objects = num_objects

if self.nAVUs > 0:
# - set the AVUs on the collection's objects:
for data_obj_path in map(
lambda d: d[Collection.name] + "/" + d[DataObject.name],
self.session.query(*q_params).filter(
Collection.name == self.test_collection.path
),
self.session.query(*q_params).filter(Collection.name == self.test_collection.path),
):

@@ -944,5 +904,3 @@ data_obj = self.session.data_objects.get(data_obj_path)

# - The "with" statement receives, as context variable, a zero-arg function to build the query
return lambda: self.session.query(*q_params).filter(
Collection.name == self.test_collection.path
)
return lambda: self.session.query(*q_params).filter(Collection.name == self.test_collection.path)

@@ -965,6 +923,3 @@ def __exit__(self, *_): # - clean up after context block

with self.Issue_166_context(
self.sess, num_objects=self.More_than_one_batch
) as buildQuery:
with self.Issue_166_context(self.sess, num_objects=self.More_than_one_batch) as buildQuery:
for dummy_i in self.Iterate_to_exhaust_statement_table:

@@ -979,8 +934,4 @@ query = buildQuery()

with self.Issue_166_context(
self.sess, num_objects=self.More_than_one_batch
) as buildQuery:
with self.Issue_166_context(self.sess, num_objects=self.More_than_one_batch) as buildQuery:
for dummy_i in self.Iterate_to_exhaust_statement_table:
for dummy_row in buildQuery():

@@ -994,3 +945,2 @@ break # single iteration

) as buildQuery:
pages = [b for b in buildQuery().get_batches()]

@@ -1002,3 +952,2 @@ self.assertTrue(len(pages) > 2 and len(pages[0]) < self.More_than_one_batch)

for _ in self.Iterate_to_exhaust_statement_table:
for batch in buildQuery().get_batches():

@@ -1018,11 +967,7 @@ to_compare.append(batch)

Set1 = {Compare_Key(dct) for dct in to_compare[1]}
self.assertTrue(
len(Set0 & Set1) == 0
) # assert intersection is null set
self.assertTrue(len(Set0 & Set1) == 0) # assert intersection is null set
def test_paging_get_results__166(self):
with self.Issue_166_context(
self.sess, num_objects=self.More_than_one_batch
) as buildQuery:
with self.Issue_166_context(self.sess, num_objects=self.More_than_one_batch) as buildQuery:
batch_size = 0

@@ -1080,10 +1025,18 @@ for result_set in buildQuery().get_batches():

def test_negating_columns_in_genquery1_results__issue_755(self):
columns_to_negate = {Collection.map_id, DataObject.map_id, DataObject.status, DataObject.type, DataObject.collection_id}
columns_to_negate = {
Collection.map_id,
DataObject.map_id,
DataObject.status,
DataObject.type,
DataObject.collection_id,
}
columns_to_negate_D = columns_to_negate & set(DataObject._columns)
columns_to_negate_C = columns_to_negate & set(Collection._columns)
cases = { (Collection, DataObject): columns_to_negate,
(Collection,): columns_to_negate_C,
(DataObject,): columns_to_negate_D, }
cases = {
(Collection, DataObject): columns_to_negate,
(Collection,): columns_to_negate_C,
(DataObject,): columns_to_negate_D,
}
for requested,intersect in cases.items():
for requested, intersect in cases.items():
q = self.sess.query(*requested, *{-col for col in columns_to_negate}).limit(1)

@@ -1096,3 +1049,3 @@ row = list(q.all())[0]

# Remove the if/continue when irods/irods #8574 is resolved.
if self.sess.server_version > (5,0,0) and len(requested) > 1:
if self.sess.server_version > (5, 0, 0) and len(requested) > 1:
continue

@@ -1119,10 +1072,12 @@

# Test a query limit via configuration.
with config.loadlines(
entries=[dict(setting="genquery1.irods_query_limit", value=num_limited_results)]
):
limited_results = list(self.sess.query(DataObject.id).filter(Like(DataObject.name, '%issue_712_obj%')).all())
with config.loadlines(entries=[dict(setting="genquery1.irods_query_limit", value=num_limited_results)]):
limited_results = list(
self.sess.query(DataObject.id).filter(Like(DataObject.name, '%issue_712_obj%')).all()
)
self.assertEqual(num_limited_results, len(limited_results))
# Test the query limit is no longer in effect.
non_limited_results = list(self.sess.query(DataObject.id).filter(Like(DataObject.name, '%issue_712_obj%')).all())
non_limited_results = list(
self.sess.query(DataObject.id).filter(Like(DataObject.name, '%issue_712_obj%')).all()
)
self.assertEqual(num_total_objects, len(non_limited_results))

@@ -1135,3 +1090,2 @@ finally:

class TestSpecificQuery(unittest.TestCase):
def setUp(self):

@@ -1148,5 +1102,3 @@ super(TestSpecificQuery, self).setUp()

test_collection_size = 3 * MAX_SQL_ROWS
test_collection_path = "/{0}/home/{1}/test_collection".format(
self.session.zone, self.session.username
)
test_collection_path = "/{0}/home/{1}/test_collection".format(self.session.zone, self.session.username)
self.test_collection = helpers.make_test_collection(

@@ -1186,5 +1138,3 @@ self.session, test_collection_path, obj_count=test_collection_size

test_collection_size = 3 * MAX_SQL_ROWS
test_collection_path = "/{0}/home/{1}/test_collection".format(
self.session.zone, self.session.username
)
test_collection_path = "/{0}/home/{1}/test_collection".format(self.session.zone, self.session.username)
self.test_collection = helpers.make_test_collection(

@@ -1191,0 +1141,0 @@ self.session, test_collection_path, obj_count=test_collection_size

@@ -13,3 +13,2 @@ #! /usr/bin/env python

class TestResource(unittest.TestCase):
create_simple_resc_hierarchy = helpers.create_simple_resc_hierarchy

@@ -29,5 +28,6 @@ create_simple_resc = helpers.create_simple_resc

# Create two (passthru + storage) hierarchies below the root: ie. pt0;leaf0 and pt1;leaf1
with self.create_simple_resc_hierarchy(
pt + "_0", leaf + "_0"
), self.create_simple_resc_hierarchy(pt + "_1", leaf + "_1"):
with (
self.create_simple_resc_hierarchy(pt + "_0", leaf + "_0"),
self.create_simple_resc_hierarchy(pt + "_1", leaf + "_1"),
):
try:

@@ -47,12 +47,6 @@ # Adopt both passthru's as children under the main root (deferred) node.

hier_str += ";{}".format(resc.name)
self.assertEqual(
resc.parent_id, (None if n == 0 else parent_resc.id)
)
self.assertEqual(
resc.parent_name, (None if n == 0 else parent_resc.name)
)
self.assertEqual(resc.parent_id, (None if n == 0 else parent_resc.id))
self.assertEqual(resc.parent_name, (None if n == 0 else parent_resc.name))
self.assertEqual(resc.hierarchy_string, hier_str)
self.assertIs(
type(resc.hierarchy_string), str
) # type of hierarchy field is string.
self.assertIs(type(resc.hierarchy_string), str) # type of hierarchy field is string.
if resc.parent is None:

@@ -62,8 +56,4 @@ self.assertIs(resc.parent_id, None)

else:
self.assertIs(
type(resc.parent_id), int
) # type of a non-null id field is integer.
self.assertIs(
type(resc.parent_name), str
) # type of a non-null name field is string.
self.assertIs(type(resc.parent_id), int) # type of a non-null id field is integer.
self.assertIs(type(resc.parent_name), str) # type of a non-null name field is string.
parent_resc = resc

@@ -93,6 +83,3 @@ finally:

self.sess.data_objects.put(
small_file,
"{home}/{small_file}".format(**locals()),
return_data_object=True,
**put_opts
small_file, "{home}/{small_file}".format(**locals()), return_data_object=True, **put_opts
)

@@ -107,6 +94,3 @@ )

self.sess.data_objects.put(
large_file,
"{home}/{large_file}".format(**locals()),
return_data_object=True,
**put_opts
large_file, "{home}/{large_file}".format(**locals()), return_data_object=True, **put_opts
)

@@ -113,0 +97,0 @@ )

@@ -23,3 +23,3 @@ #! /usr/bin/env python

os.environ.get("PYTHON_RULE_ENGINE_INSTALLED", "*").lower()[:1] == "y",
"Test depends on server having Python-REP installed beyond the default options",
"Test depends on server having Python-REP installed (set PYTHON_RULE_ENGINE_INSTALLED=yes in environment).",
)

@@ -69,5 +69,3 @@

INPUT *object="{object_path}",*name="{attr_name}",*value="{attr_value}"
OUTPUT ruleExecOut""".format(
**locals()
)
OUTPUT ruleExecOut""".format(**locals())
)

@@ -156,3 +154,2 @@

for i in rule_instances_list:
if rule_dict:

@@ -179,9 +176,3 @@ rule_to_call = rule_dict[i]

len(err_hash),
len(
[
val
for val in err_hash.values()
if val[0].startswith("rule exec failed")
]
),
len([val for val in err_hash.values() if val[0].startswith("rule exec failed")]),
)

@@ -216,3 +207,3 @@ return err_hash

output="ruleExecOut",
**{key: val for key, val in kw.items() if key == "instance_name"}
**{key: val for key, val in kw.items() if key == "instance_name"},
)

@@ -244,5 +235,3 @@ output = rule.execute()

writeLine("{stream_name}","*value")
""".format(
**locals()
)
""".format(**locals())
)

@@ -260,9 +249,3 @@

myrule = Rule(
session,
body=rule_body,
params=input_params,
output=output_param,
**extra_options
)
myrule = Rule(session, body=rule_body, params=input_params, output=output_param, **extra_options)
output = myrule.execute()

@@ -367,5 +350,3 @@

INPUT *some_string="{some_string}",*some_other_string="{some_other_string}",*err_string="{err_string}"
OUTPUT ruleExecOut""".format(
**locals()
)
OUTPUT ruleExecOut""".format(**locals())
)

@@ -431,3 +412,3 @@

lines = self.lines_from_stdout_buf(output)
self.assertRegexpMatches(lines[0], r".*\[Hello world!\]")
self.assertRegex(lines[0], r".*\[Hello world!\]")

@@ -454,4 +435,4 @@ def test_rulefile_in_file_like_object_2__336(self):

lines = self.lines_from_stdout_buf(output)
self.assertRegexpMatches(lines[0], r"\[STRING\]\[\]")
self.assertRegexpMatches(lines[1], r"\[STRING\]\[\]")
self.assertRegex(lines[0], r"\[STRING\]\[\]")
self.assertRegex(lines[1], r"\[STRING\]\[\]")

@@ -465,4 +446,4 @@ r = Rule(

lines = self.lines_from_stdout_buf(output)
self.assertRegexpMatches(lines[0], r"\[INTEGER\]\[5\]")
self.assertRegexpMatches(lines[1], r"\[STRING\]\[A String\]")
self.assertRegex(lines[0], r"\[INTEGER\]\[5\]")
self.assertRegex(lines[1], r"\[STRING\]\[A String\]")

@@ -469,0 +450,0 @@

@@ -9,3 +9,3 @@ #!/usr/bin/env python

import argparse
import os

@@ -20,22 +20,77 @@ import sys

h = logging.StreamHandler()
f = logging.Formatter(
"%(asctime)s %(name)s-%(levelname)s [%(pathname)s %(lineno)d] %(message)s"
)
f = logging.Formatter("%(asctime)s %(name)s-%(levelname)s [%(pathname)s %(lineno)d] %(message)s")
h.setFormatter(f)
logger.addHandler(h)
parser = argparse.ArgumentParser()
# Load all tests in the current directory and run them
def abs_path(initial_dir, levels_up=0):
directory = initial_dir
while levels_up > 0:
levels_up -= 1
directory = os.path.join(directory, '..')
return os.path.abspath(directory)
# Load all tests in the current directory and run them.
if __name__ == "__main__":
# must set the path for the imported tests
sys.path.insert(0, os.path.abspath("../.."))
# Get path to script directory for test import and/or discovery.
script_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
loader = TestLoader()
suite = TestSuite(
loader.discover(start_dir=".", pattern="*_test.py", top_level_dir=".")
# Must set the path for the imported tests.
sys.path.insert(0, abs_path(script_dir, levels_up=2))
parser.add_argument('--tests', '-t', metavar='TESTS', dest='tests', nargs='+', help='List of tests to run.')
parser.add_argument(
'--environment_variable',
'-e',
metavar='ENVIRONMENT_VARIABLE',
dest='env_var',
type=str,
help='Name of environment variable name to scan for in reason strings when filtering skipped test names to be output.',
)
result = xmlrunner.XMLTestRunner(
verbosity=2, output="/tmp/python-irodsclient/test-reports"
).run(suite)
parser.add_argument(
'--output_tests_skipped',
'-s',
metavar='SKIPPED_TESTS_OUTPUT_FILENAME',
dest='skipped_tests_output_filename',
type=str,
help='Name of a file into which to write names of skipped tests.',
)
parser.add_argument(
'--tests_file',
'-f',
metavar='TESTS_FILE',
dest='tests_file',
help='Name of a file containing a list of tests to run.',
)
args = parser.parse_args()
if args.tests_file:
if args.tests:
print('Cannot specify both --tests and --tests_file', file=sys.stderr)
exit(2)
args.tests = filter(None, open(args.tests_file).read().split("\n"))
loader = TestLoader()
if args.tests:
suite = TestSuite(loader.loadTestsFromNames(args.tests))
else:
suite = TestSuite(loader.discover(start_dir=script_dir, pattern='*_test.py', top_level_dir=script_dir))
result = xmlrunner.XMLTestRunner(verbosity=2, output="/tmp/python-irodsclient/test-reports").run(suite)
if args.skipped_tests_output_filename:
with open(args.skipped_tests_output_filename, 'w') as skip_file:
do_output = lambda reason: (args.env_var in reason) if args.env_var else True
for testinfo, reason in result.skipped:
if do_output(reason):
print(testinfo.test_id, file=skip_file)
if result.wasSuccessful():

@@ -42,0 +97,0 @@ sys.exit(0)

@@ -11,5 +11,3 @@ from irods.test.helpers import make_session, home_collection

connections = session.pool.active | session.pool.idle
is_SSL = len(connections) > 0 and all(
isinstance(conn.socket, ssl.SSLSocket) for conn in connections
)
is_SSL = len(connections) > 0 and all(isinstance(conn.socket, ssl.SSLSocket) for conn in connections)
exit(0 if is_SSL else 1)

@@ -51,3 +51,2 @@ #! /usr/bin/env python

) as session:
# do something that connects to the server

@@ -78,3 +77,2 @@ session.users.get(self.admin.username)

) as session:
# do something that connects to the server

@@ -81,0 +79,0 @@ session.users.get(self.new_user)

#! /usr/bin/env python
import calendar
import datetime
import os
import sys
import tempfile
import time
import unittest
import time
import calendar
import irods.test.helpers as helpers
import tempfile
from irods.session import iRODSSession
import irods.exception as ex
import irods.keywords as kw
from irods.ticket import Ticket
from irods.models import TicketQuery, DataObject, Collection
from irods.models import Collection, DataObject, TicketQuery
from irods.session import iRODSSession
from irods.test import helpers
from irods.ticket import Ticket, ticket_iterator
# As with most of the modules in this test suite, session objects created via

@@ -26,5 +26,4 @@ # make_session() are implicitly agents of a rodsadmin unless otherwise indicated.

def gmtime_to_timestamp(gmt_struct):
return (
"{0.tm_year:04d}-{0.tm_mon:02d}-{0.tm_mday:02d}."
"{0.tm_hour:02d}:{0.tm_min:02d}:{0.tm_sec:02d}".format(gmt_struct)
return "{0.tm_year:04d}-{0.tm_mon:02d}-{0.tm_mday:02d}.{0.tm_hour:02d}:{0.tm_min:02d}:{0.tm_sec:02d}".format(
gmt_struct
)

@@ -35,5 +34,3 @@

my_userid = session.users.get(session.username).id
my_tickets = session.query(TicketQuery.Ticket).filter(
TicketQuery.Ticket.user_id == my_userid
)
my_tickets = session.query(TicketQuery.Ticket).filter(TicketQuery.Ticket.user_id == my_userid)
for res in my_tickets:

@@ -44,3 +41,2 @@ Ticket(session, result=res).delete()

class TestRodsUserTicketOps(unittest.TestCase):
def login(self, user):

@@ -65,4 +61,3 @@ return iRODSSession(

return [
"{}/{}".format(o[Collection.name], o[DataObject.name])
for o in sess.query(Collection.name, DataObject.name)
"{}/{}".format(o[Collection.name], o[DataObject.name]) for o in sess.query(Collection.name, DataObject.name)
]

@@ -80,2 +75,4 @@

self.skipTest("""Test runnable only by rodsadmin.""")
self.rods_admin_name = ses.username
self.host = ses.host

@@ -135,6 +132,3 @@ self.port = ses.port

alice_home_path = self.irods_homedir(alice, path_only=True)
ticket_strings = [
Ticket(alice).issue("read", alice_home_path).string
for _ in range(N_TICKETS)
]
ticket_strings = [Ticket(alice).issue("read", alice_home_path).string for _ in range(N_TICKETS)]

@@ -146,5 +140,3 @@ # As rodsadmin, use the ADMIN_KW flag to delete alice's tickets.

t[TicketQuery.Ticket.string]
for t in ses.query(TicketQuery.Ticket).filter(
TicketQuery.Owner.name == "alice"
)
for t in ses.query(TicketQuery.Ticket).filter(TicketQuery.Owner.name == "alice")
]

@@ -156,5 +148,3 @@ self.assertEqual(len(alices_tickets), N_TICKETS)

t[TicketQuery.Ticket.string]
for t in ses.query(TicketQuery.Ticket).filter(
TicketQuery.Owner.name == "alice"
)
for t in ses.query(TicketQuery.Ticket).filter(TicketQuery.Owner.name == "alice")
]

@@ -185,8 +175,4 @@ self.assertEqual(len(alices_tickets), 0)

]
t1.modify(
"expire", later_ts
) # - Specify expiry with the human readable timestamp.
t2.modify(
"expire", later_epoch
) # - Specify expiry formatted as epoch seconds.
t1.modify("expire", later_ts) # - Specify expiry with the human readable timestamp.
t2.modify("expire", later_epoch) # - Specify expiry formatted as epoch seconds.

@@ -202,7 +188,3 @@ # Check normal access succeeds prior to expiration

for ticket_string in tickets:
t = (
ses.query(TicketQuery.Ticket)
.filter(TicketQuery.Ticket.string == ticket_string)
.one()
)
t = ses.query(TicketQuery.Ticket).filter(TicketQuery.Ticket.string == ticket_string).one()
timestamps.append(t[TicketQuery.Ticket.expiry_ts])

@@ -219,5 +201,3 @@ self.assertEqual(len(timestamps), 2)

Expected_Exception = (
ex.CAT_TICKET_EXPIRED
if ses.server_version >= (4, 2, 9)
else ex.SYS_FILE_DESC_OUT_OF_RANGE
ex.CAT_TICKET_EXPIRED if ses.server_version >= (4, 2, 9) else ex.SYS_FILE_DESC_OUT_OF_RANGE
)

@@ -227,10 +207,6 @@

for ticket_string in tickets:
with self.login(
self.alice
) as alice, tempfile.NamedTemporaryFile() as f:
with self.login(self.alice) as alice, tempfile.NamedTemporaryFile() as f:
Ticket(alice, ticket_string).supply()
with self.assertRaises(Expected_Exception):
alice.data_objects.get(
dobj.path, f.name, **{kw.FORCE_FLAG_KW: ""}
)
alice.data_objects.get(dobj.path, f.name, **{kw.FORCE_FLAG_KW: ""})

@@ -258,6 +234,3 @@ finally:

# Create 'R' and 'W' in alice's home collection.
data_objs = [
helpers.make_object(alice, home.path + "/" + name, content="abcxyz")
for name in ("R", "W")
]
data_objs = [helpers.make_object(alice, home.path + "/" + name, content="abcxyz") for name in ("R", "W")]
tickets = {

@@ -289,15 +262,11 @@ "R": Ticket(alice).issue("read", home.path + "/R"),

# Test upload was successful, by getting and confirming contents.
with self.login(
self.bob
) as bob: # This check must be in a new session or we get CollectionDoesNotExist. - Possibly a new issue [ ]
with (
self.login(self.bob) as bob
): # This check must be in a new session or we get CollectionDoesNotExist. - Possibly a new issue [ ]
for name in ("R", "W"):
bob.cleanup() # clear out existing connections
Ticket(bob, tickets[name].string).supply()
bob.data_objects.get(
home.path + "/" + name, rw_names[name], **{kw.FORCE_FLAG_KW: ""}
)
bob.data_objects.get(home.path + "/" + name, rw_names[name], **{kw.FORCE_FLAG_KW: ""})
with open(rw_names[name], "r") as tmpread:
self.assertEqual(
tmpread.read(), "abcxyz" if name == "R" else "hello"
)
self.assertEqual(tmpread.read(), "abcxyz" if name == "R" else "hello")
finally:

@@ -324,6 +293,3 @@ for t in tickets.values():

# Create 'x' and 'y' in alice's home collection
data_objs = [
helpers.make_object(alice, home.path + "/" + name, content="abcxyz")
for name in ("x", "y")
]
data_objs = [helpers.make_object(alice, home.path + "/" + name, content="abcxyz") for name in ("x", "y")]

@@ -339,5 +305,3 @@ with self.login(self.bob) as bob:

tmpfiles += [tmpf]
bob.data_objects.get(
home.path + "/" + name, tmpf.name, **{kw.FORCE_FLAG_KW: ""}
)
bob.data_objects.get(home.path + "/" + name, tmpf.name, **{kw.FORCE_FLAG_KW: ""})
with open(tmpf.name, "r") as tmpread:

@@ -359,5 +323,3 @@ self.assertEqual(tmpread.read(), "abcxyz")

tmpfiles += [tmpf]
bob.data_objects.get(
home.path + "/x", tmpf.name, **{kw.FORCE_FLAG_KW: ""}
)
bob.data_objects.get(home.path + "/x", tmpf.name, **{kw.FORCE_FLAG_KW: ""})
with open(tmpf.name, "r") as tmpread:

@@ -378,5 +340,23 @@ self.assertEqual(tmpread.read(), "abcxyz")

def test_modify_time_and_create_time_attributes_in_tickets__issue_801(self):
# Specifically we are testing that 'modify_time' and 'create_time' attributes function as expected,
bobs_ticket = None
try:
with self.login(self.bob) as bob:
bobs_ticket = Ticket(bob).issue('write', helpers.home_collection(bob))
time.sleep(4)
bobs_ticket.modify('add', 'user', self.rods_admin_name)
# Reload the ticket, this time with the full complement of attributes present.
bobs_ticket = next(ticket_iterator(bob, filter_args=[TicketQuery.Ticket.string == bobs_ticket.string]))
self.assertGreater(bobs_ticket.modify_time, bobs_ticket.create_time + datetime.timedelta(seconds=2))
finally:
if bobs_ticket:
bobs_ticket.delete()
class TestTicketOps(unittest.TestCase):
def setUp(self):

@@ -476,3 +456,21 @@ """Create objects for test"""

def test_ticket_iterator__issue_120(self):
ses = self.sess
t = None
try:
# t first assigned as a "utility" Ticket object
t = Ticket(ses).issue('read', helpers.home_collection(ses))
# This time, t receives attributes from a query result: notably the id, which we use for the next test.
t = Ticket(ses, result=ses.query(TicketQuery.Ticket).filter(TicketQuery.Ticket.string == t.string).one())
# Check an id attribute is present and listed in the results from list_tickets
self.assertIn(t.id, (ticket.id for ticket in ticket_iterator(ses)))
finally:
if t:
t.delete()
if __name__ == "__main__":

@@ -479,0 +477,0 @@ # let the tests find the parent irods lib

@@ -20,5 +20,3 @@ #! /usr/bin/env python

UNICODE_TEST_FILE = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "unicode_sampler.xml"
)
UNICODE_TEST_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "unicode_sampler.xml")

@@ -63,8 +61,5 @@

class TestUnicodeNames(unittest.TestCase):
def setUp(self):
self.sess = helpers.make_session()
self.coll_path = "/{}/home/{}/test_dir".format(
self.sess.zone, self.sess.username
)
self.coll_path = "/{}/home/{}/test_dir".format(self.sess.zone, self.sess.username)

@@ -98,5 +93,3 @@ # make list of unicode filenames, from file

# Query for all files in test collection
query = self.sess.query(DataObject.name, Collection.name).filter(
Collection.name == self.coll_path
)
query = self.sess.query(DataObject.name, Collection.name).filter(Collection.name == self.coll_path)

@@ -112,5 +105,3 @@ # Python2 compatibility note: In keeping with the principle of least surprise, we now ensure

# fyi
logger.info(
"{0}/{1}".format(result[Collection.name], result[DataObject.name])
)
logger.info("{0}/{1}".format(result[Collection.name], result[DataObject.name]))

@@ -117,0 +108,0 @@ # remove from set

@@ -50,3 +50,2 @@ #! /usr/bin/env python

class TestUserAndGroup(unittest.TestCase):
def setUp(self):

@@ -116,5 +115,3 @@ self.sess = helpers.make_session()

ENV_DIR = tempfile.mkdtemp()
d = dict(
password=OLDPASS, user="alice", host=ses.host, port=ses.port, zone=ses.zone
)
d = dict(password=OLDPASS, user="alice", host=ses.host, port=ses.port, zone=ses.zone)
(alice_env, alice_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d)

@@ -128,5 +125,3 @@ try:

True,
lambda: helpers.make_session(
irods_env_file=alice_env, irods_authentication_file=alice_auth
),
lambda: helpers.make_session(irods_env_file=alice_env, irods_authentication_file=alice_auth),
),

@@ -137,10 +132,6 @@ ]:

alice = alice_ses.users.get(alice_ses.username)
alice.modify_password(
OLDPASS, NEWPASS, modify_irods_authentication_file=modify_option
)
alice.modify_password(OLDPASS, NEWPASS, modify_irods_authentication_file=modify_option)
d["password"] = NEWPASS
with iRODSSession(**d) as session:
self.do_something(
session
) # can we still do stuff with the final value of the password?
self.do_something(session) # can we still do stuff with the final value of the password?
finally:

@@ -160,5 +151,3 @@ shutil.rmtree(ENV_DIR)

# Test different combinations of new and old password lengths
tuples_of_old_and_new_password = [
("a" * x, "b" * y) for x in pw_lengths for y in pw_lengths
]
tuples_of_old_and_new_password = [("a" * x, "b" * y) for x in pw_lengths for y in pw_lengths]
ses.users.create("alice", "rodsuser")

@@ -261,12 +250,6 @@

)
(alice_env, alice_auth) = helpers.make_environment_and_auth_files(
ENV_DIR, **d
)
(alice_env, alice_auth) = helpers.make_environment_and_auth_files(ENV_DIR, **d)
session_factories = [
(lambda: iRODSSession(**d)),
(
lambda: helpers.make_session(
irods_env_file=alice_env, irods_authentication_file=alice_auth
)
),
(lambda: helpers.make_session(irods_env_file=alice_env, irods_authentication_file=alice_auth)),
]

@@ -296,5 +279,3 @@ for factory in session_factories:

self.assertEqual(group.name, group_name)
self.assertEqual(
repr(group), "<iRODSGroup {0} {1}>".format(group.id, group_name)
)
self.assertEqual(repr(group), "<iRODSGroup {0} {1}>".format(group.id, group_name))

@@ -401,12 +382,5 @@ # delete group

result = (
self.sess.query(UserMeta, Group)
.filter(Group.name == group_name, UserMeta.name == "key")
.one()
)
result = self.sess.query(UserMeta, Group).filter(Group.name == group_name, UserMeta.name == "key").one()
self.assertTrue(
[result[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)]
== triple
)
self.assertTrue([result[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] == triple)

@@ -430,5 +404,3 @@ finally:

user.metadata["key0"] = iRODSMeta("key0", "value", "units")
sorted_triples = sorted(
[["key1", "value0", "units0"], ["key1", "value1", "units1"]]
)
sorted_triples = sorted([["key1", "value0", "units0"], ["key1", "value1", "units1"]])
for m in sorted_triples:

@@ -438,21 +410,11 @@ user.metadata.add(iRODSMeta(*m))

# general query gives the right results?
result_0 = (
self.sess.query(UserMeta, User)
.filter(User.name == user_name, UserMeta.name == "key0")
.one()
)
result_0 = self.sess.query(UserMeta, User).filter(User.name == user_name, UserMeta.name == "key0").one()
self.assertTrue(
[result_0[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)]
== ["key0", "value", "units"]
[result_0[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] == ["key0", "value", "units"]
)
results_1 = self.sess.query(UserMeta, User).filter(
User.name == user_name, UserMeta.name == "key1"
)
results_1 = self.sess.query(UserMeta, User).filter(User.name == user_name, UserMeta.name == "key1")
retrieved_triples = [
[res[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)]
for res in results_1
]
retrieved_triples = [[res[k] for k in (UserMeta.name, UserMeta.value, UserMeta.units)] for res in results_1]

@@ -530,5 +492,3 @@ self.assertTrue(sorted_triples == sorted(retrieved_triples))

# Generate a random password.
ga_password = helpers.unique_name(
helpers.my_function_name(), datetime.datetime.now()
)[:MAX_PASSWORD_LENGTH]
ga_password = helpers.unique_name(helpers.my_function_name(), datetime.datetime.now())[:MAX_PASSWORD_LENGTH]

@@ -557,5 +517,3 @@ # Create a groupadmin user with that password, and a session object for logging in as that user.

alice.modify("password", "apass")
groupadmin, groupadmin_session = self.create_groupadmin_user_and_session(
GROUP_ADMIN
)
groupadmin, groupadmin_session = self.create_groupadmin_user_and_session(GROUP_ADMIN)

@@ -581,6 +539,3 @@ # As the groupadmin:

# Check that our members got removed.
self.assertFalse(
set(("alice", GROUP_ADMIN))
& set(member.name for member in lab.members)
)
self.assertFalse(set(("alice", GROUP_ADMIN)) & set(member.name for member in lab.members))
finally:

@@ -598,5 +553,3 @@ if groupadmin:

if admin.server_version < (4, 2, 12) or admin.server_version == (4, 3, 0):
self.skipTest(
"Password initialization is broken before iRODS 4.2.12, and in 4.3.0"
)
self.skipTest("Password initialization is broken before iRODS 4.2.12, and in 4.3.0")
rodsuser = groupadmin = None

@@ -607,5 +560,3 @@ rodsuser_name = "bob"

# Create a groupadmin.
groupadmin, groupadmin_session = self.create_groupadmin_user_and_session(
"groupadmin_428"
)
groupadmin, groupadmin_session = self.create_groupadmin_user_and_session("groupadmin_428")

@@ -615,5 +566,3 @@ # Use the groupadmin to create a new user initialized with a known password; then, test the

with groupadmin_session:
rodsuser = groupadmin_session.users.create_with_password(
rodsuser_name, rodsuser_password
)
rodsuser = groupadmin_session.users.create_with_password(rodsuser_name, rodsuser_password)
with iRODSSession(

@@ -626,5 +575,3 @@ user=rodsuser_name,

) as rodsuser_session:
rodsuser_session.collections.get(
helpers.home_collection(rodsuser_session)
)
rodsuser_session.collections.get(helpers.home_collection(rodsuser_session))
finally:

@@ -722,5 +669,3 @@ if groupadmin:

) as user_sess:
test_object = user_sess.data_objects.create(
"/tempZone/home/public/bob_file_testing_group_quota"
)
test_object = user_sess.data_objects.create("/tempZone/home/public/bob_file_testing_group_quota")
with test_object.open("w") as f:

@@ -746,5 +691,3 @@ f.write(b"_" * my_object_size)

if self.sess.server_version >= (4, 3):
self.skipTest(
"iRODS servers 4.3.0 and higher have dropped user quotas in favor of group quotas."
)
self.skipTest("iRODS servers 4.3.0 and higher have dropped user quotas in favor of group quotas.")
ses = self.sess

@@ -763,5 +706,3 @@ test_object = None

) as user_sess:
test_object = user_sess.data_objects.create(
"/tempZone/home/public/bobfile"
)
test_object = user_sess.data_objects.create("/tempZone/home/public/bobfile")
with test_object.open("w") as f:

@@ -786,5 +727,3 @@ f.write(b"_" * 1000)

with self.assertRaises(client_init.irodsA_already_exists):
client_init.write_native_credentials_to_secrets_file(
"somevalue", overwrite=False
)
client_init.write_native_credentials_to_secrets_file("somevalue", overwrite=False)

@@ -791,0 +730,0 @@ # Assert the auth file's contents haven't changed.

@@ -16,3 +16,2 @@ #! /usr/bin/env python

class TestRemoteZone(unittest.TestCase):
def setUp(self):

@@ -37,11 +36,7 @@ self.sess = helpers.make_session()

for result in session.query(Collection).filter(
Collection.owner_name == zBuser.name
and Collection.owner_zone == zBuser.zone
Collection.owner_name == zBuser.name and Collection.owner_zone == zBuser.zone
)
]
self.assertEqual(
[
(u[User.name], u[User.zone])
for u in session.query(User).filter(User.zone == A_ZONE_NAME)
],
[(u[User.name], u[User.zone]) for u in session.query(User).filter(User.zone == A_ZONE_NAME)],
[(A_ZONE_USER, A_ZONE_NAME)],

@@ -63,3 +58,3 @@ )

zone = None
users= []
users = []
test_zone = "remote_zone"

@@ -70,8 +65,4 @@ # TODO(#763): remove user name randomization.

zone = self.sess.zones.create(test_zone, "remote")
users.append(
self.sess.users.create(test_user, "rodsuser", user_zone=test_zone)
)
users.append(
self.sess.users.create(test_user, "rodsuser", user_zone="")
)
users.append(self.sess.users.create(test_user, "rodsuser", user_zone=test_zone))
users.append(self.sess.users.create(test_user, "rodsuser", user_zone=""))
self.assertEqual(2, len(list(self.sess.query(User).filter(User.name == test_user))))

@@ -84,2 +75,3 @@ finally:

if __name__ == "__main__":

@@ -86,0 +78,0 @@ # let the tests find the parent irods lib

@@ -1,15 +0,14 @@

from irods.api_number import api_number
from irods.message import iRODSMessage, TicketAdminRequest
from irods.models import TicketQuery
import calendar
import contextlib
import datetime
import random
import string
import logging
import datetime
import calendar
from typing import Any, Optional, Type, Union # noqa: UP035
from irods.api_number import api_number
from irods.column import Column
from irods.message import TicketAdminRequest, iRODSMessage
from irods.models import TicketQuery
logger = logging.getLogger(__name__)
def get_epoch_seconds(utc_timestamp):

@@ -31,16 +30,59 @@ epoch = None

def ticket_iterator(session, filter_args=()):
"""
Enumerate the Tickets visible to the user.
Args:
session: an iRODSSession object with which to perform a query.
filter_args: optional arguments for filtering the query.
Returns:
An iterator over a range of Ticket objects.
"""
return (Ticket(session, result=row) for row in session.query(TicketQuery.Ticket).filter(*filter_args))
_COLUMN_KEY = Union[Column, Type[Column]] # noqa: UP006
class Ticket:
def __init__(self, session, ticket="", result=None, allow_punctuation=False):
def __init__(self, session, ticket="", result: Optional[dict[_COLUMN_KEY, Any]] = None, allow_punctuation=False): # noqa: FA100
"""
Initialize a Ticket object. If no 'result' or 'ticket' string is provided, then generate a new
Ticket string automatically.
Args:
session: an iRODSSession object through which API endpoints shall be called.
ticket: an optional ticket string, if a particular one is desired for ticket creation or deletion.
result: a row result from a query, containing at least the columns of irods.models.TicketQuery.Ticket.
allow_punctuation: True if punctuation characters are to be allowed in generating a Ticket string.
(By default, all characters will be digits or letters of the latin alphabet.)
Raises:
RuntimeError: if the given ticket parameter mismatches the result, or if result is of the wrong type.
"""
self._session = session
# Do an initial error and sanity check on result.
try:
if result is not None:
ticket = result[TicketQuery.Ticket.string]
except TypeError:
raise RuntimeError(
"If specified, 'result' parameter must be a TicketQuery.Ticket search result"
)
self._ticket = (
ticket if ticket else self._generate(allow_punctuation=allow_punctuation)
)
_ticket = result[TicketQuery.Ticket.string]
except (TypeError, KeyError) as exc:
raise RuntimeError("If specified, 'result' parameter must be a TicketQuery.Ticket query result.") from exc
# Process query result if given, and set object attributes from it.
if result is not None:
if _ticket != ticket != "":
raise RuntimeError("A ticket name was specified but does not match the query result.")
ticket = _ticket
for attr, value in TicketQuery.Ticket.__dict__.items():
if value is TicketQuery.Ticket.string:
continue
if not attr.startswith("_"):
# backward compatibility with older schema versions
with contextlib.suppress(KeyError):
setattr(self, attr, result[value])
self._ticket = ticket if ticket else self._generate(allow_punctuation=allow_punctuation)
@property

@@ -62,5 +104,3 @@ def session(self):

source_characters += string.punctuation
return "".join(
random.SystemRandom().choice(source_characters) for _ in range(length)
)
return "".join(random.SystemRandom().choice(source_characters) for _ in range(length))

@@ -75,5 +115,3 @@ def _api_request(self, cmd_string, *args, **opts):

message_body = TicketAdminRequest(cmd_string, ticket_string, *args, **opts)
message = iRODSMessage(
"RODS_API_REQ", msg=message_body, int_info=api_number["TICKET_ADMIN_AN"]
)
message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["TICKET_ADMIN_AN"])
conn_.send(message)

@@ -80,0 +118,0 @@ response = conn_.recv()

@@ -13,3 +13,2 @@ from irods.models import User, Group, UserAuth

class iRODSUser:
def remove_quota(self, resource="total"):

@@ -29,8 +28,4 @@ self.manager.remove_quota(self.name, resource=resource)

self.zone = result[User.zone]
self._comment = result.get(
User.comment, _Not_Defined
) # these not needed in results for object ident,
self._info = result.get(
User.info, _Not_Defined
) # so we fetch lazily via a property
self._comment = result.get(User.comment, _Not_Defined) # these not needed in results for object ident,
self._info = result.get(User.info, _Not_Defined) # so we fetch lazily via a property
self._meta = None

@@ -41,5 +36,3 @@

if self._comment == _Not_Defined:
query = self.manager.sess.query(User.id, User.comment).filter(
User.id == self.id
)
query = self.manager.sess.query(User.id, User.comment).filter(User.id == self.id)
self._comment = query.one()[User.comment]

@@ -51,5 +44,3 @@ return self._comment

if self._info == _Not_Defined:
query = self.manager.sess.query(User.id, User.info).filter(
User.id == self.id
)
query = self.manager.sess.query(User.id, User.info).filter(User.id == self.id)
self._info = query.one()[User.info]

@@ -60,5 +51,3 @@ return self._info

def dn(self):
query = self.manager.sess.query(UserAuth.user_dn).filter(
UserAuth.user_id == self.id
)
query = self.manager.sess.query(UserAuth.user_dn).filter(UserAuth.user_id == self.id)
return [res[UserAuth.user_dn] for res in query]

@@ -69,10 +58,6 @@

if not self._meta:
self._meta = iRODSMetaCollection(
self.manager.sess.metadata, User, self.name
)
self._meta = iRODSMetaCollection(self.manager.sess.metadata, User, self.name)
return self._meta
def modify_password(
self, old_value, new_value, modify_irods_authentication_file=False
):
def modify_password(self, old_value, new_value, modify_irods_authentication_file=False):
self.manager.modify_password(

@@ -98,3 +83,2 @@ old_value,

class iRODSGroup:
type = "rodsgroup"

@@ -128,5 +112,3 @@

if not self._meta:
self._meta = iRODSMetaCollection(
self.manager.sess.metadata, User, self.name
)
self._meta = iRODSMetaCollection(self.manager.sess.metadata, User, self.name)
return self._meta

@@ -133,0 +115,0 @@

import os
__version__ = "3.2.0"
__version__ = "3.3.0"

@@ -5,0 +5,0 @@

@@ -5,3 +5,2 @@ from irods.models import Zone

class iRODSZone:
def __init__(self, manager, result=None):

@@ -8,0 +7,0 @@ """Construct an iRODSZone object."""

@@ -5,3 +5,3 @@ CHANGELOG.md

README.md
setup.cfg
pyproject.toml
setup.py

@@ -67,2 +67,3 @@ irods/__init__.py

irods/test/cleanup_functions_test.py
irods/test/client_configuration_test.py
irods/test/client_hints_test.py

@@ -79,6 +80,8 @@ irods/test/collection_test.py

irods/test/library_features_test.py
irods/test/login_auth_test_1.py
irods/test/login_auth_test_2.py
irods/test/login_auth_test_must_run_manually.py
irods/test/message_test.py
irods/test/meta_test.py
irods/test/pam_interactive_test.py
irods/test/pam_interactive_test_must_run_manually.py
irods/test/pool_test.py

@@ -89,3 +92,3 @@ irods/test/query_test.py

irods/test/runner.py
irods/test/setupssl.py
irods/test/setup_ssl.py
irods/test/ssl_test_client.py

@@ -103,3 +106,6 @@ irods/test/temp_password_test.py

irods/test/modules/test_saving_and_loading_of_settings__issue_471.py
irods/test/modules/test_signal_handling_in_multithread_get.py
irods/test/modules/test_signal_handling_in_multithread_put.py
irods/test/modules/test_xml_parser.py
irods/test/modules/tools.py
irods/test/test-data/irods_environment.json

@@ -112,2 +118,3 @@ irods/test/test-data/irods_environment_negative_refresh_field.json

python_irodsclient.egg-info/requires.txt
python_irodsclient.egg-info/top_level.txt
python_irodsclient.egg-info/top_level.txt
venv313/bin/prc_write_irodsA.py

@@ -0,1 +1,4 @@

build
htmlcov
irods
venv313

@@ -1,4 +0,1 @@

[metadata]
description_file = README.md
[egg_info]

@@ -5,0 +2,0 @@ tag_build =

@@ -1,5 +0,4 @@

from setuptools import setup, find_packages
import codecs
import os
from setuptools import setup

@@ -12,43 +11,4 @@ # Get package version

# Get description
with codecs.open("README.md", "r", "utf-8") as file:
long_description = file.read()
setup(
name="python-irodsclient",
version=version["__version__"],
author="iRODS Consortium",
author_email="support@irods.org",
description="A python API for iRODS",
long_description=long_description,
long_description_content_type="text/markdown",
license="BSD",
url="https://github.com/irods/python-irodsclient",
keywords="irods",
classifiers=[
"License :: OSI Approved :: BSD License",
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: POSIX :: Linux",
],
packages=find_packages(),
include_package_data=True,
install_requires=[
"PrettyTable>=0.7.2",
"defusedxml",
"jsonpointer",
"jsonpatch",
],
extras_require={"tests": ["unittest-xml-reporting", # for xmlrunner
"types-defusedxml", # for type checking
"progressbar", # for type checking
"types-tqdm"] # for type checking
},
scripts=["irods/prc_write_irodsA.py"],
)
import unittest
import os
import json
from irods.client_init import write_pam_interactive_irodsA_file
from unittest.mock import patch
from irods.auth import ClientAuthError, FORCE_PASSWORD_PROMPT
from irods.auth.pam_interactive import (
_pam_interactive_ClientAuthState,
PERFORM_WAITING,
PERFORM_AUTHENTICATED,
PERFORM_NEXT,
)
from irods.test.helpers import make_session
from irods.auth.pam_interactive import __NEXT_OPERATION__, __FLOW_COMPLETE__
from irods.auth.pam_interactive import _auth_api_request
class PamInteractiveTest(unittest.TestCase):
def setUp(self):
# These tests assume the irods_environment.json file is set up correctly
# and that the iRODS user 'alice' exists in the 'tempZone' zone and has the password 'rods'.
# There should also be a linux user 'alice' with the same password.
self.sess = None
self.auth_client = _pam_interactive_ClientAuthState(None, None, scheme="pam_interactive")
self.env_file_path = os.path.expanduser("~/.irods/irods_environment.json")
self.auth_file_path = os.path.expanduser("~/.irods/.irodsA")
with open(self.env_file_path) as f:
env = json.load(f)
self.user = env.get("irods_user_name", "alice")
self.zone = env.get("irods_zone_name", "tempZone")
self.password = "rods"
def tearDown(self):
if self.sess:
self.sess.cleanup()
if os.path.exists(self.auth_file_path):
os.remove(self.auth_file_path)
def test_pam_interactive_login_basic(self):
with patch("getpass.getpass", return_value=self.password):
self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive")
# Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded
home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}")
self.assertEqual(home.name, self.sess.username)
def test_pam_interactive_auth_file_creation(self):
with patch("getpass.getpass", return_value=self.password):
write_pam_interactive_irodsA_file(env_file=self.env_file_path)
self.assertTrue(os.path.exists(self.auth_file_path), ".irodsA file was not created")
with patch("getpass.getpass", return_value=self.password) as mock_getpass:
self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme= "pam_interactive")
# Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded
home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}")
self.assertEqual(home.name, self.sess.username)
mock_getpass.assert_not_called()
def test_forced_interactive_flow(self):
with patch("getpass.getpass", return_value=self.password):
write_pam_interactive_irodsA_file(env_file=self.env_file_path)
self.assertTrue(os.path.exists(self.auth_file_path), ".irodsA file was not created")
with patch("getpass.getpass", return_value=self.password) as mock_getpass:
self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive")
self.sess.set_auth_option_for_scheme("pam_interactive", FORCE_PASSWORD_PROMPT, True)
# Creating a session does not trigger auth, so the home collection is accessed to trigger and confirm auth succeeded
home = self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}")
self.assertEqual(home.name, self.sess.username)
mock_getpass.assert_called_once()
def test_failed_login_incorrect_password(self):
with patch("getpass.getpass", return_value="wrong_password"):
with self.assertRaises(ClientAuthError):
self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive")
self.sess.collections.get(f"/{self.sess.zone}/home/{self.sess.username}") # trigger auth flow
with patch("getpass.getpass", return_value="wrong_password"):
with self.assertRaises(ClientAuthError):
write_pam_interactive_irodsA_file(env_file=self.env_file_path)
def test_get_default_value(self):
test_cases = [
("simple_path", {"msg": {"default_path": "/username"}, "pstate": {"username": "alice"}}, "alice"),
("nested_path", {"msg": {"default_path": "/user/name"}, "pstate": {"user": {"name": "alice"}}}, "alice"),
("path_does_not_exist", {"msg": {"default_path": "/user/username"}, "pstate": {"username": "alice"}}, ""),
("non_string_value", {"msg": {"default_path": "/user/id"}, "pstate": {"user": {"id": 123}}}, "123"),
("no_default_path", {"msg": {}, "pstate": {"username": "alice"}}, ""),
]
for name, request, expected in test_cases:
with self.subTest(name=name):
self.assertEqual(self.auth_client._get_default_value(request), expected)
def test_patch_state(self):
test_cases = [
("add_op", {"msg": {"patch": [{"op": "add", "path": "/username", "value": "alice"}]}, "pstate": {}}, {"username": "alice"}, True),
("replace_op", {"msg": {"patch": [{"op": "replace", "path": "/username", "value": "rods"}]}, "pstate": {"username": "alice"}}, {"username": "rods"}, True),
("remove_op", {"msg": {"patch": [{"op": "remove", "path": "/username"}]}, "pstate": {"username": "rods"}}, {}, True),
("add_resp_fallback", {"msg": {"patch": [{"op": "add", "path": "/username"}]}, "pstate": {}, "resp": "alice"}, {"username": "alice"}, True),
("replace_resp_fallback", {"msg": {"patch": [{"op": "replace", "path": "/username"}]}, "pstate": {"username": "rods"}, "resp": "alice"}, {"username": "alice"}, True),
("nested_add_operation", {'msg': {'patch': [{'op': 'add', 'path': '/user/name', 'value': 'alice'}]}, 'pstate': {'user': {}}}, {'user': {'name': 'alice'}}, True),
("resp_fallback_empty", {'msg': {'patch': [{'op': 'add', 'path': '/username'}]}, 'pstate': {}}, {'username': ''}, True),
("no_patch_ops", {"msg": {}, "pstate": {"username": "alice"}, "pdirty": False}, {"username": "alice"}, False)
]
for name, request, expected_pstate, expected_pdirty in test_cases:
with self.subTest(name=name):
self.auth_client._patch_state(request)
self.assertEqual(request["pstate"], expected_pstate)
self.assertEqual(request["pdirty"], expected_pdirty)
def test_retrieve_entry(self):
test_cases = [
("surface_value", {"msg": {"retrieve": "/user"}, "pstate": {"user": "alice"}}, True, "alice"),
("nested_value", {"msg": {"retrieve": "/user/password"}, "pstate": {"user": {"password": "rods"}}}, True, "rods"),
("empty_value", {"msg": {"retrieve": "/user"}, "pstate": {"user": ""}}, True, ""),
("path_does_not_exist", {"msg": {"retrieve": "/missing"}, "pstate": {"user": "alice"}}, True, ""),
("non_string_value", {'msg': {'retrieve': '/user/id'}, 'pstate': {'user': {'id': 456}}}, True, '456'),
("no_retrieve_key", {"msg": {}, "pstate": {"user": "alice"}}, False, None)
]
for name, request, expected_result, expected_resp in test_cases:
with self.subTest(name=name):
self.assertEqual(self.auth_client._retrieve_entry(request), expected_result)
if expected_result:
self.assertEqual(request["resp"], expected_resp)
@patch('irods.auth.pam_interactive._auth_api_request', return_value={"result": "ok"})
@patch('sys.stderr.write')
def test_get_input(self, mock_stderr, mock_api_request):
test_cases = [
("non_password_input", False, 'sys.stdin.readline', "rods\n", {"msg": {"prompt": "Prompt:"}}, "rods"),
("non_password_default", False, 'sys.stdin.readline', "\n", {"msg": {"prompt": "Prompt:", "default_path": "/password"}, "pstate": {"password": "rods"}}, "rods"),
("password_input", True, 'getpass.getpass', "rods", {"msg": {"prompt": "Password:"}}, "rods"),
("password_default", True, 'getpass.getpass', "", {"msg": {"prompt": "Password:", "default_path": "/password"}, "pstate": {"password": "rods"}}, "rods")
]
for name, is_password, mock_target, user_input, request, expected_resp in test_cases:
with self.subTest(name=name), patch(mock_target, return_value=user_input), patch.object(self.auth_client, '_patch_state') as mock_patch:
resp = self.auth_client._get_input(request, is_password=is_password)
self.assertEqual(request["resp"], expected_resp)
self.assertEqual(resp, {"result": "ok"})
mock_patch.assert_called_once()
def test_pass_through_states(self):
with patch("irods.auth.pam_interactive._auth_api_request", return_value={"result": "ok"}):
request = {"msg": {"prompt": "Prompt:"}, "pstate": {}, "pdirty": False}
for state in [self.auth_client.next, self.auth_client.running, self.auth_client.ready, self.auth_client.response]:
with self.subTest(state=state.__name__):
resp = state(request)
self.assertEqual(resp, {"result": "ok"})
def test_failure_states(self):
request = {"foo": "bar"}
with patch("irods.auth.pam_interactive._logger"):
for state in [self.auth_client.error, self.auth_client.timeout, self.auth_client.not_authenticated]:
with self.subTest(state=state.__name__):
resp = state(request)
self.assertEqual(resp[__NEXT_OPERATION__], __FLOW_COMPLETE__)
self.assertEqual(self.auth_client.loggedIn, 0)
@patch("sys.stdin.readline", return_value="ABC123\n")
def test_pam_interactive_mfa_flow(self, mock_stdin):
state = {"stage": "before_mfa", "step": 0}
def mock_server(conn, req):
# Switch from the real server to the mock server when the password step is completed
if state["stage"] == "before_mfa":
resp = _auth_api_request(conn, req)
if req.get(__NEXT_OPERATION__) == PERFORM_NEXT: # Indicates the password step is complete
state["stage"] = "mfa_mock"
return resp
# MFA simulation steps
if state["step"] == 0:
return {
__NEXT_OPERATION__: PERFORM_WAITING,
"pstate": {"Password: ": self.password, "verification_code": ""},
"msg": {
"prompt": "Verification Code: ",
"default_path": "/verification_code",
"patch": [{"op": "add", "path": "/verification_code"}],
},
"pdirty": True,
}
elif state["step"] == 1:
return {
__NEXT_OPERATION__: PERFORM_NEXT,
"pstate": {"Password: ": self.password, "verification_code": ""},
"pdirty": True,
}
elif state["step"] == 2:
return {
__NEXT_OPERATION__: PERFORM_AUTHENTICATED,
"pstate": {"Password: ": self.password, "verification_code": "ABC123"},
"pdirty": True,
"request_result": "temp_token",
}
state["step"] += 1
with patch("irods.auth.pam_interactive._auth_api_request", side_effect=mock_server), \
patch("getpass.getpass", return_value=self.password), \
patch("irods.auth.pam_interactive._authenticate_native") as mock_native:
self.sess = make_session(test_server_version=False, env_file=self.env_file_path, authentication_scheme="pam_interactive")
self.sess.server_version # Trigger auth flow
mock_native.assert_called_once()
if __name__ == "__main__":
unittest.main()
#!/usr/bin/env python
import numbers
import os
import posix
import socket
import shutil
from subprocess import Popen, PIPE
import sys
IRODS_SSL_DIR = "/etc/irods/ssl"
SERVER_CERT_HOSTNAME = None
ext = ""
keep_old = False
def create_server_cert(
process_output=sys.stdout, irods_key_path="irods.key", hostname=SERVER_CERT_HOSTNAME
):
p = Popen(
"openssl req -new -x509 -key '{irods_key_path}' -out irods.crt{ext} -days 365 <<EOF{_sep_}"
"US{_sep_}North Carolina{_sep_}Chapel Hill{_sep_}UNC{_sep_}RENCI{_sep_}"
"{host}{_sep_}anon@mail.com{_sep_}EOF\n"
"".format(
ext=ext,
host=(hostname if hostname else socket.gethostname()),
_sep_="\n",
**locals()
),
shell=True,
stdout=process_output,
stderr=process_output,
)
p.wait()
return p.returncode
def create_ssl_dir(
irods_key_path="irods.key", ssl_dir="", use_strong_primes_for_dh_generation=True
):
ssl_dir = ssl_dir or IRODS_SSL_DIR
save_cwd = os.getcwd()
silent_run = {"shell": True, "stderr": PIPE, "stdout": PIPE}
try:
if not (os.path.exists(ssl_dir)):
os.mkdir(ssl_dir)
os.chdir(ssl_dir)
if not keep_old:
Popen(
"openssl genrsa -out '{irods_key_path}' 2048 && chmod 600 '{irods_key_path}'".format(
**locals()
),
**silent_run
).communicate()
with open("/dev/null", "wb") as dev_null:
if 0 == create_server_cert(
process_output=dev_null, irods_key_path=irods_key_path
):
if not keep_old:
# https://www.openssl.org/docs/man1.0.2/man1/dhparam.html#:~:text=DH%20parameter%20generation%20with%20the,that%20may%20be%20possible%20otherwise.
if use_strong_primes_for_dh_generation:
dhparam_generation_command = (
"openssl dhparam -2 -out dhparams.pem"
)
else:
dhparam_generation_command = (
"openssl dhparam -dsaparam -out dhparams.pem 4096"
)
print("cmd=", dhparam_generation_command)
Popen(dhparam_generation_command, **silent_run).communicate()
return os.listdir(".")
finally:
os.chdir(save_cwd)
def test(options, args=()):
if args:
print("warning: non-option args are ignored", file=sys.stderr)
force = "-f" in options
affirm = "n" if (os.path.exists(IRODS_SSL_DIR) and not force) else "y"
if affirm == "n" and posix.isatty(sys.stdin.fileno()):
try:
input_ = raw_input
except NameError:
input_ = input
affirm = input_(
"This will overwrite directory '{}'. Proceed(Y/N)? ".format(IRODS_SSL_DIR)
)
if affirm[:1].lower() == "y":
if not keep_old:
shutil.rmtree(IRODS_SSL_DIR, ignore_errors=True)
dh_strong_primes = "-q" not in options
wait_warning = " This may take a while." if dh_strong_primes else ""
print(
"Generating new '{}'.{}".format(IRODS_SSL_DIR, wait_warning),
file=sys.stderr,
)
ssl_dir_files = create_ssl_dir(
use_strong_primes_for_dh_generation=dh_strong_primes
)
print("ssl_dir_files=", ssl_dir_files, file=sys.stderr)
def usage(exit_code=None):
print(
"""Usage: {sys.argv[0]} [-f] [-h <hostname>] [-k] [-q] [-x <extension>]
-f Force replacement of the existing SSL directory (/etc/irods/ssl) with a new one, containing newly generated files.
-h In the generated certificate, use the given hostname rather than the value returned from socket.gethostname()
-k (Keep old secrets files.) Do not generate new key file or dhparams.pem file.
-q For testing; do a quick generation of a dhparams.pem file rather than waiting on system entropy to make it more secure.
-x Optional extra extension for appending to end of the filename for the generated certificate.
--help Print this help.
Any invalid option prints this help.
""".format(
**globals()
),
file=sys.stderr,
)
if isinstance(exit_code, numbers.Integral):
exit(exit_code)
if __name__ == "__main__":
import getopt
try:
opt, arg_list = getopt.getopt(sys.argv[1:], "x:fh:kq", ["help"])
except getopt.GetoptError:
usage(exit_code=1)
opt_lookup = dict(opt)
if "--help" in opt_lookup:
usage(exit_code=0)
ext = opt_lookup.get("-x", "")
if ext:
ext = "." + ext.lstrip(".")
keep_old = opt_lookup.get("-k") is not None
SERVER_CERT_HOSTNAME = opt_lookup.get("-h")
test(opt_lookup, arg_list)

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display