diff --git a/mealie/services/query_filter/builder.py b/mealie/services/query_filter/builder.py index 499d1a631..278056254 100644 --- a/mealie/services/query_filter/builder.py +++ b/mealie/services/query_filter/builder.py @@ -249,8 +249,9 @@ class QueryFilterBuilder: mapper = sa.inspect(current_model) relationship = mapper.relationships[proxied_attribute_link] current_model = relationship.mapper.class_ - if not allow_restricted and current_model.__filter_restricted__: - raise ValueError(f"cannot traverse into restricted model '{current_model.__name__}'") + + # Association proxies are intentional field exposures defined on the source model, + # so we do not apply the __filter_restricted__ check here. model_attr = cls._get_model_attr(current_model, next_attribute_link) # at the end of the chain there are no more relationships to inspect diff --git a/tests/unit_tests/repository_tests/test_query_filter_builder.py b/tests/unit_tests/repository_tests/test_query_filter_builder.py index 1f481c179..3333b2bf4 100644 --- a/tests/unit_tests/repository_tests/test_query_filter_builder.py +++ b/tests/unit_tests/repository_tests/test_query_filter_builder.py @@ -117,6 +117,15 @@ def test_restricted_traversal_blocked_when_disallowed(): QueryFilterBuilder.get_model_and_model_attr_from_attr_string("user.email", RecipeModel, allow_restricted=False) +def test_association_proxy_through_restricted_model_allowed(): + """Association proxies (e.g. household_id) traverse through User but are intentional + exposures on the source model and must NOT be blocked even when allow_restricted=False.""" + model, attr, _ = QueryFilterBuilder.get_model_and_model_attr_from_attr_string( + "household_id", RecipeModel, allow_restricted=False + ) + assert model is User + + def test_restricted_traversal_allowed_by_default(): """Traversing into User via RecipeModel.user should succeed when allow_restricted=True (default).""" model, attr, _ = QueryFilterBuilder.get_model_and_model_attr_from_attr_string("user.email", RecipeModel)