Skip to content

Commit cf4df50

Browse files
wskozlowskiWojtek
andauthored
restricting access by context (#796)
* restricting access by context * restricted context updates --------- Co-authored-by: Wojtek <wskozlowski@dbzero.io>
1 parent 4d4feaf commit cf4df50

23 files changed

Lines changed: 750 additions & 51 deletions

‎dbzero/dbzero/dbzero.pyi‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def open(prefix_name: str, open_mode: str = "rw", **kwargs: Any) -> None:
3030
3131
* autocommit (bool) to disable automatic commits for this prefix
3232
* restricted (bool) to restrict memo reflection access for this prefix
33+
* restricted_context (ContextVar) to dynamically restrict memo reflection access for this prefix
3334
* slab_size (int) for memory slab allocation size in bytes
3435
* meta_io_step_size (int) for metadata I/O operation chunk size
3536
* lock_flags (dict) to change locking behavior when opening the prefix for read-write
@@ -55,6 +56,14 @@ def open(prefix_name: str, open_mode: str = "rw", **kwargs: Any) -> None:
5556
"""
5657
...
5758

59+
def set_restricted(*, restricted: Optional[bool] = None, restricted_context: Any = None,
60+
prefix: Optional[str] = None) -> None:
61+
"""Increase memo restricted-access mode globally or for an open prefix.
62+
63+
The restriction level can only increase: unrestricted -> restricted_context -> restricted.
64+
"""
65+
...
66+
5867
def close(prefix_name: Optional[str] = None) -> None:
5968
"""Gracefully shut down dbzero, persisting changes and releasing resources.
6069
@@ -1545,7 +1554,7 @@ def get_config() -> Dict[str, Any]:
15451554
15461555
* autocommit (bool): True if autocommit enabled. Defaults to True.
15471556
* autocommit_interval (int): Milliseconds between commits. Defaults to 250.
1548-
* restricted (bool): True if future opened prefixes default to restricted mode.
1557+
* restricted (bool): True if future opened prefixes default to static restricted mode.
15491558
15501559
Raises
15511560
------

‎dbzero/dbzero/initialization.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def init(dbzero_root: str, **kwargs: Any) -> None:
2626
2727
Configure global dbzero behavior:
2828
* restricted (bool, default False) to restrict memo reflection access for future opened prefixes
29+
* restricted_context (ContextVar) to dynamically restrict memo reflection access
2930
* autocommit (bool, default True) to enable automatic commits
3031
* autocommit_interval (int, default 367) for commit interval in milliseconds
3132
* cache_size (int, default 2 GiB) for main object cache size in bytes
@@ -56,6 +57,9 @@ def init(dbzero_root: str, **kwargs: Any) -> None:
5657
if "restricted" in kwargs:
5758
init_kwargs["restricted"] = kwargs["restricted"]
5859

60+
if "restricted_context" in kwargs:
61+
init_kwargs["restricted_context"] = kwargs["restricted_context"]
62+
5963
rpc = kwargs.get("rpc")
6064
if rpc is not None and not isinstance(rpc, Mapping):
6165
raise TypeError("rpc must be a mapping")
@@ -71,4 +75,6 @@ def init(dbzero_root: str, **kwargs: Any) -> None:
7175
open_kwargs = {}
7276
if "restricted" in kwargs:
7377
open_kwargs["restricted"] = kwargs["restricted"]
78+
if "restricted_context" in kwargs:
79+
open_kwargs["restricted_context"] = kwargs["restricted_context"]
7480
dbzero_open(kwargs["prefix"], open_mode=open_mode, **open_kwargs)

‎dbzero/setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name='dbzero',
13-
version='0.4.1',
13+
version='0.4.2',
1414
description='DBZero community edition',
1515
packages=['dbzero'],
1616
python_requires='>=3.9',

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ requires = ['meson-python']
44

55
[project]
66
name = 'dbzero'
7-
version = '0.4.1'
7+
version = '0.4.2'
88
description = 'A state management system for Python 3.x that unifies your applications business logic, data persistence, and caching into a single, efficient layer.'
99
readme = 'README.md'
1010
requires-python = '>=3.9'

‎python_tests/test_restricted_memo_access.py‎

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import subprocess
88
import sys
99
import textwrap
10+
from contextvars import ContextVar
1011

1112
import pytest
1213
import dbzero as db0
@@ -200,3 +201,208 @@ def test_global_restricted_applies_to_auto_opened_prefix(db0_restricted_root):
200201
assert db0.get_config()["restricted"] is True
201202
assert db0.get_prefix_stats("auto-open-prefix")["restricted"] is True
202203
_assert_restricted(fetched, writable=False)
204+
205+
206+
def test_global_restricted_context_controls_existing_objects(db0_restricted_root):
207+
restricted_context = ContextVar("restricted_context", default=False)
208+
db0.init(DB0_DIR, restricted_context=restricted_context)
209+
assert db0.get_config()["restricted"] is False
210+
db0.open("dynamic-global-prefix")
211+
assert db0.get_prefix_stats("dynamic-global-prefix")["restricted"] is False
212+
obj = RestrictedMemoAccessData(123)
213+
214+
assert obj.__class__ is RestrictedMemoAccessData
215+
assert obj.get_value.__self__ is obj
216+
217+
token = restricted_context.set(True)
218+
try:
219+
_assert_restricted(obj)
220+
finally:
221+
restricted_context.reset(token)
222+
223+
assert obj.__class__ is RestrictedMemoAccessData
224+
assert obj.get_value.__self__ is obj
225+
226+
227+
def test_prefix_restricted_context_overrides_global_context(db0_restricted_root):
228+
global_context = ContextVar("global_context", default=True)
229+
prefix_context = ContextVar("prefix_context", default=False)
230+
db0.init(DB0_DIR, restricted_context=global_context)
231+
232+
db0.open("global-dynamic-prefix")
233+
global_obj = RestrictedMemoAccessData(123)
234+
235+
db0.open("prefix-dynamic-prefix", restricted_context=prefix_context)
236+
prefix_obj = RestrictedMemoAccessData(123)
237+
238+
_assert_restricted(global_obj)
239+
assert prefix_obj.__class__ is RestrictedMemoAccessData
240+
assert prefix_obj.get_value.__self__ is prefix_obj
241+
242+
token = prefix_context.set(True)
243+
try:
244+
_assert_restricted(prefix_obj)
245+
finally:
246+
prefix_context.reset(token)
247+
248+
249+
def test_prefix_restricted_context_none_cannot_replace_existing_context(db0_restricted_root):
250+
global_context = ContextVar("global_context", default=True)
251+
prefix_context = ContextVar("prefix_context", default=False)
252+
db0.init(DB0_DIR, restricted_context=global_context)
253+
db0.open("prefix-reset-dynamic-prefix", restricted_context=prefix_context)
254+
obj = RestrictedMemoAccessData(123)
255+
256+
assert obj.__class__ is RestrictedMemoAccessData
257+
assert obj.get_value.__self__ is obj
258+
259+
with pytest.raises(RuntimeError):
260+
db0.open("prefix-reset-dynamic-prefix", restricted_context=None)
261+
262+
assert obj.__class__ is RestrictedMemoAccessData
263+
264+
265+
def test_static_restricted_without_restricted_context_remains_restricted(db0_restricted_root):
266+
db0.init(DB0_DIR)
267+
db0.open("static-restricted-prefix", restricted=True)
268+
assert db0.get_prefix_stats("static-restricted-prefix")["restricted"] is True
269+
270+
_assert_restricted(RestrictedMemoAccessData(123))
271+
272+
273+
def test_static_restricted_object_does_not_resolve_later_restricted_context(db0_restricted_root):
274+
class ExplodingContext:
275+
def get(self):
276+
raise AssertionError("restricted_context should not be resolved")
277+
278+
db0.init(DB0_DIR)
279+
db0.open("static-no-context-prefix", restricted=True)
280+
obj = RestrictedMemoAccessData(123)
281+
282+
with pytest.raises(RuntimeError):
283+
db0.open("static-no-context-prefix", restricted_context=ExplodingContext())
284+
285+
_assert_restricted(obj)
286+
287+
288+
def test_unset_restricted_context_is_treated_as_false(db0_restricted_root):
289+
restricted_context = ContextVar("restricted_context")
290+
db0.init(DB0_DIR, restricted_context=restricted_context)
291+
db0.open("unset-dynamic-prefix")
292+
obj = RestrictedMemoAccessData(123)
293+
294+
assert obj.__class__ is RestrictedMemoAccessData
295+
assert obj.get_value.__self__ is obj
296+
297+
298+
def test_restricted_context_uses_python_truthiness(db0_restricted_root):
299+
restricted_context = ContextVar("restricted_context", default=[])
300+
db0.init(DB0_DIR, restricted_context=restricted_context)
301+
db0.open("truthy-dynamic-prefix")
302+
obj = RestrictedMemoAccessData(123)
303+
304+
assert obj.__class__ is RestrictedMemoAccessData
305+
306+
token = restricted_context.set([object()])
307+
try:
308+
_assert_restricted(obj)
309+
finally:
310+
restricted_context.reset(token)
311+
312+
313+
def test_init_prefix_shortcut_forwards_restricted_context(db0_restricted_root):
314+
restricted_context = ContextVar("restricted_context", default=True)
315+
db0.init(DB0_DIR, prefix="shortcut-dynamic-prefix", restricted_context=restricted_context)
316+
assert db0.get_prefix_stats("shortcut-dynamic-prefix")["restricted"] is False
317+
318+
_assert_restricted(RestrictedMemoAccessData(123))
319+
320+
321+
def test_set_restricted_context_upgrades_existing_and_future_prefixes(db0_restricted_root):
322+
restricted_context = ContextVar("restricted_context", default=False)
323+
db0.init(DB0_DIR)
324+
db0.open("dynamic-upgrade-prefix")
325+
obj = RestrictedMemoAccessData(123)
326+
327+
assert obj.__class__ is RestrictedMemoAccessData
328+
db0.set_restricted(restricted_context=restricted_context)
329+
assert db0.get_config()["restricted"] is False
330+
331+
assert obj.__class__ is RestrictedMemoAccessData
332+
token = restricted_context.set(True)
333+
try:
334+
_assert_restricted(obj)
335+
db0.open("future-dynamic-upgrade-prefix")
336+
_assert_restricted(RestrictedMemoAccessData(123))
337+
finally:
338+
restricted_context.reset(token)
339+
340+
341+
def test_set_restricted_static_upgrades_dynamic_context(db0_restricted_root):
342+
restricted_context = ContextVar("restricted_context", default=False)
343+
db0.init(DB0_DIR)
344+
db0.set_restricted(restricted_context=restricted_context)
345+
db0.open("dynamic-to-static-prefix")
346+
obj = RestrictedMemoAccessData(123)
347+
348+
assert obj.__class__ is RestrictedMemoAccessData
349+
db0.set_restricted(restricted=True)
350+
351+
_assert_restricted(obj)
352+
db0.open("future-static-upgrade-prefix")
353+
assert db0.get_prefix_stats("future-static-upgrade-prefix")["restricted"] is True
354+
_assert_restricted(RestrictedMemoAccessData(123))
355+
356+
357+
def test_set_restricted_rejects_context_after_static_restricted(db0_restricted_root):
358+
restricted_context = ContextVar("restricted_context", default=False)
359+
db0.init(DB0_DIR, restricted=True)
360+
db0.open("static-terminal-prefix")
361+
obj = RestrictedMemoAccessData(123)
362+
363+
with pytest.raises(RuntimeError):
364+
db0.set_restricted(restricted_context=restricted_context)
365+
366+
_assert_restricted(obj)
367+
368+
369+
def test_set_restricted_rejects_context_replacement(db0_restricted_root):
370+
first_context = ContextVar("first_context", default=False)
371+
second_context = ContextVar("second_context", default=True)
372+
db0.init(DB0_DIR)
373+
db0.set_restricted(restricted_context=first_context)
374+
db0.open("context-replacement-prefix")
375+
obj = RestrictedMemoAccessData(123)
376+
377+
with pytest.raises(RuntimeError):
378+
db0.set_restricted(restricted_context=second_context)
379+
380+
assert obj.__class__ is RestrictedMemoAccessData
381+
token = first_context.set(True)
382+
try:
383+
_assert_restricted(obj)
384+
finally:
385+
first_context.reset(token)
386+
387+
388+
def test_set_restricted_rejects_false(db0_restricted_root):
389+
db0.init(DB0_DIR)
390+
391+
with pytest.raises(RuntimeError):
392+
db0.set_restricted(restricted=False)
393+
394+
395+
def test_set_restricted_prefix_only_upgrades_target_prefix(db0_restricted_root):
396+
restricted_context = ContextVar("restricted_context", default=True)
397+
db0.init(DB0_DIR)
398+
399+
db0.open("target-prefix")
400+
target = RestrictedMemoAccessData(123)
401+
db0.open("other-prefix")
402+
other = RestrictedMemoAccessData(123)
403+
404+
db0.set_restricted(prefix="target-prefix", restricted_context=restricted_context)
405+
406+
_assert_restricted(target)
407+
assert other.__class__ is RestrictedMemoAccessData
408+
assert other.get_value.__self__ is other

‎src/dbzero/bindings/python/Memo.cpp‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,16 @@ namespace db0::python
7171
MemoImplT *memo_obj = reinterpret_cast<MemoImplT*>(py_type->tp_alloc(py_type, 0));
7272

7373
// if type cannot be retrieved due to access mode then defer this operation (fallback)
74-
auto restricted = fixture->isRestricted();
7574
if (type) {
7675
// prepare a new dbzero instance of a known db0 class
77-
memo_obj->makeNew(type, restricted);
76+
memo_obj->makeNew(type);
7877
} else {
7978
auto type_initializer = [py_type](db0::swine_ptr<Fixture> &fixture) {
8079
auto &class_factory = fixture->get<db0::object_model::ClassFactory>();
8180
return class_factory.getOrCreateType(py_type);
8281
};
8382
// prepare a new db0 instance of a known db0 class
84-
memo_obj->makeNew(std::move(type_initializer), restricted);
83+
memo_obj->makeNew(std::move(type_initializer));
8584
}
8685

8786
return memo_obj;
@@ -192,18 +191,17 @@ namespace db0::python
192191
// find py type associated dbzero class with the ClassFactory
193192
auto type = class_factory.tryGetOrCreateType(py_type);
194193
MemoObject *memo_obj = nullptr;
195-
auto restricted = fixture->isRestricted();
196194
if (type) {
197195
memo_obj = reinterpret_cast<MemoObject*>(py_type->tp_alloc(py_type, 0));
198-
memo_obj->makeNew(type, restricted);
196+
memo_obj->makeNew(type);
199197
} else {
200198
// if type cannot be retrieved due to access mode then deferr this operation (fallback)
201199
auto type_initializer = [py_type](db0::swine_ptr<Fixture> &fixture) {
202200
auto &class_factory = fixture->get<db0::object_model::ClassFactory>();
203201
return class_factory.getOrCreateType(py_type);
204202
};
205203
memo_obj = reinterpret_cast<MemoObject*>(py_type->tp_alloc(py_type, 0));
206-
memo_obj->makeNew(std::move(type_initializer), restricted);
204+
memo_obj->makeNew(std::move(type_initializer));
207205
}
208206

209207
return memo_obj;
@@ -260,7 +258,18 @@ namespace db0::python
260258
return false;
261259
}
262260

263-
return memo_obj->ext().isRestricted();
261+
auto fixture = memo_obj->ext().tryGetFixture();
262+
if (!fixture) {
263+
return false;
264+
}
265+
266+
if (fixture->isRestricted()) {
267+
return true;
268+
}
269+
if (!fixture->isRestrictedCtx()) {
270+
return false;
271+
}
272+
return resolveRestrictedCtx(*fixture);
264273
}
265274

266275
template <typename MemoImplT>
@@ -404,7 +413,18 @@ namespace db0::python
404413
return false;
405414
}
406415

407-
return memo_obj->ext().isRestricted() || isRestrictedMemoContextActive();
416+
auto fixture = memo_obj->ext().tryGetFixture();
417+
if (!fixture) {
418+
return false;
419+
}
420+
421+
if (fixture->isRestricted()) {
422+
return true;
423+
}
424+
if (!fixture->isRestrictedCtx()) {
425+
return false;
426+
}
427+
return resolveRestrictedCtx(*fixture);
408428
}
409429

410430
template <typename MemoImplT>

0 commit comments

Comments
 (0)