json-rest-api 1.0.21 → 1.0.23
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/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +51 -14
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/package.json +4 -4
- package/plugins/core/connectors/express-plugin.js +97 -184
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1104 -160
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
- package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
- package/plugins/core/lib/storage/storage-adapter.js +79 -2
- package/plugins/core/lib/storage/storage-mapping.js +204 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +249 -211
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +121 -83
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get.js +23 -14
- package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
- package/plugins/core/rest-api-plugin-methods/post.js +13 -8
- package/plugins/core/rest-api-plugin-methods/put.js +45 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin.js +2 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -3
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +518 -0
- package/tests/db-table-operations.test.js +781 -0
- package/tests/fastify-plugin.test.js +141 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +188 -219
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +210 -0
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
package/tests/multihome.test.js
DELETED
|
@@ -1,553 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import {
|
|
5
|
-
validateJsonApiStructure,
|
|
6
|
-
cleanTables,
|
|
7
|
-
createJsonApiDocument,
|
|
8
|
-
assertResourceAttributes,
|
|
9
|
-
createRelationship,
|
|
10
|
-
resourceIdentifier
|
|
11
|
-
} from './helpers/test-utils.js'
|
|
12
|
-
import { createMultiHomeApi } from './fixtures/api-configs.js'
|
|
13
|
-
|
|
14
|
-
// Create Knex instance for tests - always use SQLite in-memory
|
|
15
|
-
const knex = knexLib({
|
|
16
|
-
client: 'better-sqlite3',
|
|
17
|
-
connection: {
|
|
18
|
-
filename: ':memory:'
|
|
19
|
-
},
|
|
20
|
-
useNullAsDefault: true
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// API instance that persists across ALL tests
|
|
24
|
-
let api
|
|
25
|
-
|
|
26
|
-
describe('MultiHome Plugin Tests', () => {
|
|
27
|
-
// IMPORTANT: before() runs ONCE for the entire test suite
|
|
28
|
-
before(async () => {
|
|
29
|
-
// Create API instance ONCE - this is reused for all tests
|
|
30
|
-
api = await createMultiHomeApi(knex)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// IMPORTANT: after() cleans up resources
|
|
34
|
-
after(async () => {
|
|
35
|
-
// Always destroy knex connection to allow tests to exit
|
|
36
|
-
await knex.destroy()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// IMPORTANT: beforeEach() cleans data but does NOT recreate API
|
|
40
|
-
beforeEach(async () => {
|
|
41
|
-
// Clean all tables - list ALL tables your tests use
|
|
42
|
-
await cleanTables(knex, [
|
|
43
|
-
'multihome_projects',
|
|
44
|
-
'multihome_tasks',
|
|
45
|
-
'multihome_users',
|
|
46
|
-
'multihome_system_settings'
|
|
47
|
-
])
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
describe('Tenant Isolation', () => {
|
|
51
|
-
it('should automatically set tenant_id on new records', async () => {
|
|
52
|
-
// Create a project for tenant A
|
|
53
|
-
const doc = createJsonApiDocument('projects', {
|
|
54
|
-
name: 'Tenant A Project',
|
|
55
|
-
description: 'A project for tenant A'
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// Create with tenant context
|
|
59
|
-
const createResult = await api.resources.projects.post({
|
|
60
|
-
inputRecord: doc,
|
|
61
|
-
simplified: false
|
|
62
|
-
}, {
|
|
63
|
-
auth: { multihome_id: 'tenant-a' }
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
// Verify response
|
|
67
|
-
validateJsonApiStructure(createResult)
|
|
68
|
-
assert.equal(createResult.data.type, 'projects')
|
|
69
|
-
assertResourceAttributes(createResult.data, {
|
|
70
|
-
name: 'Tenant A Project',
|
|
71
|
-
description: 'A project for tenant A',
|
|
72
|
-
tenant_id: 'tenant-a' // Should be automatically set
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('should filter queries by tenant_id', async () => {
|
|
77
|
-
// Create projects for different tenants
|
|
78
|
-
const projectA1 = await api.resources.projects.post({
|
|
79
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
80
|
-
name: 'Project A1'
|
|
81
|
-
}),
|
|
82
|
-
simplified: false
|
|
83
|
-
}, {
|
|
84
|
-
auth: { multihome_id: 'tenant-a' }
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const projectA2 = await api.resources.projects.post({
|
|
88
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
89
|
-
name: 'Project A2'
|
|
90
|
-
}),
|
|
91
|
-
simplified: false
|
|
92
|
-
}, {
|
|
93
|
-
auth: { multihome_id: 'tenant-a' }
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
const projectB1 = await api.resources.projects.post({
|
|
97
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
98
|
-
name: 'Project B1'
|
|
99
|
-
}),
|
|
100
|
-
simplified: false
|
|
101
|
-
}, {
|
|
102
|
-
auth: { multihome_id: 'tenant-b' }
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
// Query as tenant A - should only see tenant A's projects
|
|
106
|
-
const resultA = await api.resources.projects.query({
|
|
107
|
-
simplified: false
|
|
108
|
-
}, {
|
|
109
|
-
auth: { multihome_id: 'tenant-a' }
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
validateJsonApiStructure(resultA, true) // true for collection
|
|
113
|
-
assert.equal(resultA.data.length, 2, 'Tenant A should see 2 projects')
|
|
114
|
-
assert(resultA.data.every(p => p.attributes.name.startsWith('Project A')))
|
|
115
|
-
|
|
116
|
-
// Query as tenant B - should only see tenant B's projects
|
|
117
|
-
const resultB = await api.resources.projects.query({
|
|
118
|
-
simplified: false
|
|
119
|
-
}, {
|
|
120
|
-
auth: { multihome_id: 'tenant-b' }
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
validateJsonApiStructure(resultB, true) // true for collection
|
|
124
|
-
assert.equal(resultB.data.length, 1, 'Tenant B should see 1 project')
|
|
125
|
-
assert.equal(resultB.data[0].attributes.name, 'Project B1')
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('should prevent access to other tenant records', async () => {
|
|
129
|
-
// Create a project for tenant A
|
|
130
|
-
const projectA = await api.resources.projects.post({
|
|
131
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
132
|
-
name: 'Tenant A Secret Project'
|
|
133
|
-
}),
|
|
134
|
-
simplified: false
|
|
135
|
-
}, {
|
|
136
|
-
auth: { multihome_id: 'tenant-a' }
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// Try to access as tenant B - should fail
|
|
140
|
-
await assert.rejects(
|
|
141
|
-
async () => {
|
|
142
|
-
await api.resources.projects.get({
|
|
143
|
-
id: projectA.data.id,
|
|
144
|
-
simplified: false
|
|
145
|
-
}, {
|
|
146
|
-
auth: { multihome_id: 'tenant-b' }
|
|
147
|
-
})
|
|
148
|
-
},
|
|
149
|
-
(err) => {
|
|
150
|
-
return err.code === 'REST_API_RESOURCE'
|
|
151
|
-
},
|
|
152
|
-
'Should not be able to access other tenant data'
|
|
153
|
-
)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('should prevent updating tenant_id to different value', async () => {
|
|
157
|
-
// Create a project for tenant A
|
|
158
|
-
const project = await api.resources.projects.post({
|
|
159
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
160
|
-
name: 'Original Project'
|
|
161
|
-
}),
|
|
162
|
-
simplified: false
|
|
163
|
-
}, {
|
|
164
|
-
auth: { multihome_id: 'tenant-a' }
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Try to update with different tenant_id - should fail
|
|
168
|
-
await assert.rejects(
|
|
169
|
-
async () => {
|
|
170
|
-
await api.resources.projects.patch({
|
|
171
|
-
id: project.data.id,
|
|
172
|
-
inputRecord: {
|
|
173
|
-
data: {
|
|
174
|
-
type: 'projects',
|
|
175
|
-
id: project.data.id,
|
|
176
|
-
attributes: {
|
|
177
|
-
name: 'Updated Project',
|
|
178
|
-
tenant_id: 'tenant-b' // Trying to change tenant
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
simplified: false
|
|
183
|
-
}, {
|
|
184
|
-
auth: { multihome_id: 'tenant-a' }
|
|
185
|
-
})
|
|
186
|
-
},
|
|
187
|
-
(err) => {
|
|
188
|
-
console.log('PATCH error:', err.message)
|
|
189
|
-
console.log('PATCH error stack:', err.stack)
|
|
190
|
-
return err.message.includes('must match current context')
|
|
191
|
-
},
|
|
192
|
-
'Should not be able to change tenant_id'
|
|
193
|
-
)
|
|
194
|
-
})
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
describe('Relationships Across Tenants', () => {
|
|
198
|
-
it('should maintain tenant isolation for related records', async () => {
|
|
199
|
-
// Create projects for different tenants
|
|
200
|
-
const projectA = await api.resources.projects.post({
|
|
201
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
202
|
-
name: 'Tenant A Project'
|
|
203
|
-
}),
|
|
204
|
-
simplified: false
|
|
205
|
-
}, {
|
|
206
|
-
auth: { multihome_id: 'tenant-a' }
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
const projectB = await api.resources.projects.post({
|
|
210
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
211
|
-
name: 'Tenant B Project'
|
|
212
|
-
}),
|
|
213
|
-
simplified: false
|
|
214
|
-
}, {
|
|
215
|
-
auth: { multihome_id: 'tenant-b' }
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
// Create task for tenant A project
|
|
219
|
-
const taskA = await api.resources.tasks.post({
|
|
220
|
-
inputRecord: createJsonApiDocument('tasks',
|
|
221
|
-
{
|
|
222
|
-
title: 'Task for Tenant A'
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
project: createRelationship(resourceIdentifier('projects', projectA.data.id))
|
|
226
|
-
}
|
|
227
|
-
),
|
|
228
|
-
simplified: false
|
|
229
|
-
}, {
|
|
230
|
-
auth: { multihome_id: 'tenant-a' }
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
// Try to create task for tenant B project as tenant A - should fail
|
|
234
|
-
await assert.rejects(
|
|
235
|
-
async () => {
|
|
236
|
-
await api.resources.tasks.post({
|
|
237
|
-
inputRecord: createJsonApiDocument('tasks',
|
|
238
|
-
{
|
|
239
|
-
title: 'Invalid Task'
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
project: createRelationship(resourceIdentifier('projects', projectB.data.id))
|
|
243
|
-
}
|
|
244
|
-
),
|
|
245
|
-
simplified: false
|
|
246
|
-
}, {
|
|
247
|
-
auth: { multihome_id: 'tenant-a' }
|
|
248
|
-
})
|
|
249
|
-
},
|
|
250
|
-
(err) => {
|
|
251
|
-
// Should fail because project belongs to different tenant
|
|
252
|
-
return err.code === 'REST_API_VALIDATION' || err.code === 'REST_API_RESOURCE'
|
|
253
|
-
},
|
|
254
|
-
'Should not be able to link to other tenant resources'
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
// Query with includes - should only see own tenant data
|
|
258
|
-
const projectWithTasks = await api.resources.projects.get({
|
|
259
|
-
id: projectA.data.id,
|
|
260
|
-
queryParams: {
|
|
261
|
-
include: ['tasks']
|
|
262
|
-
},
|
|
263
|
-
simplified: false
|
|
264
|
-
}, {
|
|
265
|
-
auth: { multihome_id: 'tenant-a' }
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
validateJsonApiStructure(projectWithTasks)
|
|
269
|
-
assert(projectWithTasks.included, 'Should have included tasks')
|
|
270
|
-
assert.equal(projectWithTasks.included.length, 1, 'Should include one task')
|
|
271
|
-
assert.equal(projectWithTasks.included[0].attributes.tenant_id, 'tenant-a')
|
|
272
|
-
})
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
describe('Excluded Resources', () => {
|
|
276
|
-
it('should allow access to excluded resources without tenant context', async () => {
|
|
277
|
-
// Create system setting without tenant context
|
|
278
|
-
const setting = await api.resources.system_settings.post({
|
|
279
|
-
inputRecord: createJsonApiDocument('system_settings', {
|
|
280
|
-
key: 'app.version',
|
|
281
|
-
value: '1.0.0'
|
|
282
|
-
}),
|
|
283
|
-
simplified: false
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
validateJsonApiStructure(setting)
|
|
287
|
-
assert.equal(setting.data.type, 'system_settings')
|
|
288
|
-
assert(!setting.data.attributes.tenant_id, 'Should not have tenant_id')
|
|
289
|
-
|
|
290
|
-
// Query without tenant context should work
|
|
291
|
-
const allSettings = await api.resources.system_settings.query({
|
|
292
|
-
simplified: false
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
assert.equal(allSettings.data.length, 1)
|
|
296
|
-
|
|
297
|
-
// Should be accessible by any tenant
|
|
298
|
-
const settingByTenantA = await api.resources.system_settings.get({
|
|
299
|
-
id: setting.data.id,
|
|
300
|
-
simplified: false
|
|
301
|
-
}, {
|
|
302
|
-
auth: { multihome_id: 'tenant-a' }
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
assert.equal(settingByTenantA.data.id, setting.data.id)
|
|
306
|
-
})
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
describe('Configuration Options', () => {
|
|
310
|
-
it('should require tenant context when requireAuth is true', async () => {
|
|
311
|
-
// Try to create without tenant context - should fail
|
|
312
|
-
await assert.rejects(
|
|
313
|
-
async () => {
|
|
314
|
-
await api.resources.projects.post({
|
|
315
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
316
|
-
name: 'No Tenant Project'
|
|
317
|
-
}),
|
|
318
|
-
simplified: false
|
|
319
|
-
})
|
|
320
|
-
},
|
|
321
|
-
(err) => {
|
|
322
|
-
return err.message.includes('multihome context')
|
|
323
|
-
},
|
|
324
|
-
'Should require tenant context when requireAuth is true'
|
|
325
|
-
)
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
it('should validate resources have tenant field when allowMissing is false', async () => {
|
|
329
|
-
// This test validates that the plugin threw an error during setup
|
|
330
|
-
// if a resource was missing the tenant field. Since our test resources
|
|
331
|
-
// all have tenant_id (except excluded ones), we can verify the
|
|
332
|
-
// configuration is working by checking vars
|
|
333
|
-
assert.equal(api.vars.multihome.field, 'tenant_id')
|
|
334
|
-
assert.equal(api.vars.multihome.allowMissing, false)
|
|
335
|
-
assert(api.vars.multihome.excludeResources.includes('system_settings'))
|
|
336
|
-
})
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
describe('Complex Queries', () => {
|
|
340
|
-
it('should apply tenant filter to filtered queries', async () => {
|
|
341
|
-
// Create users for different tenants
|
|
342
|
-
await api.resources.users.post({
|
|
343
|
-
inputRecord: createJsonApiDocument('users', {
|
|
344
|
-
email: 'admin@tenant-a.com',
|
|
345
|
-
name: 'Admin A',
|
|
346
|
-
role: 'admin'
|
|
347
|
-
}),
|
|
348
|
-
simplified: false
|
|
349
|
-
}, {
|
|
350
|
-
auth: { multihome_id: 'tenant-a' }
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
await api.resources.users.post({
|
|
354
|
-
inputRecord: createJsonApiDocument('users', {
|
|
355
|
-
email: 'user@tenant-a.com',
|
|
356
|
-
name: 'User A',
|
|
357
|
-
role: 'member'
|
|
358
|
-
}),
|
|
359
|
-
simplified: false
|
|
360
|
-
}, {
|
|
361
|
-
auth: { multihome_id: 'tenant-a' }
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
await api.resources.users.post({
|
|
365
|
-
inputRecord: createJsonApiDocument('users', {
|
|
366
|
-
email: 'admin@tenant-b.com',
|
|
367
|
-
name: 'Admin B',
|
|
368
|
-
role: 'admin'
|
|
369
|
-
}),
|
|
370
|
-
simplified: false
|
|
371
|
-
}, {
|
|
372
|
-
auth: { multihome_id: 'tenant-b' }
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
// Query for admins as tenant A - should only see tenant A admin
|
|
376
|
-
const adminsA = await api.resources.users.query({
|
|
377
|
-
queryParams: {
|
|
378
|
-
filters: { role: 'admin' }
|
|
379
|
-
},
|
|
380
|
-
simplified: false
|
|
381
|
-
}, {
|
|
382
|
-
auth: { multihome_id: 'tenant-a' }
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
assert.equal(adminsA.data.length, 1, 'Should only see tenant A admin')
|
|
386
|
-
assert.equal(adminsA.data[0].attributes.email, 'admin@tenant-a.com')
|
|
387
|
-
|
|
388
|
-
// Query for all users as tenant B - should only see tenant B users
|
|
389
|
-
const usersB = await api.resources.users.query({
|
|
390
|
-
simplified: false
|
|
391
|
-
}, {
|
|
392
|
-
auth: { multihome_id: 'tenant-b' }
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
assert.equal(usersB.data.length, 1, 'Should only see tenant B users')
|
|
396
|
-
assert.equal(usersB.data[0].attributes.email, 'admin@tenant-b.com')
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
it('should apply tenant filter with sorting and pagination', async () => {
|
|
400
|
-
// Create multiple projects for tenant A
|
|
401
|
-
for (let i = 1; i <= 5; i++) {
|
|
402
|
-
await api.resources.projects.post({
|
|
403
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
404
|
-
name: `Project A${i}`,
|
|
405
|
-
status: i % 2 === 0 ? 'active' : 'inactive'
|
|
406
|
-
}),
|
|
407
|
-
simplified: false
|
|
408
|
-
}, {
|
|
409
|
-
auth: { multihome_id: 'tenant-a' }
|
|
410
|
-
})
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Create projects for tenant B
|
|
414
|
-
for (let i = 1; i <= 3; i++) {
|
|
415
|
-
await api.resources.projects.post({
|
|
416
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
417
|
-
name: `Project B${i}`
|
|
418
|
-
}),
|
|
419
|
-
simplified: false
|
|
420
|
-
}, {
|
|
421
|
-
auth: { multihome_id: 'tenant-b' }
|
|
422
|
-
})
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Query with pagination as tenant A
|
|
426
|
-
const page1 = await api.resources.projects.query({
|
|
427
|
-
queryParams: {
|
|
428
|
-
page: { size: 2, number: 1 },
|
|
429
|
-
sort: ['-name'] // Sort by name descending
|
|
430
|
-
},
|
|
431
|
-
simplified: false
|
|
432
|
-
}, {
|
|
433
|
-
auth: { multihome_id: 'tenant-a' }
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
assert.equal(page1.data.length, 2, 'Should return page size')
|
|
437
|
-
assert.equal(page1.data[0].attributes.name, 'Project A5', 'Should be sorted descending')
|
|
438
|
-
assert.equal(page1.data[1].attributes.name, 'Project A4')
|
|
439
|
-
|
|
440
|
-
// Get page 2
|
|
441
|
-
const page2 = await api.resources.projects.query({
|
|
442
|
-
queryParams: {
|
|
443
|
-
page: { size: 2, number: 2 },
|
|
444
|
-
sort: ['-name']
|
|
445
|
-
},
|
|
446
|
-
simplified: false
|
|
447
|
-
}, {
|
|
448
|
-
auth: { multihome_id: 'tenant-a' }
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
assert.equal(page2.data.length, 2, 'Should return page size')
|
|
452
|
-
assert(page2.data.every(p => p.attributes.tenant_id === 'tenant-a'))
|
|
453
|
-
})
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
describe('Error Handling', () => {
|
|
457
|
-
it('should provide clear error when no tenant context available', async () => {
|
|
458
|
-
await assert.rejects(
|
|
459
|
-
async () => {
|
|
460
|
-
await api.resources.projects.query({
|
|
461
|
-
simplified: false
|
|
462
|
-
})
|
|
463
|
-
},
|
|
464
|
-
(err) => {
|
|
465
|
-
return err.message.includes('No multihome context available')
|
|
466
|
-
},
|
|
467
|
-
'Should provide clear error message'
|
|
468
|
-
)
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
it('should handle missing tenant field gracefully for excluded resources', async () => {
|
|
472
|
-
// system_settings is excluded and doesn't have tenant_id field
|
|
473
|
-
const result = await api.resources.system_settings.post({
|
|
474
|
-
inputRecord: createJsonApiDocument('system_settings', {
|
|
475
|
-
key: 'test.setting',
|
|
476
|
-
value: 'test'
|
|
477
|
-
}),
|
|
478
|
-
simplified: false
|
|
479
|
-
}, {
|
|
480
|
-
auth: { multihome_id: 'tenant-a' } // Should be ignored for excluded resources
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
validateJsonApiStructure(result)
|
|
484
|
-
assert(!result.data.attributes.tenant_id, 'Excluded resource should not have tenant_id')
|
|
485
|
-
})
|
|
486
|
-
})
|
|
487
|
-
|
|
488
|
-
describe('Security', () => {
|
|
489
|
-
it('should prevent bypassing tenant filter with malicious IDs', async () => {
|
|
490
|
-
// Create a project for tenant A
|
|
491
|
-
const project = await api.resources.projects.post({
|
|
492
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
493
|
-
name: 'Secure Project'
|
|
494
|
-
}),
|
|
495
|
-
simplified: false
|
|
496
|
-
}, {
|
|
497
|
-
auth: { multihome_id: 'tenant-a' }
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
// Try various bypass attempts as tenant B
|
|
501
|
-
const maliciousIds = [
|
|
502
|
-
project.data.id,
|
|
503
|
-
`${project.data.id} OR 1=1`,
|
|
504
|
-
`${project.data.id}'; DROP TABLE projects; --`
|
|
505
|
-
]
|
|
506
|
-
|
|
507
|
-
for (const id of maliciousIds) {
|
|
508
|
-
await assert.rejects(
|
|
509
|
-
async () => {
|
|
510
|
-
await api.resources.projects.get({
|
|
511
|
-
id,
|
|
512
|
-
simplified: false
|
|
513
|
-
}, {
|
|
514
|
-
auth: { multihome_id: 'tenant-b' }
|
|
515
|
-
})
|
|
516
|
-
},
|
|
517
|
-
undefined, // Any error is fine
|
|
518
|
-
`Should not bypass security with ID: ${id}`
|
|
519
|
-
)
|
|
520
|
-
}
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
it('should log security violations', async () => {
|
|
524
|
-
// This test verifies that the plugin logs security violations
|
|
525
|
-
// In a real implementation, you might capture logs and verify
|
|
526
|
-
// For now, we just ensure the security check happens
|
|
527
|
-
|
|
528
|
-
// Create data for tenant A
|
|
529
|
-
const project = await api.resources.projects.post({
|
|
530
|
-
inputRecord: createJsonApiDocument('projects', {
|
|
531
|
-
name: 'Tenant A Data'
|
|
532
|
-
}),
|
|
533
|
-
simplified: false
|
|
534
|
-
}, {
|
|
535
|
-
auth: { multihome_id: 'tenant-a' }
|
|
536
|
-
})
|
|
537
|
-
|
|
538
|
-
// Attempt access as tenant B
|
|
539
|
-
try {
|
|
540
|
-
await api.resources.projects.get({
|
|
541
|
-
id: project.data.id,
|
|
542
|
-
simplified: false
|
|
543
|
-
}, {
|
|
544
|
-
auth: { multihome_id: 'tenant-b' }
|
|
545
|
-
})
|
|
546
|
-
assert.fail('Should have thrown error')
|
|
547
|
-
} catch (err) {
|
|
548
|
-
// Error is expected - security violation should be logged
|
|
549
|
-
assert(err.code === 'REST_API_RESOURCE' || err.message.includes('Access denied'))
|
|
550
|
-
}
|
|
551
|
-
})
|
|
552
|
-
})
|
|
553
|
-
})
|