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
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2118,6 +2118,62 @@ membership, this can be achieved with another query.
`<session>.permissions` was therefore removed in v2.0.0
in favor of `<session>.acls`.

Atomically setting permissions
------------------------------

A list of permissions may be added to an object atomically using
the AccessManager's `apply_atomic_operations` method:
```py
from irods.access import ACLOperation
from irods.helpers import home_collection
session = irods.helpers.make_session()
myCollection = session.collections.create(f"{home_collection(session)}/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 equivalence comparisons and
also permitting intermixed sequences to be sorted (using the `__lt__` method
if no sort `key` parameter is given).

Care should be taken however to normalize the objects before such comparisons
and sorting, and with connected uses of the `in` operator (which leverages `__eq__`).

The following code sorts the objects based on their lexical order starting with the
normalized `access_name`, which serves to group identical permissions together:
```py
from irods.access import *
normalize = lambda acl: acl.copy(decanonicalize=-1, implied_zone='tempZone')
acls = [
iRODSAccess('read_object', '/tempZone/home/alice', 'bob', 'tempZone'),
ACLOperation('write', 'rods'),
ACLOperation('read', 'bob'),
]
print(normalize(acls[0]) == normalize(acls[2]))
acls.sort(key=normalize)
print(normalize(iRODSAccess('read', '/tempZone/home/alice', 'bob')) in map(normalize, acls))
```

If strict order of permissions is desired, we can use code such as the following:
```py
from irods.access import *
from pprint import pp
pp(sorted(
[
ACLOperation('read', 'bob'),
ACLOperation('own', 'rods'),
ACLOperation('read_object', 'alice')
],
key=lambda acl: (all_permissions[acl.access_name], normalize(acl))
))
```

Quotas (v2.0.0)
---------------

Expand Down
252 changes: 191 additions & 61 deletions irods/access.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,70 @@
import collections
import copy
import warnings

from irods.collection import iRODSCollection
from irods.data_object import iRODSDataObject
from irods.path import iRODSPath

Check failure on line 7 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff I001

I001: Import block is un-sorted or un-formatted [isort:unsorted-imports]


_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):

@staticmethod

Check failure on line 28 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
def _codes():
return collections.OrderedDict(
(key_, value_)
for key_, value_ in sorted(
dict(
# copied from iRODS source code in
# ./server/core/include/irods/catalog_utilities.hpp:
null=1000,
execute=1010,
read_annotation=1020,
read_system_metadata=1030,
read_metadata=1040,
read_object=1050,
write_annotation=1060,
create_metadata=1070,
modify_metadata=1080,
delete_metadata=1090,
administer_object=1100,
create_object=1110,
modify_object=1120,
delete_object=1130,
create_token=1140,
delete_token=1150,
curate=1160,
own=1200,
).items(),

Check failure on line 54 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff C408

C408: Unnecessary `dict()` call (rewrite as a literal) [flake8-comprehensions:unnecessary-collection-call]
key=lambda _: _[1],
)
if key_ in _permissions
)

@property
def codes(metaclass_target):

Check failure on line 61 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff N804

N804: First argument of a class method should be named `cls` [pep8-naming:invalid-first-argument-name-for-class-method]
return metaclass_target._codes()

Check failure on line 62 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff SLF001

SLF001: Private member accessed: `_codes` [flake8-self:private-member-access]

@property
def strings(metaclass_target):

Check failure on line 65 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff N804

N804: First argument of a class method should be named `cls` [pep8-naming:invalid-first-argument-name-for-class-method]
return collections.OrderedDict((number, string) for string, number in metaclass_target._codes().items())

Check failure on line 66 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff SLF001

SLF001: Private member accessed: `_codes` [flake8-self:private-member-access]

def __getitem__(self, key):
return self.codes[key]

Expand All @@ -19,7 +78,7 @@
return list(zip(self.keys(), self.values()))


class iRODSAccess(metaclass=_Access_LookupMeta):
class _iRODSAccess_base:

Check failure on line 81 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff N801

N801: Class name `_iRODSAccess_base` should use CapWords convention [pep8-naming:invalid-class-name]
@classmethod
def to_int(cls, key):
return cls.codes[key]
Expand All @@ -28,54 +87,7 @@
def to_string(cls, key):
return cls.strings[key]

codes = collections.OrderedDict(
(key_, value_)
for key_, value_ in sorted(
dict(
# copied from iRODS source code in
# ./server/core/include/irods/catalog_utilities.hpp:
null=1000,
execute=1010,
read_annotation=1020,
read_system_metadata=1030,
read_metadata=1040,
read_object=1050,
write_annotation=1060,
create_metadata=1070,
modify_metadata=1080,
delete_metadata=1090,
administer_object=1100,
create_object=1110,
modify_object=1120,
delete_object=1130,
create_token=1140,
delete_token=1150,
curate=1160,
own=1200,
).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",
)
)

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

def __init__(self, access_name, path, user_name="", user_zone="", user_type=None):
def __init__(self, access_name, path, user_name, user_zone, user_type):
self.access_name = access_name
if isinstance(path, (iRODSCollection, iRODSDataObject)):
self.path = path.path
Expand All @@ -91,6 +103,14 @@
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
Expand All @@ -102,16 +122,50 @@
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=''):
"""
Create a copy of the object, possibly in a normalized form.

Args:
decanonicalize: Whether to modify to access_name field to a more human-readable (1/True) or more standard (-1) form.

Check failure on line 130 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff E501

E501: Line too long (128 > 120) [pycodestyle:line-too-long]
If the former, then a more organic style is favored, i.e. "read" and "write". If the latter, the new access_name

Check failure on line 131 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff E501

E501: Line too long (129 > 120) [pycodestyle:line-too-long]
will be more machine-friendly for operators __lt__ (for sorting) and __eq__ (for equivalence or use with 'in').

Check failure on line 132 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff E501

E501: Line too long (127 > 120) [pycodestyle:line-too-long]
implied_zone: If a nonzero-length name, compare this against the zone_name field of the old object, and if they match,
force the zone_name to zero-length in the new object.

Returns:
A copy of the invoking object, normalized if requested.
"""
other = copy.deepcopy(self)
if decanonicalize:
replacement_string = {

access_name = self.access_name

if decanonicalize == 1:
if (new_access_name := {
"read object": "read",
"read_object": "read",
"modify object": "write",
"modify_object": "write",
}.get(self.access_name)
other.access_name = replacement_string if replacement_string is not None else self.access_name
}.get(access_name)) != None: access_name = new_access_name
elif decanonicalize == -1:
# Canonicalize, ie. change out old access_name for an unambiguous "standard" value.
access_name = access_name.replace(" ","_")
if (new_access_name := {
"read": "read_object",
"write": "modify_object",
}.get(access_name)) != None: access_name = new_access_name

Check failure on line 156 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
elif decanonicalize == 0:
pass
else:
msg = "Improper value for 'decanonicalize' parameter"
raise RuntimerError(msg)

other.access_name = access_name

# 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 = ''

return other

def __repr__(self):
Expand All @@ -121,10 +175,86 @@
return f"<iRODSAccess {access_name} {self.path} {self.user_name}{user_type_hint} {self.user_zone}>"


class _iRODSAccess_pre_4_3_0(iRODSAccess):
codes = collections.OrderedDict(
(key.replace("_", " "), value)
for key, value in iRODSAccess.codes.items()
if key in ("own", "write", "modify_object", "read", "read_object", "null")
)
strings = collections.OrderedDict((number, string) for string, number in codes.items())
class iRODSAccess(_iRODSAccess_base, metaclass=_Access_LookupMeta):
def __init__(self, access_name, path, user_name="", user_zone="", user_type=None):
self.codes = self.__class__.codes
self.strings = self.__class__.strings
super().__init__(access_name, path, user_name, user_zone, user_type)


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"<ACLOperation {self.access_name} {self.user_name} {self.user_zone}>"


(
_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[_synonym_mapping[key]] for key in _synonym_mapping},
}

canonical_permissions = dict(
(k,v) for k,v in all_permissions.items() if ' ' not in k and k not in ('read','write')
)

class _deprecated:
class _iRODSAccess_pre_4_3_0(_iRODSAccess_base):
codes = collections.OrderedDict(
(key.replace("_", " "), value)
for key, value in iRODSAccess.codes.items()
if key in ("own", "write", "modify_object", "read", "read_object", "null")
)
strings = collections.OrderedDict((number, string) for string, number in codes.items())
def __init__(self, *args, **kwargs):
warnings.warn(
"_iRODSAccess_pre_4_3_0 is deprecated and will be removed in a future version. Use iRODSAccess instead.",
DeprecationWarning,
stacklevel=2
)
super().__init__(*args,**kwargs)

_deprecated_names = {'_iRODSAccess_pre_4_3_0':_deprecated._iRODSAccess_pre_4_3_0}

Check failure on line 254 in irods/access.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting

def __getattr__(name):
if name in _deprecated_names:
warnings.warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2)
return _deprecated_names[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
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
Loading
Loading