@pikku/core 0.9.4 → 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 +19 -0
- package/dist/function/function-runner.d.ts +1 -1
- package/dist/function/function-runner.js +22 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware-runner.d.ts +19 -11
- package/dist/middleware-runner.js +27 -13
- package/dist/permissions.d.ts +19 -3
- package/dist/permissions.js +61 -1
- package/dist/pikku-state.d.ts +2 -2
- package/dist/pikku-state.js +3 -3
- package/dist/types/core.types.d.ts +3 -4
- package/dist/wirings/channel/local/local-channel-runner.js +5 -2
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +5 -2
- package/dist/wirings/http/http-runner.js +9 -2
- package/dist/wirings/http/http.types.d.ts +4 -4
- package/dist/wirings/mcp/mcp-runner.js +8 -3
- package/dist/wirings/queue/queue-runner.js +9 -2
- package/dist/wirings/rpc/rpc-runner.d.ts +1 -2
- package/dist/wirings/scheduler/scheduler-runner.js +8 -3
- package/package.json +1 -1
- package/src/function/function-runner.test.ts +450 -0
- package/src/function/function-runner.ts +23 -43
- package/src/index.ts +6 -2
- package/src/middleware-runner.test.ts +329 -0
- package/src/middleware-runner.ts +38 -17
- package/src/permissions.test.ts +403 -42
- package/src/permissions.ts +115 -5
- package/src/pikku-state.ts +9 -5
- package/src/types/core.types.ts +3 -4
- package/src/wirings/channel/local/local-channel-runner.ts +5 -5
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +5 -5
- package/src/wirings/http/http-runner.ts +10 -2
- package/src/wirings/http/http.types.ts +4 -4
- package/src/wirings/mcp/mcp-runner.ts +8 -3
- package/src/wirings/queue/queue-runner.ts +12 -2
- package/src/wirings/scheduler/scheduler-runner.ts +10 -3
- 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,6 +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'
|
|
3
6
|
import { pikkuState } from './pikku-state.js'
|
|
7
|
+
import { ForbiddenError } from './errors/errors.js'
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
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.
|
|
@@ -66,7 +70,10 @@ export const verifyPermissions = async (
|
|
|
66
70
|
* addPermission('api', [readPermission])
|
|
67
71
|
* ```
|
|
68
72
|
*/
|
|
69
|
-
export const addPermission = (
|
|
73
|
+
export const addPermission = (
|
|
74
|
+
tag: string,
|
|
75
|
+
permissions: CorePermissionGroup | CorePikkuPermission[]
|
|
76
|
+
) => {
|
|
70
77
|
const permissionsStore = pikkuState('misc', 'permissions')
|
|
71
78
|
|
|
72
79
|
// Check if tag already exists
|
|
@@ -94,21 +101,124 @@ export const addPermission = (tag: string, permissions: any[]) => {
|
|
|
94
101
|
* const permissions = getPermissionsForTags(['api', 'auth'])
|
|
95
102
|
* ```
|
|
96
103
|
*/
|
|
97
|
-
export const getPermissionsForTags = (
|
|
104
|
+
export const getPermissionsForTags = (
|
|
105
|
+
tags?: string[]
|
|
106
|
+
): Array<CorePermissionGroup | CorePikkuPermission> => {
|
|
98
107
|
if (!tags || tags.length === 0) {
|
|
99
108
|
return []
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
const permissionsStore = pikkuState('misc', 'permissions')
|
|
103
|
-
const applicablePermissions:
|
|
112
|
+
const applicablePermissions: Array<
|
|
113
|
+
CorePermissionGroup | CorePikkuPermission
|
|
114
|
+
> = []
|
|
104
115
|
|
|
105
116
|
// Collect permissions for all matching tags
|
|
106
117
|
for (const tag of new Set(tags)) {
|
|
107
118
|
const tagPermissions = permissionsStore[tag]
|
|
108
119
|
if (tagPermissions) {
|
|
109
|
-
|
|
120
|
+
if (Array.isArray(tagPermissions)) {
|
|
121
|
+
applicablePermissions.push(...tagPermissions)
|
|
122
|
+
} else {
|
|
123
|
+
applicablePermissions.push(tagPermissions)
|
|
124
|
+
}
|
|
110
125
|
}
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
return applicablePermissions
|
|
114
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,
|
|
@@ -68,7 +72,7 @@ interface PikkuState {
|
|
|
68
72
|
errors: Map<PikkuError, ErrorDetails>
|
|
69
73
|
schemas: Map<string, any>
|
|
70
74
|
middleware: Record<string, CorePikkuMiddleware[]>
|
|
71
|
-
permissions: Record<string,
|
|
75
|
+
permissions: Record<string, CorePermissionGroup | CorePikkuPermission[]>
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -109,9 +113,9 @@ export const resetPikkuState = () => {
|
|
|
109
113
|
},
|
|
110
114
|
misc: {
|
|
111
115
|
errors: globalThis.pikkuState?.misc?.errors || new Map(),
|
|
112
|
-
schemas:
|
|
113
|
-
middleware:
|
|
114
|
-
permissions:
|
|
116
|
+
schemas: new Map(),
|
|
117
|
+
middleware: {},
|
|
118
|
+
permissions: {},
|
|
115
119
|
},
|
|
116
120
|
} as PikkuState
|
|
117
121
|
}
|
package/src/types/core.types.ts
CHANGED
|
@@ -114,14 +114,13 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
114
114
|
/**
|
|
115
115
|
* Represents different forms of interaction within Pikku and the outside world.
|
|
116
116
|
*/
|
|
117
|
-
export type PikkuInteraction = Partial<{
|
|
118
|
-
http: PikkuHTTP
|
|
117
|
+
export type PikkuInteraction<In = unknown, Out = unknown> = Partial<{
|
|
118
|
+
http: PikkuHTTP<In>
|
|
119
119
|
mcp: PikkuMCP
|
|
120
120
|
rpc: PikkuRPC
|
|
121
|
-
channel: PikkuChannel<unknown,
|
|
121
|
+
channel: PikkuChannel<unknown, Out>
|
|
122
122
|
scheduledTask: PikkuScheduledTask
|
|
123
123
|
queue: PikkuQueue
|
|
124
|
-
s
|
|
125
124
|
}>
|
|
126
125
|
|
|
127
126
|
/**
|
|
@@ -10,10 +10,7 @@ import {
|
|
|
10
10
|
import { PikkuLocalChannelHandler } from './local-channel-handler.js'
|
|
11
11
|
import { SessionServices } from '../../../types/core.types.js'
|
|
12
12
|
import { handleHTTPError } from '../../../handle-error.js'
|
|
13
|
-
import {
|
|
14
|
-
addMiddlewareForTags,
|
|
15
|
-
runMiddleware,
|
|
16
|
-
} from '../../../middleware-runner.js'
|
|
13
|
+
import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js'
|
|
17
14
|
import { PikkuUserSessionService } from '../../../services/user-session-service.js'
|
|
18
15
|
import { PikkuHTTP } from '../../http/http.types.js'
|
|
19
16
|
import { runPikkuFuncDirectly } from '../../../function/function-runner.js'
|
|
@@ -149,7 +146,10 @@ export const runLocalChannel = async ({
|
|
|
149
146
|
userSession,
|
|
150
147
|
},
|
|
151
148
|
{ http },
|
|
152
|
-
|
|
149
|
+
combineMiddleware({
|
|
150
|
+
wiringMiddleware: channelConfig.middleware,
|
|
151
|
+
wiringTags: channelConfig.tags,
|
|
152
|
+
}),
|
|
153
153
|
main
|
|
154
154
|
)
|
|
155
155
|
|