Skip to content

Commit 82ab107

Browse files
Make dict views behave like their unrestricted versions (#147)
unlike the restricted versions, the unrestricted versions: - are not iterators, they are views - have a len - are false when the mapping is empty, true otherwise - are instances of collections.abc.MappingView This change aligns the behavior of restricted versions with the behavior of unrestricted one, while keeping the "guard", which validates the access with the security manager. During this refactoring, also change `.items()` to validate each keys and values, like `.keys()` and `.values()` do. Co-authored-by: Dieter Maurer <[email protected]>
1 parent 38a89f0 commit 82ab107

File tree

4 files changed

+113
-20
lines changed

4 files changed

+113
-20
lines changed

CHANGES.rst

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

99
- Nothing changed yet.
1010

11+
- Make dict views (`.keys()`, `.items()` and `.values()`) behave like their
12+
unrestricted versions.
13+
(`#147 <https://github.com/zopefoundation/AccessControl/pull/147>`_)
14+
15+
- Make `.items()` validate each keys and values, like `.keys()` and
16+
`.values()` do.
17+
1118

1219
6.3 (2023-11-20)
1320
----------------

src/AccessControl/ZopeGuards.py

+48-11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
##############################################################################
1313

1414

15+
import collections.abc
1516
import math
1617
import random
1718
import string
@@ -127,13 +128,18 @@ def guarded_pop(key, default=_marker):
127128
return guarded_pop
128129

129130

130-
def get_iter(c, name):
131-
iter = getattr(c, name)
131+
def get_mapping_view(c, name):
132132

133-
def guarded_iter():
134-
return SafeIter(iter(), c)
133+
view_class = {
134+
'keys': SafeKeysView,
135+
'items': SafeItemsView,
136+
'values': SafeValuesView,
137+
}
135138

136-
return guarded_iter
139+
def guarded_mapping_view():
140+
return view_class[name](c)
141+
142+
return guarded_mapping_view
137143

138144

139145
def get_list_pop(lst, name):
@@ -153,18 +159,15 @@ def guarded_pop(index=-1):
153159
'copy': 1,
154160
'fromkeys': 1,
155161
'get': get_dict_get,
156-
'items': 1,
162+
'items': get_mapping_view,
163+
'keys': get_mapping_view,
157164
'pop': get_dict_pop,
158165
'popitem': 1,
159166
'setdefault': 1,
160167
'update': 1,
168+
'values': get_mapping_view,
161169
}
162170

163-
_dict_white_list.update({
164-
'keys': get_iter,
165-
'values': get_iter,
166-
})
167-
168171

169172
def _check_dict_access(name, value):
170173
# Check whether value is a dict method
@@ -272,6 +275,40 @@ def __next__(self):
272275
next = __next__
273276

274277

278+
# The following three view classes are used only for mappings of type `dict`
279+
# (not subclasses). Therefore, the mapping does not have security assertions
280+
# and cannot acquire ones. As a consequence, the `guard` calls used in their
281+
# methods verify that the checked key or value is accessible based solely on
282+
# its own virtues, i.e. either because it is public or has its own security
283+
# assertions allowing access.
284+
class _SafeMappingView:
285+
__allow_access_to_unprotected_subobjects__ = 1
286+
287+
def __iter__(self):
288+
for e in super().__iter__():
289+
guard(self._mapping, e)
290+
yield e
291+
292+
293+
class SafeKeysView(_SafeMappingView, collections.abc.KeysView):
294+
pass
295+
296+
297+
class SafeValuesView(_SafeMappingView, collections.abc.ValuesView):
298+
pass
299+
300+
301+
class SafeItemsView(_SafeMappingView, collections.abc.ItemsView):
302+
def __iter__(self):
303+
for k, v in super().__iter__():
304+
guard(self._mapping, k)
305+
# When checking the value, we check the guard with index=None,
306+
# not with index=k, the key name does not matter. guard just
307+
# needs to verify that the value itself can be accessed.
308+
guard(self._mapping, v)
309+
yield k, v
310+
311+
275312
def _error(index):
276313
raise Unauthorized('unauthorized access to element')
277314

src/AccessControl/tests/actual_python.py

+33
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,39 @@ def f7():
123123
access = getattr(d, meth)
124124
result = sorted(access())
125125
assert result == expected[kind], (meth, kind, result, expected[kind])
126+
assert len(access()) == len(expected[kind]), (meth, kind, "len")
127+
iter_ = access() # iterate twice on the same view
128+
assert list(iter_) == list(iter_)
129+
130+
assert sorted([k for k in getattr(d, meth)()]) == expected[kind]
131+
assert sorted(k for k in getattr(d, meth)()) == expected[kind]
132+
assert {k: v for k, v in d.items()} == d
133+
134+
assert 1 in d
135+
assert 1 in d.keys()
136+
assert 2 in d.values()
137+
assert (1, 2) in d.items()
138+
139+
assert d
140+
assert d.keys()
141+
assert d.values()
142+
assert d.items()
143+
144+
empty_d = {}
145+
assert not empty_d
146+
assert not empty_d.keys()
147+
assert not empty_d.values()
148+
assert not empty_d.items()
149+
150+
smaller_d = {1: 2}
151+
for m, _ in methods:
152+
assert getattr(d, m)() != getattr(smaller_d, m)()
153+
assert not getattr(d, m)() == getattr(smaller_d, m)()
154+
if m != 'values':
155+
assert getattr(d, m)() > getattr(smaller_d, m)()
156+
assert getattr(d, m)() >= getattr(smaller_d, m)()
157+
assert getattr(smaller_d, m)() < getattr(d, m)()
158+
assert getattr(smaller_d, m)() <= getattr(d, m)()
126159

127160

128161
f7()

src/AccessControl/tests/testZopeGuards.py

+25-9
Original file line numberDiff line numberDiff line change
@@ -258,42 +258,58 @@ def test_pop_validates(self):
258258
self.assertTrue(sm.calls)
259259

260260
def test_keys_empty(self):
261-
from AccessControl.ZopeGuards import get_iter
262-
keys = get_iter({}, 'keys')
261+
from AccessControl.ZopeGuards import get_mapping_view
262+
keys = get_mapping_view({}, 'keys')
263263
self.assertEqual(list(keys()), [])
264264

265+
def test_kvi_len(self):
266+
from AccessControl.ZopeGuards import get_mapping_view
267+
for attr in ("keys", "values", "items"):
268+
with self.subTest(attr):
269+
view = get_mapping_view({'a': 1}, attr)
270+
self.assertEqual(len(view()), 1)
271+
265272
def test_keys_validates(self):
266273
sm = SecurityManager()
267274
old = self.setSecurityManager(sm)
268275
keys = guarded_getattr({GuardTestCase: 1}, 'keys')
269276
try:
270-
next(keys())
277+
next(iter(keys()))
271278
finally:
272279
self.setSecurityManager(old)
273280
self.assertTrue(sm.calls)
274281

282+
def test_items_validates(self):
283+
sm = SecurityManager()
284+
old = self.setSecurityManager(sm)
285+
items = guarded_getattr({GuardTestCase: GuardTestCase}, 'items')
286+
try:
287+
next(iter(items()))
288+
finally:
289+
self.setSecurityManager(old)
290+
self.assertEqual(len(sm.calls), 2)
291+
275292
def test_values_empty(self):
276-
from AccessControl.ZopeGuards import get_iter
277-
values = get_iter({}, 'values')
293+
from AccessControl.ZopeGuards import get_mapping_view
294+
values = get_mapping_view({}, 'values')
278295
self.assertEqual(list(values()), [])
279296

280297
def test_values_validates(self):
281298
sm = SecurityManager()
282299
old = self.setSecurityManager(sm)
283300
values = guarded_getattr({GuardTestCase: 1}, 'values')
284301
try:
285-
next(values())
302+
next(iter(values()))
286303
finally:
287304
self.setSecurityManager(old)
288305
self.assertTrue(sm.calls)
289306

290307
def test_kvi_iteration(self):
291-
from AccessControl.ZopeGuards import SafeIter
292308
d = dict(a=1, b=2)
293309
for attr in ("keys", "values", "items"):
294310
v = getattr(d, attr)()
295-
si = SafeIter(v)
296-
self.assertEqual(next(si), next(iter(v)))
311+
si = guarded_getattr(d, attr)()
312+
self.assertEqual(next(iter(si)), next(iter(v)))
297313

298314

299315
class TestListGuards(GuardTestCase):

0 commit comments

Comments
 (0)