fix(search): apply property_filters and validate group_id on write path#1633
fix(search): apply property_filters and validate group_id on write path#1633danielchalef wants to merge 3 commits into
Conversation
Two related correctness/security fixes in the search-filter and partition-key handling. 1. SearchFilters.property_filters was declared but never read by node_search_filter_query_constructor or edge_search_filter_query_constructor, so property filters were silently ignored and searches returned unfiltered results. Both constructors now build parameterized Cypher fragments from property_filters via a shared property_filter_query_constructor helper. Property names cannot be parameterized in Cypher, so they are validated against a safe-identifier pattern (validate_property_name) both at the Pydantic layer and defensively in the constructor to prevent injection. 2. The write path accepted arbitrary group_id values while the read/delete path rejects anything outside [a-zA-Z0-9_-], allowing records to be written that are permanently unreachable and undeletable. group_id is now validated on the core Node and Edge models and on the AddMessagesRequest / AddEntityNodeRequest server DTOs. GroupIdValidationError now subclasses ValueError so Pydantic surfaces it as a normal validation error (HTTP 422). Adds regression tests covering property-filter construction, null operators, property-name injection rejection, and write-path group_id validation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gj4mbjvGJ4oEtqdzTN1UqP
|
I have read the CLA Document and I hereby sign the CLA behalf on myself, e-mail: example@example.com or I have read the CLA Document and I hereby sign the CLA behalf of my company, e-mail: example@example.com Signature is valid for 6 months. This bot will be retriggered when the Contributor License Agreement comment has been provided. Posted by the CLA Assistant Lite bot. |
|
Claude finished @danielchalef's task in 3m 21s —— View job PR Review: apply property_filters and validate group_id on write pathI reviewed the diff against Findings
💡 Suggestion — test coverage gaps (low)
💡 Suggestion — dead helper (low, pre-existing) What I verified looks correct
Nice work overall — the core injection/asymmetry fixes are sound. The main thing to resolve is whether hydration of legacy |
| @field_validator('group_id') | ||
| @classmethod | ||
| def validate_group_id_field(cls, value: str) -> str: | ||
| validate_group_id(value) | ||
| return value |
There was a problem hiding this comment.
Warning (medium confidence): This validator runs on every Node/EntityNode construction, including DB hydration — get_entity_node_from_record (nodes.py:1074) builds an EntityNode directly from the stored group_id. The equivalent edge path does the same.
The write path via add_episode already validated group_id (graphiti.py:1078), but add_triplet, direct model construction, bulk ops, and the server's AddEntityNodeRequest did not — so a deployment can hold legacy records whose group_id violates [a-zA-Z0-9_-]. After this change, any read that hydrates such a record will raise ValidationError, not just writes. This is reachable: fulltext/similarity searches only filter by group_id when group_ids is not None, so a cross-partition search (group_ids=None) returns and then tries to construct nodes of any group_id.
Net effect: the PR closes the write gap but converts previously "unreachable" legacy records into a hard read failure for cross-partition queries. Consider validating only at write boundaries (save()/DTOs/add_triplet) or using model_construct on the hydration path so deserialization of existing data can't break. At minimum, worth calling out in the migration note.
Addresses PR review: the previous change validated group_id in the Node and
Edge model field validators, which fire on *every* construction — including DB
hydration in get_*_from_record. A cross-partition (group_ids=None) read that
hydrated a legacy record whose stored group_id violates [a-zA-Z0-9_-] would then
raise instead of only failing on writes, converting harmless-but-unreachable
rows into hard read failures.
Move validation to the persistence boundary so hydration stays tolerant of any
stored value:
* Remove the group_id field validators from Node and Edge.
* Validate in EntityNode.save() and EntityEdge.save().
* Validate every element in add_nodes_and_edges_bulk_tx (the bulk path bypasses
save(); this also covers add_triplet, which persists via that path).
* Server DTOs (AddMessagesRequest / AddEntityNodeRequest) re-raise as ValueError
so Pydantic surfaces a 422 regardless of the installed graphiti-core's
GroupIdValidationError class hierarchy.
add_episode / add_episode_bulk already validate group_id at entry.
Also from review feedback:
* Document that a single property_filters list is intentionally applied to node
alias `n` and edge alias `e`.
* Tests: node construction is tolerant of any group_id; save()/edge save reject
unsafe group_ids; is_not_null operator; shared node/edge filter semantics; and
server DTO rejection/acceptance of group_ids.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gj4mbjvGJ4oEtqdzTN1UqP
|
Thanks for the review — addressed in d38e1e9. Hydration hard-fail (main finding): moved
Suggestions:
Generated by Claude Code |
|
Claude finished @danielchalef's task in 1m 46s —— View job PR Review — apply property_filters and validate group_id on write pathI re-reviewed the latest state (commit Prior findings — all resolved ✅
What I verified this round
Findings💡 Suggestion — write-boundary validation only covers VerdictThe core fixes (silent |
…elper Follow-up to review: previously only EntityNode.save() and EntityEdge.save() validated group_id at the write boundary. A direct EpisodicNode(...).save() / CommunityNode(...).save() (and the episodic/community/saga edge saves) with a non-conforming group_id could still create an unreachable record — a latent version of the same write/read asymmetry this PR closes. Lift the check into Node._validate_for_write() / Edge._validate_for_write() and call it from every concrete save() override, so all node and edge writes are guarded consistently. Hydration still constructs models directly and stays tolerant of any stored value. Adds a regression test that a non-entity save (EpisodicNode) also rejects an unsafe group_id. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gj4mbjvGJ4oEtqdzTN1UqP
|
Claude finished @danielchalef's task in 1m 14s —— View job PR Review — apply property_filters and validate group_id on write pathRe-reviewed the latest state (commit
Prior finding — resolved ✅The last round's low-severity completeness note (only
What I verified this round
FindingsNo new issues. The remaining item from the previous review is resolved, and I found no regressions introduced by the shared-helper refactor. VerdictAll three original fixes (silently-ignored |
|
Addressed the remaining completeness note in 7e95161: lifted the write-boundary check into Generated by Claude Code |
|
@danielchalef Verified this: property_filters passed into search were being silently ignored, and invalid group_ids were accepted on write. With your patch the filters generate real clauses and bad group_ids get rejected with a clear error. The 20 validation tests pass. |
Summary
Fixes two related correctness/security issues in search-filter and partition-key (
group_id) handling.1.
SearchFilters.property_filterswas silently ignored.The field was declared on
SearchFiltersbut never read bynode_search_filter_query_constructororedge_search_filter_query_constructor, so searches always executed without the intended property filter and returned data callers believed was restricted. Both constructors now build parameterized Cypher fragments fromproperty_filtersvia a sharedproperty_filter_query_constructorhelper (n.<prop>/e.<prop>with the value passed as a query parameter;IS NULL/IS NOT NULLemit no parameter). Because Cypher property keys cannot be parameterized,property_nameis validated against a safe-identifier pattern (validate_property_name) both at the Pydantic layer and defensively inside the constructor, preventing query injection.2. Write/read asymmetry on
group_id.The write path accepted arbitrary
group_idvalues while the read/delete path rejects anything outside[a-zA-Z0-9_-], allowing records to be written that are permanently unreachable and undeletable via any API path.group_idis now validated on the coreNodeandEdgemodels and on theAddMessagesRequest/AddEntityNodeRequestserver DTOs.GroupIdValidationErrornow also subclassesValueErrorso Pydantic surfaces it as a normal validation error (HTTP 422) instead of a 500.Type of Change
Objective
Restore the documented behavior of
property_filters(operators rely on it to scope results) and close the write-path gap that let unreachable/undeletable records be persisted.Testing
New regression tests in
tests/utils/search/test_search_security.pycover:IS NULL/IS NOT NULLoperators emitting no paramsmodel_construct)group_idrejection onEntityNode/EntityEdgeand acceptance of valid/default valuesExisting search-security, node, edge, and attribute unit tests pass;
make format,ruff check, andpyrightare clean on the changed files.Breaking Changes
Behavioral note (not a code-contract break): callers that were previously writing
group_idvalues containing characters outside[a-zA-Z0-9_-]will now receive a validation error at write time instead of silently creating unreachable records. This aligns the write path with the already-enforced read/delete path.Checklist
make lintpasses)Related Issues
N/A
🤖 Generated with Claude Code
Generated by Claude Code