Skip to content

Commit f6c2c55

Browse files
wskozlowskiWojtek
andauthored
init data masking (#750)
* init data masking * post-review cleanups and fixes --------- Co-authored-by: Wojtek <wskozlowski@dbzero.io>
1 parent 36312c0 commit f6c2c55

15 files changed

Lines changed: 529 additions & 11 deletions

File tree

‎dbzero/dbzero/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) 2025 DBZero Software sp. z o.o.
33

44
from .dbzero import *
5+
from .dbzero import _init_data_masking
56
from .memo import *
67
from .enum import *
78
from .fast_query import *
@@ -13,4 +14,4 @@
1314
from .decorators import *
1415
from .select import *
1516
from .compare import *
16-
from .initialization import init
17+
from .initialization import init

‎dbzero/dbzero/dbzero.pyi‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,15 @@ def get_field_access(class_obj: type, account_id: int) -> Iterable[Tuple[str, Tu
619619
"""Return protected-field access flags for a memo class and account."""
620620
...
621621

622+
def _init_data_masking(
623+
context_var: Any,
624+
prefix: Union[str, Any, Sequence[Any], None] = None,
625+
missing_value_placeholder: Optional[Any] = None,
626+
mode: str = "RELEASE",
627+
) -> None:
628+
"""Initialize workspace-wide or prefix-scoped data masking for the current process."""
629+
...
630+
622631
# Cache management
623632

624633
def clear_cache() -> None:

‎python_tests/test_data_masking.py‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# SPDX-License-Identifier: LGPL-2.1-or-later
2+
# Copyright (c) 2025 DBZero Software sp. z o.o.
3+
4+
from contextvars import ContextVar
5+
6+
import pytest
7+
8+
import dbzero as db0
9+
10+
11+
account_id = ContextVar("account_id")
12+
missing_value = object()
13+
14+
15+
def test_init_data_masking_prefix_scoped_lifecycle(db0_fixture):
16+
current_prefix = db0.get_current_prefix()
17+
18+
db0._init_data_masking(
19+
account_id,
20+
prefix=current_prefix,
21+
missing_value_placeholder=missing_value,
22+
mode="DEBUG",
23+
)
24+
25+
db0._init_data_masking(
26+
account_id,
27+
prefix=current_prefix.name,
28+
missing_value_placeholder=missing_value,
29+
mode="DEBUG",
30+
)
31+
32+
db0.open("data-masking-extra-prefix")
33+
db0._init_data_masking(
34+
account_id,
35+
prefix=["data-masking-extra-prefix"],
36+
missing_value_placeholder=missing_value,
37+
mode="DEBUG",
38+
)
39+
40+
41+
def test_init_data_masking_general_scope_lifecycle(db0_fixture):
42+
db0._init_data_masking(account_id)
43+
44+
db0._init_data_masking(account_id, mode="RELEASE")
45+
46+
with pytest.raises(RuntimeError, match="binding"):
47+
db0._init_data_masking(ContextVar("other_general_account_id"))
48+
49+
db0.open("data-masking-general-prefix")
50+
with pytest.raises(RuntimeError, match="binding"):
51+
db0._init_data_masking(
52+
ContextVar("other_general_prefix_account_id"),
53+
prefix="data-masking-general-prefix",
54+
)
55+
56+
57+
def test_init_data_masking_requires_open_prefix(db0_fixture):
58+
with pytest.raises(ValueError, match="open"):
59+
db0._init_data_masking(account_id, prefix="not-opened")
60+
61+
db0.open("readonly-data-masking-prefix")
62+
db0.close("readonly-data-masking-prefix")
63+
db0.open("readonly-data-masking-prefix", "r")
64+
db0._init_data_masking(account_id, prefix="readonly-data-masking-prefix")
65+
66+
67+
def test_init_data_masking_rejects_parameter_changes(db0_fixture):
68+
db0._init_data_masking(
69+
account_id,
70+
prefix=db0.get_current_prefix(),
71+
missing_value_placeholder=missing_value,
72+
mode="DEBUG",
73+
)
74+
75+
other_account_id = ContextVar("other_account_id")
76+
with pytest.raises(RuntimeError, match="binding"):
77+
db0._init_data_masking(
78+
other_account_id,
79+
prefix=db0.get_current_prefix(),
80+
missing_value_placeholder=missing_value,
81+
mode="DEBUG",
82+
)
83+
84+
with pytest.raises(RuntimeError, match="binding"):
85+
db0._init_data_masking(
86+
account_id,
87+
prefix=db0.get_current_prefix(),
88+
missing_value_placeholder=missing_value,
89+
mode="RELEASE",
90+
)
91+
92+
with pytest.raises(RuntimeError, match="binding"):
93+
db0._init_data_masking(
94+
account_id,
95+
prefix=db0.get_current_prefix(),
96+
missing_value_placeholder=object(),
97+
mode="DEBUG",
98+
)
99+
100+
101+
def test_init_data_masking_defaults_mode_to_release(db0_fixture):
102+
db0._init_data_masking(account_id, prefix=db0.get_current_prefix())
103+
104+
db0._init_data_masking(
105+
account_id,
106+
prefix=db0.get_current_prefix(),
107+
mode="RELEASE",
108+
)
109+
110+
with pytest.raises(RuntimeError, match="binding"):
111+
db0._init_data_masking(
112+
account_id,
113+
prefix=db0.get_current_prefix(),
114+
mode="DEBUG",
115+
)
116+
117+
118+
def test_init_data_masking_binding_survives_prefix_reopen(db0_fixture):
119+
prefix_name = db0.get_current_prefix().name
120+
121+
db0._init_data_masking(account_id, prefix=prefix_name)
122+
db0.close(prefix_name)
123+
db0.open(prefix_name)
124+
125+
with pytest.raises(RuntimeError, match="binding"):
126+
db0._init_data_masking(
127+
ContextVar("reopened_prefix_account_id"),
128+
prefix=prefix_name,
129+
)
130+
131+
132+
def test_init_data_masking_allows_different_bindings_for_different_prefixes(db0_fixture):
133+
db0.open("first-data-masking-binding")
134+
db0._init_data_masking(account_id, prefix="first-data-masking-binding", mode="DEBUG")
135+
136+
other_account_id = ContextVar("different_prefix_account_id")
137+
db0.open("different-data-masking-binding")
138+
db0._init_data_masking(
139+
other_account_id,
140+
prefix="different-data-masking-binding",
141+
missing_value_placeholder=object(),
142+
mode="RELEASE",
143+
)

0 commit comments

Comments
 (0)