refactor(storage): gate clone() behind an opt-in allowlist; expand duplicate tests
Follow-up polish from the duplicate-subgraph review: - BaseSqliteStore.clone() now requires an opt-in `_cloneable` flag (default off), so a new or secret-bearing store can never be cloned by accident — defence-in-depth on top of the endpoint's `_DUPLICABLE_KINDS` restriction. Only value-source and colour-strip stores are flagged. - Fix `_check_name_unique` annotation (`str | None = None`). - Add tests: `layers[].brightness_source_id` remap, the warnings safety net firing, the clone allowlist invariant, and clone() refusing a non-cloneable store.
This commit is contained in:
@@ -29,6 +29,10 @@ class BaseSqliteStore(Generic[T]):
|
||||
|
||||
_table_name: str
|
||||
_entity_name: str
|
||||
# Opt-in allowlist for clone(): defaults off so a new (possibly
|
||||
# secret-bearing) store is never cloneable by accident. Subclasses that hold
|
||||
# no inline secrets and are safe to duplicate set this True.
|
||||
_cloneable: bool = False
|
||||
|
||||
def __init__(self, db: Database, deserializer: Callable[[dict], T]):
|
||||
self._db = db
|
||||
@@ -153,6 +157,8 @@ class BaseSqliteStore(Generic[T]):
|
||||
model might hold. Callers must restrict cloning to non-secret-bearing
|
||||
kinds (see ``_DUPLICABLE_KINDS`` in ``api/routes/graph.py``).
|
||||
"""
|
||||
if not self._cloneable:
|
||||
raise NotImplementedError(f"{self._entity_name} store does not support cloning")
|
||||
with self._lock:
|
||||
if item_id not in self._items:
|
||||
from ledgrab.storage.base_store import EntityNotFoundError
|
||||
@@ -180,7 +186,7 @@ class BaseSqliteStore(Generic[T]):
|
||||
|
||||
# -- Helpers -------------------------------------------------------------
|
||||
|
||||
def _check_name_unique(self, name: str, exclude_id: str = None) -> None:
|
||||
def _check_name_unique(self, name: str, exclude_id: str | None = None) -> None:
|
||||
"""Raise ValueError if *name* is empty or already taken.
|
||||
|
||||
Must be called while holding ``self._lock``.
|
||||
|
||||
@@ -33,6 +33,7 @@ class ColorStripStore(BaseSqliteStore[ColorStripSource]):
|
||||
|
||||
_table_name = "color_strip_sources"
|
||||
_entity_name = "Color strip source"
|
||||
_cloneable = True # no inline secrets — only references shared entities by id
|
||||
|
||||
def __init__(self, db: Database):
|
||||
super().__init__(db, ColorStripSource.from_dict)
|
||||
|
||||
@@ -27,6 +27,7 @@ class ValueSourceStore(BaseSqliteStore[ValueSource]):
|
||||
|
||||
_table_name = "value_sources"
|
||||
_entity_name = "Value source"
|
||||
_cloneable = True # no inline secrets — only references shared entities by id
|
||||
|
||||
def __init__(self, db: Database):
|
||||
super().__init__(db, ValueSource.from_dict)
|
||||
|
||||
Reference in New Issue
Block a user