diff --git a/irods/access.py b/irods/access.py index 465585dd..84e29318 100644 --- a/irods/access.py +++ b/irods/access.py @@ -91,6 +91,19 @@ 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, + 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 ( self.access_name == other.access_name @@ -102,8 +115,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): + def copy(self, decanonicalize=False, implied_zone=''): other = copy.deepcopy(self) + if decanonicalize: replacement_string = { "read object": "read", @@ -112,6 +126,11 @@ 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 + + # 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 def __repr__(self): @@ -121,6 +140,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/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/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 diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index bf32dc28..58ac26f8 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,35 @@ 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 apply_atomic_acl_operations(self, logical_path : str, *operations, admin=False): + 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( + "RODS_API_REQ", + JSON_Message(request_text, conn.server_version), + int_info=api_number["ATOMIC_APPLY_ACL_OPERATIONS_APN"], + ) + 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..22e25c12 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 @@ -497,6 +497,44 @@ def test_iRODSAccess_cannot_be_constructed_using_unsupported_type__issue_558(sel self.sess, ) + def test_atomic_acls_505(self): + ses = self.sess + 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.apply_atomic_acl_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), + ) + + normalize = lambda access: access.copy(decanonicalize=True, implied_zone=ses.zone) + + 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. + 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: + zone.remove() if __name__ == "__main__": # let the tests find the parent irods lib