Skip to content

Commit c671140

Browse files
committed
doc updates, review and ruff changes, others as follows:
- add true ACLOperation/iRODSAccess canonicalization for api calls and comparison - hash function for ACLOperation - extra normalizer method "normal" and consistent eq/hash semantics for use as dict keys and/or set members.
1 parent 32f6a24 commit c671140

6 files changed

Lines changed: 227 additions & 97 deletions

File tree

README.md

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2118,32 +2118,65 @@ membership, this can be achieved with another query.
21182118
`<session>.permissions` was therefore removed in v2.0.0
21192119
in favor of `<session>.acls`.
21202120

2121-
Atomic ACLs
2122-
-----------
2121+
Atomically setting permissions
2122+
------------------------------
21232123

21242124
A list of permissions may be added to an object atomically using
2125-
the AccessManager's apply_atomic_operations method:
2126-
```
2125+
the AccessManager's `apply_atomic_operations` method:
2126+
```py
21272127
from irods.access import ACLOperation
21282128
from irods.helpers import home_collection
21292129
session = irods.helpers.make_session()
2130-
myCollection = session.collections.create(f"{home_collection(session).path}/newCollection")
2130+
myCollection = session.collections.create(f"{home_collection(session)}/newCollection")
2131+
2132+
session.acls.apply_atomic_operations(
2133+
myCollection.path,
2134+
*[
2135+
ACLOperation("read", "public"),
2136+
ACLOperation("write", "bob", "otherZone")
2137+
]
2138+
)
2139+
```
2140+
`ACLOperation` objects form a linear order with `iRODSAccess` objects, and
2141+
indeed are subclassed from them as well, allowing equivalence comparisons and
2142+
also permitting intermixed sequences to be sorted (using the `__lt__` method
2143+
if no sort `key` parameter is given).
2144+
2145+
Care should be taken however to normalize the objects before such comparisons
2146+
and sorting, and with connected uses of the `in` operator (which leverages `__eq__`).
2147+
2148+
The following code sorts the objects based on their lexical order starting with the
2149+
normalized `access_name`, which serves to group identical permissions together:
2150+
```py
2151+
from irods.access import *
2152+
import irods.helpers
2153+
acls = [
2154+
iRODSAccess('read_object', '/tempZone/home/alice', 'bob', 'tempZone'),
2155+
ACLOperation('write', 'rods'),
2156+
ACLOperation('read', 'bob'),
2157+
]
21312158

2132-
session.acls.apply_atomic_operations(myCollection.path,
2133-
*[ACLOperation("read", "public"),
2134-
ACLOperation("write", "bob", "otherZone")
2135-
])
2136-
```
2137-
ACLOperation objects form a linear order with iRODSAccess objects, and
2138-
indeed are subclassed from them as well, allowing equivalency testing:
2159+
session = irods.helpers.make_session()
2160+
N = lambda acl: acl.normalize(local_zone=session.zone)
21392161

2140-
Thus, for example:
2162+
print(N(acls[0]) == N(acls[2]))
2163+
acls.sort(key=N)
2164+
print(N(iRODSAccess('read', '', 'bob')) in map(N, acls))
21412165
```
2142-
ACLOperation('read','public') in sess.acls.get(object)
2166+
2167+
If strict order of permissions is desired, we can use code such as the following:
2168+
```py
2169+
from irods.access import *
2170+
from pprint import pp
2171+
pp(sorted(
2172+
[
2173+
ACLOperation('read', 'bob' ),
2174+
ACLOperation('own', 'rods'),
2175+
ACLOperation('read_object', 'alice')
2176+
],
2177+
key=lambda acl: (all_permissions[acl.access_name], acl.normalize())
2178+
))
21432179
```
2144-
is a valid operation, so that an application that tends to cache object
2145-
permissions client-side might use such checks in optimizing atomic ACL
2146-
requests against the inclusion of any redundant ACLOperations.
21472180

21482181
Quotas (v2.0.0)
21492182
---------------

irods/access.py

Lines changed: 133 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22
import copy
33
from irods.collection import iRODSCollection
44
from irods.data_object import iRODSDataObject
5-
from irods.path import iRODSPath
65

7-
8-
_ichmod_listed_permissions = (
6+
_permissions = (
97
"own",
108
"delete_object",
119
"write",
@@ -22,46 +20,45 @@
2220

2321

2422
class _Access_LookupMeta(type):
25-
2623
@staticmethod
2724
def _codes():
2825
return collections.OrderedDict(
2926
(key_, value_)
3027
for key_, value_ in sorted(
31-
dict(
32-
# copied from iRODS source code in
28+
{
29+
# adapted from iRODS source code in
3330
# ./server/core/include/irods/catalog_utilities.hpp:
34-
null=1000,
35-
execute=1010,
36-
read_annotation=1020,
37-
read_system_metadata=1030,
38-
read_metadata=1040,
39-
read_object=1050,
40-
write_annotation=1060,
41-
create_metadata=1070,
42-
modify_metadata=1080,
43-
delete_metadata=1090,
44-
administer_object=1100,
45-
create_object=1110,
46-
modify_object=1120,
47-
delete_object=1130,
48-
create_token=1140,
49-
delete_token=1150,
50-
curate=1160,
51-
own=1200,
52-
).items(),
31+
"null": 1000,
32+
"execute": 1010,
33+
"read_annotation": 1020,
34+
"read_system_metadata": 1030,
35+
"read_metadata": 1040,
36+
"read_object": 1050,
37+
"write_annotation": 1060,
38+
"create_metadata": 1070,
39+
"modify_metadata": 1080,
40+
"delete_metadata": 1090,
41+
"administer_object": 1100,
42+
"create_object": 1110,
43+
"modify_object": 1120,
44+
"delete_object": 1130,
45+
"create_token": 1140,
46+
"delete_token": 1150,
47+
"curate": 1160,
48+
"own": 1200,
49+
}.items(),
5350
key=lambda _: _[1],
5451
)
55-
if key_ in _ichmod_listed_permissions
52+
if key_ in _permissions
5653
)
5754

5855
@property
59-
def codes(metaclass_target):
60-
return metaclass_target._codes()
56+
def codes(cls):
57+
return cls._codes()
6158

6259
@property
63-
def strings(metaclass_target):
64-
return collections.OrderedDict((number, string) for string, number in metaclass_target._codes().items())
60+
def strings(cls):
61+
return collections.OrderedDict((number, string) for string, number in cls._codes().items())
6562

6663
def __getitem__(self, key):
6764
return self.codes[key]
@@ -102,37 +99,95 @@ def __init__(self, access_name, path, user_name, user_zone, user_type):
10299
self.user_type = user_type
103100

104101
def __lt__(self, other):
105-
return (self.access_name, self.user_name, self.user_zone, iRODSPath(self.path)) < (
102+
return (self.access_name, self.user_name, self.user_zone, str(self.path)) < (
106103
other.access_name,
107104
other.user_name,
108105
other.user_zone,
109-
iRODSPath(other.path),
106+
str(other.path),
110107
)
111108

112109
def __eq__(self, other):
113110
return (
114111
self.access_name == other.access_name
115-
and iRODSPath(self.path) == iRODSPath(other.path)
112+
and str(self.path) == str(other.path)
116113
and self.user_name == other.user_name
117114
and self.user_zone == other.user_zone
118115
)
119116

120117
def __hash__(self):
121-
return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone))
118+
return hash((self.access_name, str(self.path), self.user_name, self.user_zone))
119+
120+
def normalize(self, local_zone=""):
121+
"""
122+
Create a normalized version of the object for comparison in sorting or determining equivalence.
123+
124+
Args:
125+
local_zone: the name of the home zone, if any, in which client user directly authenticates.
126+
The purpose is zone name normalization; if this parameter is a nonzero-length string which
127+
matches the zone_name in the source object, the copy will contain a null zone_name field.
128+
129+
Returns:
130+
The normalized copy of the source object. In practice, this will be an ACLOperation or iRODSAccess
131+
object, according to the type of the source object.
132+
"""
133+
normalized_form = self.copy(decanonicalize=-1, implied_zone=local_zone)
134+
normalized_form.path = ""
135+
return normalized_form
122136

123137
def copy(self, decanonicalize=False, implied_zone=''):
138+
"""
139+
Create a copy of the object, possibly in a normalized form.
140+
141+
Args:
142+
decanonicalize: Whether to modify the access_name field to a more human-readable form
143+
(when 1 or True) or a more standard form (when -1). If the former, then a more
144+
organic style is favored, i.e. "read" and "write". If the latter, the new
145+
access_name will be more machine-friendly for operators __lt__ (for sorting) and
146+
__eq__ (for equivalence or use with 'in'). If equal to 0 (or False), no adjustment
147+
is done.
148+
implied_zone: If a nonzero-length name, compare this against the zone_name field of the
149+
old object, and if they match, force the zone_name to zero-length in the new object.
150+
151+
Returns:
152+
A copy of the invoking object, normalized if requested.
153+
154+
Raises:
155+
RuntimeError: if decanonicalize parameter is not one of {-1,0,False,1,True}.
156+
"""
124157
other = copy.deepcopy(self)
125158

126-
if decanonicalize:
127-
replacement_string = {
128-
"read object": "read",
129-
"read_object": "read",
130-
"modify object": "write",
131-
"modify_object": "write",
132-
}.get(self.access_name)
133-
other.access_name = replacement_string if replacement_string is not None else self.access_name
159+
access_name = self.access_name
160+
161+
if decanonicalize == 1:
162+
if (
163+
new_access_name := {
164+
"read object": "read",
165+
"read_object": "read",
166+
"modify object": "write",
167+
"modify_object": "write",
168+
}.get(access_name)
169+
) is not None:
170+
access_name = new_access_name
171+
elif decanonicalize == -1:
172+
# Canonicalize, ie. change out old access_name for an unambiguous "standard" value.
173+
access_name = access_name.replace(" ", "_")
174+
if (
175+
new_access_name := {
176+
"read": "read_object",
177+
"write": "modify_object",
178+
}.get(access_name)
179+
) is not None:
180+
access_name = new_access_name
181+
elif decanonicalize == 0:
182+
pass
183+
else:
184+
msg = "Improper value for 'decanonicalize' parameter"
185+
raise RuntimeError(msg)
186+
187+
other.access_name = access_name
134188

135-
# Useful if we wish to force an explicitly specified local zone to an implicit zone spec in the copy, for equality testing:
189+
# Useful if we wish to force an explicitly specified local zone to an implicit zone spec in the copy, for
190+
# equality testing:
136191
if '' != implied_zone == other.user_zone:
137192
other.user_zone = ''
138193

@@ -146,14 +201,32 @@ def __repr__(self):
146201

147202

148203
class iRODSAccess(_iRODSAccess_base, metaclass=_Access_LookupMeta):
149-
def __init__(self, access_name, path, user_name="", user_zone="", user_type=None):
204+
"""
205+
Represents an ACL in iRODS.
206+
207+
An instance of this class functions as a data container to convey information to the iRODS
208+
server (in the `set` call) and back again to the client again (in the `get` call).
209+
"""
210+
211+
def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): # noqa: D107
150212
self.codes = self.__class__.codes
151213
self.strings = self.__class__.strings
152214
super().__init__(access_name, path, user_name, user_zone, user_type)
153215

154216

155217
class ACLOperation(iRODSAccess):
156-
def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""):
218+
"""
219+
Represents an operation to be performed in iRODS' atomic ACL api.
220+
221+
Similar to its base class, iRODSAccess, this class names an ACL to be set on an object.
222+
It differs, however, in that it forgoes option to store a logical object path. (In the atomic
223+
API call, there is always a single logical path to which all such operations apply, thus
224+
it is appropriate that the path parameter is in a location separate from the operations.)
225+
""" # noqa: D400
226+
227+
# ruff: noqa: D105 on
228+
229+
def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""): # noqa: D107
157230
super().__init__(
158231
access_name=access_name,
159232
path="",
@@ -172,6 +245,16 @@ def __eq__(self, other):
172245
other.user_zone,
173246
)
174247

248+
def __hash__(self):
249+
250+
# Hash in a way consistent with an iRODSAccess having path "".
251+
return hash((
252+
self.access_name,
253+
"", # path
254+
self.user_name,
255+
self.user_zone,
256+
))
257+
175258
def __lt__(self, other):
176259
return (
177260
self.access_name,
@@ -186,19 +269,20 @@ def __lt__(self, other):
186269
def __repr__(self):
187270
return f"<ACLOperation {self.access_name} {self.user_name} {self.user_zone}>"
188271

272+
# ruff: noqa: D105 off
273+
189274

190275
(
191-
_ichmod_synonym_mapping := {
192-
# syn : canonical
276+
_synonym_mapping := {
193277
"write": "modify_object",
194278
"read": "read_object",
195279
}
196-
).update((key.replace("_", " "), key) for key in iRODSAccess.codes.keys())
280+
).update((key.replace("_", " "), key) for key in iRODSAccess.codes)
197281

198282

199283
all_permissions = {
200284
**iRODSAccess.codes,
201-
**{key: iRODSAccess.codes[_ichmod_synonym_mapping[key]] for key in _ichmod_synonym_mapping},
285+
**{key: iRODSAccess.codes[_synonym_mapping[key]] for key in _synonym_mapping},
202286
}
203287

204288

irods/exception.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
# if you're copying these from the docs, you might find the following regex helpful:
22
# s/\(\w\+\)\s\+\(-\d\+\)/class \1(SystemException):\r code = \2/g
33

4-
54
import errno
65
import numbers
76
import os
87
import sys
98
from typing import Dict
109

1110

11+
# ruff: noqa: N801 D101 off
12+
13+
1214
class PycommandsException(Exception):
1315
pass
1416

@@ -2124,3 +2126,6 @@ class PAM_AUTH_PASSWORD_FAILED(PAMException):
21242126

21252127
class PAM_AUTH_PASSWORD_INVALID_TTL(PAMException):
21262128
code = -994000
2129+
2130+
2131+
# ruff: noqa: N801 D101 on

0 commit comments

Comments
 (0)