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
15 changes: 15 additions & 0 deletions crates/bindings-csharp/Codegen.Tests/fixtures/diag/Lib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,18 @@ public partial struct TestScheduleIssues
[SpacetimeDB.Reducer]
public static void DummyScheduledReducer(ReducerContext ctx, TestScheduleIssues table) { }
}

public partial class Module
{
// Invalid: not public static readonly
[SpacetimeDB.ClientVisibilityFilter]
private Filter MY_FILTER = new Filter.Sql("SELECT * FROM TestAutoIncNotInteger");

// Invalid: not public static readonly
[SpacetimeDB.ClientVisibilityFilter]
public static Filter MY_SECOND_FILTER = new Filter.Sql("SELECT * FROM TestAutoIncNotInteger");

// Invalid: not a Filter
[SpacetimeDB.ClientVisibilityFilter]
public static readonly string MY_THIRD_FILTER = "SELECT * FROM TestAutoIncNotInteger";
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,51 @@ partial struct TestTypeParams<T> : System.IEquatable<TestTypeParams>, Spacetime
NotConfigurable
]
}
},
{/*
SpacetimeDB.Internal.Module.RegisterTable<global::TestUniqueNotEquatable, SpacetimeDB.Internal.TableHandles.TestUniqueNotEquatable>();
SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_FILTER);
^^^^^^^^^
SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_SECOND_FILTER);
*/
Message: 'Module.MY_FILTER' is inaccessible due to its protection level,
Severity: Error,
Descriptor: {
Id: CS0122,
Title: ,
HelpLink: https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0122),
MessageFormat: '{0}' is inaccessible due to its protection level,
Category: Compiler,
DefaultSeverity: Error,
IsEnabledByDefault: true,
CustomTags: [
Compiler,
Telemetry,
NotConfigurable
]
}
},
{/*
SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_SECOND_FILTER);
SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(global::Module.MY_THIRD_FILTER);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
*/
Message: Argument 1: cannot convert from 'string' to 'SpacetimeDB.Filter',
Severity: Error,
Descriptor: {
Id: CS1503,
Title: ,
HelpLink: https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS1503),
MessageFormat: Argument {0}: cannot convert from '{1}' to '{2}',
Category: Compiler,
DefaultSeverity: Error,
IsEnabledByDefault: true,
CustomTags: [
Compiler,
Telemetry,
NotConfigurable
]
}
}
]

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

Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,57 @@ public partial struct TestScheduleIssues
DefaultSeverity: Error,
IsEnabledByDefault: true
}
},
{/*
[SpacetimeDB.ClientVisibilityFilter]
private Filter MY_FILTER = new Filter.Sql("SELECT * FROM TestAutoIncNotInteger");
^^^^^^^^^

*/
Message: Field MY_FILTER is marked as [ClientVisibilityFilter] but it is not public static readonly,
Severity: Error,
Descriptor: {
Id: STDB0016,
Title: ClientVisibilityFilters must be public static readonly,
MessageFormat: Field {0} is marked as [ClientVisibilityFilter] but it is not public static readonly,
Category: SpacetimeDB,
DefaultSeverity: Error,
IsEnabledByDefault: true
}
},
{/*
[SpacetimeDB.ClientVisibilityFilter]
public static Filter MY_SECOND_FILTER = new Filter.Sql("SELECT * FROM TestAutoIncNotInteger");
^^^^^^^^^^^^^^^^

*/
Message: Field MY_SECOND_FILTER is marked as [ClientVisibilityFilter] but it is not public static readonly,
Severity: Error,
Descriptor: {
Id: STDB0016,
Title: ClientVisibilityFilters must be public static readonly,
MessageFormat: Field {0} is marked as [ClientVisibilityFilter] but it is not public static readonly,
Category: SpacetimeDB,
DefaultSeverity: Error,
IsEnabledByDefault: true
}
},
{/*
[SpacetimeDB.ClientVisibilityFilter]
public static readonly string MY_THIRD_FILTER = "SELECT * FROM TestAutoIncNotInteger";
^^^^^^^^^^^^^^^
}
*/
Message: Field MY_THIRD_FILTER is marked as ClientVisibilityFilter but it has type string which is not SpacetimeDB.Filter,
Severity: Error,
Descriptor: {
Id: STDB0015,
Title: ClientVisibilityFilters must be Filters,
MessageFormat: Field {0} is marked as ClientVisibilityFilter but it has type {1} which is not SpacetimeDB.Filter,
Category: SpacetimeDB,
DefaultSeverity: Error,
IsEnabledByDefault: true
}
}
]
}
6 changes: 6 additions & 0 deletions crates/bindings-csharp/Codegen.Tests/fixtures/server/Lib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,9 @@ partial struct RegressionMultipleUniqueIndexesHadSameName
[SpacetimeDB.Unique]
public uint Unique2;
}

public class Module
{
[SpacetimeDB.ClientVisibilityFilter]
public static readonly Filter ALL_PUBLIC_TABLES = new Filter.Sql("SELECT * FROM PublicTable");
}
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,9 @@ public static void Main()
global::Timers.SendMessageTimer,
SpacetimeDB.Internal.TableHandles.SendMessageTimer
>();
SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter(
global::Module.ALL_PUBLIC_TABLES
);
}

// Exports only work from the main assembly, so we need to generate forwarding methods.
Expand Down
18 changes: 18 additions & 0 deletions crates/bindings-csharp/Codegen/Diag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,22 @@ string typeName
ctx => $"Could not find the specified column {ctx.columnName} in {ctx.typeName}.",
ctx => ctx.attr
);

public static readonly ErrorDescriptor<IFieldSymbol> ClientVisibilityNotFilter =
new(
group,
"ClientVisibilityFilters must be Filters",
field =>
$"Field {field.Name} is marked as ClientVisibilityFilter but it has type {field.Type} which is not SpacetimeDB.Filter",
field => field
);

public static readonly ErrorDescriptor<IFieldSymbol> ClientVisibilityNotPublicStaticReadonly =
new(
group,
"ClientVisibilityFilters must be public static readonly",
field =>
$"Field {field.Name} is marked as [ClientVisibilityFilter] but it is not public static readonly",
field => field
);
}
63 changes: 61 additions & 2 deletions crates/bindings-csharp/Codegen/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,40 @@ public Scope.Extensions GenerateSchedule()
}
}

record ClientVisibilityFilterDeclaration
{
public readonly string FullName;

public string GlobalName
{
get => $"global::{FullName}";
}

public ClientVisibilityFilterDeclaration(
GeneratorAttributeSyntaxContext context,
DiagReporter diag
)
{
var fieldSymbol = (IFieldSymbol)context.TargetSymbol;

if (
!fieldSymbol.IsStatic
|| !fieldSymbol.IsReadOnly
|| fieldSymbol.DeclaredAccessibility != Accessibility.Public
)
{
diag.Report(ErrorDescriptor.ClientVisibilityNotPublicStaticReadonly, fieldSymbol);
}

if (fieldSymbol.Type.ToString() is not "SpacetimeDB.Filter")
{
diag.Report(ErrorDescriptor.ClientVisibilityNotFilter, fieldSymbol);
}

FullName = SymbolToName(fieldSymbol);
}
}

[Generator]
public class Module : IIncrementalGenerator
{
Expand Down Expand Up @@ -797,11 +831,32 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
v => v.tableName
);

var rlsFilters = context
.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: typeof(ClientVisibilityFilterAttribute).FullName,
predicate: (node, ct) => true,
transform: (context, ct) =>
context.ParseWithDiags(diag => new ClientVisibilityFilterDeclaration(
context,
diag
))
)
.ReportDiagnostics(context)
.WithTrackingName("SpacetimeDB.ClientVisibilityFilter.Parse");

var rlsFiltersArray = CollectDistinct(
"ClientVisibilityFilter",
context,
rlsFilters,
(f) => f.FullName,
(f) => f.FullName
);

context.RegisterSourceOutput(
tableViews.Combine(addReducers),
tableViews.Combine(addReducers).Combine(rlsFiltersArray),
(context, tuple) =>
{
var (tableViews, addReducers) = tuple;
var ((tableViews, addReducers), rlsFilters) = tuple;
// Don't generate the FFI boilerplate if there are no tables or reducers.
if (tableViews.Array.IsEmpty && addReducers.Array.IsEmpty)
{
Expand Down Expand Up @@ -868,6 +923,10 @@ public static void Main() {
"\n",
tableViews.Select(t => $"SpacetimeDB.Internal.Module.RegisterTable<{t.tableName}, SpacetimeDB.Internal.TableHandles.{t.viewName}>();")
)}}
{{string.Join(
"\n",
rlsFilters.Select(f => $"SpacetimeDB.Internal.Module.RegisterClientVisibilityFilter({f.GlobalName});")
)}}
}

// Exports only work from the main assembly, so we need to generate forwarding methods.
Expand Down
15 changes: 15 additions & 0 deletions crates/bindings-csharp/Runtime/Attrs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ public abstract class ColumnAttribute : Attribute
}
}

/// <summary>
/// Generates code for registering a row-level security rule.
///
/// This attribute must be applied to a <c>static</c> field of type <c>Filter</c>.
/// It will be interpreted as a filter on the table to which it applies, for all client queries.
/// If a module contains multiple <c>client_visibility_filter</c>s for the same table,
/// they will be unioned together as if by SQL <c>OR</c>,
/// so that any row permitted by at least one filter is visible.
///
/// The query follows the same syntax as a subscription query.
/// See the <see href="https://spacetimedb.com/docs/sql">SQL reference</see> for more information.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public sealed class ClientVisibilityFilterAttribute : Attribute { }

/// <summary>
/// Registers a type as the row structure of a SpacetimeDB table, enabling codegen for it.
///
Expand Down
22 changes: 22 additions & 0 deletions crates/bindings-csharp/Runtime/Filter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace SpacetimeDB;

/// <summary>
/// A row-level security filter,
/// which can be registered using the <c>[SpacetimeDB.ClientVisibilityFilter]</c> attribute.
///
/// Currently, the only valid value for a filter is a <c>Filter.Sql</c>.
/// This is a filter written as 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 <c>[SpacetimeDB.ClientVisibilityFilter]</c> rules
/// will be reported during <c>spacetime publish</c>, not at compile time.
/// </summary>
// Note: _Unused is needed because C# doesn't support single-element named tuples :/
[Type]
public partial record Filter : TaggedEnum<(string Sql, Unit _Unused)> { }
15 changes: 15 additions & 0 deletions crates/bindings-csharp/Runtime/Internal/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ internal AlgebraicType.Ref RegisterType<T>(Func<AlgebraicType.Ref, AlgebraicType
internal void RegisterReducer(RawReducerDefV9 reducer) => Reducers.Add(reducer);

internal void RegisterTable(RawTableDefV9 table) => Tables.Add(table);

internal void RegisterRowLevelSecurity(RawRowLevelSecurityDefV9 rls) =>
RowLevelSecurity.Add(rls);
}

public static class Module
Expand Down Expand Up @@ -95,6 +98,18 @@ public static void RegisterTable<T, View>()
moduleDef.RegisterTable(View.MakeTableDesc(typeRegistrar));
}

public static void RegisterClientVisibilityFilter(Filter rlsFilter)
{
if (rlsFilter is Filter.Sql(var rlsSql))
{
moduleDef.RegisterRowLevelSecurity(new RawRowLevelSecurityDefV9 { Sql = rlsSql });
}
else
{
throw new Exception($"Unimplemented row level security type: {rlsFilter}");
}
}

private static byte[] Consume(this BytesSource source)
{
if (source == BytesSource.INVALID)
Expand Down
25 changes: 20 additions & 5 deletions crates/schema/tests/ensure_same_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,34 @@ fn get_normalized_schema(module_name: &str) -> ModuleDef {
fn assert_identical_modules(module_name_prefix: &str) {
let rs = get_normalized_schema(module_name_prefix);
let cs = get_normalized_schema(&format!("{module_name_prefix}-cs"));
let diff = ponder_auto_migrate(&cs, &rs)
let mut diff = ponder_auto_migrate(&cs, &rs)
.expect("could not compute a diff between Rust and C#")
.steps;

// Ignore RLS steps for now, as they are not yet implemented in C#.
// TODO: remove this when C#-friendly RLS is implemented.
let mut diff = diff;
diff.retain(|step| !matches!(step, AutoMigrateStep::AddRowLevelSecurity(_)));
// In any migration plan, all `RowLevelSecurityDef`s are ALWAYS removed and
// re-added to ensure the core engine reinintializes the policies.
// This is slightly silly (and arguably should be hidden inside `core`),
// but for now, we just ignore these steps and manually compare the `RowLevelSecurityDef`s.
diff.retain(|step| {
!matches!(
step,
AutoMigrateStep::AddRowLevelSecurity(_) | AutoMigrateStep::RemoveRowLevelSecurity(_)
)
});

assert!(
diff.is_empty(),
"Rust and C# modules are not identical. Here are the steps to migrate from C# to Rust: {diff:#?}"
);

let mut rls_rs = rs.row_level_security().collect::<Vec<_>>();
rls_rs.sort();
let mut rls_cs = cs.row_level_security().collect::<Vec<_>>();
rls_cs.sort();
assert_eq!(
rls_rs, rls_cs,
"Rust and C# modules are not identical: different row level security policies"
)
}

macro_rules! declare_tests {
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-test-cs/Lib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,9 @@ public static void insert_table_holds_table(ReducerContext ctx, OneU8 a, VecU8 b
[SpacetimeDB.Reducer]
public static void no_op_succeeds(ReducerContext ctx) { }

[SpacetimeDB.ClientVisibilityFilter]
public static readonly Filter ONE_U8_VISIBLE = new Filter.Sql("SELECT * FROM one_u8");

[SpacetimeDB.Table(
Name = "scheduled_table",
Scheduled = nameof(send_scheduled_message),
Expand Down
Loading