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