Skip to content

Commit 87079f7

Browse files
Align behavior with objects raising in __getattr__ (#157)
* tests: test C and PYTHON implementations in testPermissionRole * Align behavior with objects raising in `__getattr__` The observed problem was a behavior different between C and python implementation on python 3, happening with Zope python script. When the context can not be accessed by the current user, Zope binds a `Shared.DC.Scripts.Bindings.UnauthorizedBinding`, a class that raises an Unauthorized error when the context is actually accessed, in order to postpone the Unauthorized if something is actually accessed. This class does implements this by raising Unauthorized in `__getattr__`. The python implementation of `rolesForPermissionOn` used `hasattr` and `hasattr` has changed between python2 and python3, on python2 it was ignoring all exceptions, including potential Unauthorized errors and just returning False, but on python3 these errors are now raised. This change of behavior of python causes `rolesForPermissionOn` to behave differently: when using python implementation on python2 or when using C implementation, such Unauthorized errors were gracefully handled and caused `checkPermission` to return False, but on python3 the Unauthorized is raised. The C implementation of `rolesForPermissionOn` uses a construct equivalent to the python2 version of `hasattr`. For consistency - and because ignoring errors is usually not good - we also want to change it to be have like the python3 implementation. This change make this scenario behave the same between python and C implementations: - `Unauthorized` errors raised in `__getattr__` are supported on py3. - Other errors than `AttributeError` and `Unauthorized` raised in `__getattr__` are no longer ignored in the C implementation. Co-authored-by: Dieter Maurer <[email protected]>
1 parent 94282bd commit 87079f7

File tree

5 files changed

+134
-50
lines changed

5 files changed

+134
-50
lines changed

CHANGES.rst

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ For changes before version 3.0, see ``HISTORY.rst``.
88

99
- Respect ``PURE_PYTHON`` environment variable set to ``0`` when running tests.
1010

11+
- Let the roles access in ``rolesForPermissionOn`` interpret ``AttributeError`` and ``Unauthorized`` as "no roles definition for this permission at this object" and report any other exception (for the Python and C implementation). We have to treat ``Unauthorized`` like ``AttributeError`` to support ``Shared.DC.Scripts.Bindings.UnauthorizedBinding`` which raises ``Unauthorized`` for any access.
12+
1113

1214
7.0 (2024-05-30)
1315
----------------

src/AccessControl/ImplPython.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from Acquisition import aq_parent
3232
from ExtensionClass import Base
3333
from zope.interface import implementer
34+
from zExceptions import Unauthorized as zExceptions_Unauthorized
3435

3536
PURE_PYTHON = int(os.environ.get('PURE_PYTHON', '0'))
3637
if PURE_PYTHON:
@@ -71,8 +72,11 @@ def rolesForPermissionOn(perm, object, default=_default_roles, n=None):
7172
r = None
7273

7374
while True:
74-
if hasattr(object, n):
75+
try:
7576
roles = getattr(object, n)
77+
except (AttributeError, zExceptions_Unauthorized):
78+
pass
79+
else:
7680
if roles is None:
7781
if _embed_permission_in_roles:
7882
return (('Anonymous',), n)

src/AccessControl/cAccessControl.c

+24-2
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ static PyExtensionClass imPermissionRoleType = {
652652
static PyObject *Containers = NULL;
653653
static PyObject *ContainerAssertions = NULL;
654654
static PyObject *Unauthorized = NULL;
655+
static PyObject *zExceptions_Unauthorized = NULL;
655656
static PyObject *warn= NULL;
656657
static PyObject *NoSequenceFormat = NULL;
657658
static PyObject *_what_not_even_god_should_do = NULL;
@@ -1847,15 +1848,27 @@ c_rolesForPermissionOn(PyObject *perm, PyObject *object,
18471848
Py_INCREF(r);
18481849

18491850
/*
1850-
while 1:
1851+
while True:
18511852
*/
18521853
while (1)
18531854
{
18541855
/*
1855-
if hasattr(object, n):
1856+
try:
18561857
roles = getattr(object, n)
1858+
except (AttributeError, zExceptions_Unauthorized):
1859+
pass
1860+
else:
18571861
*/
18581862
PyObject *roles = PyObject_GetAttr(object, n);
1863+
if (roles == NULL)
1864+
{
1865+
if (! (PyErr_ExceptionMatches(PyExc_AttributeError)
1866+
|| PyErr_ExceptionMatches(zExceptions_Unauthorized)))
1867+
{
1868+
/* re-raise */
1869+
return NULL;
1870+
}
1871+
}
18591872
if (roles != NULL)
18601873
{
18611874

@@ -2313,6 +2326,7 @@ static struct PyMethodDef dtml_methods[] = {
23132326
*/
23142327
#define IMPORT(module, name) if ((module = PyImport_ImportModule(name)) == NULL) return NULL;
23152328
#define GETATTR(module, name) if ((name = PyObject_GetAttrString(module, #name)) == NULL) return NULL;
2329+
#define GETATTR_AS(module, name, as_name) if ((as_name = PyObject_GetAttrString(module, name)) == NULL) return NULL;
23162330

23172331
static struct PyModuleDef moduledef =
23182332
{
@@ -2400,6 +2414,14 @@ module_init(void) {
24002414
Py_DECREF(tmp);
24012415
tmp = NULL;
24022416

2417+
/*| from zExceptions import Unauthorized as zExceptions_Unauthorized
2418+
*/
2419+
2420+
IMPORT(tmp, "zExceptions");
2421+
GETATTR_AS(tmp, "Unauthorized", zExceptions_Unauthorized);
2422+
Py_DECREF(tmp);
2423+
tmp = NULL;
2424+
24032425
/*| from AccessControl.SecurityManagement import getSecurityManager
24042426
*/
24052427

src/AccessControl/tests/testPermissionRole.py

+88-47
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from Acquisition import Implicit
2020
from Acquisition import aq_base
2121

22-
from AccessControl.PermissionRole import PermissionRole
22+
from ..Implementation import PURE_PYTHON
2323

2424

2525
ViewPermission = 'View'
@@ -50,40 +50,39 @@ class PermissiveObject(Explicit):
5050
_Edit_Things__Permission = ['Anonymous']
5151

5252

53-
def assertPRoles(ob, permission, expect):
54-
"""
55-
Asserts that in the context of ob, the given permission maps to
56-
the given roles.
57-
"""
58-
pr = PermissionRole(permission)
59-
roles = pr.__of__(ob)
60-
roles2 = aq_base(pr).__of__(ob)
61-
assert roles == roles2 or tuple(roles) == tuple(roles2), (
62-
'Different methods of checking roles computed unequal results')
63-
same = 0
64-
if roles:
65-
# When verbose security is enabled, permission names are
66-
# embedded in the computed roles. Remove the permission
67-
# names.
68-
roles = [r for r in roles if not r.endswith('_Permission')]
69-
70-
if roles is None or expect is None:
71-
if (roles is None or tuple(roles) == ('Anonymous', )) and \
72-
(expect is None or tuple(expect) == ('Anonymous', )):
73-
same = 1
74-
else:
75-
got = {}
76-
for r in roles:
77-
got[r] = 1
78-
expected = {}
79-
for r in expect:
80-
expected[r] = 1
81-
if got == expected: # Dict compare does the Right Thing.
82-
same = 1
83-
assert same, f'Expected roles: {expect!r}, got: {roles!r}'
84-
85-
86-
class PermissionRoleTests (unittest.TestCase):
53+
class PermissionRoleTestBase:
54+
55+
def assertPRoles(self, ob, permission, expect):
56+
"""
57+
Asserts that in the context of ob, the given permission maps to
58+
the given roles.
59+
"""
60+
pr = self._getTargetClass()(permission)
61+
roles = pr.__of__(ob)
62+
roles2 = aq_base(pr).__of__(ob)
63+
assert roles == roles2 or tuple(roles) == tuple(roles2), (
64+
'Different methods of checking roles computed unequal results')
65+
same = 0
66+
if roles:
67+
# When verbose security is enabled, permission names are
68+
# embedded in the computed roles. Remove the permission
69+
# names.
70+
roles = [r for r in roles if not r.endswith('_Permission')]
71+
72+
if roles is None or expect is None:
73+
if (roles is None or tuple(roles) == ('Anonymous', )) and \
74+
(expect is None or tuple(expect) == ('Anonymous', )):
75+
same = 1
76+
else:
77+
got = {}
78+
for r in roles:
79+
got[r] = 1
80+
expected = {}
81+
for r in expect:
82+
expected[r] = 1
83+
if got == expected: # Dict compare does the Right Thing.
84+
same = 1
85+
self.assertTrue(same, f'Expected roles: {expect!r}, got: {roles!r}')
8786

8887
def testRestrictive(self, explicit=0):
8988
app = AppRoot()
@@ -93,9 +92,9 @@ def testRestrictive(self, explicit=0):
9392
app.c = ImplicitContainer()
9493
app.c.o = RestrictiveObject()
9594
o = app.c.o
96-
assertPRoles(o, ViewPermission, ('Manager', ))
97-
assertPRoles(o, EditThingsPermission, ('Manager', 'Owner'))
98-
assertPRoles(o, DeletePermission, ())
95+
self.assertPRoles(o, ViewPermission, ('Manager', ))
96+
self.assertPRoles(o, EditThingsPermission, ('Manager', 'Owner'))
97+
self.assertPRoles(o, DeletePermission, ())
9998

10099
def testPermissive(self, explicit=0):
101100
app = AppRoot()
@@ -105,25 +104,67 @@ def testPermissive(self, explicit=0):
105104
app.c = ImplicitContainer()
106105
app.c.o = PermissiveObject()
107106
o = app.c.o
108-
assertPRoles(o, ViewPermission, ('Anonymous', ))
109-
assertPRoles(o, EditThingsPermission, ('Anonymous',
110-
'Manager',
111-
'Owner'))
112-
assertPRoles(o, DeletePermission, ('Manager', ))
107+
self.assertPRoles(o, ViewPermission, ('Anonymous', ))
108+
self.assertPRoles(o, EditThingsPermission, ('Anonymous',
109+
'Manager',
110+
'Owner'))
111+
self.assertPRoles(o, DeletePermission, ('Manager', ))
113112

114113
def testExplicit(self):
115114
self.testRestrictive(1)
116115
self.testPermissive(1)
117116

118117
def testAppDefaults(self):
119118
o = AppRoot()
120-
assertPRoles(o, ViewPermission, ('Anonymous', ))
121-
assertPRoles(o, EditThingsPermission, ('Manager', 'Owner'))
122-
assertPRoles(o, DeletePermission, ('Manager', ))
119+
self.assertPRoles(o, ViewPermission, ('Anonymous', ))
120+
self.assertPRoles(o, EditThingsPermission, ('Manager', 'Owner'))
121+
self.assertPRoles(o, DeletePermission, ('Manager', ))
123122

124123
def testPermissionRoleSupportsGetattr(self):
125-
a = PermissionRole('a')
124+
a = self._getTargetClass()('a')
126125
self.assertEqual(getattr(a, '__roles__'), ('Manager', ))
127126
self.assertEqual(getattr(a, '_d'), ('Manager', ))
128127
self.assertEqual(getattr(a, '__name__'), 'a')
129128
self.assertEqual(getattr(a, '_p'), '_a_Permission')
129+
130+
def testErrorsDuringGetattr(self):
131+
pr = self._getTargetClass()('View')
132+
133+
class AttributeErrorObject(Implicit):
134+
pass
135+
self.assertEqual(
136+
tuple(pr.__of__(AttributeErrorObject())), ('Manager', ))
137+
138+
# Unauthorized errors are tolerated and equivalent to no
139+
# permission declaration
140+
class UnauthorizedErrorObject(Implicit):
141+
def __getattr__(self, name):
142+
from zExceptions import Unauthorized
143+
if name == '_View_Permission':
144+
raise Unauthorized(name)
145+
raise AttributeError(name)
146+
self.assertEqual(
147+
tuple(pr.__of__(UnauthorizedErrorObject())), ('Manager', ))
148+
149+
# other exceptions propagate
150+
class ErrorObject(Implicit):
151+
def __getattr__(self, name):
152+
if name == '_View_Permission':
153+
raise RuntimeError("Error raised during getattr")
154+
raise AttributeError(name)
155+
with self.assertRaisesRegex(
156+
RuntimeError, "Error raised during getattr"):
157+
tuple(pr.__of__(ErrorObject()))
158+
159+
160+
class Python_PermissionRoleTests(PermissionRoleTestBase, unittest.TestCase):
161+
def _getTargetClass(self):
162+
from AccessControl.ImplPython import PermissionRole
163+
return PermissionRole
164+
165+
166+
@unittest.skipIf(PURE_PYTHON, reason="Test expects C impl.")
167+
class C__PermissionRoleTests(PermissionRoleTestBase, unittest.TestCase):
168+
def _getTargetClass(self):
169+
from AccessControl.ImplC import PermissionRole
170+
return PermissionRole

src/AccessControl/tests/testZopeSecurityPolicy.py

+15
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ class PartlyProtectedSimpleItem3 (PartlyProtectedSimpleItem1):
158158
__roles__ = sysadmin_roles
159159

160160

161+
class DynamicallyUnauthorized(SimpleItemish):
162+
# This class raises an Unauthorized on attribute access,
163+
# similar to Zope's Shared.DC.Scripts.Bindings.UnauthorizedBinding
164+
__ac_local_roles__ = {}
165+
166+
def __getattr__(self, name):
167+
raise Unauthorized('Not authorized to access: %s' % name)
168+
169+
161170
class SimpleClass:
162171
attr = 1
163172

@@ -174,6 +183,7 @@ def setUp(self):
174183
a.item1 = PartlyProtectedSimpleItem1()
175184
a.item2 = PartlyProtectedSimpleItem2()
176185
a.item3 = PartlyProtectedSimpleItem3()
186+
a.d_item = DynamicallyUnauthorized()
177187
uf = UserFolder()
178188
a.acl_users = uf
179189
self.uf = a.acl_users
@@ -352,6 +362,11 @@ def test_checkPermission_proxy_role_scope(self):
352362
r_subitem,
353363
context))
354364

365+
def test_checkPermission_dynamically_unauthorized(self):
366+
d_item = self.a.d_item
367+
context = self.context
368+
self.assertFalse(self.policy.checkPermission('View', d_item, context))
369+
355370
def testUnicodeRolesForPermission(self):
356371
r_item = self.a.r_item
357372
context = self.context

0 commit comments

Comments
 (0)