11import { convexTest } from "convex-test" ;
22import { v } from "convex/values" ;
33import { describe , expect , test } from "vitest" ;
4- import { wrapDatabaseWriter } from "./rowLevelSecurity.js" ;
4+ import { wrapDatabaseReader , wrapDatabaseWriter } from "./rowLevelSecurity.js" ;
55import type {
66 Auth ,
77 DataModelFromSchemaDefinition ,
@@ -26,6 +26,13 @@ const schema = defineSchema({
2626 note : v . string ( ) ,
2727 userId : v . id ( "users" ) ,
2828 } ) ,
29+ publicData : defineTable ( {
30+ content : v . string ( ) ,
31+ } ) ,
32+ privateData : defineTable ( {
33+ content : v . string ( ) ,
34+ ownerId : v . id ( "users" ) ,
35+ } ) ,
2936} ) ;
3037
3138type DataModel = DataModelFromSchemaDefinition < typeof schema > ;
@@ -100,6 +107,196 @@ describe("row level security", () => {
100107 return rls . db . delete ( noteId ) ;
101108 } ) ;
102109 } ) ;
110+
111+ test ( "default allow policy permits access to tables without rules" , async ( ) => {
112+ const t = convexTest ( schema , modules ) ;
113+ await t . run ( async ( ctx ) => {
114+ const userId = await ctx . db . insert ( "users" , {
115+ tokenIdentifier : "Person A" ,
116+ } ) ;
117+ await ctx . db . insert ( "publicData" , { content : "Public content" } ) ;
118+ await ctx . db . insert ( "privateData" , {
119+ content : "Private content" ,
120+ ownerId : userId ,
121+ } ) ;
122+ } ) ;
123+
124+ const asA = t . withIdentity ( { tokenIdentifier : "Person A" } ) ;
125+ const result = await asA . run ( async ( ctx ) => {
126+ const tokenIdentifier = ( await ctx . auth . getUserIdentity ( ) )
127+ ?. tokenIdentifier ;
128+ if ( ! tokenIdentifier ) throw new Error ( "Unauthenticated" ) ;
129+
130+ // Default allow - no config specified
131+ const db = wrapDatabaseReader ( { tokenIdentifier } , ctx . db , {
132+ notes : {
133+ read : async ( { tokenIdentifier } , doc ) => {
134+ const author = await ctx . db . get ( doc . userId ) ;
135+ return tokenIdentifier === author ?. tokenIdentifier ;
136+ } ,
137+ } ,
138+ } ) ;
139+
140+ // Should be able to read publicData (no rules defined)
141+ const publicData = await db . query ( "publicData" ) . collect ( ) ;
142+ // Should be able to read privateData (no rules defined)
143+ const privateData = await db . query ( "privateData" ) . collect ( ) ;
144+
145+ return { publicData, privateData } ;
146+ } ) ;
147+
148+ expect ( result . publicData ) . toHaveLength ( 1 ) ;
149+ expect ( result . privateData ) . toHaveLength ( 1 ) ;
150+ } ) ;
151+
152+ test ( "default deny policy blocks access to tables without rules" , async ( ) => {
153+ const t = convexTest ( schema , modules ) ;
154+ await t . run ( async ( ctx ) => {
155+ const userId = await ctx . db . insert ( "users" , {
156+ tokenIdentifier : "Person A" ,
157+ } ) ;
158+ await ctx . db . insert ( "publicData" , { content : "Public content" } ) ;
159+ await ctx . db . insert ( "privateData" , {
160+ content : "Private content" ,
161+ ownerId : userId ,
162+ } ) ;
163+ } ) ;
164+
165+ const asA = t . withIdentity ( { tokenIdentifier : "Person A" } ) ;
166+ const result = await asA . run ( async ( ctx ) => {
167+ const tokenIdentifier = ( await ctx . auth . getUserIdentity ( ) )
168+ ?. tokenIdentifier ;
169+ if ( ! tokenIdentifier ) throw new Error ( "Unauthenticated" ) ;
170+
171+ // Default deny policy
172+ const db = wrapDatabaseReader (
173+ { tokenIdentifier } ,
174+ ctx . db ,
175+ {
176+ notes : {
177+ read : async ( { tokenIdentifier } , doc ) => {
178+ const author = await ctx . db . get ( doc . userId ) ;
179+ return tokenIdentifier === author ?. tokenIdentifier ;
180+ } ,
181+ } ,
182+ // Explicitly allow publicData
183+ publicData : {
184+ read : async ( ) => true ,
185+ } ,
186+ } ,
187+ { defaultPolicy : "deny" } ,
188+ ) ;
189+
190+ // Should be able to read publicData (has explicit allow rule)
191+ const publicData = await db . query ( "publicData" ) . collect ( ) ;
192+ // Should NOT be able to read privateData (no rules, default deny)
193+ const privateData = await db . query ( "privateData" ) . collect ( ) ;
194+
195+ return { publicData, privateData } ;
196+ } ) ;
197+
198+ expect ( result . publicData ) . toHaveLength ( 1 ) ;
199+ expect ( result . privateData ) . toHaveLength ( 0 ) ;
200+ } ) ;
201+
202+ test ( "default deny policy blocks inserts to tables without rules" , async ( ) => {
203+ const t = convexTest ( schema , modules ) ;
204+ await t . run ( async ( ctx ) => {
205+ await ctx . db . insert ( "users" , { tokenIdentifier : "Person A" } ) ;
206+ } ) ;
207+
208+ const asA = t . withIdentity ( { tokenIdentifier : "Person A" } ) ;
209+
210+ // Test with default allow
211+ await asA . run ( async ( ctx ) => {
212+ const tokenIdentifier = ( await ctx . auth . getUserIdentity ( ) )
213+ ?. tokenIdentifier ;
214+ if ( ! tokenIdentifier ) throw new Error ( "Unauthenticated" ) ;
215+
216+ const db = wrapDatabaseWriter (
217+ { tokenIdentifier } ,
218+ ctx . db ,
219+ { } ,
220+ { defaultPolicy : "allow" } ,
221+ ) ;
222+
223+ // Should be able to insert (no rules, default allow)
224+ await db . insert ( "publicData" , { content : "Allowed content" } ) ;
225+ } ) ;
226+
227+ // Test with default deny
228+ await expect ( ( ) =>
229+ asA . run ( async ( ctx ) => {
230+ const tokenIdentifier = ( await ctx . auth . getUserIdentity ( ) )
231+ ?. tokenIdentifier ;
232+ if ( ! tokenIdentifier ) throw new Error ( "Unauthenticated" ) ;
233+
234+ const db = wrapDatabaseWriter (
235+ { tokenIdentifier } ,
236+ ctx . db ,
237+ { } ,
238+ { defaultPolicy : "deny" } ,
239+ ) ;
240+
241+ // Should NOT be able to insert (no rules, default deny)
242+ await db . insert ( "publicData" , { content : "Blocked content" } ) ;
243+ } ) ,
244+ ) . rejects . toThrow ( / i n s e r t a c c e s s n o t a l l o w e d / ) ;
245+ } ) ;
246+
247+ test ( "default deny policy blocks modifications to tables without rules" , async ( ) => {
248+ const t = convexTest ( schema , modules ) ;
249+ const docId = await t . run ( async ( ctx ) => {
250+ await ctx . db . insert ( "users" , { tokenIdentifier : "Person A" } ) ;
251+ return ctx . db . insert ( "publicData" , { content : "Initial content" } ) ;
252+ } ) ;
253+
254+ const asA = t . withIdentity ( { tokenIdentifier : "Person A" } ) ;
255+
256+ // Test with default allow
257+ await asA . run ( async ( ctx ) => {
258+ const tokenIdentifier = ( await ctx . auth . getUserIdentity ( ) )
259+ ?. tokenIdentifier ;
260+ if ( ! tokenIdentifier ) throw new Error ( "Unauthenticated" ) ;
261+
262+ const db = wrapDatabaseWriter (
263+ { tokenIdentifier } ,
264+ ctx . db ,
265+ {
266+ publicData : {
267+ read : async ( ) => true , // Allow reads
268+ } ,
269+ } ,
270+ { defaultPolicy : "allow" } ,
271+ ) ;
272+
273+ // Should be able to modify (no modify rule, default allow)
274+ await db . patch ( docId , { content : "Modified content" } ) ;
275+ } ) ;
276+
277+ // Test with default deny
278+ await expect ( ( ) =>
279+ asA . run ( async ( ctx ) => {
280+ const tokenIdentifier = ( await ctx . auth . getUserIdentity ( ) )
281+ ?. tokenIdentifier ;
282+ if ( ! tokenIdentifier ) throw new Error ( "Unauthenticated" ) ;
283+
284+ const db = wrapDatabaseWriter (
285+ { tokenIdentifier } ,
286+ ctx . db ,
287+ {
288+ publicData : {
289+ read : async ( ) => true , // Allow reads but no modify rule
290+ } ,
291+ } ,
292+ { defaultPolicy : "deny" } ,
293+ ) ;
294+
295+ // Should NOT be able to modify (no modify rule, default deny)
296+ await db . patch ( docId , { content : "Blocked modification" } ) ;
297+ } ) ,
298+ ) . rejects . toThrow ( / w r i t e a c c e s s n o t a l l o w e d / ) ;
299+ } ) ;
103300} ) ;
104301
105302const mutation = mutationGeneric as MutationBuilder < DataModel , "public" > ;
0 commit comments