json-rest-api 1.0.10 → 1.0.11
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/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +5 -9
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { RestApiResourceError } from '../../lib/rest-api-errors.js'
|
|
2
|
+
|
|
3
|
+
export const AccessPlugin = {
|
|
4
|
+
name: 'rest-api-access',
|
|
5
|
+
dependencies: ['rest-api'],
|
|
6
|
+
|
|
7
|
+
async install({ addHook, helpers, log, pluginOptions = {}, scopes }) {
|
|
8
|
+
const ownershipOptions = pluginOptions.ownership || pluginOptions.autoOwnership || {}
|
|
9
|
+
|
|
10
|
+
const config = {
|
|
11
|
+
ownership: {
|
|
12
|
+
enabled: ownershipOptions.enabled !== false,
|
|
13
|
+
field: ownershipOptions.field || 'user_id',
|
|
14
|
+
userResource: ownershipOptions.userResource || 'users',
|
|
15
|
+
excludeResources: ownershipOptions.excludeResources || [],
|
|
16
|
+
filterByOwner: ownershipOptions.filterByOwner !== false,
|
|
17
|
+
requireOwnership: ownershipOptions.requireOwnership || false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const state = {
|
|
22
|
+
authCheckers: new Map()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ownershipField = () => config.ownership.field
|
|
26
|
+
|
|
27
|
+
const evaluateOwnership = ({ record, field, schemaInfo, userId }) => {
|
|
28
|
+
if (!record) return 'unknown'
|
|
29
|
+
|
|
30
|
+
debugger
|
|
31
|
+
const idProperty = schemaInfo?.idProperty || 'id'
|
|
32
|
+
const schemaStructure = schemaInfo?.schemaStructure || {}
|
|
33
|
+
const schemaRelationships = schemaInfo?.schemaRelationships || {}
|
|
34
|
+
|
|
35
|
+
const matchesUser = (value) => value !== undefined && value !== null && String(value) === userId
|
|
36
|
+
|
|
37
|
+
if (field === idProperty) {
|
|
38
|
+
if (record.id === undefined || record.id === null) {
|
|
39
|
+
return 'unknown'
|
|
40
|
+
}
|
|
41
|
+
return matchesUser(record.id) ? 'match' : 'mismatch'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fieldSchema = schemaStructure[field]
|
|
45
|
+
|
|
46
|
+
if (record.type && record.attributes && fieldSchema && !fieldSchema.belongsTo) {
|
|
47
|
+
const attributeValue = record.attributes[field]
|
|
48
|
+
if (attributeValue !== undefined && attributeValue !== null) {
|
|
49
|
+
return matchesUser(attributeValue) ? 'match' : 'mismatch'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let relationshipName
|
|
54
|
+
if (fieldSchema?.belongsTo) {
|
|
55
|
+
relationshipName = fieldSchema.as || field
|
|
56
|
+
} else if (schemaRelationships[field]) {
|
|
57
|
+
relationshipName = field
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!relationshipName) {
|
|
61
|
+
return 'unknown'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const relationship = record.relationships?.[relationshipName]
|
|
65
|
+
const relData = relationship?.data
|
|
66
|
+
if (!relData) {
|
|
67
|
+
return 'unknown'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(relData)) {
|
|
71
|
+
return relData.some((item) => matchesUser(item?.id)) ? 'match' : 'mismatch'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (relData?.id === undefined || relData?.id === null) {
|
|
75
|
+
return 'unknown'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return matchesUser(relData.id) ? 'match' : 'mismatch'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resolveStorageAdapter = (scopeName, hookContext = {}) => {
|
|
82
|
+
if (!scopeName) return null
|
|
83
|
+
|
|
84
|
+
if (hookContext.storageAdapter) return hookContext.storageAdapter
|
|
85
|
+
if (hookContext.knexQuery?.storageAdapter) return hookContext.knexQuery.storageAdapter
|
|
86
|
+
|
|
87
|
+
if (helpers.getStorageAdapter) {
|
|
88
|
+
return helpers.getStorageAdapter(scopeName)
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const translateColumnReference = ({ field, tableName, scopeName, hookContext }) => {
|
|
94
|
+
const storageAdapter = resolveStorageAdapter(scopeName, hookContext)
|
|
95
|
+
const translated = storageAdapter?.translateColumn?.(field) ?? field
|
|
96
|
+
if (tableName) {
|
|
97
|
+
return `${tableName}.${translated}`
|
|
98
|
+
}
|
|
99
|
+
return translated
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const translateFilterValue = ({ field, value, scopeName, hookContext }) => {
|
|
103
|
+
const storageAdapter = resolveStorageAdapter(scopeName, hookContext)
|
|
104
|
+
if (!storageAdapter?.translateFilterValue) return value
|
|
105
|
+
return storageAdapter.translateFilterValue(field, value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function registerBuiltinCheckers() {
|
|
109
|
+
state.authCheckers.set('public', () => true)
|
|
110
|
+
|
|
111
|
+
state.authCheckers.set('authenticated', (context) => {
|
|
112
|
+
if (context.auth?.system === true) return true
|
|
113
|
+
return !!(context.auth?.userId || context.auth?.providerId)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
state.authCheckers.set('owns', (context, { existingRecord, scopeVars }) => {
|
|
117
|
+
if (context.auth?.system === true) return true
|
|
118
|
+
if (!context.auth?.userId) return false
|
|
119
|
+
if (context.method === 'post') return true
|
|
120
|
+
|
|
121
|
+
// Minimal record is always prepared upstream (dataGetMinimal for reads, request payload snapshot for POST).
|
|
122
|
+
const record = existingRecord
|
|
123
|
+
if (!record) return true
|
|
124
|
+
|
|
125
|
+
const userId = String(context.auth.userId)
|
|
126
|
+
const field = scopeVars?.ownershipField || ownershipField()
|
|
127
|
+
const schemaInfo = scopeVars?.schemaInfo || {}
|
|
128
|
+
|
|
129
|
+
const ownershipStatus = evaluateOwnership({
|
|
130
|
+
record,
|
|
131
|
+
field,
|
|
132
|
+
schemaInfo,
|
|
133
|
+
userId
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return ownershipStatus === 'match'
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
registerBuiltinCheckers()
|
|
141
|
+
|
|
142
|
+
addHook('scope:added', 'rest-auth-process-rules', {}, ({ context, scopes }) => {
|
|
143
|
+
const { scopeName } = context
|
|
144
|
+
const scope = scopes[scopeName]
|
|
145
|
+
|
|
146
|
+
const auth = context.scopeOptions?.auth
|
|
147
|
+
if (!auth) return
|
|
148
|
+
|
|
149
|
+
debugger
|
|
150
|
+
scope.vars.authRules = auth
|
|
151
|
+
scope.vars.ownershipField = context.scopeOptions?.ownershipField
|
|
152
|
+
|
|
153
|
+
log?.info?.(`Auth rules registered for ${scopeName}`, {
|
|
154
|
+
query: auth.query,
|
|
155
|
+
get: auth.get,
|
|
156
|
+
post: auth.post,
|
|
157
|
+
patch: auth.patch,
|
|
158
|
+
delete: auth.delete
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
addHook('schema:enrich', 'rest-auth-auto-ownership-field', {}, ({ context, scopeOptions }) => {
|
|
163
|
+
if (!config.ownership.enabled) return
|
|
164
|
+
const { fields, scopeName } = context
|
|
165
|
+
if (config.ownership.excludeResources.includes(scopeName)) return
|
|
166
|
+
if (scopeOptions?.ownership === false) return
|
|
167
|
+
|
|
168
|
+
const field = ownershipField()
|
|
169
|
+
const relationshipName = fields[field]?.as || fields[field]?.belongsTo || field
|
|
170
|
+
|
|
171
|
+
if (fields[field]) {
|
|
172
|
+
if (!fields[field].belongsTo) {
|
|
173
|
+
fields[field].belongsTo = config.ownership.userResource
|
|
174
|
+
}
|
|
175
|
+
if (!fields[field].as) {
|
|
176
|
+
fields[field].as = relationshipName
|
|
177
|
+
}
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fields[field] = {
|
|
182
|
+
type: 'number',
|
|
183
|
+
belongsTo: config.ownership.userResource,
|
|
184
|
+
...(relationshipName ? { as: relationshipName } : {}),
|
|
185
|
+
nullable: true,
|
|
186
|
+
indexed: true,
|
|
187
|
+
description: 'Automatically managed ownership field'
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
function setOwnerOnInput({ context, scopeName, scopes, scopeOptions }) {
|
|
192
|
+
if (!config.ownership.enabled) return
|
|
193
|
+
if (config.ownership.excludeResources.includes(scopeName)) return
|
|
194
|
+
if (!context.auth?.userId) {
|
|
195
|
+
if (config.ownership.requireOwnership) {
|
|
196
|
+
throw new Error(`Cannot operate on ${scopeName} without authentication`)
|
|
197
|
+
}
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (scopeOptions?.ownership === false) return
|
|
202
|
+
|
|
203
|
+
const field = ownershipField()
|
|
204
|
+
const scope = scopes[scopeName]
|
|
205
|
+
const schemaInfo = scope?.vars?.schemaInfo
|
|
206
|
+
const fieldSchema = schemaInfo?.schemaStructure?.[field]
|
|
207
|
+
const hasField = !!fieldSchema
|
|
208
|
+
const shouldSet = scopeOptions?.ownership === true || (scopeOptions?.ownership === undefined && hasField)
|
|
209
|
+
if (!shouldSet || !hasField) return
|
|
210
|
+
|
|
211
|
+
if (context.auth.roles?.includes?.('admin')) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!context.inputRecord?.data) return
|
|
216
|
+
|
|
217
|
+
const relationships = context.inputRecord.data.relationships = context.inputRecord.data.relationships || {}
|
|
218
|
+
const attributes = context.inputRecord.data.attributes = context.inputRecord.data.attributes || {}
|
|
219
|
+
|
|
220
|
+
const relationshipName = fieldSchema.as || fieldSchema.belongsTo || field
|
|
221
|
+
|
|
222
|
+
if (fieldSchema.belongsTo) {
|
|
223
|
+
relationships[relationshipName] = {
|
|
224
|
+
data: {
|
|
225
|
+
type: fieldSchema.belongsTo || config.ownership.userResource || 'users',
|
|
226
|
+
id: String(context.auth.userId)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
attributes[field] = context.auth.userId
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
addHook('beforeProcessingPost', 'rest-auth-auto-set-owner', {}, ({ context, scopeName, scopes, scopeOptions }) => {
|
|
235
|
+
setOwnerOnInput({ context, scopeName, scopes, scopeOptions })
|
|
236
|
+
})
|
|
237
|
+
addHook('beforeProcessingPatch', 'rest-auth-auto-set-owner', {}, ({ context, scopeName, scopes, scopeOptions }) => {
|
|
238
|
+
setOwnerOnInput({ context, scopeName, scopes, scopeOptions })
|
|
239
|
+
})
|
|
240
|
+
addHook('beforeProcessingPut', 'rest-auth-auto-set-owner', {}, ({ context, scopeName, scopes, scopeOptions }) => {
|
|
241
|
+
setOwnerOnInput({ context, scopeName, scopes, scopeOptions })
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
addHook('knexQueryFiltering', 'rest-auth-filter-by-owner', { sequence: -40 }, ({ context, scopes, scopeOptions }) => {
|
|
245
|
+
if (!config.ownership.enabled || !config.ownership.filterByOwner) return
|
|
246
|
+
|
|
247
|
+
const { query, tableName, scopeName } = context.knexQuery || {}
|
|
248
|
+
|
|
249
|
+
if (!query || !tableName) {
|
|
250
|
+
throw new Error('AccessPlugin: knexQuery must provide query and tableName for ownership filtering')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (config.ownership.excludeResources.includes(scopeName)) return
|
|
254
|
+
if (!context.auth?.userId) {
|
|
255
|
+
if (config.ownership.requireOwnership) {
|
|
256
|
+
throw new Error(`Cannot query ${scopeName} without authentication`)
|
|
257
|
+
}
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (context.auth.roles?.includes?.('admin')) return
|
|
262
|
+
if (scopeOptions?.ownership === false) return
|
|
263
|
+
|
|
264
|
+
const field = ownershipField()
|
|
265
|
+
const scope = scopes[scopeName]
|
|
266
|
+
const schemaInfo = scope?.vars?.schemaInfo
|
|
267
|
+
const hasField = !!schemaInfo?.schemaStructure?.[field]
|
|
268
|
+
const shouldFilter = scopeOptions?.ownership === true || (scopeOptions?.ownership === undefined && hasField)
|
|
269
|
+
if (!shouldFilter || !hasField) return
|
|
270
|
+
|
|
271
|
+
const storageAdapter = resolveStorageAdapter(scopeName, context)
|
|
272
|
+
if (storageAdapter && !context.storageAdapter) {
|
|
273
|
+
context.storageAdapter = storageAdapter
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const columnRef = translateColumnReference({ field, tableName, scopeName, hookContext: context })
|
|
277
|
+
const ownerValue = translateFilterValue({ field, value: context.auth.userId, scopeName, hookContext: context })
|
|
278
|
+
|
|
279
|
+
if (ownerValue === null) {
|
|
280
|
+
query.whereNull(columnRef)
|
|
281
|
+
} else {
|
|
282
|
+
query.where(columnRef, ownerValue)
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
addHook('checkPermissions', 'rest-auth-enforce', { sequence: -100 }, async ({ context, scope, scopeName }) => {
|
|
287
|
+
const operation = context.method
|
|
288
|
+
debugger
|
|
289
|
+
const scopeVars = scope?.vars
|
|
290
|
+
const authRules = scopeVars?.authRules
|
|
291
|
+
const minimalRecord = context.originalContext?.minimalRecord
|
|
292
|
+
|
|
293
|
+
if (!authRules) return
|
|
294
|
+
const rules = authRules[operation]
|
|
295
|
+
if (!rules) {
|
|
296
|
+
throw new Error(`Operation '${operation}' not allowed on resource '${scopeName}'`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let passed = false
|
|
300
|
+
const failures = []
|
|
301
|
+
|
|
302
|
+
for (const rule of rules) {
|
|
303
|
+
// if (rule === 'owns') debugger
|
|
304
|
+
try {
|
|
305
|
+
const [checkerName, ...paramParts] = rule.split(':')
|
|
306
|
+
const checker = state.authCheckers.get(checkerName)
|
|
307
|
+
const param = paramParts.join(':') || undefined
|
|
308
|
+
if (!checker) {
|
|
309
|
+
throw new Error(`Unknown auth rule: ${rule}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const result = await checker(context.originalContext || context, {
|
|
313
|
+
existingRecord: minimalRecord,
|
|
314
|
+
scopeVars,
|
|
315
|
+
param
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
if (result) {
|
|
319
|
+
passed = true
|
|
320
|
+
break
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
failures.push(rule)
|
|
324
|
+
} catch (error) {
|
|
325
|
+
failures.push(`${rule} (error: ${error.message})`)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!passed) {
|
|
330
|
+
const err = new Error(
|
|
331
|
+
`Access denied. Required one of: ${rules.join(', ')}. Failed checks: ${failures.join(', ')}`
|
|
332
|
+
)
|
|
333
|
+
err.statusCode = 403
|
|
334
|
+
throw err
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
context.authGranted = true
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
addHook('checkPermissions', 'rest-auth-check-get-ownership', { sequence: -80 }, ({ context, scope, scopeName, scopeOptions }) => {
|
|
341
|
+
if (!config.ownership.enabled || !config.ownership.filterByOwner) return
|
|
342
|
+
if (config.ownership.excludeResources.includes(scopeName)) return
|
|
343
|
+
|
|
344
|
+
if (!['get', 'put', 'patch', 'delete'].includes(context.method)) return
|
|
345
|
+
|
|
346
|
+
const auth = context.originalContext?.auth || context.auth
|
|
347
|
+
if (!auth?.userId) return
|
|
348
|
+
if (auth.roles?.includes?.('admin')) return
|
|
349
|
+
if (scopeOptions?.ownership === false) return
|
|
350
|
+
|
|
351
|
+
const field = ownershipField()
|
|
352
|
+
const schemaInfo = scope?.vars?.schemaInfo || {}
|
|
353
|
+
const hasField = !!schemaInfo?.schemaStructure?.[field]
|
|
354
|
+
const shouldCheck = scopeOptions?.ownership === true || (scopeOptions?.ownership === undefined && hasField)
|
|
355
|
+
if (!shouldCheck) return
|
|
356
|
+
|
|
357
|
+
const record = context.originalContext?.minimalRecord
|
|
358
|
+
if (!record) return
|
|
359
|
+
|
|
360
|
+
const idProperty = schemaInfo.idProperty || 'id'
|
|
361
|
+
const userId = String(auth.userId)
|
|
362
|
+
|
|
363
|
+
// Ownership by primary key (e.g., users modifying themselves)
|
|
364
|
+
if (field === idProperty) {
|
|
365
|
+
if (record.id !== undefined && record.id !== null && String(record.id) !== userId) {
|
|
366
|
+
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
367
|
+
}
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const ownershipStatus = evaluateOwnership({
|
|
372
|
+
record,
|
|
373
|
+
field,
|
|
374
|
+
schemaInfo,
|
|
375
|
+
userId
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
if (ownershipStatus === 'mismatch') {
|
|
379
|
+
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
if (!helpers.auth) {
|
|
384
|
+
helpers.auth = {}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
helpers.auth.requireAuth = function requireAuth(context) {
|
|
388
|
+
if (!context.auth?.userId) {
|
|
389
|
+
const error = new Error('Authentication required')
|
|
390
|
+
error.statusCode = 401
|
|
391
|
+
throw error
|
|
392
|
+
}
|
|
393
|
+
return context.auth
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
helpers.auth.requireOwnership = function requireOwnership(context, resourceOrUserId) {
|
|
397
|
+
const auth = helpers.auth.requireAuth(context)
|
|
398
|
+
const field = ownershipField()
|
|
399
|
+
|
|
400
|
+
let ownerId
|
|
401
|
+
|
|
402
|
+
if (typeof resourceOrUserId === 'object' && resourceOrUserId !== null) {
|
|
403
|
+
ownerId = resourceOrUserId[field]
|
|
404
|
+
if (!ownerId) {
|
|
405
|
+
throw new Error(`Resource does not have ownership field '${field}'`)
|
|
406
|
+
}
|
|
407
|
+
} else if (resourceOrUserId !== undefined) {
|
|
408
|
+
ownerId = resourceOrUserId
|
|
409
|
+
} else if (context.existingRecord) {
|
|
410
|
+
ownerId = context.existingRecord[field]
|
|
411
|
+
if (!ownerId) {
|
|
412
|
+
throw new Error(`Resource does not have ownership field '${field}'`)
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
throw new Error('No resource or user ID provided for ownership check')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (String(auth.userId) !== String(ownerId)) {
|
|
419
|
+
const error = new Error('Access denied: you do not own this resource')
|
|
420
|
+
error.statusCode = 403
|
|
421
|
+
throw error
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return auth
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
helpers.auth.registerChecker = function registerChecker(name, checkerFn) {
|
|
428
|
+
state.authCheckers.set(name, checkerFn)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
helpers.auth.checkPermission = async function checkPermission(context, rules, options = {}) {
|
|
432
|
+
if (!rules || rules.length === 0) return true
|
|
433
|
+
|
|
434
|
+
const { existingRecord, scopeVars } = options
|
|
435
|
+
|
|
436
|
+
for (const rule of rules) {
|
|
437
|
+
const [checkerName, ...paramParts] = rule.split(':')
|
|
438
|
+
const checker = state.authCheckers.get(checkerName)
|
|
439
|
+
if (!checker) continue
|
|
440
|
+
|
|
441
|
+
const param = paramParts.join(':') || undefined
|
|
442
|
+
if (await checker(context, { existingRecord, scopeVars, param })) {
|
|
443
|
+
return true
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return false
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
helpers.auth.cleanup = function cleanup() {
|
|
451
|
+
state.authCheckers.clear()
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export default AccessPlugin
|