1
1
import { convexTest } from "convex-test" ;
2
2
import { v } from "convex/values" ;
3
3
import { describe , expect , test } from "vitest" ;
4
- import { wrapDatabaseWriter } from "./rowLevelSecurity.js" ;
4
+ import { wrapDatabaseReader , wrapDatabaseWriter } from "./rowLevelSecurity.js" ;
5
5
import type {
6
6
Auth ,
7
7
DataModelFromSchemaDefinition ,
@@ -26,6 +26,13 @@ const schema = defineSchema({
26
26
note : v . string ( ) ,
27
27
userId : v . id ( "users" ) ,
28
28
} ) ,
29
+ publicData : defineTable ( {
30
+ content : v . string ( ) ,
31
+ } ) ,
32
+ privateData : defineTable ( {
33
+ content : v . string ( ) ,
34
+ ownerId : v . id ( "users" ) ,
35
+ } ) ,
29
36
} ) ;
30
37
31
38
type DataModel = DataModelFromSchemaDefinition < typeof schema > ;
@@ -100,6 +107,196 @@ describe("row level security", () => {
100
107
return rls . db . delete ( noteId ) ;
101
108
} ) ;
102
109
} ) ;
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
+ } ) ;
103
300
} ) ;
104
301
105
302
const mutation = mutationGeneric as MutationBuilder < DataModel , "public" > ;
0 commit comments