mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
Forbid arrayJoin in row policy filter expressions
A row policy USING expression is applied as a per-row filter at the storage read stage (a row-level, PREWHERE-like step). arrayJoin changes the number of rows, which breaks the reader's invariant and aborts with the logical error 'column->size() == num_rows'. Reject arrayJoin in a row policy filter both when the policy is created or altered and when it is applied, so existing on-disk or config-defined policies fail the query cleanly instead of crashing. arrayJoin scoped inside a nested subquery is still allowed, as it does not change the outer row count. CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=109493&sha=6774b64cd3bd77f1c9b010978b9810caa9dd015d&name_0=PR&name_1=Stress%20test%20%28amd_tsan%29 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e01cbab452
commit
a3bdef6580
@@ -557,6 +557,9 @@ RowPolicyFilterPtr ContextAccess::getRowPolicyFilter(const String & database, co
|
||||
}
|
||||
}
|
||||
|
||||
if (filter)
|
||||
checkRowPolicyFilterExpression(filter->expression);
|
||||
|
||||
if (filter && filter->policies.empty())
|
||||
{
|
||||
if (access_control->shouldThrowOnUnmatchedRowPolicies())
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <Access/RowPolicy.h>
|
||||
#include <Common/Exception.h>
|
||||
#include <Common/quoteString.h>
|
||||
#include <Parsers/ASTFunction.h>
|
||||
#include <Parsers/ASTSelectQuery.h>
|
||||
#include <boost/range/algorithm/equal.hpp>
|
||||
|
||||
|
||||
@@ -9,6 +11,31 @@ namespace DB
|
||||
namespace ErrorCodes
|
||||
{
|
||||
extern const int NOT_IMPLEMENTED;
|
||||
extern const int ILLEGAL_PREWHERE;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
/// `arrayJoin` inside a nested subquery has its own scope and does not multiply the outer rows.
|
||||
bool expressionContainsArrayJoin(const ASTPtr & ast)
|
||||
{
|
||||
if (!ast)
|
||||
return false;
|
||||
if (const auto * function = ast->as<ASTFunction>(); function && function->name == "arrayJoin")
|
||||
return true;
|
||||
for (const auto & child : ast->children)
|
||||
{
|
||||
if (!child->as<ASTSelectQuery>() && expressionContainsArrayJoin(child))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void checkRowPolicyFilterExpression(const ASTPtr & expression)
|
||||
{
|
||||
if (expressionContainsArrayJoin(expression))
|
||||
throw Exception(ErrorCodes::ILLEGAL_PREWHERE, "arrayJoin is not allowed in a row policy filter expression");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <Access/RolesOrUsersSet.h>
|
||||
#include <Access/Common/RowPolicyDefs.h>
|
||||
#include <Core/Types.h>
|
||||
#include <Parsers/IAST_fwd.h>
|
||||
#include <array>
|
||||
|
||||
|
||||
@@ -70,4 +71,8 @@ private:
|
||||
|
||||
using RowPolicyPtr = std::shared_ptr<const RowPolicy>;
|
||||
|
||||
/// A row policy filter is applied as a per-row predicate at the storage read stage, so a
|
||||
/// row-count-changing function like `arrayJoin` breaks the reader's invariants. Reject it.
|
||||
void checkRowPolicyFilterExpression(const ASTPtr & expression);
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ namespace
|
||||
policy.setRestrictive(*query.is_restrictive);
|
||||
|
||||
for (const auto & [filter_type, filter] : query.filters)
|
||||
{
|
||||
checkRowPolicyFilterExpression(filter);
|
||||
policy.filters[static_cast<size_t>(filter_type)] = filter ? filter->formatWithSecretsOneLine() : String{};
|
||||
}
|
||||
|
||||
if (override_to_roles)
|
||||
policy.to_roles = *override_to_roles;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
1 10
|
||||
2 20
|
||||
1 10
|
||||
2 20
|
||||
@@ -0,0 +1,27 @@
|
||||
-- A row policy filter is applied as a per-row predicate at the storage read stage. `arrayJoin`
|
||||
-- changes the number of rows, which used to abort with 'column->size() == num_rows' (a logical
|
||||
-- error). It must be rejected instead: when the policy is created, when it is altered, and when
|
||||
-- it is applied.
|
||||
|
||||
DROP ROW POLICY IF EXISTS policy_with_array_join ON row_policy_table;
|
||||
DROP ROW POLICY IF EXISTS valid_policy ON row_policy_table;
|
||||
DROP TABLE IF EXISTS row_policy_table;
|
||||
|
||||
CREATE TABLE row_policy_table (id UInt32, value UInt32) ENGINE = MergeTree ORDER BY id;
|
||||
INSERT INTO row_policy_table VALUES (1, 10), (2, 20);
|
||||
|
||||
-- Creating a policy whose filter contains arrayJoin is rejected.
|
||||
CREATE ROW POLICY policy_with_array_join ON row_policy_table FOR SELECT USING arrayJoin([1, 2]) OR (value = 0) TO ALL; -- { serverError ILLEGAL_PREWHERE }
|
||||
|
||||
-- Altering a valid policy to introduce arrayJoin is rejected too; the original filter is kept.
|
||||
CREATE ROW POLICY valid_policy ON row_policy_table FOR SELECT USING value > 0 TO ALL;
|
||||
ALTER ROW POLICY valid_policy ON row_policy_table FOR SELECT USING arrayJoin([1, 2]) OR (value > 0); -- { serverError ILLEGAL_PREWHERE }
|
||||
SELECT * FROM row_policy_table ORDER BY id;
|
||||
DROP ROW POLICY valid_policy ON row_policy_table;
|
||||
|
||||
-- arrayJoin scoped inside a subquery does not change the outer row count and stays allowed.
|
||||
CREATE ROW POLICY valid_policy ON row_policy_table FOR SELECT USING value IN (SELECT arrayJoin([10, 20])) TO ALL;
|
||||
SELECT * FROM row_policy_table ORDER BY id;
|
||||
|
||||
DROP ROW POLICY valid_policy ON row_policy_table;
|
||||
DROP TABLE row_policy_table;
|
||||
Reference in New Issue
Block a user