Skip to content

Commit af8b63c

Browse files
Always use identity when hashing queries from owners
1 parent 013e268 commit af8b63c

5 files changed

Lines changed: 106 additions & 11 deletions

File tree

crates/core/src/subscription/module_subscription_actor.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,83 @@ mod tests {
15341534
Ok(())
15351535
}
15361536

1537+
/// Test that a client and the database owner can subscribe to the same query
1538+
#[tokio::test]
1539+
async fn test_rls_for_owner() -> anyhow::Result<()> {
1540+
// Establish a connection for owner and client
1541+
let (tx_for_a, mut rx_for_a) = client_connection(client_id_from_u8(0));
1542+
let (tx_for_b, mut rx_for_b) = client_connection(client_id_from_u8(1));
1543+
1544+
let db = relational_db()?;
1545+
let subs = ModuleSubscriptions::for_test_enclosing_runtime(db.clone());
1546+
1547+
// Create table `t`
1548+
let table_id = db.create_table_for_test("t", &[("id", AlgebraicType::identity())], &[0.into()])?;
1549+
1550+
// Restrict access to `t`
1551+
insert_rls_rules(&db, [table_id], ["select * from t where id = :sender"])?;
1552+
1553+
let mut query_ids = 0;
1554+
1555+
// Have owner and client subscribe to `t`
1556+
subscribe_multi(&subs, &["select * from t"], tx_for_a, &mut query_ids)?;
1557+
subscribe_multi(&subs, &["select * from t"], tx_for_b, &mut query_ids)?;
1558+
1559+
// Wait for both subscriptions
1560+
assert_matches!(
1561+
rx_for_a.recv().await,
1562+
Some(SerializableMessage::Subscription(SubscriptionMessage {
1563+
result: SubscriptionResult::SubscribeMulti(_),
1564+
..
1565+
}))
1566+
);
1567+
assert_matches!(
1568+
rx_for_b.recv().await,
1569+
Some(SerializableMessage::Subscription(SubscriptionMessage {
1570+
result: SubscriptionResult::SubscribeMulti(_),
1571+
..
1572+
}))
1573+
);
1574+
1575+
let schema = ProductType::from([AlgebraicType::identity()]);
1576+
1577+
let id_for_b = identity_from_u8(1);
1578+
let id_for_c = identity_from_u8(2);
1579+
1580+
commit_tx(
1581+
&db,
1582+
&subs,
1583+
[],
1584+
[
1585+
// Insert an identity for client `b` plus a random identity
1586+
(table_id, product![id_for_b]),
1587+
(table_id, product![id_for_c]),
1588+
],
1589+
)?;
1590+
1591+
assert_tx_update_for_table(
1592+
&mut rx_for_a,
1593+
table_id,
1594+
&schema,
1595+
// The owner should receive both identities
1596+
[product![id_for_b], product![id_for_c]],
1597+
[],
1598+
)
1599+
.await;
1600+
1601+
assert_tx_update_for_table(
1602+
&mut rx_for_b,
1603+
table_id,
1604+
&schema,
1605+
// Client `b` should only receive its identity
1606+
[product![id_for_b]],
1607+
[],
1608+
)
1609+
.await;
1610+
1611+
Ok(())
1612+
}
1613+
15371614
/// Test that we do not send empty updates to clients
15381615
#[tokio::test]
15391616
async fn test_no_empty_updates() -> anyhow::Result<()> {

crates/core/src/subscription/query.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ pub fn compile_query_with_hashes(
107107
let tx = SchemaViewer::new(tx, auth);
108108
let (plans, has_param) = SubscriptionPlan::compile(input, &tx, auth)?;
109109

110-
if has_param {
110+
if auth.is_owner() || has_param {
111+
// Note that when generating hashes for queries from owners,
112+
// we always treat them as if they were parameterized by :sender.
113+
// This is because RLS is not applicable to owners.
114+
// Hence owner hashes must never overlap with client hashes.
111115
return Ok(Plan::new(plans, hash_with_param, input.to_owned()));
112116
}
113117
Ok(Plan::new(plans, hash, input.to_owned()))

crates/core/src/subscription/subscription.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -614,13 +614,25 @@ pub(crate) fn get_all(relational_db: &RelationalDB, tx: &Tx, auth: &AuthCtx) ->
614614
.get_all_tables(tx)?
615615
.iter()
616616
.map(Deref::deref)
617-
.filter(|t| {
618-
t.table_type == StTableType::User && (auth.owner == auth.caller || t.table_access == StAccess::Public)
619-
})
617+
.filter(|t| t.table_type == StTableType::User && (auth.is_owner() || t.table_access == StAccess::Public))
620618
.map(|schema| {
621619
let sql = format!("SELECT * FROM {}", schema.table_name);
622-
SubscriptionPlan::compile(&sql, &SchemaViewer::new(tx, auth), auth)
623-
.map(|(plans, has_param)| Plan::new(plans, QueryHash::from_string(&sql, auth.caller, has_param), sql))
620+
let tx = SchemaViewer::new(tx, auth);
621+
SubscriptionPlan::compile(&sql, &tx, auth).map(|(plans, has_param)| {
622+
Plan::new(
623+
plans,
624+
QueryHash::from_string(
625+
&sql,
626+
auth.caller,
627+
// Note that when generating hashes for queries from owners,
628+
// we always treat them as if they were parameterized by :sender.
629+
// This is because RLS is not applicable to owners.
630+
// Hence owner hashes must never overlap with client hashes.
631+
auth.is_owner() || has_param,
632+
),
633+
sql,
634+
)
635+
})
624636
})
625637
.collect::<Result<_, _>>()?)
626638
}
@@ -638,9 +650,7 @@ pub(crate) fn legacy_get_all(
638650
.get_all_tables(tx)?
639651
.iter()
640652
.map(Deref::deref)
641-
.filter(|t| {
642-
t.table_type == StTableType::User && (auth.owner == auth.caller || t.table_access == StAccess::Public)
643-
})
653+
.filter(|t| t.table_type == StTableType::User && (auth.is_owner() || t.table_access == StAccess::Public))
644654
.map(|src| SupportedQuery {
645655
kind: query::Supported::Select,
646656
expr: QueryExpr::new(src),

crates/expr/src/rls.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub fn resolve_views_for_sub(
1818
has_param: &mut bool,
1919
) -> anyhow::Result<Vec<ProjectName>> {
2020
// RLS does not apply to the database owner
21-
if auth.caller == auth.owner {
21+
if auth.is_owner() {
2222
return Ok(vec![expr]);
2323
}
2424

@@ -56,7 +56,7 @@ pub fn resolve_views_for_sub(
5656
/// Mainly a wrapper around [resolve_views_for_expr].
5757
pub fn resolve_views_for_sql(tx: &impl SchemaView, expr: ProjectList, auth: &AuthCtx) -> anyhow::Result<ProjectList> {
5858
// RLS does not apply to the database owner
59-
if auth.caller == auth.owner {
59+
if auth.is_owner() {
6060
return Ok(expr);
6161
}
6262
// The subscription language is a subset of the sql language.

crates/lib/src/identity.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ impl AuthCtx {
2222
pub fn for_current(owner: Identity) -> Self {
2323
Self { owner, caller: owner }
2424
}
25+
/// Does `owner == caller`
26+
pub fn is_owner(&self) -> bool {
27+
self.owner == self.caller
28+
}
2529
/// WARNING: Use this only for simple test were the `auth` don't matter
2630
pub fn for_testing() -> Self {
2731
AuthCtx {

0 commit comments

Comments
 (0)