@pikku/core 0.9.3 → 0.9.5
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/CHANGELOG.md +25 -0
- package/dist/function/function-runner.d.ts +2 -1
- package/dist/function/function-runner.js +22 -15
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/middleware-runner.d.ts +70 -0
- package/dist/middleware-runner.js +105 -2
- package/dist/permissions.d.ts +57 -1
- package/dist/permissions.js +122 -0
- package/dist/pikku-state.d.ts +3 -1
- package/dist/pikku-state.js +3 -1
- package/dist/types/core.types.d.ts +10 -5
- package/dist/wirings/channel/channel-handler.js +1 -0
- package/dist/wirings/channel/local/local-channel-runner.js +26 -15
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
- package/dist/wirings/http/http-runner.js +20 -3
- package/dist/wirings/http/http.types.d.ts +5 -4
- package/dist/wirings/mcp/mcp-runner.js +27 -14
- package/dist/wirings/mcp/mcp.types.d.ts +7 -3
- package/dist/wirings/queue/index.d.ts +1 -1
- package/dist/wirings/queue/index.js +1 -1
- package/dist/wirings/queue/queue-runner.d.ts +15 -1
- package/dist/wirings/queue/queue-runner.js +71 -16
- package/dist/wirings/queue/queue.types.d.ts +19 -2
- package/dist/wirings/rpc/rpc-runner.d.ts +4 -2
- package/dist/wirings/scheduler/scheduler-runner.js +54 -24
- package/dist/wirings/scheduler/scheduler.types.d.ts +17 -2
- package/package.json +1 -1
- package/src/function/function-runner.test.ts +450 -0
- package/src/function/function-runner.ts +25 -26
- package/src/function/functions.types.ts +3 -1
- package/src/index.ts +10 -1
- package/src/middleware-runner.test.ts +329 -0
- package/src/middleware-runner.ts +131 -2
- package/src/permissions.test.ts +403 -42
- package/src/permissions.ts +182 -1
- package/src/pikku-state.ts +10 -2
- package/src/types/core.types.ts +10 -5
- package/src/wirings/channel/channel-handler.ts +1 -0
- package/src/wirings/channel/local/local-channel-runner.ts +33 -16
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -2
- package/src/wirings/http/http-runner.ts +21 -3
- package/src/wirings/http/http.types.ts +5 -4
- package/src/wirings/mcp/mcp-runner.ts +37 -17
- package/src/wirings/mcp/mcp.types.ts +7 -0
- package/src/wirings/queue/index.ts +2 -0
- package/src/wirings/queue/queue-runner.ts +92 -19
- package/src/wirings/queue/queue.types.ts +20 -1
- package/src/wirings/scheduler/scheduler-runner.ts +80 -32
- package/src/wirings/scheduler/scheduler.types.ts +22 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/permissions.test.ts
CHANGED
|
@@ -1,58 +1,419 @@
|
|
|
1
|
-
import { test,
|
|
2
|
-
import assert from 'assert'
|
|
3
|
-
import { getErrorResponse, addError } from './errors/error-handler.js'
|
|
1
|
+
import { describe, test, beforeEach } from 'node:test'
|
|
2
|
+
import * as assert from 'node:assert'
|
|
4
3
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from './
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
addPermission,
|
|
5
|
+
getPermissionsForTags,
|
|
6
|
+
runPermissions,
|
|
7
|
+
} from './permissions.js'
|
|
8
|
+
import { resetPikkuState } from './pikku-state.js'
|
|
9
|
+
import { CoreServices, CoreUserSession } from './types/core.types.js'
|
|
10
|
+
import { CorePermissionGroup } from './function/functions.types.js'
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
resetPikkuState()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const mockServices: CoreServices = {
|
|
17
|
+
logger: {
|
|
18
|
+
info: () => {},
|
|
19
|
+
warn: () => {},
|
|
20
|
+
error: () => {},
|
|
21
|
+
debug: () => {},
|
|
22
|
+
},
|
|
23
|
+
} as any
|
|
24
|
+
|
|
25
|
+
const mockSession: CoreUserSession = {
|
|
26
|
+
userId: 'test-user',
|
|
27
|
+
} as any
|
|
28
|
+
|
|
29
|
+
describe('addPermission', () => {
|
|
30
|
+
test('should add single permission for a tag', () => {
|
|
31
|
+
const mockPermission = async () => true
|
|
32
|
+
|
|
33
|
+
addPermission('testTag', [mockPermission])
|
|
34
|
+
|
|
35
|
+
const permissions = getPermissionsForTags(['testTag'])
|
|
36
|
+
assert.equal(permissions.length, 1)
|
|
37
|
+
assert.equal(permissions[0], mockPermission)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('should add multiple permissions for a tag', () => {
|
|
41
|
+
const mockPermission1 = async () => true
|
|
42
|
+
const mockPermission2 = async () => false
|
|
43
|
+
|
|
44
|
+
addPermission('multiTestTag', [mockPermission1, mockPermission2])
|
|
45
|
+
|
|
46
|
+
const permissions = getPermissionsForTags(['multiTestTag'])
|
|
47
|
+
assert.equal(permissions.length, 2)
|
|
48
|
+
assert.equal(permissions[0], mockPermission1)
|
|
49
|
+
assert.equal(permissions[1], mockPermission2)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('should add permission group object for a tag', () => {
|
|
53
|
+
const mockPermissionGroup: CorePermissionGroup = {
|
|
54
|
+
permissions: [async () => true, async () => false],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addPermission('groupTestTag', mockPermissionGroup)
|
|
58
|
+
|
|
59
|
+
const permissions = getPermissionsForTags(['groupTestTag'])
|
|
60
|
+
assert.equal(permissions.length, 1)
|
|
61
|
+
assert.equal(permissions[0], mockPermissionGroup)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('should throw error when tag already exists', () => {
|
|
65
|
+
const mockPermission = async () => true
|
|
66
|
+
|
|
67
|
+
addPermission('duplicateTag', [mockPermission])
|
|
68
|
+
|
|
69
|
+
assert.throws(
|
|
70
|
+
() => {
|
|
71
|
+
addPermission('duplicateTag', [mockPermission])
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
message:
|
|
75
|
+
"Permissions for tag 'duplicateTag' already exist. Use a different tag or remove the existing permissions first.",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('getPermissionsForTags', () => {
|
|
82
|
+
test('should return empty array when no tags provided', () => {
|
|
83
|
+
const permissions = getPermissionsForTags([])
|
|
84
|
+
assert.deepEqual(permissions, [])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('should return empty array when tags is undefined', () => {
|
|
88
|
+
const permissions = getPermissionsForTags(undefined)
|
|
89
|
+
assert.deepEqual(permissions, [])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('should return permissions for single tag', () => {
|
|
93
|
+
const mockPermission = async () => true
|
|
94
|
+
addPermission('singleTestTag', [mockPermission])
|
|
95
|
+
|
|
96
|
+
const permissions = getPermissionsForTags(['singleTestTag'])
|
|
97
|
+
assert.equal(permissions.length, 1)
|
|
98
|
+
assert.equal(permissions[0], mockPermission)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('should return permissions for multiple tags', () => {
|
|
102
|
+
const mockPermission1 = async () => true
|
|
103
|
+
const mockPermission2 = async () => false
|
|
104
|
+
|
|
105
|
+
addPermission('tag1', [mockPermission1])
|
|
106
|
+
addPermission('tag2', [mockPermission2])
|
|
107
|
+
|
|
108
|
+
const permissions = getPermissionsForTags(['tag1', 'tag2'])
|
|
109
|
+
assert.equal(permissions.length, 2)
|
|
110
|
+
assert.equal(permissions[0], mockPermission1)
|
|
111
|
+
assert.equal(permissions[1], mockPermission2)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('should handle array permissions correctly', () => {
|
|
115
|
+
const mockPermission1 = async () => true
|
|
116
|
+
const mockPermission2 = async () => false
|
|
117
|
+
|
|
118
|
+
addPermission('arrayHandleTestTag', [mockPermission1, mockPermission2])
|
|
119
|
+
|
|
120
|
+
const permissions = getPermissionsForTags(['arrayHandleTestTag'])
|
|
121
|
+
assert.equal(permissions.length, 2)
|
|
122
|
+
assert.equal(permissions[0], mockPermission1)
|
|
123
|
+
assert.equal(permissions[1], mockPermission2)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('should handle non-array permissions correctly', () => {
|
|
127
|
+
const mockPermissionGroup: CorePermissionGroup = {
|
|
128
|
+
permissions: [async () => true],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
addPermission('objectHandleTestTag', mockPermissionGroup)
|
|
132
|
+
|
|
133
|
+
const permissions = getPermissionsForTags(['objectHandleTestTag'])
|
|
134
|
+
assert.equal(permissions.length, 1)
|
|
135
|
+
assert.equal(permissions[0], mockPermissionGroup)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('should deduplicate tags', () => {
|
|
139
|
+
const mockPermission = async () => true
|
|
140
|
+
addPermission('dedupeTestTag', [mockPermission])
|
|
141
|
+
|
|
142
|
+
const permissions = getPermissionsForTags([
|
|
143
|
+
'dedupeTestTag',
|
|
144
|
+
'dedupeTestTag',
|
|
145
|
+
])
|
|
146
|
+
assert.equal(permissions.length, 1)
|
|
147
|
+
assert.equal(permissions[0], mockPermission)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('should ignore non-existent tags', () => {
|
|
151
|
+
const mockPermission = async () => true
|
|
152
|
+
addPermission('existingTag', [mockPermission])
|
|
153
|
+
|
|
154
|
+
const permissions = getPermissionsForTags(['existingTag', 'nonExistentTag'])
|
|
155
|
+
assert.equal(permissions.length, 1)
|
|
156
|
+
assert.equal(permissions[0], mockPermission)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('runPermissions', () => {
|
|
161
|
+
test('should pass when no permissions are defined', async () => {
|
|
162
|
+
// Should not throw
|
|
163
|
+
await runPermissions({
|
|
164
|
+
allServices: mockServices,
|
|
165
|
+
data: {},
|
|
166
|
+
session: mockSession,
|
|
18
167
|
})
|
|
19
168
|
})
|
|
20
169
|
|
|
21
|
-
test('should
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
170
|
+
test('should execute wiring tag permissions first', async () => {
|
|
171
|
+
const executionOrder: string[] = []
|
|
172
|
+
const wiringTagPermission = async () => {
|
|
173
|
+
executionOrder.push('wiringTag')
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addPermission('wiringTag', [wiringTagPermission])
|
|
178
|
+
|
|
179
|
+
await runPermissions({
|
|
180
|
+
wiringTags: ['wiringTag'],
|
|
181
|
+
allServices: mockServices,
|
|
182
|
+
data: {},
|
|
183
|
+
session: mockSession,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
assert.deepEqual(executionOrder, ['wiringTag'])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('should execute all permission levels in correct order', async () => {
|
|
190
|
+
const executionOrder: string[] = []
|
|
191
|
+
|
|
192
|
+
const wiringTagPermission = async () => {
|
|
193
|
+
executionOrder.push('wiringTag')
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
const funcTagPermission = async () => {
|
|
197
|
+
executionOrder.push('funcTag')
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
addPermission('wiringTag', [wiringTagPermission])
|
|
202
|
+
addPermission('funcTag', [funcTagPermission])
|
|
203
|
+
|
|
204
|
+
const wiringPermissions: CorePermissionGroup = {
|
|
205
|
+
permissions: [
|
|
206
|
+
async () => {
|
|
207
|
+
executionOrder.push('wiringPermission')
|
|
208
|
+
return true
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const funcPermissions: CorePermissionGroup = {
|
|
214
|
+
permissions: [
|
|
215
|
+
async () => {
|
|
216
|
+
executionOrder.push('funcPermission')
|
|
217
|
+
return true
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await runPermissions({
|
|
223
|
+
wiringTags: ['wiringTag'],
|
|
224
|
+
wiringPermissions,
|
|
225
|
+
funcTags: ['funcTag'],
|
|
226
|
+
funcPermissions,
|
|
227
|
+
allServices: mockServices,
|
|
228
|
+
data: {},
|
|
229
|
+
session: mockSession,
|
|
27
230
|
})
|
|
231
|
+
|
|
232
|
+
// Order: wiringTags → wiringPermissions → funcTags → funcPermissions
|
|
233
|
+
assert.deepEqual(executionOrder, [
|
|
234
|
+
'wiringTag',
|
|
235
|
+
'wiringPermission',
|
|
236
|
+
'funcTag',
|
|
237
|
+
'funcPermission',
|
|
238
|
+
])
|
|
28
239
|
})
|
|
29
240
|
|
|
30
|
-
test('should
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
241
|
+
test('should implement "at least one must pass" logic for tag permissions', async () => {
|
|
242
|
+
const failingPermission = async () => false
|
|
243
|
+
const passingPermission = async () => true
|
|
244
|
+
|
|
245
|
+
addPermission('atLeastOneTestTag', [failingPermission, passingPermission])
|
|
246
|
+
|
|
247
|
+
// Should not throw because at least one permission passes
|
|
248
|
+
await runPermissions({
|
|
249
|
+
wiringTags: ['atLeastOneTestTag'],
|
|
250
|
+
allServices: mockServices,
|
|
251
|
+
data: {},
|
|
252
|
+
session: mockSession,
|
|
37
253
|
})
|
|
38
254
|
})
|
|
39
255
|
|
|
40
|
-
test('should
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
256
|
+
test('should throw error when all wiring tag permissions fail', async () => {
|
|
257
|
+
const failingPermission1 = async () => false
|
|
258
|
+
const failingPermission2 = async () => false
|
|
259
|
+
|
|
260
|
+
addPermission('allFailTestTag', [failingPermission1, failingPermission2])
|
|
261
|
+
|
|
262
|
+
await assert.rejects(
|
|
263
|
+
runPermissions({
|
|
264
|
+
wiringTags: ['allFailTestTag'],
|
|
265
|
+
allServices: mockServices,
|
|
266
|
+
data: {},
|
|
267
|
+
session: mockSession,
|
|
268
|
+
}),
|
|
269
|
+
{
|
|
270
|
+
message: 'Permission denied - wiring tag permissions',
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('should throw error when wiring permissions fail', async () => {
|
|
276
|
+
const wiringPermissions: CorePermissionGroup = {
|
|
277
|
+
permissions: [async () => false],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await assert.rejects(
|
|
281
|
+
runPermissions({
|
|
282
|
+
wiringPermissions,
|
|
283
|
+
allServices: mockServices,
|
|
284
|
+
data: {},
|
|
285
|
+
session: mockSession,
|
|
286
|
+
}),
|
|
287
|
+
{
|
|
288
|
+
message: 'Permission denied - wiring permissions',
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('should throw error when all function tag permissions fail', async () => {
|
|
294
|
+
const failingPermission = async () => false
|
|
295
|
+
|
|
296
|
+
addPermission('funcTag', [failingPermission])
|
|
297
|
+
|
|
298
|
+
await assert.rejects(
|
|
299
|
+
runPermissions({
|
|
300
|
+
funcTags: ['funcTag'],
|
|
301
|
+
allServices: mockServices,
|
|
302
|
+
data: {},
|
|
303
|
+
session: mockSession,
|
|
304
|
+
}),
|
|
305
|
+
{
|
|
306
|
+
message: 'Permission denied - function tag permissions',
|
|
307
|
+
}
|
|
308
|
+
)
|
|
45
309
|
})
|
|
46
310
|
|
|
47
|
-
test('should
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
311
|
+
test('should throw error when function permissions fail', async () => {
|
|
312
|
+
const funcPermissions: CorePermissionGroup = {
|
|
313
|
+
permissions: [async () => false],
|
|
314
|
+
}
|
|
51
315
|
|
|
52
|
-
|
|
53
|
-
|
|
316
|
+
await assert.rejects(
|
|
317
|
+
runPermissions({
|
|
318
|
+
funcPermissions,
|
|
319
|
+
allServices: mockServices,
|
|
320
|
+
data: {},
|
|
321
|
+
session: mockSession,
|
|
322
|
+
}),
|
|
323
|
+
{
|
|
324
|
+
message: 'Permission denied - function permissions',
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('should stop execution at first failing level', async () => {
|
|
330
|
+
const executionOrder: string[] = []
|
|
331
|
+
|
|
332
|
+
const failingWiringTagPermission = async () => {
|
|
333
|
+
executionOrder.push('wiringTag')
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
addPermission('failingWiringTag', [failingWiringTagPermission])
|
|
338
|
+
|
|
339
|
+
const wiringPermissions: CorePermissionGroup = {
|
|
340
|
+
permissions: [
|
|
341
|
+
async () => {
|
|
342
|
+
executionOrder.push('wiringPermission')
|
|
343
|
+
return true
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await assert.rejects(
|
|
349
|
+
runPermissions({
|
|
350
|
+
wiringTags: ['failingWiringTag'],
|
|
351
|
+
wiringPermissions,
|
|
352
|
+
allServices: mockServices,
|
|
353
|
+
data: {},
|
|
354
|
+
session: mockSession,
|
|
355
|
+
})
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
// Should only execute wiring tag permissions, not wiring permissions
|
|
359
|
+
assert.deepEqual(executionOrder, ['wiringTag'])
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('should handle array permissions in tag-based permissions', async () => {
|
|
363
|
+
const arrayPermission = [async () => true, async () => false]
|
|
364
|
+
|
|
365
|
+
addPermission('arrayTestTag', arrayPermission)
|
|
366
|
+
|
|
367
|
+
// Should not throw because verifyPermissions handles array properly
|
|
368
|
+
await runPermissions({
|
|
369
|
+
wiringTags: ['arrayTestTag'],
|
|
370
|
+
allServices: mockServices,
|
|
371
|
+
data: {},
|
|
372
|
+
session: mockSession,
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test('should handle permission group objects in tag-based permissions', async () => {
|
|
377
|
+
const permissionGroup: CorePermissionGroup = {
|
|
378
|
+
permissions: [async () => true],
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
addPermission('objectTestTag', permissionGroup)
|
|
382
|
+
|
|
383
|
+
// Should not throw because verifyPermissions handles objects properly
|
|
384
|
+
await runPermissions({
|
|
385
|
+
wiringTags: ['objectTestTag'],
|
|
386
|
+
allServices: mockServices,
|
|
387
|
+
data: {},
|
|
388
|
+
session: mockSession,
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('should pass correct parameters to permission functions', async () => {
|
|
393
|
+
let receivedServices: any
|
|
394
|
+
let receivedData: any
|
|
395
|
+
let receivedSession: any
|
|
396
|
+
|
|
397
|
+
const testPermission = async (services: any, data: any, session: any) => {
|
|
398
|
+
receivedServices = services
|
|
399
|
+
receivedData = data
|
|
400
|
+
receivedSession = session
|
|
401
|
+
return true
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const testData = { test: 'data' }
|
|
405
|
+
|
|
406
|
+
addPermission('paramTestTag', [testPermission])
|
|
407
|
+
|
|
408
|
+
await runPermissions({
|
|
409
|
+
wiringTags: ['paramTestTag'],
|
|
410
|
+
allServices: mockServices,
|
|
411
|
+
data: testData,
|
|
412
|
+
session: mockSession,
|
|
413
|
+
})
|
|
54
414
|
|
|
55
|
-
|
|
56
|
-
assert.
|
|
415
|
+
assert.equal(receivedServices, mockServices)
|
|
416
|
+
assert.equal(receivedData, testData)
|
|
417
|
+
assert.equal(receivedSession, mockSession)
|
|
57
418
|
})
|
|
58
419
|
})
|
package/src/permissions.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { CoreServices, CoreUserSession } from './types/core.types.js'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CorePermissionGroup,
|
|
4
|
+
CorePikkuPermission,
|
|
5
|
+
} from './function/functions.types.js'
|
|
6
|
+
import { pikkuState } from './pikku-state.js'
|
|
7
|
+
import { ForbiddenError } from './errors/errors.js'
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* This function validates permissions by iterating over permission groups and executing the corresponding permission functions. If all functions in at least one group return true, the permission is considered valid.
|
|
@@ -41,3 +46,179 @@ export const verifyPermissions = async (
|
|
|
41
46
|
}
|
|
42
47
|
return false
|
|
43
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds global permissions for a specific tag.
|
|
52
|
+
*
|
|
53
|
+
* This function allows you to register permissions that will be applied to
|
|
54
|
+
* any wiring (HTTP, Channel, Queue, Scheduler, MCP) that includes the matching tag.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} tag - The tag that the permissions should apply to.
|
|
57
|
+
* @param {any[]} permissions - The permissions array to apply for the specified tag.
|
|
58
|
+
*
|
|
59
|
+
* @throws {Error} If permissions for the tag already exist.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // Add admin permissions for admin endpoints
|
|
64
|
+
* addPermission('admin', [adminPermission])
|
|
65
|
+
*
|
|
66
|
+
* // Add authentication permissions for auth endpoints
|
|
67
|
+
* addPermission('auth', [authPermission])
|
|
68
|
+
*
|
|
69
|
+
* // Add read permissions for all API endpoints
|
|
70
|
+
* addPermission('api', [readPermission])
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export const addPermission = (
|
|
74
|
+
tag: string,
|
|
75
|
+
permissions: CorePermissionGroup | CorePikkuPermission[]
|
|
76
|
+
) => {
|
|
77
|
+
const permissionsStore = pikkuState('misc', 'permissions')
|
|
78
|
+
|
|
79
|
+
// Check if tag already exists
|
|
80
|
+
if (permissionsStore[tag]) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Permissions for tag '${tag}' already exist. Use a different tag or remove the existing permissions first.`
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
permissionsStore[tag] = permissions
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Retrieves permissions for a given set of tags.
|
|
91
|
+
*
|
|
92
|
+
* This function looks up all permissions registered for any of the provided tags
|
|
93
|
+
* and returns them as a flattened array.
|
|
94
|
+
*
|
|
95
|
+
* @param {string[]} tags - Array of tags to look up permissions for.
|
|
96
|
+
* @returns {any[]} Array of permission functions that apply to the given tags.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* // Get all permissions for tags 'api' and 'auth'
|
|
101
|
+
* const permissions = getPermissionsForTags(['api', 'auth'])
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export const getPermissionsForTags = (
|
|
105
|
+
tags?: string[]
|
|
106
|
+
): Array<CorePermissionGroup | CorePikkuPermission> => {
|
|
107
|
+
if (!tags || tags.length === 0) {
|
|
108
|
+
return []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const permissionsStore = pikkuState('misc', 'permissions')
|
|
112
|
+
const applicablePermissions: Array<
|
|
113
|
+
CorePermissionGroup | CorePikkuPermission
|
|
114
|
+
> = []
|
|
115
|
+
|
|
116
|
+
// Collect permissions for all matching tags
|
|
117
|
+
for (const tag of new Set(tags)) {
|
|
118
|
+
const tagPermissions = permissionsStore[tag]
|
|
119
|
+
if (tagPermissions) {
|
|
120
|
+
if (Array.isArray(tagPermissions)) {
|
|
121
|
+
applicablePermissions.push(...tagPermissions)
|
|
122
|
+
} else {
|
|
123
|
+
applicablePermissions.push(tagPermissions)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return applicablePermissions
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Runs permission checks in the specified order:
|
|
133
|
+
* 1) wiring tag permissions - at least one must pass if any exist
|
|
134
|
+
* 2) wiring permissions - must pass if defined
|
|
135
|
+
* 3) function tag permissions - at least one must pass if any exist
|
|
136
|
+
* 4) function permissions - must pass if defined
|
|
137
|
+
*/
|
|
138
|
+
export const runPermissions = async ({
|
|
139
|
+
wiringTags,
|
|
140
|
+
wiringPermissions,
|
|
141
|
+
funcTags,
|
|
142
|
+
funcPermissions,
|
|
143
|
+
allServices,
|
|
144
|
+
data,
|
|
145
|
+
session,
|
|
146
|
+
}: {
|
|
147
|
+
wiringTags?: string[]
|
|
148
|
+
wiringPermissions?: CorePermissionGroup
|
|
149
|
+
funcTags?: string[]
|
|
150
|
+
funcPermissions?: CorePermissionGroup
|
|
151
|
+
allServices: CoreServices
|
|
152
|
+
data: any
|
|
153
|
+
session?: CoreUserSession
|
|
154
|
+
}) => {
|
|
155
|
+
let permissioned = true
|
|
156
|
+
|
|
157
|
+
// 1. Wiring tag permissions - at least one must pass if any exist
|
|
158
|
+
const wiringTaggedPermissions = getPermissionsForTags(wiringTags || [])
|
|
159
|
+
if (wiringTaggedPermissions.length > 0) {
|
|
160
|
+
permissioned = false // Start false, need at least one to pass
|
|
161
|
+
for (const permissions of wiringTaggedPermissions) {
|
|
162
|
+
const result = await verifyPermissions(
|
|
163
|
+
typeof permissions === 'function' ? { permissions } : permissions,
|
|
164
|
+
allServices,
|
|
165
|
+
data,
|
|
166
|
+
session
|
|
167
|
+
)
|
|
168
|
+
if (result) {
|
|
169
|
+
permissioned = true
|
|
170
|
+
break // At least one passed, we're good at this level
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!permissioned) {
|
|
174
|
+
throw new ForbiddenError('Permission denied - wiring tag permissions')
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. Wiring permissions - must pass if defined
|
|
179
|
+
if (wiringPermissions) {
|
|
180
|
+
permissioned = await verifyPermissions(
|
|
181
|
+
wiringPermissions,
|
|
182
|
+
allServices,
|
|
183
|
+
data,
|
|
184
|
+
session
|
|
185
|
+
)
|
|
186
|
+
if (!permissioned) {
|
|
187
|
+
throw new ForbiddenError('Permission denied - wiring permissions')
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3. Function tag permissions - at least one must pass if any exist
|
|
192
|
+
const funcTaggedPermissions = getPermissionsForTags(funcTags || [])
|
|
193
|
+
if (funcTaggedPermissions.length > 0) {
|
|
194
|
+
permissioned = false // Start false, need at least one to pass
|
|
195
|
+
for (const permissions of funcTaggedPermissions) {
|
|
196
|
+
const result = await verifyPermissions(
|
|
197
|
+
typeof permissions === 'function' ? { permissions } : permissions,
|
|
198
|
+
allServices,
|
|
199
|
+
data,
|
|
200
|
+
session
|
|
201
|
+
)
|
|
202
|
+
if (result) {
|
|
203
|
+
permissioned = true
|
|
204
|
+
break // At least one passed, we're good at this level
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!permissioned) {
|
|
208
|
+
throw new ForbiddenError('Permission denied - function tag permissions')
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 4. Function permissions - must pass if defined
|
|
213
|
+
if (funcPermissions) {
|
|
214
|
+
permissioned = await verifyPermissions(
|
|
215
|
+
funcPermissions,
|
|
216
|
+
allServices,
|
|
217
|
+
data,
|
|
218
|
+
session
|
|
219
|
+
)
|
|
220
|
+
if (!permissioned) {
|
|
221
|
+
throw new ForbiddenError('Permission denied - function permissions')
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/pikku-state.ts
CHANGED
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
ScheduledTasksMeta,
|
|
11
11
|
} from './wirings/scheduler/scheduler.types.js'
|
|
12
12
|
import { ErrorDetails, PikkuError } from './errors/error-handler.js'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
CorePermissionGroup,
|
|
15
|
+
CorePikkuFunctionConfig,
|
|
16
|
+
CorePikkuPermission,
|
|
17
|
+
} from './function/functions.types.js'
|
|
14
18
|
import {
|
|
15
19
|
queueWorkersMeta,
|
|
16
20
|
CoreQueueWorker,
|
|
@@ -67,6 +71,8 @@ interface PikkuState {
|
|
|
67
71
|
misc: {
|
|
68
72
|
errors: Map<PikkuError, ErrorDetails>
|
|
69
73
|
schemas: Map<string, any>
|
|
74
|
+
middleware: Record<string, CorePikkuMiddleware[]>
|
|
75
|
+
permissions: Record<string, CorePermissionGroup | CorePikkuPermission[]>
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -107,7 +113,9 @@ export const resetPikkuState = () => {
|
|
|
107
113
|
},
|
|
108
114
|
misc: {
|
|
109
115
|
errors: globalThis.pikkuState?.misc?.errors || new Map(),
|
|
110
|
-
schemas:
|
|
116
|
+
schemas: new Map(),
|
|
117
|
+
middleware: {},
|
|
118
|
+
permissions: {},
|
|
111
119
|
},
|
|
112
120
|
} as PikkuState
|
|
113
121
|
}
|