From 7a811847ec02bdbc3bc5e8adc7913cb8e03067cd Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 20 Mar 2026 19:06:28 -0400 Subject: [PATCH 01/17] add SYS_INTERNAL_ERR --- irods/exception.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/irods/exception.py b/irods/exception.py index b9551fdc..4dbbd66d 100644 --- a/irods/exception.py +++ b/irods/exception.py @@ -650,6 +650,10 @@ class SYS_INVALID_INPUT_PARAM(SystemException): code = -130000 +class SYS_INTERNAL_ERR(SystemException): + code = -154000 + + class SYS_BAD_INPUT(iRODSException): code = -158000 From 228bf860d906d6bde877b13fb6faf747de54dd12 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 19 Mar 2026 02:40:53 -0400 Subject: [PATCH 02/17] [_505,sq] atomic ACLs endpoint --- irods/access.py | 5 ++++- irods/manager/access_manager.py | 30 +++++++++++++++++++++++++- irods/test/access_test.py | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index 465585dd..3c0d206d 100644 --- a/irods/access.py +++ b/irods/access.py @@ -102,7 +102,7 @@ def __eq__(self, other): def __hash__(self): return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone)) - def copy(self, decanonicalize=False): + def copy(self, decanonicalize=False, ref_zone=''): other = copy.deepcopy(self) if decanonicalize: replacement_string = { @@ -112,6 +112,9 @@ def copy(self, decanonicalize=False): "modify_object": "write", }.get(self.access_name) other.access_name = replacement_string if replacement_string is not None else self.access_name + if '' != ref_zone == other.user_zone: + other.user_zone = '' + return other def __repr__(self): diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index bf32dc28..58cec810 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -2,7 +2,7 @@ from irods.manager import Manager from irods.api_number import api_number -from irods.message import ModAclRequest, iRODSMessage +from irods.message import ModAclRequest, iRODSMessage, JSON_Message from irods.data_object import iRODSDataObject, irods_dirname, irods_basename from irods.collection import iRODSCollection from irods.models import ( @@ -14,6 +14,7 @@ CollectionAccess, ) from irods.access import iRODSAccess +import irods.exception as ex from irods.column import In from irods.user import iRODSUser @@ -36,6 +37,33 @@ def users_by_ids(session, ids=()): class AccessManager(Manager): + + def _ACL_operation(self, op_input: iRODSAccess): + return { + "acl": op_input.access_name, + "entity_name": op_input.user_name, + **( + {} if not (z := op_input.user_zone) + else {"zone": z} + ) + } + + def _call_atomic_acl_api(self, logical_path : str, *operations, admin=False): + request_text = {"logical_path": logical_path} + request_text["admin_mode"] = admin + request_text["operations"] = [self._ACL_operation(op) for op in operations] + + with self.sess.pool.get_connection() as conn: + request_msg = iRODSMessage( + "RODS_API_REQ", + JSON_Message(request_text, conn.server_version), + int_info=20005, + ) + conn.send(request_msg) + response = conn.recv() + response_msg = response.get_json_encoded_struct() + logger.debug("in atomic ACL api, server responded with: %r", response_msg) + def get(self, target, report_raw_acls=True, **kw): if report_raw_acls: diff --git a/irods/test/access_test.py b/irods/test/access_test.py index fadc6a7d..5bf397d8 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -497,6 +497,43 @@ def test_iRODSAccess_cannot_be_constructed_using_unsupported_type__issue_558(sel self.sess, ) + def test_atomic_acls_505(self): + #import pdb;pdb.set_trace() + ses = self.sess + zone = user1 = user2 = group = None + try: + zone = ses.zones.create("twilight","remote") + user1 = ses.users.create("test_user_505", "rodsuser") + user2 = ses.users.create("rod_serling_505#twilight", "rodsuser") + group = ses.groups.create("test_group_505") + ses.acls._call_atomic_acl_api( + self.coll_path, + a1:=iRODSAccess("write", "", user1.name, user1.zone), + a2:=iRODSAccess("read", "", user2.name, user2.zone), + a3:=iRODSAccess("read", "", group.name), + ) + + accesses = ses.acls.get(self.coll) + + # For purposes of equality tests, assign the path name of interest into each ACL. + for p in (a1, a2, a3): + p.path = self.coll_path + + # Assert that the ACLs we added are among those listed for the object in the catalog. + normalize = lambda access: access.copy(decanonicalize=True, ref_zone=ses.zone) + self.assertLess( + set(normalize(_) for _ in (a1,a2,a3)), + set(normalize(_) for _ in accesses) + ) + finally: + if user1: + user1.remove() + if user2: + user2.remove() + if group: + group.remove() + if zone: + zone.remove() if __name__ == "__main__": # let the tests find the parent irods lib From d277eb72b225d6a3bc99288e14d04da1aa50c82f Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sat, 21 Mar 2026 00:45:03 -0400 Subject: [PATCH 03/17] ruff1 --- irods/test/access_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 5bf397d8..88e1db70 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -498,7 +498,6 @@ def test_iRODSAccess_cannot_be_constructed_using_unsupported_type__issue_558(sel ) def test_atomic_acls_505(self): - #import pdb;pdb.set_trace() ses = self.sess zone = user1 = user2 = group = None try: From 64b870120e278588138632a201f867c39455813c Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 23 Mar 2026 13:39:07 -0400 Subject: [PATCH 04/17] [_505] create ACLOperation based on iRODSAccess --- irods/access.py | 53 ++++++++++++++++++++++++++++++++++++--- irods/test/access_test.py | 23 ++++++++--------- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/irods/access.py b/irods/access.py index 3c0d206d..88e27f58 100644 --- a/irods/access.py +++ b/irods/access.py @@ -91,6 +91,13 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None self.user_zone = user_zone self.user_type = user_type + def __lt__(self, other): + return ( + self.access_name < other.access_name + and self.user_name < other.user_name + and self.user_zone < other.user_zone + ) and iRODSPath(self.path) < iRODSPath(other.path) + def __eq__(self, other): return ( self.access_name == other.access_name @@ -102,8 +109,9 @@ def __eq__(self, other): def __hash__(self): return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone)) - def copy(self, decanonicalize=False, ref_zone=''): + def copy(self, decanonicalize=False, implied_zone=''): other = copy.deepcopy(self) + if decanonicalize: replacement_string = { "read object": "read", @@ -112,8 +120,10 @@ def copy(self, decanonicalize=False, ref_zone=''): "modify_object": "write", }.get(self.access_name) other.access_name = replacement_string if replacement_string is not None else self.access_name - if '' != ref_zone == other.user_zone: - other.user_zone = '' + + # Useful if we wish to force an explicitly specified local zone to null length for equality testing: + if '' != implied_zone == other.user_zone: + other.user_zone = '' return other @@ -124,6 +134,43 @@ def __repr__(self): return f"" +class ACLOperation(iRODSAccess): + + def __init__(self, access_name: str, user_name: str="", user_zone: str=""): + super().__init__( + access_name=access_name, + path="", + user_name=user_name, + user_zone=user_zone, + ) + + def __eq__(self, other): + return ( + self.access_name, + self.user_name, + self.user_zone, + ) == ( + other.access_name, + other.user_name, + other.user_zone, + ) + + def __lt__(self, other): + return ( + self.access_name, + self.user_name, + self.user_zone, + ) < ( + other.access_name, + other.user_name, + other.user_zone, + ) + + def __repr__(self): + return f"" + class _iRODSAccess_pre_4_3_0(iRODSAccess): codes = collections.OrderedDict( (key.replace("_", " "), value) diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 88e1db70..5ac481a1 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -4,7 +4,7 @@ import sys import unittest -from irods.access import iRODSAccess +from irods.access import iRODSAccess, ACLOperation from irods.collection import iRODSCollection from irods.column import In, Like from irods.exception import UserDoesNotExist @@ -507,23 +507,20 @@ def test_atomic_acls_505(self): group = ses.groups.create("test_group_505") ses.acls._call_atomic_acl_api( self.coll_path, - a1:=iRODSAccess("write", "", user1.name, user1.zone), - a2:=iRODSAccess("read", "", user2.name, user2.zone), - a3:=iRODSAccess("read", "", group.name), + a1:=ACLOperation("write", user1.name, user1.zone), + a2:=ACLOperation("read", user2.name, user2.zone), + a3:=ACLOperation("read", group.name), ) - accesses = ses.acls.get(self.coll) + normalize = lambda access: access.copy(decanonicalize=True, implied_zone=ses.zone) - # For purposes of equality tests, assign the path name of interest into each ACL. - for p in (a1, a2, a3): - p.path = self.coll_path + accesses = [normalize(acl) for acl in ses.acls.get(self.coll)] # Assert that the ACLs we added are among those listed for the object in the catalog. - normalize = lambda access: access.copy(decanonicalize=True, ref_zone=ses.zone) - self.assertLess( - set(normalize(_) for _ in (a1,a2,a3)), - set(normalize(_) for _ in accesses) - ) + self.assertIn(normalize(a1), accesses) + self.assertIn(normalize(a2), accesses) + self.assertIn(normalize(a3), accesses) + finally: if user1: user1.remove() From 1803ddade2e29aaf75b47542bb293a0be5e7586d Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 23 Mar 2026 13:45:04 -0400 Subject: [PATCH 05/17] [_505] test local users both with and without zone --- irods/test/access_test.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 5ac481a1..b3d16ce4 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -499,17 +499,19 @@ def test_iRODSAccess_cannot_be_constructed_using_unsupported_type__issue_558(sel def test_atomic_acls_505(self): ses = self.sess - zone = user1 = user2 = group = None + zone = user1 = user2 = user3 = group = None try: zone = ses.zones.create("twilight","remote") user1 = ses.users.create("test_user_505", "rodsuser") user2 = ses.users.create("rod_serling_505#twilight", "rodsuser") + user3 = ses.users.create("local_test_user_505", "rodsuser") group = ses.groups.create("test_group_505") ses.acls._call_atomic_acl_api( self.coll_path, - a1:=ACLOperation("write", user1.name, user1.zone), + a1:=ACLOperation("write", user1.name, user1.zone), a2:=ACLOperation("read", user2.name, user2.zone), - a3:=ACLOperation("read", group.name), + a3:=ACLOperation("read", user3.name, user3.zone), + a4:=ACLOperation("read", group.name), ) normalize = lambda access: access.copy(decanonicalize=True, implied_zone=ses.zone) @@ -520,12 +522,15 @@ def test_atomic_acls_505(self): self.assertIn(normalize(a1), accesses) self.assertIn(normalize(a2), accesses) self.assertIn(normalize(a3), accesses) + self.assertIn(normalize(a4), accesses) finally: if user1: user1.remove() if user2: user2.remove() + if user3: + user3.remove() if group: group.remove() if zone: From d419d0e501bbe53a67133a26320b9022ffaf15dd Mon Sep 17 00:00:00 2001 From: Daniel Moore Date: Tue, 24 Mar 2026 00:46:53 -0400 Subject: [PATCH 06/17] Update irods/manager/access_manager.py Co-authored-by: Kory Draughn --- irods/manager/access_manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 58cec810..2f19b7a7 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -49,9 +49,11 @@ def _ACL_operation(self, op_input: iRODSAccess): } def _call_atomic_acl_api(self, logical_path : str, *operations, admin=False): - request_text = {"logical_path": logical_path} - request_text["admin_mode"] = admin - request_text["operations"] = [self._ACL_operation(op) for op in operations] + request_text = { + "logical_path": logical_path, + "admin_mode": admin + "operations": [self._ACL_operation(op) for op in operations] + } with self.sess.pool.get_connection() as conn: request_msg = iRODSMessage( From a746e2ae35dfb31b6b26c3b0cb8892261770f9c9 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Tue, 24 Mar 2026 15:34:14 -0400 Subject: [PATCH 07/17] correct __lt__ comparison function --- irods/access.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/irods/access.py b/irods/access.py index 88e27f58..84e29318 100644 --- a/irods/access.py +++ b/irods/access.py @@ -93,10 +93,16 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None def __lt__(self, other): return ( - self.access_name < other.access_name - and self.user_name < other.user_name - and self.user_zone < other.user_zone - ) and iRODSPath(self.path) < iRODSPath(other.path) + self.access_name, + self.user_name, + self.user_zone, + iRODSPath(self.path) + ) < ( + other.access_name, + other.user_name, + other.user_zone, + iRODSPath(other.path) + ) def __eq__(self, other): return ( From ef0fe69d887056725c40b5879e1d30ad82021b26 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Tue, 24 Mar 2026 15:50:12 -0400 Subject: [PATCH 08/17] correction to dict literal --- irods/manager/access_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 2f19b7a7..4ece4aab 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -51,7 +51,7 @@ def _ACL_operation(self, op_input: iRODSAccess): def _call_atomic_acl_api(self, logical_path : str, *operations, admin=False): request_text = { "logical_path": logical_path, - "admin_mode": admin + "admin_mode": admin, "operations": [self._ACL_operation(op) for op in operations] } From b70413d2116eb7dd2e1b609a2e1f1d819dffc5ad Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Tue, 24 Mar 2026 16:02:12 -0400 Subject: [PATCH 09/17] api_number --- irods/api_number.py | 1 + irods/manager/access_manager.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/irods/api_number.py b/irods/api_number.py index fe614ffc..03ac3de8 100644 --- a/irods/api_number.py +++ b/irods/api_number.py @@ -177,6 +177,7 @@ "ATOMIC_APPLY_METADATA_OPERATIONS_APN": 20002, "GET_FILE_DESCRIPTOR_INFO_APN": 20000, "REPLICA_CLOSE_APN": 20004, + "ATOMIC_APPLY_ACL_OPERATIONS_APN": 20005, "TOUCH_APN": 20007, "AUTH_PLUG_REQ_AN": 1201, "AUTHENTICATION_APN": 110000, diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 4ece4aab..ff784e5e 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -59,7 +59,7 @@ def _call_atomic_acl_api(self, logical_path : str, *operations, admin=False): request_msg = iRODSMessage( "RODS_API_REQ", JSON_Message(request_text, conn.server_version), - int_info=20005, + int_info=api_number["ATOMIC_APPLY_ACL_OPERATIONS_APN"], ) conn.send(request_msg) response = conn.recv() From 8a656a0db86555cd05423e8a44c34fc937680e04 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Tue, 24 Mar 2026 17:44:57 -0400 Subject: [PATCH 10/17] rename interface method --- irods/manager/access_manager.py | 2 +- irods/test/access_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index ff784e5e..58ac26f8 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -48,7 +48,7 @@ def _ACL_operation(self, op_input: iRODSAccess): ) } - def _call_atomic_acl_api(self, logical_path : str, *operations, admin=False): + def apply_atomic_acl_operations(self, logical_path : str, *operations, admin=False): request_text = { "logical_path": logical_path, "admin_mode": admin, diff --git a/irods/test/access_test.py b/irods/test/access_test.py index b3d16ce4..22e25c12 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -506,7 +506,7 @@ def test_atomic_acls_505(self): user2 = ses.users.create("rod_serling_505#twilight", "rodsuser") user3 = ses.users.create("local_test_user_505", "rodsuser") group = ses.groups.create("test_group_505") - ses.acls._call_atomic_acl_api( + ses.acls.apply_atomic_acl_operations( self.coll_path, a1:=ACLOperation("write", user1.name, user1.zone), a2:=ACLOperation("read", user2.name, user2.zone), From 6921fa8b5e4b1384520a2d4aa02b0bd3ad4b957e Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 25 Mar 2026 06:47:42 -0400 Subject: [PATCH 11/17] minor updates/corrections --- irods/access.py | 6 ++---- irods/manager/access_manager.py | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/irods/access.py b/irods/access.py index 84e29318..ea192aed 100644 --- a/irods/access.py +++ b/irods/access.py @@ -127,7 +127,7 @@ def copy(self, decanonicalize=False, implied_zone=''): }.get(self.access_name) other.access_name = replacement_string if replacement_string is not None else self.access_name - # Useful if we wish to force an explicitly specified local zone to null length for equality testing: + # Useful if we wish to force an explicitly specified local zone to an implicit zone spec in the copy, for equality testing: if '' != implied_zone == other.user_zone: other.user_zone = '' @@ -173,9 +173,7 @@ def __lt__(self, other): ) def __repr__(self): - return f"" + return f"" class _iRODSAccess_pre_4_3_0(iRODSAccess): codes = collections.OrderedDict( diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 58ac26f8..0e52dd21 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -38,7 +38,8 @@ def users_by_ids(session, ids=()): class AccessManager(Manager): - def _ACL_operation(self, op_input: iRODSAccess): + @staticmethod + def _to_acl_operation_json(op_input: iRODSAccess): return { "acl": op_input.access_name, "entity_name": op_input.user_name, @@ -52,7 +53,7 @@ def apply_atomic_acl_operations(self, logical_path : str, *operations, admin=Fal request_text = { "logical_path": logical_path, "admin_mode": admin, - "operations": [self._ACL_operation(op) for op in operations] + "operations": [self._to_acl_operation_json(op) for op in operations] } with self.sess.pool.get_connection() as conn: From 968322e8c534b959e34c69456bb2aa482eb5c31f Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 25 Mar 2026 12:19:01 -0400 Subject: [PATCH 12/17] rename apply_atomic_acl_operations to omit "acl_" --- irods/manager/access_manager.py | 2 +- irods/test/access_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 0e52dd21..7d9b7878 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -49,7 +49,7 @@ def _to_acl_operation_json(op_input: iRODSAccess): ) } - def apply_atomic_acl_operations(self, logical_path : str, *operations, admin=False): + def apply_atomic_operations(self, logical_path : str, *operations, admin=False): request_text = { "logical_path": logical_path, "admin_mode": admin, diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 22e25c12..3580aa91 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -506,7 +506,7 @@ def test_atomic_acls_505(self): user2 = ses.users.create("rod_serling_505#twilight", "rodsuser") user3 = ses.users.create("local_test_user_505", "rodsuser") group = ses.groups.create("test_group_505") - ses.acls.apply_atomic_acl_operations( + ses.acls.apply_atomic_operations( self.coll_path, a1:=ACLOperation("write", user1.name, user1.zone), a2:=ACLOperation("read", user2.name, user2.zone), From 91bfc5a12c3c2bbfe0ff7e3e82a277e78b724cbf Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 26 Mar 2026 10:32:29 -0400 Subject: [PATCH 13/17] readme -atomic ACLs --- README.md | 28 ++++++++++++++++++++++++++++ irods/access.py | 47 +++++++++++++++++++++++++++++++---------------- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5bd19320..199e1729 100644 --- a/README.md +++ b/README.md @@ -2118,6 +2118,34 @@ membership, this can be achieved with another query. `.permissions` was therefore removed in v2.0.0 in favor of `.acls`. +Atomic ACLs +----------- + +A list of permissions may be added to an object atomically using +the AccessManager's apply_atomic_operations method: +``` +from irods.access import ACLOperation +from irods.helpers import home_collection +session = irods.helpers.make_session() +myCollection = session.collections.create(f"{home_collection(session).path}/newCollection") + +session.acls.apply_atomic_operations(myCollection.path, + *[ACLOperation("read", "public"), + ACLOperation("write", "bob", "otherZone") + ]) +``` +ACLOperation objects form a linear order with iRODSAccess objects, and +indeed are subclassed from them as well, allowing equivalency testing: + +Thus, for example: +``` +ACLOperation('read','public') in sess.acls.get(object) +``` +is a valid operation. Consequently, any client application that habitually +caches object permissions could use similar code to check new ACLOperations against the cache +and conceivably be able to optimize size of an atomic ACLs request by eliminating +any ACLOperations that might have been redundant. + Quotas (v2.0.0) --------------- diff --git a/irods/access.py b/irods/access.py index ea192aed..be49eb05 100644 --- a/irods/access.py +++ b/irods/access.py @@ -5,6 +5,22 @@ from irods.path import iRODSPath +_ichmod_listed_permissions = ( + "own", + "delete_object", + "write", + "modify_object", + "create_object", + "delete_metadata", + "modify_metadata", + "create_metadata", + "read", + "read_object", + "read_metadata", + "null", + ) + + class _Access_LookupMeta(type): def __getitem__(self, key): return self.codes[key] @@ -55,22 +71,7 @@ def to_string(cls, key): ).items(), key=lambda _: _[1], ) - if key_ - in ( - # These are copied from ichmod help text. - "own", - "delete_object", - "write", - "modify_object", - "create_object", - "delete_metadata", - "modify_metadata", - "create_metadata", - "read", - "read_object", - "read_metadata", - "null", - ) + if key_ in _ichmod_listed_permissions ) strings = collections.OrderedDict((number, string) for string, number in codes.items()) @@ -175,6 +176,20 @@ def __lt__(self, other): def __repr__(self): return f"" + +(_ichmod_synonym_mapping := { + # syn : canonical + "write": "modify_object", + "read": "read_object" + }).update( + (key.replace("_"," "),key) for key in iRODSAccess.codes.keys()) + + +all_permissions = { **iRODSAccess.codes, + **{key:iRODSAccess.codes[_ichmod_synonym_mapping[key]] + for key in _ichmod_synonym_mapping}} + + class _iRODSAccess_pre_4_3_0(iRODSAccess): codes = collections.OrderedDict( (key.replace("_", " "), value) From 3d66c24d6ec900a0a9cfdd0bff4e794b665fc5b3 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 26 Mar 2026 10:39:27 -0400 Subject: [PATCH 14/17] ruff recommended --- irods/access.py | 58 +++++++++++++++------------------ irods/manager/access_manager.py | 10 ++---- irods/test/access_test.py | 11 ++++--- 3 files changed, 36 insertions(+), 43 deletions(-) diff --git a/irods/access.py b/irods/access.py index be49eb05..74828ba6 100644 --- a/irods/access.py +++ b/irods/access.py @@ -6,19 +6,19 @@ _ichmod_listed_permissions = ( - "own", - "delete_object", - "write", - "modify_object", - "create_object", - "delete_metadata", - "modify_metadata", - "create_metadata", - "read", - "read_object", - "read_metadata", - "null", - ) + "own", + "delete_object", + "write", + "modify_object", + "create_object", + "delete_metadata", + "modify_metadata", + "create_metadata", + "read", + "read_object", + "read_metadata", + "null", +) class _Access_LookupMeta(type): @@ -93,16 +93,11 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None self.user_type = user_type def __lt__(self, other): - return ( - self.access_name, - self.user_name, - self.user_zone, - iRODSPath(self.path) - ) < ( + return (self.access_name, self.user_name, self.user_zone, iRODSPath(self.path)) < ( other.access_name, other.user_name, other.user_zone, - iRODSPath(other.path) + iRODSPath(other.path), ) def __eq__(self, other): @@ -142,8 +137,7 @@ def __repr__(self): class ACLOperation(iRODSAccess): - - def __init__(self, access_name: str, user_name: str="", user_zone: str=""): + def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""): super().__init__( access_name=access_name, path="", @@ -177,17 +171,19 @@ def __repr__(self): return f"" -(_ichmod_synonym_mapping := { - # syn : canonical - "write": "modify_object", - "read": "read_object" - }).update( - (key.replace("_"," "),key) for key in iRODSAccess.codes.keys()) +( + _ichmod_synonym_mapping := { + # syn : canonical + "write": "modify_object", + "read": "read_object", + } +).update((key.replace("_", " "), key) for key in iRODSAccess.codes.keys()) -all_permissions = { **iRODSAccess.codes, - **{key:iRODSAccess.codes[_ichmod_synonym_mapping[key]] - for key in _ichmod_synonym_mapping}} +all_permissions = { + **iRODSAccess.codes, + **{key: iRODSAccess.codes[_ichmod_synonym_mapping[key]] for key in _ichmod_synonym_mapping}, +} class _iRODSAccess_pre_4_3_0(iRODSAccess): diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 7d9b7878..204aa79f 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -37,23 +37,19 @@ def users_by_ids(session, ids=()): class AccessManager(Manager): - @staticmethod def _to_acl_operation_json(op_input: iRODSAccess): return { "acl": op_input.access_name, "entity_name": op_input.user_name, - **( - {} if not (z := op_input.user_zone) - else {"zone": z} - ) + **({} if not (z := op_input.user_zone) else {"zone": z}), } - def apply_atomic_operations(self, logical_path : str, *operations, admin=False): + def apply_atomic_operations(self, logical_path: str, *operations, admin=False): request_text = { "logical_path": logical_path, "admin_mode": admin, - "operations": [self._to_acl_operation_json(op) for op in operations] + "operations": [self._to_acl_operation_json(op) for op in operations], } with self.sess.pool.get_connection() as conn: diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 3580aa91..ec5f6484 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -501,17 +501,17 @@ def test_atomic_acls_505(self): ses = self.sess zone = user1 = user2 = user3 = group = None try: - zone = ses.zones.create("twilight","remote") + zone = ses.zones.create("twilight", "remote") user1 = ses.users.create("test_user_505", "rodsuser") user2 = ses.users.create("rod_serling_505#twilight", "rodsuser") user3 = ses.users.create("local_test_user_505", "rodsuser") group = ses.groups.create("test_group_505") ses.acls.apply_atomic_operations( self.coll_path, - a1:=ACLOperation("write", user1.name, user1.zone), - a2:=ACLOperation("read", user2.name, user2.zone), - a3:=ACLOperation("read", user3.name, user3.zone), - a4:=ACLOperation("read", group.name), + a1 := ACLOperation("write", user1.name, user1.zone), + a2 := ACLOperation("read", user2.name, user2.zone), + a3 := ACLOperation("read", user3.name, user3.zone), + a4 := ACLOperation("read", group.name), ) normalize = lambda access: access.copy(decanonicalize=True, implied_zone=ses.zone) @@ -536,6 +536,7 @@ def test_atomic_acls_505(self): if zone: zone.remove() + if __name__ == "__main__": # let the tests find the parent irods lib sys.path.insert(0, os.path.abspath("../..")) From 4ef268756616459a29d14ea27b259aaf99bc7656 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 25 Mar 2026 21:11:05 -0400 Subject: [PATCH 15/17] one local user explicitly without zone --- irods/test/access_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/irods/test/access_test.py b/irods/test/access_test.py index ec5f6484..3c65566e 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -508,10 +508,10 @@ def test_atomic_acls_505(self): group = ses.groups.create("test_group_505") ses.acls.apply_atomic_operations( self.coll_path, - a1 := ACLOperation("write", user1.name, user1.zone), - a2 := ACLOperation("read", user2.name, user2.zone), - a3 := ACLOperation("read", user3.name, user3.zone), - a4 := ACLOperation("read", group.name), + a1:=ACLOperation("write", user1.name, user1.zone), + a2:=ACLOperation("read", user2.name, user2.zone), + a3:=ACLOperation("read", user3.name), + a4:=ACLOperation("read", group.name), ) normalize = lambda access: access.copy(decanonicalize=True, implied_zone=ses.zone) From 6842412aaa79b91ddc24f6ab6874089da817169b Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 26 Mar 2026 10:48:01 -0400 Subject: [PATCH 16/17] ws in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 199e1729..dc3aed6a 100644 --- a/README.md +++ b/README.md @@ -2129,7 +2129,7 @@ from irods.helpers import home_collection session = irods.helpers.make_session() myCollection = session.collections.create(f"{home_collection(session).path}/newCollection") -session.acls.apply_atomic_operations(myCollection.path, +session.acls.apply_atomic_operations(myCollection.path, *[ACLOperation("read", "public"), ACLOperation("write", "bob", "otherZone") ]) From adc8296e974054f8850e7af6eec1a1af8e5796df Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 26 Mar 2026 11:33:57 -0400 Subject: [PATCH 17/17] dummy commit --- a | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 a diff --git a/a b/a new file mode 100644 index 00000000..e69de29b