From a7d9d7cc8bef643bc5b2f11a5511acad463da1d9 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 5 Feb 2026 22:05:31 -0500 Subject: [PATCH 1/7] [_763] Adapt PRC remove remote user call style for GEN_ADMIN When calling to GEN_ADMIN to remove a remote user with separate user and zone parameters, the server will not remove the /tempZone/**/user#hello collections. So, we now force PRC to use an integrated user#zone format. --- irods/manager/user_manager.py | 30 ++++++++++++++++++++++++++++-- irods/test/admin_test.py | 23 ++++++++++++++++++++--- irods/test/user_group_test.py | 1 + 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/irods/manager/user_manager.py b/irods/manager/user_manager.py index 90f4be813..278695728 100644 --- a/irods/manager/user_manager.py +++ b/irods/manager/user_manager.py @@ -45,7 +45,28 @@ def remove_quota(self, user_name, resource="total"): self._get_session, "set-quota", "user", user_name, resource, "0" ) + @staticmethod + def _parse_user_and_zone(user_param, zone_param): + """Parse out the uesr name ane zone name from USER and ZONE string parameters. + If the USER string contains # and a non-null-length ZONE spec, ensure that + multiply specified zone names agree. + """ + 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") + else: + 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: user_zone = self.sess.zone @@ -121,6 +142,12 @@ def create(self, user_name, user_type, user_zone="", auth_str=""): def remove(self, user_name, user_zone="", _object=None): if _object is None: _object = self.get(user_name, user_zone) + + if _object.type == "rodsgroup": + uz_args = (f"{_object.name}",) + else: + uz_args = (f"{_object.name}#{_object.zone}",) + message_body = GeneralAdminRequest( "rm", ( @@ -128,8 +155,7 @@ def remove(self, user_name, user_zone="", _object=None): if (_object.type != "rodsgroup" or self.sess.server_version < (4, 3, 2)) else "group" ), - user_name, - user_zone, + *uz_args, ) request = iRODSMessage( "RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"] diff --git a/irods/test/admin_test.py b/irods/test/admin_test.py index 91faad393..b0760061b 100644 --- a/irods/test/admin_test.py +++ b/irods/test/admin_test.py @@ -4,16 +4,19 @@ import os import sys import unittest -from irods.models import User, Group + +from irods.column import Like from irods.exception import ( UserDoesNotExist, ResourceDoesNotExist, SYS_NO_API_PRIV, ) -from irods.session import iRODSSession +from irods.models import Collection, Group, User from irods.resource import iRODSResource -import irods.test.helpers as helpers +from irods.session import iRODSSession + import irods.keywords as kw +import irods.test.helpers as helpers class TestAdmin(unittest.TestCase): @@ -531,6 +534,20 @@ def test_set_user_info(self): with self.assertRaises(UserDoesNotExist): self.sess.users.get(self.new_user_name) + def test_deleting_remote_user_including_home_collection_and_trash_artifact__issue_763(self): + 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) + remote_user.remove() + remaining_collections = list( + sess.query(Collection).filter(Like(Collection.name, f'%/{remote_user}#{remote_zone}')) + ) + self.assertEqual(len(remaining_collections), 0) + finally: + if remote_zone: + remote_zone.remove() + if __name__ == "__main__": # let the tests find the parent irods lib diff --git a/irods/test/user_group_test.py b/irods/test/user_group_test.py index 5ce8b84aa..4d9f0c529 100644 --- a/irods/test/user_group_test.py +++ b/irods/test/user_group_test.py @@ -143,6 +143,7 @@ def generator(p=OLDPASS): shutil.rmtree(ENV_DIR) ses.users.remove("alice") + def test_modifying_password_at_various_lengths__issue_328(self): ses = self.sess try: From 506b81fcddbe97a1d11391b4e2e5535dae3b3de2 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Tue, 17 Feb 2026 11:33:31 -0500 Subject: [PATCH 2/7] patch, ruff chgs. nr.3 --- irods/manager/user_manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/irods/manager/user_manager.py b/irods/manager/user_manager.py index 278695728..9a934c304 100644 --- a/irods/manager/user_manager.py +++ b/irods/manager/user_manager.py @@ -52,15 +52,17 @@ def _parse_user_and_zone(user_param, zone_param): multiply specified zone names agree. """ if '#' in user_param: - u_parsed_user, u_parsed_zone = user_param.split('#',1) + 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") else: 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.") + 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 From ec43458de2aefeb980db9997baa82a72be8cb5ba Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 19 Feb 2026 04:22:35 -0500 Subject: [PATCH 3/7] noqa --- irods/manager/user_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/irods/manager/user_manager.py b/irods/manager/user_manager.py index 9a934c304..d045a8227 100644 --- a/irods/manager/user_manager.py +++ b/irods/manager/user_manager.py @@ -145,7 +145,8 @@ def remove(self, user_name, user_zone="", _object=None): if _object is None: _object = self.get(user_name, user_zone) - if _object.type == "rodsgroup": + + if _object.type == "rodsgroup": # noqa: SIM106 uz_args = (f"{_object.name}",) else: uz_args = (f"{_object.name}#{_object.zone}",) From ff5fd8e1446bf46fff719d8fe0dcc5475a3bf810 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 19 Feb 2026 04:54:19 -0500 Subject: [PATCH 4/7] review suggestions --- irods/manager/user_manager.py | 4 ++-- irods/test/admin_test.py | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/irods/manager/user_manager.py b/irods/manager/user_manager.py index d045a8227..28cf7ce16 100644 --- a/irods/manager/user_manager.py +++ b/irods/manager/user_manager.py @@ -47,8 +47,8 @@ def remove_quota(self, user_name, resource="total"): @staticmethod def _parse_user_and_zone(user_param, zone_param): - """Parse out the uesr name ane zone name from USER and ZONE string parameters. - If the USER string contains # and a non-null-length ZONE spec, ensure that + """Parse out the user and zone components from user_param and zone_param. + If user_param contains # and a non-null-length zone spec, ensure that multiply specified zone names agree. """ if '#' in user_param: diff --git a/irods/test/admin_test.py b/irods/test/admin_test.py index b0760061b..47a1a0873 100644 --- a/irods/test/admin_test.py +++ b/irods/test/admin_test.py @@ -535,15 +535,27 @@ def test_set_user_info(self): self.sess.users.get(self.new_user_name) 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: + # //home/remote_user#remote_zone + # //trash/home/remote_user#remote_zone + self.assertEqual(len(get_collection_artifacts()), 2) + remote_user.remove() - remaining_collections = list( - sess.query(Collection).filter(Like(Collection.name, f'%/{remote_user}#{remote_zone}')) - ) - self.assertEqual(len(remaining_collections), 0) + + # 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() From f79256864c19287e24cb5f743b9e5cfa873d36bb Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 19 Feb 2026 04:55:22 -0500 Subject: [PATCH 5/7] single quotes for clarity in commenet --- irods/manager/user_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irods/manager/user_manager.py b/irods/manager/user_manager.py index 28cf7ce16..7c416731c 100644 --- a/irods/manager/user_manager.py +++ b/irods/manager/user_manager.py @@ -48,7 +48,7 @@ def remove_quota(self, user_name, resource="total"): @staticmethod def _parse_user_and_zone(user_param, zone_param): """Parse out the user and zone components from user_param and zone_param. - If user_param contains # and a non-null-length zone spec, ensure that + If user_param contains '#' and a non-null-length zone spec, ensure that multiply specified zone names agree. """ if '#' in user_param: From 018281ce92e3cc90b21c2525051bb36f6ae10173 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 20 Feb 2026 05:02:49 -0500 Subject: [PATCH 6/7] change (non existing) SIM106 to SIM108 --- irods/manager/user_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irods/manager/user_manager.py b/irods/manager/user_manager.py index 7c416731c..6626bd3e3 100644 --- a/irods/manager/user_manager.py +++ b/irods/manager/user_manager.py @@ -146,7 +146,7 @@ def remove(self, user_name, user_zone="", _object=None): _object = self.get(user_name, user_zone) - if _object.type == "rodsgroup": # noqa: SIM106 + if _object.type == "rodsgroup": # noqa: SIM108 uz_args = (f"{_object.name}",) else: uz_args = (f"{_object.name}#{_object.zone}",) From 2c008789ed328e0c5fded1be1e2b2a39dd4a7cc7 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Tue, 24 Feb 2026 05:00:10 -0500 Subject: [PATCH 7/7] ruff --- irods/manager/user_manager.py | 2 +- irods/test/admin_test.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/irods/manager/user_manager.py b/irods/manager/user_manager.py index 6626bd3e3..6a124cabb 100644 --- a/irods/manager/user_manager.py +++ b/irods/manager/user_manager.py @@ -146,7 +146,7 @@ def remove(self, user_name, user_zone="", _object=None): _object = self.get(user_name, user_zone) - if _object.type == "rodsgroup": # noqa: SIM108 + if _object.type == "rodsgroup": # noqa: SIM108 uz_args = (f"{_object.name}",) else: uz_args = (f"{_object.name}#{_object.zone}",) diff --git a/irods/test/admin_test.py b/irods/test/admin_test.py index 47a1a0873..f6bbbcfd4 100644 --- a/irods/test/admin_test.py +++ b/irods/test/admin_test.py @@ -16,7 +16,7 @@ from irods.session import iRODSSession import irods.keywords as kw -import irods.test.helpers as helpers +from irods.test import helpers class TestAdmin(unittest.TestCase): @@ -542,10 +542,12 @@ def test_deleting_remote_user_including_home_collection_and_trash_artifact__issu 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}')) + return list( + sess.query(Collection).filter(Like(Collection.name, f'%/{remote_user.name}#{remote_zone.name}')) ) + # Two collection artifacts should be present, with names: # //home/remote_user#remote_zone # //trash/home/remote_user#remote_zone