Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion irods/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@
self.user_zone = user_zone
self.user_type = user_type

def __lt__(self, other):

Check failure on line 94 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D105

D105: Missing docstring in magic method [pydocstyle:undocumented-magic-method]
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)

Check failure on line 104 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
)

def __eq__(self, other):
return (
self.access_name == other.access_name
Expand All @@ -102,8 +115,9 @@
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=''):

Check failure on line 118 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D102

D102: Missing docstring in public method [pydocstyle:undocumented-public-method]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is implied_zone?
Do we expect users of the PRC to ever use this parameter?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's useful for comparison purposes if you can tell __eq__ that a null length zone field implies the current zone name.

other = copy.deepcopy(self)

if decanonicalize:
replacement_string = {
"read object": "read",
Expand All @@ -112,6 +126,11 @@
"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):
Expand All @@ -121,6 +140,43 @@
return f"<iRODSAccess {access_name} {self.path} {self.user_name}{user_type_hint} {self.user_zone}>"


class ACLOperation(iRODSAccess):

Check failure on line 143 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D101

D101: Missing docstring in public class [pydocstyle:undocumented-public-class]

Check failure on line 143 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff PLW1641

PLW1641: Object does not implement `__hash__` method [Pylint:eq-without-hash]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason behind inheriting from iRODSAccess?
Are there benefits to tying these types together?

Copy link
Collaborator Author

@d-w-moore d-w-moore Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benefits are basically that we can test equality and therefore in operator should work when mixing types up and down the inheritance hierarchy. So:

ACLOperation('own','bob') in [iRODSAccess('own','/my/object/path','bob')] 

is True. This is leveraged in the test to affirm that the ACLOperations just atomically set are included in the list of iRODSAccess objects returned by session.acls.get.

I think the only other benefit is code reuse.


def __init__(self, access_name: str, user_name: str="", user_zone: str=""):

Check failure on line 145 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D107

D107: Missing docstring in `__init__` [pydocstyle:undocumented-public-init]
super().__init__(
access_name=access_name,
path="",
user_name=user_name,
user_zone=user_zone,
)

def __eq__(self, other):

Check failure on line 153 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D105

D105: Missing docstring in magic method [pydocstyle:undocumented-magic-method]
return (
self.access_name,
self.user_name,
self.user_zone,
) == (
other.access_name,
other.user_name,
other.user_zone,
)

def __lt__(self, other):

Check failure on line 164 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D105

D105: Missing docstring in magic method [pydocstyle:undocumented-magic-method]
return (
self.access_name,
self.user_name,
self.user_zone,
) < (
other.access_name,
other.user_name,
other.user_zone,
)

def __repr__(self):

Check failure on line 175 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D105

D105: Missing docstring in magic method [pydocstyle:undocumented-magic-method]
return f"<ACLOperation: access_name={self.access_name!r} "\
f"user_name={self.user_name!r} "\

Check failure on line 177 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff ISC002

ISC002: Implicitly concatenated string literals over multiple lines [flake8-implicit-str-concat:multi-line-implicit-string-concatenation]
f"user_zone={self.user_zone!r}>"

Check failure on line 178 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff ISC002

ISC002: Implicitly concatenated string literals over multiple lines [flake8-implicit-str-concat:multi-line-implicit-string-concatenation]

Check failure on line 178 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting

class _iRODSAccess_pre_4_3_0(iRODSAccess):
codes = collections.OrderedDict(
(key.replace("_", " "), value)
Expand Down
1 change: 1 addition & 0 deletions irods/api_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions irods/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 31 additions & 1 deletion irods/manager/access_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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

Expand All @@ -36,6 +37,35 @@


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]

Check failure on line 55 in irods/manager/access_manager.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
}

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:
Expand Down
40 changes: 39 additions & 1 deletion irods/test/access_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -497,6 +497,44 @@
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),

Check failure on line 514 in irods/test/access_test.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
)

normalize = lambda access: access.copy(decanonicalize=True, implied_zone=ses.zone)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this line do?

Copy link
Collaborator Author

@d-w-moore d-w-moore Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used to "flatten" iRODSAccess and ACLOperations to render common values into the appropriate fields to satisfy eq , ie. allow them to be "de facto equal" for test's purposes when they should be considered equivalent. THus we can effectively satisfy, for instance:

normalize( iRODSAccess('read', '/some/path','bob') )== normalize( ACLOperation('read_object','bob','tempZone'))


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
Expand Down
Loading