json-rest-api 1.0.15 → 1.0.17
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/package.json
CHANGED
|
@@ -184,7 +184,8 @@ export async function compileSchemas (scope, deps) {
|
|
|
184
184
|
const schemaContext = {
|
|
185
185
|
fields: enrichedFields, // Mutable
|
|
186
186
|
originalFields: rawFields, // Read-only
|
|
187
|
-
scopeName
|
|
187
|
+
scopeName,
|
|
188
|
+
scopeOptions: scope.scopeOptions || {}
|
|
188
189
|
}
|
|
189
190
|
await runHooks('schema:enrich', schemaContext)
|
|
190
191
|
|
|
@@ -24,10 +24,16 @@ export const AccessPlugin = {
|
|
|
24
24
|
|
|
25
25
|
const ownershipField = () => config.ownership.field
|
|
26
26
|
|
|
27
|
+
// Minimal helper: treat ownerField false/null as ownership disabled (alias)
|
|
28
|
+
const isOwnershipDisabled = (scopeOptions = {}) => (
|
|
29
|
+
scopeOptions?.ownership === false ||
|
|
30
|
+
scopeOptions?.ownerField === false ||
|
|
31
|
+
scopeOptions?.ownerField === null
|
|
32
|
+
)
|
|
33
|
+
|
|
27
34
|
const evaluateOwnership = ({ record, field, schemaInfo, userId }) => {
|
|
28
35
|
if (!record) return 'unknown'
|
|
29
36
|
|
|
30
|
-
debugger
|
|
31
37
|
const idProperty = schemaInfo?.idProperty || 'id'
|
|
32
38
|
const schemaStructure = schemaInfo?.schemaStructure || {}
|
|
33
39
|
const schemaRelationships = schemaInfo?.schemaRelationships || {}
|
|
@@ -146,7 +152,6 @@ export const AccessPlugin = {
|
|
|
146
152
|
const auth = context.scopeOptions?.auth
|
|
147
153
|
if (!auth) return
|
|
148
154
|
|
|
149
|
-
debugger
|
|
150
155
|
scope.vars.authRules = auth
|
|
151
156
|
scope.vars.ownershipField = context.scopeOptions?.ownershipField
|
|
152
157
|
|
|
@@ -159,11 +164,11 @@ export const AccessPlugin = {
|
|
|
159
164
|
})
|
|
160
165
|
})
|
|
161
166
|
|
|
162
|
-
addHook('schema:enrich', 'rest-auth-auto-ownership-field', {}, ({ context
|
|
167
|
+
addHook('schema:enrich', 'rest-auth-auto-ownership-field', {}, ({ context }) => {
|
|
163
168
|
if (!config.ownership.enabled) return
|
|
164
|
-
const { fields, scopeName } = context
|
|
169
|
+
const { fields, scopeName, scopeOptions = {} } = context
|
|
165
170
|
if (config.ownership.excludeResources.includes(scopeName)) return
|
|
166
|
-
if (scopeOptions
|
|
171
|
+
if (isOwnershipDisabled(scopeOptions)) return
|
|
167
172
|
|
|
168
173
|
const field = ownershipField()
|
|
169
174
|
const relationshipName = fields[field]?.as || fields[field]?.belongsTo || field
|
|
@@ -198,7 +203,7 @@ export const AccessPlugin = {
|
|
|
198
203
|
return
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
if (scopeOptions
|
|
206
|
+
if (isOwnershipDisabled(scopeOptions)) return
|
|
202
207
|
|
|
203
208
|
const field = ownershipField()
|
|
204
209
|
const scope = scopes[scopeName]
|
|
@@ -259,7 +264,7 @@ export const AccessPlugin = {
|
|
|
259
264
|
}
|
|
260
265
|
|
|
261
266
|
if (context.auth.roles?.includes?.('admin')) return
|
|
262
|
-
if (scopeOptions
|
|
267
|
+
if (isOwnershipDisabled(scopeOptions)) return
|
|
263
268
|
|
|
264
269
|
const field = ownershipField()
|
|
265
270
|
const scope = scopes[scopeName]
|
|
@@ -285,7 +290,6 @@ export const AccessPlugin = {
|
|
|
285
290
|
|
|
286
291
|
addHook('checkPermissions', 'rest-auth-enforce', { sequence: -100 }, async ({ context, scope, scopeName }) => {
|
|
287
292
|
const operation = context.method
|
|
288
|
-
debugger
|
|
289
293
|
const scopeVars = scope?.vars
|
|
290
294
|
const authRules = scopeVars?.authRules
|
|
291
295
|
const minimalRecord = context.originalContext?.minimalRecord
|
|
@@ -300,7 +304,6 @@ export const AccessPlugin = {
|
|
|
300
304
|
const failures = []
|
|
301
305
|
|
|
302
306
|
for (const rule of rules) {
|
|
303
|
-
// if (rule === 'owns') debugger
|
|
304
307
|
try {
|
|
305
308
|
const [checkerName, ...paramParts] = rule.split(':')
|
|
306
309
|
const checker = state.authCheckers.get(checkerName)
|
|
@@ -346,7 +349,7 @@ export const AccessPlugin = {
|
|
|
346
349
|
const auth = context.originalContext?.auth || context.auth
|
|
347
350
|
if (!auth?.userId) return
|
|
348
351
|
if (auth.roles?.includes?.('admin')) return
|
|
349
|
-
if (scopeOptions
|
|
352
|
+
if (isOwnershipDisabled(scopeOptions)) return
|
|
350
353
|
|
|
351
354
|
const field = ownershipField()
|
|
352
355
|
const schemaInfo = scope?.vars?.schemaInfo || {}
|
|
@@ -1872,3 +1872,77 @@ export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
|
|
|
1872
1872
|
|
|
1873
1873
|
return api
|
|
1874
1874
|
}
|
|
1875
|
+
|
|
1876
|
+
|
|
1877
|
+
/**
|
|
1878
|
+
* Creates an API with resources that OPT OUT of ownership
|
|
1879
|
+
* - public_items_ownerfield: uses ownerField: false
|
|
1880
|
+
* - public_items_ownership: uses ownership: false
|
|
1881
|
+
* Both tables intentionally DO NOT have a user_id column.
|
|
1882
|
+
*/
|
|
1883
|
+
export async function createOwnershipOptOutApi (knex, pluginOptions = {}) {
|
|
1884
|
+
const apiName = pluginOptions.apiName || 'ownership-optout-api'
|
|
1885
|
+
const tablePrefix = pluginOptions.tablePrefix || 'ownopt'
|
|
1886
|
+
if (storageMode.isAnyApi()) {
|
|
1887
|
+
storageMode.clearRegistry()
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
const api = new Api({
|
|
1891
|
+
name: apiName,
|
|
1892
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
1893
|
+
})
|
|
1894
|
+
|
|
1895
|
+
const previousTenant = storageMode.currentTenant
|
|
1896
|
+
const tenantId = storageMode.isAnyApi()
|
|
1897
|
+
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
1898
|
+
: storageMode.defaultTenant
|
|
1899
|
+
if (storageMode.isAnyApi()) {
|
|
1900
|
+
storageMode.setCurrentTenant(tenantId)
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
await api.use(RestApiPlugin, {
|
|
1904
|
+
simplifiedApi: false,
|
|
1905
|
+
simplifiedTransport: false,
|
|
1906
|
+
returnRecordApi: { post: true, put: false, patch: false },
|
|
1907
|
+
returnRecordTransport: { post: 'full', put: 'no', patch: 'minimal' },
|
|
1908
|
+
sortableFields: ['id', 'name']
|
|
1909
|
+
})
|
|
1910
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
1911
|
+
await resetAnyApiTables(knex)
|
|
1912
|
+
|
|
1913
|
+
try {
|
|
1914
|
+
// Access plugin (defaults: ownership enabled, filterByOwner true)
|
|
1915
|
+
await api.use(AccessPlugin, pluginOptions.access || {})
|
|
1916
|
+
|
|
1917
|
+
// Resource A: disable via ownerField:false
|
|
1918
|
+
await api.addResource('public_items_ownerfield', {
|
|
1919
|
+
schema: {
|
|
1920
|
+
id: { type: 'id' },
|
|
1921
|
+
name: { type: 'string', required: true }
|
|
1922
|
+
},
|
|
1923
|
+
// Important: no user_id column in schema; ownerField disables ownership logic
|
|
1924
|
+
ownerField: false,
|
|
1925
|
+
tableName: `${tablePrefix}_public_items_of`
|
|
1926
|
+
})
|
|
1927
|
+
await api.resources.public_items_ownerfield.createKnexTable()
|
|
1928
|
+
mapTable(`${tablePrefix}_public_items_of`, 'public_items_ownerfield')
|
|
1929
|
+
|
|
1930
|
+
// Resource B: disable via ownership:false
|
|
1931
|
+
await api.addResource('public_items_ownership', {
|
|
1932
|
+
schema: {
|
|
1933
|
+
id: { type: 'id' },
|
|
1934
|
+
name: { type: 'string', required: true }
|
|
1935
|
+
},
|
|
1936
|
+
ownership: false,
|
|
1937
|
+
tableName: `${tablePrefix}_public_items_own`
|
|
1938
|
+
})
|
|
1939
|
+
await api.resources.public_items_ownership.createKnexTable()
|
|
1940
|
+
mapTable(`${tablePrefix}_public_items_own`, 'public_items_ownership')
|
|
1941
|
+
|
|
1942
|
+
return api
|
|
1943
|
+
} finally {
|
|
1944
|
+
if (storageMode.isAnyApi()) {
|
|
1945
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
} from './helpers/test-utils.js'
|
|
10
|
+
import { createOwnershipOptOutApi } from './fixtures/api-configs.js'
|
|
11
|
+
|
|
12
|
+
const knex = knexLib({
|
|
13
|
+
client: 'better-sqlite3',
|
|
14
|
+
connection: { filename: ':memory:' },
|
|
15
|
+
useNullAsDefault: true
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
let api
|
|
19
|
+
|
|
20
|
+
async function createPublicItem (resourceName, name, authUserId) {
|
|
21
|
+
const doc = createJsonApiDocument(resourceName, { name })
|
|
22
|
+
const res = await api.resources[resourceName].post({
|
|
23
|
+
inputRecord: doc,
|
|
24
|
+
simplified: false
|
|
25
|
+
}, { auth: { userId: authUserId } })
|
|
26
|
+
|
|
27
|
+
validateJsonApiStructure(res)
|
|
28
|
+
assertResourceAttributes(res.data, { name })
|
|
29
|
+
return res.data
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('Ownership opt-out (ownerField:false / ownership:false)', () => {
|
|
33
|
+
before(async () => {
|
|
34
|
+
api = await createOwnershipOptOutApi(knex)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
after(async () => {
|
|
38
|
+
await knex.destroy()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
await cleanTables(knex, [
|
|
43
|
+
'ownopt_public_items_of',
|
|
44
|
+
'ownopt_public_items_own'
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('ownerField:false - allows create+query without user_id column present', async () => {
|
|
49
|
+
await createPublicItem('public_items_ownerfield', 'Alpha', 1)
|
|
50
|
+
await createPublicItem('public_items_ownerfield', 'Beta', 2)
|
|
51
|
+
|
|
52
|
+
// Query with auth
|
|
53
|
+
const resultAuth = await api.resources.public_items_ownerfield.query({ simplified: false }, { auth: { userId: 1 } })
|
|
54
|
+
validateJsonApiStructure(resultAuth, true)
|
|
55
|
+
assert.equal(resultAuth.data.length, 2)
|
|
56
|
+
|
|
57
|
+
// Query without auth (ownership disabled -> no filtering)
|
|
58
|
+
const resultNoAuth = await api.resources.public_items_ownerfield.query({ simplified: false })
|
|
59
|
+
validateJsonApiStructure(resultNoAuth, true)
|
|
60
|
+
assert.equal(resultNoAuth.data.length, 2)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('ownership:false - allows create+query without user_id column present', async () => {
|
|
64
|
+
await createPublicItem('public_items_ownership', 'Gamma', 1)
|
|
65
|
+
await createPublicItem('public_items_ownership', 'Delta', 2)
|
|
66
|
+
|
|
67
|
+
const resultAuth = await api.resources.public_items_ownership.query({ simplified: false }, { auth: { userId: 99 } })
|
|
68
|
+
validateJsonApiStructure(resultAuth, true)
|
|
69
|
+
assert.equal(resultAuth.data.length, 2)
|
|
70
|
+
|
|
71
|
+
const resultNoAuth = await api.resources.public_items_ownership.query({ simplified: false })
|
|
72
|
+
validateJsonApiStructure(resultNoAuth, true)
|
|
73
|
+
assert.equal(resultNoAuth.data.length, 2)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|