Skip to content

fix: keep writes inside the store's scope; refuse operator keys on writes - #15

Merged
thorwhalen merged 3 commits into
masterfrom
scope-filter-and-operator-keys
Sep 22, 2026
Merged

thorwhalen merged 3 commits into
masterfrom
scope-filter-and-operator-keys

Conversation

@thorwhalen

@thorwhalen thorwhalen commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Closes #14

What changes

  • Scope contradictions raise. _build_doc used to merge as dict(filter, **k, **v), so a key or value repeating a scope field with a different value wrote the doc outside the store's scope. It now raises ValueError naming the field, comparing as MongoDB does (BSON equality: True is not 1, subdocument field order matters). $eq/$in scope fields are checked by membership; other operator scopes ($ne, $gt, ...) are not checked and such writes are let through (documented). Dotted scope fields refuse writes with a clear error (those writes produced literal dotted field names before). Writes that agree with the scope produce exactly the same documents as before.
  • No operators in write/delete keys. __setitem__ (replace_one), __delitem__ (delete_one), MongoCollectionMultipleDocsPersister.__setitem__ (its delete_many) and MongoBulkWritesMixin now go through _write_filter_for_key, which refuses keys containing $-prefixed field names or regex values (re.Pattern, bson.regex.Regex) at any depth. The check runs before any call to the collection; MongoCollectionMultipleDocsPersister also builds and validates every new doc before its delete_many, so a refused write deletes nothing. Keyword-only allow_operators_in_write_keys=True (constructor arg, or class attribute) restores the old behaviour.
  • Replace/delete queries confined by on_write_filter too (previously only by filter), so a store stamping on_write_filter={"tenant": "a"} over an unscoped filter can no longer replace or delete another tenant's docs.
  • Reads are unchanged: s[k] / k in s keep accepting query keys (used by recipes.disallow_sourced_interval_overlaps), and remain confined by the scope through $and.
  • Tests: mongodol/tests/write_scope_test.py, using a recording stand-in for the collection (no server needed; hosted CI also runs the full suite against its MongoDB service).

Compatibility

Behaviour changes for: writes that contradicted the scope; write/delete keys with operators or regexes; writes through dotted scope fields; and stores whose on_write_filter stamps a field that older docs lack — replace/delete by key no longer matches those older docs (an upsert on an existing _id then fails with a duplicate-key error, and MultipleDocs writes leave the old docs in place). mongodol has no fleet dependents.

Review

Independent refute-review by a sub-agent found three blockers in the first version (regex keys still wiped the scope; MultipleDocs deleted before validating; operator scopes broke legitimate writes) plus several should-fixes. All were fixed in c16d08f; a re-review found no remaining blockers. Remaining known gaps: operator scopes other than $eq/$in fail open (documented); no test yet for the bulk-write mixin path.

🤖 Generated with Claude Code

thorwhalen and others added 3 commits September 22, 2026 16:17
…ites

- `_build_doc` raises ValueError when a key or value contradicts a field of
  the write filter (`on_write_filter`, else `filter`), instead of silently
  writing the doc outside the store's scope. Consistent writes are unchanged.
- Keys used by `__setitem__`, `__delitem__`, MultipleDocs `__setitem__`
  (its `delete_many`) and the bulk-write mixin may not contain `$`-operators;
  keyword-only `allow_operators_in_write_keys=True` restores the old behaviour.
  Read keys (`s[k]`, `k in s`) still accept queries, always within the scope.
- New helper `operator_field_names`.

Closes #14

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Write/delete keys also refuse regex values (re.Pattern, bson Regex).
- MultipleDocs __setitem__ builds and validates all docs before delete_many.
- Scope contradiction uses MongoDB (BSON) equality; `$eq`/`$in` scopes are
  checked by membership, other operator scopes are not checked.
- Dotted scope fields refuse writes with a clear error.
- Replace/delete queries are confined by on_write_filter as well as filter.
- allow_operators_in_write_keys defaults to None so a class attribute works.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… writes

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thorwhalen
thorwhalen merged commit 21cec5b into master Sep 22, 2026
10 checks passed
@thorwhalen
thorwhalen deleted the scope-filter-and-operator-keys branch September 22, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scope filter can be overridden by keys/values; reject operator keys

1 participant