@opensaas/stack-core 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/access/engine.d.ts +9 -3
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +6 -2
- package/dist/access/engine.js.map +1 -1
- package/dist/access/types.d.ts +1 -0
- package/dist/access/types.d.ts.map +1 -1
- package/dist/context/index.d.ts +11 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +116 -86
- package/dist/context/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/engine.ts +10 -5
- package/src/access/types.ts +1 -0
- package/src/context/index.ts +137 -95
- package/tests/sudo.test.ts +406 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { getContext } from '../src/context/index.js'
|
|
3
|
+
import { config, list } from '../src/config/index.js'
|
|
4
|
+
import { text, integer } from '../src/fields/index.js'
|
|
5
|
+
import type { PrismaClient } from '@prisma/client'
|
|
6
|
+
|
|
7
|
+
describe('Sudo Context', () => {
|
|
8
|
+
// Mock Prisma client
|
|
9
|
+
const mockPrisma = {
|
|
10
|
+
post: {
|
|
11
|
+
findFirst: vi.fn(),
|
|
12
|
+
findMany: vi.fn(),
|
|
13
|
+
findUnique: vi.fn(),
|
|
14
|
+
create: vi.fn(),
|
|
15
|
+
update: vi.fn(),
|
|
16
|
+
delete: vi.fn(),
|
|
17
|
+
count: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
} as unknown as PrismaClient
|
|
20
|
+
|
|
21
|
+
// Track hook execution
|
|
22
|
+
const hookExecutions: string[] = []
|
|
23
|
+
|
|
24
|
+
const testConfig = config({
|
|
25
|
+
db: {
|
|
26
|
+
provider: 'sqlite',
|
|
27
|
+
url: 'file:./test.db',
|
|
28
|
+
},
|
|
29
|
+
lists: {
|
|
30
|
+
Post: list({
|
|
31
|
+
fields: {
|
|
32
|
+
title: text({
|
|
33
|
+
validation: { isRequired: true },
|
|
34
|
+
hooks: {
|
|
35
|
+
resolveInput: async ({ inputValue }) => {
|
|
36
|
+
hookExecutions.push('field-resolveInput')
|
|
37
|
+
return inputValue
|
|
38
|
+
},
|
|
39
|
+
beforeOperation: async () => {
|
|
40
|
+
hookExecutions.push('field-beforeOperation')
|
|
41
|
+
},
|
|
42
|
+
afterOperation: async () => {
|
|
43
|
+
hookExecutions.push('field-afterOperation')
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
secretField: text({
|
|
48
|
+
access: {
|
|
49
|
+
read: async () => false,
|
|
50
|
+
create: async () => false,
|
|
51
|
+
update: async () => false,
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
views: integer({ defaultValue: 0 }),
|
|
55
|
+
},
|
|
56
|
+
access: {
|
|
57
|
+
operation: {
|
|
58
|
+
query: async () => false,
|
|
59
|
+
create: async () => false,
|
|
60
|
+
update: async () => false,
|
|
61
|
+
delete: async () => false,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
hooks: {
|
|
65
|
+
resolveInput: async ({ resolvedData }) => {
|
|
66
|
+
hookExecutions.push('list-resolveInput')
|
|
67
|
+
return resolvedData
|
|
68
|
+
},
|
|
69
|
+
validateInput: async () => {
|
|
70
|
+
hookExecutions.push('list-validateInput')
|
|
71
|
+
},
|
|
72
|
+
beforeOperation: async () => {
|
|
73
|
+
hookExecutions.push('list-beforeOperation')
|
|
74
|
+
},
|
|
75
|
+
afterOperation: async () => {
|
|
76
|
+
hookExecutions.push('list-afterOperation')
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
vi.clearAllMocks()
|
|
85
|
+
hookExecutions.length = 0
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('Query Operations', () => {
|
|
89
|
+
it('should bypass operation-level access control with sudo()', async () => {
|
|
90
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
91
|
+
const sudoContext = context.sudo()
|
|
92
|
+
|
|
93
|
+
mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
|
|
94
|
+
|
|
95
|
+
// Regular context should return empty array (access denied)
|
|
96
|
+
const regularResult = await context.db.post.findMany()
|
|
97
|
+
expect(regularResult).toEqual([])
|
|
98
|
+
|
|
99
|
+
// Sudo context should return results
|
|
100
|
+
const sudoResult = await sudoContext.db.post.findMany()
|
|
101
|
+
expect(sudoResult).toHaveLength(1)
|
|
102
|
+
expect(sudoResult[0].title).toBe('Test Post')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should bypass field-level read access control with sudo()', async () => {
|
|
106
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
107
|
+
const sudoContext = context.sudo()
|
|
108
|
+
|
|
109
|
+
mockPrisma.post.findFirst.mockResolvedValue({
|
|
110
|
+
id: '1',
|
|
111
|
+
title: 'Test Post',
|
|
112
|
+
secretField: 'secret-value',
|
|
113
|
+
views: 10,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Sudo context should return all fields including secretField
|
|
117
|
+
const sudoResult = await sudoContext.db.post.findUnique({ where: { id: '1' } })
|
|
118
|
+
expect(sudoResult?.secretField).toBe('secret-value')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should execute field afterOperation hooks with sudo()', async () => {
|
|
122
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
123
|
+
const sudoContext = context.sudo()
|
|
124
|
+
|
|
125
|
+
mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
|
|
126
|
+
|
|
127
|
+
await sudoContext.db.post.findMany()
|
|
128
|
+
|
|
129
|
+
expect(hookExecutions).toContain('field-afterOperation')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('Create Operations', () => {
|
|
134
|
+
it('should bypass operation-level access control with sudo()', async () => {
|
|
135
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
136
|
+
const sudoContext = context.sudo()
|
|
137
|
+
|
|
138
|
+
const mockPost = { id: '1', title: 'New Post', views: 0 }
|
|
139
|
+
mockPrisma.post.create.mockResolvedValue(mockPost)
|
|
140
|
+
|
|
141
|
+
// Regular context should return null (access denied)
|
|
142
|
+
const regularResult = await context.db.post.create({
|
|
143
|
+
data: { title: 'New Post' },
|
|
144
|
+
})
|
|
145
|
+
expect(regularResult).toBeNull()
|
|
146
|
+
|
|
147
|
+
// Sudo context should create successfully
|
|
148
|
+
hookExecutions.length = 0
|
|
149
|
+
const sudoResult = await sudoContext.db.post.create({
|
|
150
|
+
data: { title: 'New Post' },
|
|
151
|
+
})
|
|
152
|
+
expect(sudoResult).toMatchObject({ title: 'New Post' })
|
|
153
|
+
expect(mockPrisma.post.create).toHaveBeenCalledWith({
|
|
154
|
+
data: { title: 'New Post' },
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should bypass field-level write access control with sudo()', async () => {
|
|
159
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
160
|
+
const sudoContext = context.sudo()
|
|
161
|
+
|
|
162
|
+
const mockPost = { id: '1', title: 'New Post', secretField: 'secret', views: 0 }
|
|
163
|
+
mockPrisma.post.create.mockResolvedValue(mockPost)
|
|
164
|
+
|
|
165
|
+
// Sudo context should allow writing to secretField
|
|
166
|
+
await sudoContext.db.post.create({
|
|
167
|
+
data: { title: 'New Post', secretField: 'secret' },
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Verify that secretField was passed to Prisma
|
|
171
|
+
expect(mockPrisma.post.create).toHaveBeenCalledWith({
|
|
172
|
+
data: { title: 'New Post', secretField: 'secret' },
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should execute all hooks with sudo()', async () => {
|
|
177
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
178
|
+
const sudoContext = context.sudo()
|
|
179
|
+
|
|
180
|
+
const mockPost = { id: '1', title: 'New Post', views: 0 }
|
|
181
|
+
mockPrisma.post.create.mockResolvedValue(mockPost)
|
|
182
|
+
|
|
183
|
+
await sudoContext.db.post.create({
|
|
184
|
+
data: { title: 'New Post' },
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Verify all hooks were executed
|
|
188
|
+
expect(hookExecutions).toContain('list-resolveInput')
|
|
189
|
+
expect(hookExecutions).toContain('field-resolveInput')
|
|
190
|
+
expect(hookExecutions).toContain('list-validateInput')
|
|
191
|
+
expect(hookExecutions).toContain('field-beforeOperation')
|
|
192
|
+
expect(hookExecutions).toContain('list-beforeOperation')
|
|
193
|
+
expect(hookExecutions).toContain('list-afterOperation')
|
|
194
|
+
expect(hookExecutions).toContain('field-afterOperation')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should still validate required fields with sudo()', async () => {
|
|
198
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
199
|
+
const sudoContext = context.sudo()
|
|
200
|
+
|
|
201
|
+
// Should throw validation error for missing required field
|
|
202
|
+
await expect(
|
|
203
|
+
sudoContext.db.post.create({
|
|
204
|
+
data: { views: 10 },
|
|
205
|
+
}),
|
|
206
|
+
).rejects.toThrow('Title must be text')
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('Update Operations', () => {
|
|
211
|
+
it('should bypass operation-level access control with sudo()', async () => {
|
|
212
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
213
|
+
const sudoContext = context.sudo()
|
|
214
|
+
|
|
215
|
+
const existingPost = { id: '1', title: 'Old Title', views: 5 }
|
|
216
|
+
const updatedPost = { id: '1', title: 'New Title', views: 5 }
|
|
217
|
+
|
|
218
|
+
mockPrisma.post.findUnique.mockResolvedValue(existingPost)
|
|
219
|
+
mockPrisma.post.findFirst.mockResolvedValue(null)
|
|
220
|
+
mockPrisma.post.update.mockResolvedValue(updatedPost)
|
|
221
|
+
|
|
222
|
+
// Regular context should return null (access denied)
|
|
223
|
+
const regularResult = await context.db.post.update({
|
|
224
|
+
where: { id: '1' },
|
|
225
|
+
data: { title: 'New Title' },
|
|
226
|
+
})
|
|
227
|
+
expect(regularResult).toBeNull()
|
|
228
|
+
|
|
229
|
+
// Reset mocks
|
|
230
|
+
mockPrisma.post.findUnique.mockResolvedValue(existingPost)
|
|
231
|
+
hookExecutions.length = 0
|
|
232
|
+
|
|
233
|
+
// Sudo context should update successfully
|
|
234
|
+
const sudoResult = await sudoContext.db.post.update({
|
|
235
|
+
where: { id: '1' },
|
|
236
|
+
data: { title: 'New Title' },
|
|
237
|
+
})
|
|
238
|
+
expect(sudoResult).toMatchObject({ title: 'New Title' })
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should bypass field-level write access control with sudo()', async () => {
|
|
242
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
243
|
+
const sudoContext = context.sudo()
|
|
244
|
+
|
|
245
|
+
const existingPost = { id: '1', title: 'Old Title', secretField: 'old-secret', views: 5 }
|
|
246
|
+
const updatedPost = { id: '1', title: 'Old Title', secretField: 'new-secret', views: 5 }
|
|
247
|
+
|
|
248
|
+
mockPrisma.post.findUnique.mockResolvedValue(existingPost)
|
|
249
|
+
mockPrisma.post.update.mockResolvedValue(updatedPost)
|
|
250
|
+
|
|
251
|
+
// Sudo context should allow updating secretField
|
|
252
|
+
await sudoContext.db.post.update({
|
|
253
|
+
where: { id: '1' },
|
|
254
|
+
data: { secretField: 'new-secret' },
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Verify that secretField was passed to Prisma
|
|
258
|
+
expect(mockPrisma.post.update).toHaveBeenCalledWith({
|
|
259
|
+
where: { id: '1' },
|
|
260
|
+
data: { secretField: 'new-secret' },
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('should execute all hooks with sudo()', async () => {
|
|
265
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
266
|
+
const sudoContext = context.sudo()
|
|
267
|
+
|
|
268
|
+
const existingPost = { id: '1', title: 'Old Title', views: 5 }
|
|
269
|
+
const updatedPost = { id: '1', title: 'New Title', views: 5 }
|
|
270
|
+
|
|
271
|
+
mockPrisma.post.findUnique.mockResolvedValue(existingPost)
|
|
272
|
+
mockPrisma.post.update.mockResolvedValue(updatedPost)
|
|
273
|
+
|
|
274
|
+
await sudoContext.db.post.update({
|
|
275
|
+
where: { id: '1' },
|
|
276
|
+
data: { title: 'New Title' },
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Verify all hooks were executed
|
|
280
|
+
expect(hookExecutions).toContain('list-resolveInput')
|
|
281
|
+
expect(hookExecutions).toContain('field-resolveInput')
|
|
282
|
+
expect(hookExecutions).toContain('list-validateInput')
|
|
283
|
+
expect(hookExecutions).toContain('field-beforeOperation')
|
|
284
|
+
expect(hookExecutions).toContain('list-beforeOperation')
|
|
285
|
+
expect(hookExecutions).toContain('list-afterOperation')
|
|
286
|
+
expect(hookExecutions).toContain('field-afterOperation')
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('Delete Operations', () => {
|
|
291
|
+
it('should bypass operation-level access control with sudo()', async () => {
|
|
292
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
293
|
+
const sudoContext = context.sudo()
|
|
294
|
+
|
|
295
|
+
const existingPost = { id: '1', title: 'Post to Delete', views: 5 }
|
|
296
|
+
|
|
297
|
+
mockPrisma.post.findUnique.mockResolvedValue(existingPost)
|
|
298
|
+
mockPrisma.post.findFirst.mockResolvedValue(null)
|
|
299
|
+
mockPrisma.post.delete.mockResolvedValue(existingPost)
|
|
300
|
+
|
|
301
|
+
// Regular context should return null (access denied)
|
|
302
|
+
const regularResult = await context.db.post.delete({
|
|
303
|
+
where: { id: '1' },
|
|
304
|
+
})
|
|
305
|
+
expect(regularResult).toBeNull()
|
|
306
|
+
|
|
307
|
+
// Reset mocks
|
|
308
|
+
mockPrisma.post.findUnique.mockResolvedValue(existingPost)
|
|
309
|
+
hookExecutions.length = 0
|
|
310
|
+
|
|
311
|
+
// Sudo context should delete successfully
|
|
312
|
+
const sudoResult = await sudoContext.db.post.delete({
|
|
313
|
+
where: { id: '1' },
|
|
314
|
+
})
|
|
315
|
+
expect(sudoResult).toMatchObject({ title: 'Post to Delete' })
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('should execute all hooks with sudo()', async () => {
|
|
319
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
320
|
+
const sudoContext = context.sudo()
|
|
321
|
+
|
|
322
|
+
const existingPost = { id: '1', title: 'Post to Delete', views: 5 }
|
|
323
|
+
|
|
324
|
+
mockPrisma.post.findUnique.mockResolvedValue(existingPost)
|
|
325
|
+
mockPrisma.post.delete.mockResolvedValue(existingPost)
|
|
326
|
+
|
|
327
|
+
await sudoContext.db.post.delete({
|
|
328
|
+
where: { id: '1' },
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Verify hooks were executed
|
|
332
|
+
expect(hookExecutions).toContain('field-beforeOperation')
|
|
333
|
+
expect(hookExecutions).toContain('list-beforeOperation')
|
|
334
|
+
expect(hookExecutions).toContain('list-afterOperation')
|
|
335
|
+
expect(hookExecutions).toContain('field-afterOperation')
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
describe('Count Operations', () => {
|
|
340
|
+
it('should bypass operation-level access control with sudo()', async () => {
|
|
341
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
342
|
+
const sudoContext = context.sudo()
|
|
343
|
+
|
|
344
|
+
mockPrisma.post.count.mockResolvedValue(10)
|
|
345
|
+
|
|
346
|
+
// Regular context should return 0 (access denied)
|
|
347
|
+
const regularResult = await context.db.post.count()
|
|
348
|
+
expect(regularResult).toBe(0)
|
|
349
|
+
|
|
350
|
+
// Sudo context should return actual count
|
|
351
|
+
const sudoResult = await sudoContext.db.post.count()
|
|
352
|
+
expect(sudoResult).toBe(10)
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe('Sudo Context Properties', () => {
|
|
357
|
+
it('should maintain the same session object', async () => {
|
|
358
|
+
const session = { userId: 'user-123', role: 'admin' }
|
|
359
|
+
const context = getContext(testConfig, mockPrisma, session)
|
|
360
|
+
const sudoContext = context.sudo()
|
|
361
|
+
|
|
362
|
+
expect(sudoContext.session).toEqual(session)
|
|
363
|
+
expect(sudoContext.session).toBe(context.session)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('should maintain the same prisma client', async () => {
|
|
367
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
368
|
+
const sudoContext = context.sudo()
|
|
369
|
+
|
|
370
|
+
expect(sudoContext.prisma).toBe(context.prisma)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('should maintain the same storage utilities', async () => {
|
|
374
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
375
|
+
const sudoContext = context.sudo()
|
|
376
|
+
|
|
377
|
+
expect(sudoContext.storage).toBe(context.storage)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('should allow chaining sudo() calls', async () => {
|
|
381
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
382
|
+
const sudoContext1 = context.sudo()
|
|
383
|
+
const sudoContext2 = sudoContext1.sudo()
|
|
384
|
+
|
|
385
|
+
mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
|
|
386
|
+
|
|
387
|
+
const result = await sudoContext2.db.post.findMany()
|
|
388
|
+
expect(result).toHaveLength(1)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('should create independent contexts', async () => {
|
|
392
|
+
const context = getContext(testConfig, mockPrisma, null)
|
|
393
|
+
const sudoContext = context.sudo()
|
|
394
|
+
|
|
395
|
+
mockPrisma.post.findMany.mockResolvedValue([{ id: '1', title: 'Test Post' }])
|
|
396
|
+
|
|
397
|
+
// Regular context still denies access
|
|
398
|
+
const regularResult = await context.db.post.findMany()
|
|
399
|
+
expect(regularResult).toEqual([])
|
|
400
|
+
|
|
401
|
+
// Sudo context allows access
|
|
402
|
+
const sudoResult = await sudoContext.db.post.findMany()
|
|
403
|
+
expect(sudoResult).toHaveLength(1)
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
})
|