Skip to content

Commit a2a42bb

Browse files
fix(auth): stop BLOB view predicate lookups at LIMIT under query-auth
A query-auth read with BLOB columns resolves the views its predicates read before LIMIT applies, a whole batch at a time, so it could look up references past the quota and fail on one no row needed. Read one candidate at a time in that case, as the unrestricted read already does.
1 parent 1cb0197 commit a2a42bb

2 files changed

Lines changed: 41 additions & 8 deletions

File tree

‎crates/paimon/src/table/table_read.rs‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,17 +1169,29 @@ impl<'a> PaimonTableRead<'a> {
11691169
})
11701170
});
11711171
predicates.predicates = before_mask;
1172+
let blob_options = has_blobs.then(|| self.masked_blob_options(&masks));
11721173
let unresolved_table;
1173-
if has_blobs {
1174+
if let Some(payload_options) = &blob_options {
11741175
// Keep payload predicates above authorization too: they can open
11751176
// a descriptor belonging to a row the server excludes.
11761177
inner.data_predicates.clear();
1177-
unresolved_table = self
1178-
.table
1179-
.copy_with_options(std::collections::HashMap::from([
1180-
("blob-as-descriptor".to_string(), "true".to_string()),
1181-
("blob-view.resolve.enabled".to_string(), "false".to_string()),
1182-
]));
1178+
let mut options = std::collections::HashMap::from([
1179+
("blob-as-descriptor".to_string(), "true".to_string()),
1180+
("blob-view.resolve.enabled".to_string(), "false".to_string()),
1181+
]);
1182+
// Views a predicate reads resolve before LIMIT applies. Read one
1183+
// candidate at a time, as the unrestricted read does, so no
1184+
// reference past the quota is looked up.
1185+
let payload_options = CoreOptions::new(payload_options);
1186+
if self.limit.is_some()
1187+
&& payload_options.blob_view_resolve_enabled()
1188+
&& self.table.rest_env().is_some()
1189+
&& !super::query_auth::leaf_names(&pending)
1190+
.is_disjoint(&payload_options.blob_view_fields())
1191+
{
1192+
options.insert("read.batch-size".to_string(), "1".to_string());
1193+
}
1194+
unresolved_table = self.table.copy_with_options(options);
11831195
inner.table = &unresolved_table;
11841196
}
11851197
let stream = inner.read_splits(data_splits, &inner.table.schema.core_options())?;
@@ -1191,7 +1203,6 @@ impl<'a> PaimonTableRead<'a> {
11911203
row_filter_factory: None,
11921204
file_fields: schema_fields.clone(),
11931205
};
1194-
let blob_options = has_blobs.then(|| self.masked_blob_options(&masks));
11951206
let stream = stream.map(move |batch| {
11961207
let batch = batch?;
11971208
let names_match = batch.num_columns() == physical.len()

‎crates/paimon/tests/rest_catalog_test.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,28 @@ async fn test_blob_view_limit_only_resolves_selected_references() {
12591259
);
12601260
}
12611261

1262+
// As without authorization, a predicate on the resolved view under LIMIT
1263+
// must stop before the invalid fourth reference the server still admits.
1264+
server.set_auth_response(
1265+
"default",
1266+
"blob_view_target",
1267+
rules(&[int_leaf(0, "id", "LESS_THAN", 5)], &[]),
1268+
);
1269+
let mut limited = authorized.new_read_builder();
1270+
limited.with_limit(2);
1271+
limited.with_filter(
1272+
PredicateBuilder::new(authorized.schema().fields())
1273+
.is_not_null("picture")
1274+
.unwrap(),
1275+
);
1276+
assert_eq!(
1277+
collect_blob_rows(&read_all(&limited).await.unwrap()),
1278+
vec![
1279+
(1, "Updated".to_string(), Some(first_payload.clone())),
1280+
(3, "Repeated again".to_string(), Some(b"bob".to_vec())),
1281+
]
1282+
);
1283+
12621284
// A view reference and its upstream BLOB can disagree on nullness.
12631285
// Until rules can resolve that dependency safely, reject instead of
12641286
// authorizing rows by the reference bytes.

0 commit comments

Comments
 (0)