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
1 change: 0 additions & 1 deletion irods/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"null",
)


class _Access_LookupMeta(type):
@staticmethod
def _codes():
Expand Down
138 changes: 137 additions & 1 deletion irods/manager/access_manager.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
"""The access manager is a collection of methods useful for managing iRODS ACLs."""

import logging
from os.path import basename, dirname
import typing

from irods.access import iRODSAccess
from irods.api_number import api_number
from irods.collection import iRODSCollection
from irods.column import In
from irods.data_object import irods_basename, irods_dirname, iRODSDataObject
from irods.exception import CAT_NO_ROWS_FOUND
from irods.manager import Manager
from irods.message import JSON_Message, ModAclRequest, iRODSMessage
from irods.models import (
Collection,
CollectionAccess,
CollectionUser,
DataAccess,
DataObject,
Group,
User,
)
from irods.user import iRODSUser
from irods.query import SpecificQuery
from irods.user import iRODSUser, iRODSGroup

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff unsorted-imports

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


logger = logging.getLogger(__name__)

Expand All @@ -35,7 +40,138 @@
return [iRODSUser(session.users, i) for i in session.query(User.id, User.name, User.type, User.zone).filter(*cond)]


class QueryParameterError(ValueError):

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff undocumented-public-class

undocumented-public-class: Missing docstring in public class [check:undocumented-public-class]
pass

# This is the alias where we expect to find a specific query
alias_for_get_maximum_permission_for_path = 'get_maximum_permission_for_path'

class _iRODSPermission(typing.NamedTuple):

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff invalid-class-name

invalid-class-name: Class name `_iRODSPermission` should use CapWords convention [check:invalid-class-name]
as_string: str
as_integer: int

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting

class AccessManager(Manager):
def get_maximum_permission_for_path(self,
Comment thread
d-w-moore marked this conversation as resolved.
Resolved
user : [iRODSUser|iRODSGroup],

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff future-required-type-annotation

future-required-type-annotation: Missing `from __future__ import annotations`, but uses PEP 604 union [check:future-required-type-annotation]
coll : [str|iRODSCollection|None]=None,

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff future-required-type-annotation

future-required-type-annotation: Missing `from __future__ import annotations`, but uses PEP 604 union [check:future-required-type-annotation]

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff future-required-type-annotation

future-required-type-annotation: Missing `from __future__ import annotations`, but uses PEP 604 union [check:future-required-type-annotation]
data : [str|iRODSDataObject|None]=None,

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff future-required-type-annotation

future-required-type-annotation: Missing `from __future__ import annotations`, but uses PEP 604 union [check:future-required-type-annotation]

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff future-required-type-annotation

future-required-type-annotation: Missing `from __future__ import annotations`, but uses PEP 604 union [check:future-required-type-annotation]
specific_query : [SpecificQuery|str|None]='',

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff future-required-type-annotation

future-required-type-annotation: Missing `from __future__ import annotations`, but uses PEP 604 union [check:future-required-type-annotation]

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff future-required-type-annotation

future-required-type-annotation: Missing `from __future__ import annotations`, but uses PEP 604 union [check:future-required-type-annotation]
) -> int|str:
"""
Return the highest permission the named user/group has with respect to the
given data object or collection. In calculating this result, all ACLs and
group memberships are factored in.

Only one of 'data' or 'coll' may be specified.

Args:
user: an iRODSUser or iRODSGroup object for whom the permission is to be calculated.
coll: an iRODS logical path or iRODSCollection.
data: an iRODS logical path or iRODSDataObject.
specific_query (default ""): a SpecificQuery object or an alias specifying
the specific query used for the calculation. The default value of "" will be
interpreted to mean the designated specific query preconfigured for this express
purpose. If the server does not cache a query under that alias, or if None is
given as a value, then the calculation will be done via a general query.

Returns:
a named tuple representing the permission results in terms of both the symbolic
string (<retval>.as_string) and integer code (<retval>.as_integer). If no ACLs
exist granting permission for the named user or group, then None is returned.
"""
ses = self.sess

# Convert user or group to object. Need to know if we are dealing with a rodsadmin
if isinstance(user, (iRODSUser, iRODSGroup)):
pass
else:
msg = "'user' argument should be of type iRODSUser or iRODSGroup."
raise QueryParameterError(msg)

_specific_query = None

if specific_query == '':
specific_query = alias_for_get_maximum_permission_for_path

if isinstance(specific_query, SpecificQuery):
_specific_query = specific_query
elif isinstance(specific_query, str):
_specific_query = SpecificQuery(ses, alias=specific_query)
logger=logging.getLogger('issue_561_query_type_detection')
# Dispatch to the available/chosen query method.
if _specific_query is not None:
logger.debug('SPECIFIC')
def denullify(s):
return s if s else "x"
_specific_query.reset_for_new_query(
args=[
user.name,
getattr(user,'zone',ses.zone),
denullify(coll.path if isinstance(coll, iRODSCollection) else coll),
denullify(data.path if isinstance(data, iRODSDataObject) else data),
]
)
max_permission = self._get_maximum_permission_for_path_via_SpecificQuery(_specific_query)
res = [max_permission]
else:
logger.debug('GENERAL')
res = None
groups = {g[Group.id] for g in ses.query(Group, User).filter(Group.id != user.id, User.id == user.id)}
groups |= {user.id}

if data:
if isinstance(data, str):
collN = irods_dirname(data)
dataN = irods_basename(data)
join_criteria = [DataObject.name == dataN, Collection.name == collN]
elif isinstance(data, iRODSDataObject):
join_criteria = [ DataAccess.data_id == data.id]
else:
msg="'data' parameter must be full logical path or data object"
raise ValueError(msg)

q = ses.query(DataAccess.type).filter(
In(DataAccess.user_id, groups),
*join_criteria
).order_by(DataAccess.type,order='desc')

res = [_[DataAccess.type] for _ in q]

elif coll:
if isinstance(coll, str):
join_criteria = [Collection.name == coll]
elif isinstance(coll, iRODSCollection):
join_criteria = [CollectionAccess.access_id == coll.id] # TODO: access_id is not an accurate name, should be coll_id
else:
msg="'coll' parameter must be full logical path or collection object"
raise ValueError(msg)

q=ses.query(CollectionAccess).filter(
In(CollectionAccess.user_id, groups),
*join_criteria
).order_by(CollectionAccess.type,order='desc')

res = [_[CollectionAccess.type] for _ in q]

else:
msg = 'Need a collection or data object'
raise QueryParameterError(msg)

res = res[0] if res else None
return _iRODSPermission(iRODSAccess.strings[res], as_integer=res) if isinstance(res, int) else None

@staticmethod
def _get_maximum_permission_for_path_via_SpecificQuery(spec_query):
try:
row = next(iter(
spec_query.execute()
))

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
except (StopIteration, CAT_NO_ROWS_FOUND):
row = {}

highest_perm = row.get(0)
return None if highest_perm is None else int(highest_perm)

@staticmethod
def _to_acl_operation_json(op_input: iRODSAccess):
return {
Expand Down
8 changes: 8 additions & 0 deletions irods/manager/user_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
UserDoesNotExist,
GroupDoesNotExist,
NoResultFound,
MultipleResultsFound,
CAT_SQL_ERR,
)
from irods.api_number import api_number
Expand Down Expand Up @@ -281,12 +282,19 @@

class GroupManager(UserManager):
def get(self, name, user_zone=""):
if user_zone not in ("", local_zone:=self.sess.zone):

Check failure on line 285 in irods/manager/user_manager.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
msg = 'Specified user_zone must be local, since remote groups are not listed locally.'
raise GroupDoesNotExist(msg)

query = self.sess.query(Group).filter(Group.name == name)

try:
result = query.one()
except MultipleResultsFound:
result = query.filter(User.name == name, User.zone == local_zone).one()
except NoResultFound:
raise GroupDoesNotExist()

return iRODSGroup(self, result)

def _api_info(self, group_admin_flag):
Expand Down
8 changes: 8 additions & 0 deletions irods/query.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import OrderedDict
import uuid

from irods import MAX_SQL_ROWS
from irods.models import Model
Expand Down Expand Up @@ -290,6 +291,11 @@
self._columns = columns
self._args = args or []

def reset_for_new_query(self, args=None):
self._continue_index = 0
if args is not None:
self._args = args

def register(self):
if not self._sql:
raise ValueError("Empty query")
Expand Down Expand Up @@ -363,7 +369,9 @@
yield result




# Record a copy of the original value of IRODS_QUERY_LIMIT for possible future access.

Check failure on line 374 in irods/query.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
import irods.helpers as helpers

cached_values = helpers.create_value_cache(globals())
Expand Down
128 changes: 127 additions & 1 deletion irods/test/access_test.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
#! /usr/bin/env python

import io
import logging
import os
import sys
import textwrap
import unittest
import uuid

import irods.access
from irods.access import ACLOperation, iRODSAccess
from irods.collection import iRODSCollection
from irods.column import In, Like
from irods.exception import UserDoesNotExist
from irods.exception import CAT_NO_ROWS_FOUND, UserDoesNotExist
from irods.models import Collection, DataObject, User
from irods.path import iRODSPath
from irods.query import SpecificQuery
from irods.session import iRODSSession
from irods.test import helpers
from irods.user import iRODSUser

from irods.manager.access_manager import alias_for_get_maximum_permission_for_path


class TestAccess(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -532,6 +540,124 @@
if zone:
zone.remove()

def test_get_maximum_permission_for_path__issue_561(self):
SQL = textwrap.dedent('''
WITH target_user AS (
SELECT user_id
FROM r_user_main
WHERE user_name = ?
AND zone_name = ?
),
resolved_object AS (
-- The path names a collection
SELECT coll_id AS object_id
FROM r_coll_main
WHERE coll_name = ?

UNION

-- The path names a data object: collection path + '/' + data name
SELECT d.data_id AS object_id
FROM r_data_main AS d
JOIN r_coll_main AS c ON c.coll_id = d.coll_id
WHERE CONCAT(c.coll_name, '/', d.data_name) = ?
),
grants AS (
-- Direct grants
SELECT a.access_type_id
FROM r_objt_access AS a
JOIN target_user AS u ON u.user_id = a.user_id
JOIN resolved_object AS o ON o.object_id = a.object_id

UNION ALL

-- Grants to groups the user belongs to
SELECT a.access_type_id
FROM r_user_group AS ug
JOIN target_user AS u ON u.user_id = ug.user_id
JOIN r_objt_access AS a ON a.user_id = ug.group_user_id
JOIN resolved_object AS o ON o.object_id = a.object_id
)
SELECT access_type_id
FROM grants
ORDER BY access_type_id DESC
LIMIT 1;'''
)
user = group = coll = None
ses = self.sess
my_specific_query=None
query_alias = None
try:
coll = ses.collections.create('/tempZone/home/public/folder_561')
data = ses.data_objects.create('/tempZone/home/public/folder_561/file')
user = ses.users.create('bob','rodsuser')
group = ses.groups.create('lab')

# Set up test scenario with a new user and group, the group having deliberately higher permission
# so as to be able to account for the effect of group membership in the query results.
USER_PERM = 'modify_metadata'
GROUP_PERM = 'modify_object'
test_perms = { user.name: USER_PERM, group.name: GROUP_PERM }
for obj in (coll, data):
ses.acls.set(iRODSAccess(USER_PERM, obj.path, user.name))
ses.acls.set(iRODSAccess(GROUP_PERM, obj.path, group.name))

sql_arg = {}
try:
SpecificQuery(ses,
alias='findQueryByAlias',
args=(query_alias:=alias_for_get_maximum_permission_for_path,)).execute()
except CAT_NO_ROWS_FOUND:
query_alias = str(uuid.uuid1()),
sql_arg = {'sql':SQL}

my_specific_query = SpecificQuery(ses, alias=query_alias, **sql_arg)
if sql_arg:
my_specific_query.register()

logger=logging.getLogger('issue_561_query_type_detection')
logger.setLevel(logging.DEBUG)
for is_member in (False, True):
if is_member:
group.addmember(user.name,user.zone)
for usr in (user, group):
max_user = usr if usr not in group.members else group
expected_permission = test_perms[max_user.name]
for obj_type, obj in (
("coll", coll),
("coll", coll.path),
("data", data),
("data", data.path),
):
query_options = {
'GENERAL':{'specific_query':None},
'SPECIFIC':({} if not my_specific_query else {'specific_query':my_specific_query})
}
for query_index in ('SPECIFIC', 'GENERAL'):
with self.subTest(f"Issue 561 subTest: {is_member=} {usr=} {obj=} {query_index=}"):
try:
logger.addHandler(handler:=logging.StreamHandler(stream:=io.StringIO()))

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

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
permission = ses.acls.get_maximum_permission_for_path(
usr,
**query_options[query_index],
**{obj_type: obj},
).as_string
self.assertEqual(permission, expected_permission)
# Check logging output to verify which code path was taken.
self.assertEqual(stream.getvalue().rstrip(), query_index)
finally:
logger.removeHandler(handler)
finally:
if coll:
# Removes test data object as well.
coll.remove(force=True, recurse=True)
if user:
user.remove()
if group:
group.remove()
if my_specific_query and (query_alias != alias_for_get_maximum_permission_for_path):
my_specific_query.remove()


if __name__ == "__main__":
# let the tests find the parent irods lib
Expand Down
Loading
Loading