Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion crates/bindings-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ bench = false

[dependencies]
spacetimedb-primitives.workspace = true
spacetimedb-sql-parser.workspace = true

bitflags.workspace = true
humantime.workspace = true
Expand Down
50 changes: 0 additions & 50 deletions crates/bindings-macro/src/filter.rs

This file was deleted.

58 changes: 42 additions & 16 deletions crates/bindings-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
//! Defines procedural macros like `#[spacetimedb::table]`,
//! simplifying writing SpacetimeDB modules in Rust.

mod filter;
mod reducer;
mod sats;
mod table;
Expand All @@ -11,8 +10,8 @@ use proc_macro::TokenStream as StdTokenStream;
use proc_macro2::TokenStream;
use quote::quote;
use std::time::Duration;
use syn::ItemFn;
use syn::{parse::ParseStream, Attribute};
use syn::{ItemConst, ItemFn};
use util::{cvt_attr, ok_or_compile_error};

mod sym {
Expand Down Expand Up @@ -360,17 +359,24 @@ pub fn schema_type(input: StdTokenStream) -> StdTokenStream {
})
}

/// Generates code for registering a row-level security `SQL` function.
/// Generates code for registering a row-level security rule.
///
/// A row-level security function takes a `SQL` query expression that is used to filter rows.
/// This attribute must be applied to a `const` binding of type [`Filter`].
/// It will be interpreted as a filter on the table to which it applies, for all client queries.
/// If a module contains multiple `client_visibility_filter`s for the same table,
/// they will be unioned together as if by SQL `OR`,
/// so that any row permitted by at least one filter is visible.
///
/// The `const` binding's identifier must be unique within the module.
///
/// The query follows the same syntax as a subscription query.
///
/// **Example:**
/// ## Example:
///
/// ```rust,ignore
/// /// Players can only see what's in their chunk
/// spacetimedb::filter!("
/// #[spacetimedb::client_visibility_filter]
/// const PLAYERS_SEE_ENTITIES_IN_SAME_CHUNK: Filter = Filter::Sql("
/// SELECT * FROM LocationState WHERE chunk_index IN (
/// SELECT chunk_index FROM LocationState WHERE entity_id IN (
/// SELECT entity_id FROM UserState WHERE identity = @sender
Expand All @@ -379,14 +385,34 @@ pub fn schema_type(input: StdTokenStream) -> StdTokenStream {
/// ");
/// ```
///
/// **NOTE:** The `SQL` query expression is pre-parsed at compile time, but only check is a valid
/// subscription query *syntactically*, not that the query is valid when executed.
///
/// For example, it could refer to a non-existent table.
#[proc_macro]
pub fn filter(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let arg = syn::parse_macro_input!(input);
filter::filter_impl(arg)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
/// Queries are not checked for syntactic or semantic validity
/// until they are processed by the SpacetimeDB host.
/// This means that errors in queries, such as syntax errors, type errors or unknown tables,
/// will be reported during `spacetime publish`, not at compile time.
#[doc(hidden)] // TODO: RLS filters are currently unimplemented, and are not enforced.
#[proc_macro_attribute]
pub fn client_visibility_filter(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
ok_or_compile_error(|| {
if !args.is_empty() {
return Err(syn::Error::new_spanned(
TokenStream::from(args),
"The `client_visibility_filter` attribute does not accept arguments",
));
}

let item: ItemConst = syn::parse(item)?;
let rls_ident = item.ident.clone();
let register_rls_symbol = format!("__preinit__20_register_row_level_security_{rls_ident}");

Ok(quote! {
#item

const _: () = {
#[export_name = #register_rls_symbol]
extern "C" fn __register_client_visibility_filter() {
spacetimedb::rt::register_row_level_security(#rls_ident.sql_text())
}
};
})
})
}
25 changes: 25 additions & 0 deletions crates/bindings/src/client_visibility_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// A row-level security filter,
/// which can be registered using the [`crate::client_visibility_filter`] attribute.
#[non_exhaustive]
pub enum Filter {
/// A SQL query. Rows that match this query will be made visible to clients.
///
/// The query must be of the form `SELECT * FROM table` or `SELECT table.* from table`,
/// followed by any number of `JOIN` clauses and a `WHERE` clause.
/// If the query includes any `JOIN`s, it must be in the form `SELECT table.* FROM table`.
/// In any case, the query must select all of the columns from a single table, and nothing else.
///
/// SQL queries are not checked for syntactic or semantic validity
/// until they are processed by the SpacetimeDB host.
/// This means that errors in queries used as [`crate::client_visibility_filter`] rules
/// will be reported during `spacetime publish`, not at compile time.
Sql(&'static str),
}

impl Filter {
#[doc(hidden)]
pub fn sql_text(&self) -> &'static str {
let Filter::Sql(sql) = self;
sql
}
}
8 changes: 6 additions & 2 deletions crates/bindings/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Provides safe abstractions around `bindings-sys`
//! and re-exports `#[spacetimedb]` and `#[duration]`.

mod client_visibility_filter;
pub mod log_stopwatch;
mod logger;
#[cfg(feature = "rand")]
Expand All @@ -19,12 +20,15 @@ pub use log;
#[cfg(feature = "rand")]
pub use rand;

#[doc(hidden)]
pub use client_visibility_filter::Filter;
#[cfg(feature = "rand")]
pub use rng::StdbRng;
pub use sats::SpacetimeType;
#[doc(hidden)]
pub use spacetimedb_bindings_macro::__TableHelper;
pub use spacetimedb_bindings_macro::{duration, filter, reducer, table};
// TODO: move `client_visibility_filter` out of `doc(hidden)` once RLS is implemented.
pub use spacetimedb_bindings_macro::{__TableHelper, client_visibility_filter};
pub use spacetimedb_bindings_macro::{duration, reducer, table};
pub use spacetimedb_bindings_sys as sys;
pub use spacetimedb_lib;
pub use spacetimedb_lib::de::{Deserialize, DeserializeOwned};
Expand Down
23 changes: 17 additions & 6 deletions crates/bindings/src/rt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,20 @@ pub trait RowLevelSecurityInfo {
const SQL: &'static str;
}

/// A function which will be registered by [`register_describer`] into [`DESCRIBERS`],
/// which will be called by [`__describe_module__`] to construct a module definition.
///
/// May be a closure over static data, so that e.g.
/// [`register_row_level_security`] doesn't need to take a type parameter.
/// Permitted by the type system to be a [`FnMut`] mutable closure,
/// since [`DESCRIBERS`] is in a [`Mutex`] anyways,
/// but will likely cause weird misbehaviors if a non-idempotent function is used.
trait DescriberFn: FnMut(&mut ModuleBuilder) + Send + 'static {}
impl<F: FnMut(&mut ModuleBuilder) + Send + 'static> DescriberFn for F {}

/// Registers into `DESCRIBERS` a function `f` to modify the module builder.
fn register_describer(f: fn(&mut ModuleBuilder)) {
DESCRIBERS.lock().unwrap().push(f)
fn register_describer(f: impl DescriberFn) {
DESCRIBERS.lock().unwrap().push(Box::new(f))
}

/// Registers a describer for the `SpacetimeType` `T`.
Expand Down Expand Up @@ -373,9 +384,9 @@ pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>)
}

/// Registers a row-level security policy.
pub fn register_row_level_security<R: RowLevelSecurityInfo>() {
pub fn register_row_level_security(sql: &'static str) {
register_describer(|module| {
module.inner.add_row_level_security(R::SQL);
module.inner.add_row_level_security(sql);
})
}

Expand All @@ -389,7 +400,7 @@ struct ModuleBuilder {
}

// Not actually a mutex; because WASM is single-threaded this basically just turns into a refcell.
static DESCRIBERS: Mutex<Vec<fn(&mut ModuleBuilder)>> = Mutex::new(Vec::new());
static DESCRIBERS: Mutex<Vec<Box<dyn DescriberFn>>> = Mutex::new(Vec::new());

/// A reducer function takes in `(Sender, Timestamp, Args)`
/// and returns a result with a possible error message.
Expand All @@ -415,7 +426,7 @@ static REDUCERS: OnceLock<Vec<ReducerFn>> = OnceLock::new();
extern "C" fn __describe_module__(description: BytesSink) {
// Collect the `module`.
let mut module = ModuleBuilder::default();
for describer in &*DESCRIBERS.lock().unwrap() {
for describer in &mut *DESCRIBERS.lock().unwrap() {
describer(&mut module)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
source: crates/bindings/tests/deps.rs
expression: "cargo tree -p spacetimedb -f {lib} -e no-dev"
---
total crates: 69
total crates: 67
spacetimedb
├── bytemuck
├── derive_more
Expand Down Expand Up @@ -48,15 +48,6 @@ spacetimedb
│ │ ├── itertools
│ │ │ └── either
│ │ └── nohash_hasher
│ ├── spacetimedb_sql_parser
│ │ ├── derive_more (*)
│ │ ├── sqlparser
│ │ │ └── log
│ │ └── thiserror
│ │ └── thiserror_impl
│ │ ├── proc_macro2 (*)
│ │ ├── quote (*)
│ │ └── syn (*)
│ └── syn (*)
├── spacetimedb_bindings_sys
│ └── spacetimedb_primitives (*)
Expand Down Expand Up @@ -93,7 +84,11 @@ spacetimedb
│ │ │ └── equivalent
│ │ ├── nohash_hasher
│ │ ├── smallvec
│ │ └── thiserror (*)
│ │ └── thiserror
│ │ └── thiserror_impl
│ │ ├── proc_macro2 (*)
│ │ ├── quote (*)
│ │ └── syn (*)
│ ├── spacetimedb_primitives (*)
│ ├── spacetimedb_sats
│ │ ├── arrayvec
Expand Down
3 changes: 2 additions & 1 deletion modules/sdk-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,8 @@ define_tables! {
#[spacetimedb::reducer]
fn no_op_succeeds(_ctx: &ReducerContext) {}

spacetimedb::filter!("SELECT * FROM one_u8");
#[spacetimedb::client_visibility_filter]
const ONE_U8_VISIBLE: spacetimedb::Filter = spacetimedb::Filter::Sql("SELECT * FROM one_u8");

#[spacetimedb::table(name = scheduled_table, scheduled(send_scheduled_message), public)]
pub struct ScheduledTable {
Expand Down
6 changes: 4 additions & 2 deletions smoketests/tests/auto_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class AddTableAutoMigration(Smoketest):
y: f64,
}

spacetimedb::filter!("SELECT * FROM person");
#[spacetimedb::client_visibility_filter]
const PERSON_VISIBLE: spacetimedb::Filter = spacetimedb::Filter::Sql("SELECT * FROM person");
"""

MODULE_CODE_UPDATED = (
Expand All @@ -60,7 +61,8 @@ class AddTableAutoMigration(Smoketest):
}
}

spacetimedb::filter!("SELECT * FROM book");
#[spacetimedb::client_visibility_filter]
const BOOK_VISIBLE: spacetimedb::Filter = spacetimedb::Filter::Sql("SELECT * FROM book");
"""
)

Expand Down
Loading