ac-sanitizer 6.0.7 → 6.1.0
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 +18 -0
- package/README.md +25 -1
- package/index.js +25 -8
- package/package.json +1 -1
- package/test/suites/default.js +5 -0
- package/test/tests/iamPermissions.js +212 -0
- package/test/tests/index.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
# [6.1.0](https://github.com/mmpro/ac-sanitizer/compare/v6.0.7..v6.1.0) (2026-04-28 11:20:30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Feature
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
* **App:** Added iamPermission to limit access to field | MP | [dce29ae609119fcaced0e966792157b2b3747631](https://github.com/mmpro/ac-sanitizer/commit/dce29ae609119fcaced0e966792157b2b3747631)
|
|
8
|
+
Added iamPermission to limit access to field
|
|
9
|
+
Related issues:
|
|
10
|
+
### Bug Fix
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
* **App:** Prefer IAM permissions | MP | [c8fa1271c179e93c175bdf43ce0aaad43ea8ec23](https://github.com/mmpro/ac-sanitizer/commit/c8fa1271c179e93c175bdf43ce0aaad43ea8ec23)
|
|
14
|
+
If IAM permissions are set and send, ignore adminLevel
|
|
15
|
+
Related issues:
|
|
16
|
+
* **App:** Minor adjustments after CoPilot review | MP | [2847150158b7ac325a648c5fcea5a8ba3d805462](https://github.com/mmpro/ac-sanitizer/commit/2847150158b7ac325a648c5fcea5a8ba3d805462)
|
|
17
|
+
iamPermissions must be an array. Added test for deeply nested objects
|
|
18
|
+
Related issues:
|
|
1
19
|
## [6.0.7](https://github.com/mmpro/ac-sanitizer/compare/v6.0.6..v6.0.7) (2026-04-24 19:06:20)
|
|
2
20
|
|
|
3
21
|
|
package/README.md
CHANGED
|
@@ -25,6 +25,29 @@ let fieldsToCheck = {
|
|
|
25
25
|
let test = sanitizer.checkAndSanitizeValues(fieldsToCheck)
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
```
|
|
29
|
+
// IAM PERMISSIONS EXAMPLE
|
|
30
|
+
// Fields can be protected by IAM permissions. The user must have at least one of the
|
|
31
|
+
// listed permissions (OR logic). If userPermissions is not provided, the check is skipped.
|
|
32
|
+
|
|
33
|
+
const sanitizer = require('ac-sanitizer')
|
|
34
|
+
|
|
35
|
+
let fieldsToCheck = {
|
|
36
|
+
params: {
|
|
37
|
+
title: 'My Media',
|
|
38
|
+
internalNote: 'confidential'
|
|
39
|
+
},
|
|
40
|
+
fields: [
|
|
41
|
+
{ field: 'title', type: 'string' },
|
|
42
|
+
{ field: 'internalNote', type: 'string', iamPermissions: ['media.admin', 'media.write'] } // array, at least one must match
|
|
43
|
+
],
|
|
44
|
+
userPermissions: ['media.read'], // user does not have media.admin or media.write
|
|
45
|
+
omitFields: true // omit instead of returning an error
|
|
46
|
+
}
|
|
47
|
+
let test = sanitizer.checkAndSanitizeValues(fieldsToCheck)
|
|
48
|
+
// result: { params: { title: 'My Media' } } -> internalNote is silently removed
|
|
49
|
+
```
|
|
50
|
+
|
|
28
51
|
```
|
|
29
52
|
// COMPLEX EXAMPLE WITH NESTED PROPERTIES AND CONDITIONAL REQUIREMENTS
|
|
30
53
|
|
|
@@ -55,7 +78,8 @@ type | string | Type of the field to sanitize, see below for available values
|
|
|
55
78
|
required | [boolean OR string] | Set to true if required or set a path[^1] to a param (if that param is set, this value is required)
|
|
56
79
|
enum | [array OR string] | Optional list of allowed values. You can a string placeholder for certain standard lists (see below)
|
|
57
80
|
adminLevel | [integer] | Optional adminLevel required for this field
|
|
58
|
-
|
|
81
|
+
iamPermissions | [array] | Optional list of IAM permissions required to access this field. At least one permission must match (OR logic). If `userPermissions` is not provided to `checkAndSanitizeValues`, the check is skipped.
|
|
82
|
+
omitFields | [boolean] | If adminLevel/iamPermissions is set and the user does not have sufficient access, the sanitizer will just omit the field (and not return an error) if omitFields is true
|
|
59
83
|
convert | [boolean OR string] | Some types can be automatically converted (e.g. base64 to string)
|
|
60
84
|
valueType | [string] | Use it to sanitize values of an array by defining the allowed type here
|
|
61
85
|
strict | [boolean] | For objects only - if true and payload contains a property not defined, an error will be returned.
|
package/index.js
CHANGED
|
@@ -49,7 +49,7 @@ const sanitizer = function() {
|
|
|
49
49
|
*
|
|
50
50
|
* @param params.params OBJECT params object (e.g. from body paylod) to sanitize (example { id: 1, user: 'tom' })
|
|
51
51
|
* @param params.fields ARRAY array of field definitions
|
|
52
|
-
* @param params.adminLevel INT level of the requesting user, will be compared against field's adminLevel
|
|
52
|
+
* @param params.adminLevel INT DEPRECATED level of the requesting user, will be compared against field's adminLevel
|
|
53
53
|
*
|
|
54
54
|
* @param return.error OBJECT returned error message (if there is an error)
|
|
55
55
|
* @param return.params OBJECT returned sanitized object (invalid keys are removed)
|
|
@@ -61,7 +61,8 @@ const sanitizer = function() {
|
|
|
61
61
|
let fields = params.fields
|
|
62
62
|
if (!_.isArray(fields) || !_.size(fields)) { return { error: { message: 'fields_required' } } }
|
|
63
63
|
|
|
64
|
-
const adminLevel = _.get(params, 'adminLevel')
|
|
64
|
+
const adminLevel = _.get(params, 'adminLevel') // DEPRECATED, use iamPermissions instead
|
|
65
|
+
const userPermissions = _.compact(_.get(params, 'userPermissions', []))
|
|
65
66
|
const omitFields = _.get(params, 'omitFields')
|
|
66
67
|
|
|
67
68
|
let error
|
|
@@ -201,7 +202,8 @@ const sanitizer = function() {
|
|
|
201
202
|
})
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
|
-
else if (_.get(field, 'adminLevel') && adminLevel < _.get(field, 'adminLevel')) {
|
|
205
|
+
else if (_.get(field, 'adminLevel') && !(_.get(field, 'iamPermissions') && _.size(userPermissions)) && adminLevel < _.get(field, 'adminLevel')) {
|
|
206
|
+
console.warn('SANITIZER - adminLevel is deprecated for field %s, please migrate to iamPermissions', fieldName)
|
|
205
207
|
if (omitFields) {
|
|
206
208
|
fields = _.filter(fields, item => {
|
|
207
209
|
if (item.field !== fieldName) { return item }
|
|
@@ -211,6 +213,17 @@ const sanitizer = function() {
|
|
|
211
213
|
error = { message: `${fieldName}_adminLevelNotSufficient`, additionalInfo: { adminLevel, required: _.get(field, 'adminLevel') } }
|
|
212
214
|
}
|
|
213
215
|
}
|
|
216
|
+
else if (_.get(field, 'iamPermissions') && _.size(userPermissions) && !_.size(_.intersection(userPermissions, field.iamPermissions))) {
|
|
217
|
+
if (!_.isArray(field.iamPermissions)) {
|
|
218
|
+
console.error('SANITIZER - iamPermissions must be an array, field %s', fieldName)
|
|
219
|
+
}
|
|
220
|
+
if (omitFields) {
|
|
221
|
+
fields = _.filter(fields, item => item.field !== fieldName)
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
error = { message: `${fieldName}_iamPermissionNotSufficient`, additionalInfo: { required: field.iamPermissions } }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
214
227
|
else if (_.indexOf(['number', 'integer', 'long', 'short', 'float'], field.type) > -1) {
|
|
215
228
|
if (typeof value !== 'number' || isNaN(value)) { error = { message: fieldName + '_' + getTypeMapping('integer', 'errorMessage') } }
|
|
216
229
|
else {
|
|
@@ -294,11 +307,14 @@ const sanitizer = function() {
|
|
|
294
307
|
|
|
295
308
|
const fieldsToCheck = {
|
|
296
309
|
params: {},
|
|
297
|
-
fields: [{
|
|
298
|
-
field: fieldName,
|
|
299
|
-
type: valueType,
|
|
310
|
+
fields: [{
|
|
311
|
+
field: fieldName,
|
|
312
|
+
type: valueType,
|
|
300
313
|
...fieldProps
|
|
301
|
-
}]
|
|
314
|
+
}],
|
|
315
|
+
adminLevel,
|
|
316
|
+
omitFields,
|
|
317
|
+
userPermissions
|
|
302
318
|
}
|
|
303
319
|
_.set(fieldsToCheck, `params.${fieldName}`, v)
|
|
304
320
|
const check = checkAndSanitizeValues(fieldsToCheck)
|
|
@@ -345,7 +361,8 @@ const sanitizer = function() {
|
|
|
345
361
|
fields: _.get(field, 'properties'),
|
|
346
362
|
prefix: fieldName,
|
|
347
363
|
adminLevel: _.get(field, 'adminLevel', adminLevel),
|
|
348
|
-
omitFields: _.get(field, 'omitFields', omitFields)
|
|
364
|
+
omitFields: _.get(field, 'omitFields', omitFields),
|
|
365
|
+
userPermissions
|
|
349
366
|
}
|
|
350
367
|
const check = checkAndSanitizeValues(fieldsToCheck)
|
|
351
368
|
if (_.get(check, 'error')) { error = _.get(check, 'error') }
|
package/package.json
CHANGED
package/test/suites/default.js
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const sanitizer = require('../../index')
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
test: () => {
|
|
5
|
+
it('Field with iamPermissions - user has matching permission -> field is included', (done) => {
|
|
6
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
7
|
+
params: { title: 'Hello' },
|
|
8
|
+
fields: [{ field: 'title', type: 'string', iamPermissions: ['media.read'] }],
|
|
9
|
+
userPermissions: ['media.read', 'media.write']
|
|
10
|
+
})
|
|
11
|
+
expect(r.error).to.be.undefined
|
|
12
|
+
expect(r.params.title).to.equal('Hello')
|
|
13
|
+
return done()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('Field with iamPermissions as array - user has one of the required permissions (OR logic) -> field is included', (done) => {
|
|
17
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
18
|
+
params: { title: 'Hello' },
|
|
19
|
+
fields: [{ field: 'title', type: 'string', iamPermissions: ['media.admin', 'media.read'] }],
|
|
20
|
+
userPermissions: ['media.read']
|
|
21
|
+
})
|
|
22
|
+
expect(r.error).to.be.undefined
|
|
23
|
+
expect(r.params.title).to.equal('Hello')
|
|
24
|
+
return done()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('Field with iamPermissions - user has none of the required permissions -> error', (done) => {
|
|
28
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
29
|
+
params: { title: 'Hello' },
|
|
30
|
+
fields: [{ field: 'title', type: 'string', iamPermissions: ['media.admin', 'media.write'] }],
|
|
31
|
+
userPermissions: ['media.read']
|
|
32
|
+
})
|
|
33
|
+
expect(r.error.message).to.equal('title_iamPermissionNotSufficient')
|
|
34
|
+
expect(r.error.additionalInfo.required).to.eql(['media.admin', 'media.write'])
|
|
35
|
+
return done()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('Field with iamPermissions - user has none of the required permissions and omitFields is true -> field is omitted silently', (done) => {
|
|
39
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
40
|
+
params: { title: 'Hello', id: 1 },
|
|
41
|
+
fields: [
|
|
42
|
+
{ field: 'title', type: 'string', iamPermissions: ['media.admin'] },
|
|
43
|
+
{ field: 'id', type: 'integer' }
|
|
44
|
+
],
|
|
45
|
+
userPermissions: ['media.read'],
|
|
46
|
+
omitFields: true
|
|
47
|
+
})
|
|
48
|
+
expect(r.error).to.be.undefined
|
|
49
|
+
expect(r.params.title).to.be.undefined
|
|
50
|
+
expect(r.params.id).to.equal(1)
|
|
51
|
+
return done()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('Field with both adminLevel and iamPermissions - userPermissions available -> iamPermissions wins, adminLevel is ignored', (done) => {
|
|
55
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
56
|
+
params: { title: 'Hello' },
|
|
57
|
+
fields: [{ field: 'title', type: 'string', adminLevel: 100, iamPermissions: ['media.read'] }],
|
|
58
|
+
adminLevel: 0, // would fail adminLevel check
|
|
59
|
+
userPermissions: ['media.read']
|
|
60
|
+
})
|
|
61
|
+
expect(r.error).to.be.undefined
|
|
62
|
+
expect(r.params.title).to.equal('Hello')
|
|
63
|
+
return done()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('Field with both adminLevel and iamPermissions - no userPermissions -> falls back to adminLevel', (done) => {
|
|
67
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
68
|
+
params: { title: 'Hello' },
|
|
69
|
+
fields: [{ field: 'title', type: 'string', adminLevel: 100, iamPermissions: ['media.read'] }],
|
|
70
|
+
adminLevel: 0 // no userPermissions -> adminLevel check applies
|
|
71
|
+
})
|
|
72
|
+
expect(r.error.message).to.equal('title_adminLevelNotSufficient')
|
|
73
|
+
return done()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('Field with iamPermissions - no userPermissions provided -> check is skipped, field is included', (done) => {
|
|
77
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
78
|
+
params: { title: 'Hello' },
|
|
79
|
+
fields: [{ field: 'title', type: 'string', iamPermissions: ['media.admin'] }]
|
|
80
|
+
})
|
|
81
|
+
expect(r.error).to.be.undefined
|
|
82
|
+
expect(r.params.title).to.equal('Hello')
|
|
83
|
+
return done()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('Nested object field with iamPermissions - user has permission -> nested field is included', (done) => {
|
|
87
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
88
|
+
params: { settings: { level: 5, secret: 'topsecret' } },
|
|
89
|
+
fields: [{
|
|
90
|
+
field: 'settings',
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: [
|
|
93
|
+
{ field: 'level', type: 'integer' },
|
|
94
|
+
{ field: 'secret', type: 'string', iamPermissions: ['settings.admin'] }
|
|
95
|
+
]
|
|
96
|
+
}],
|
|
97
|
+
userPermissions: ['settings.admin']
|
|
98
|
+
})
|
|
99
|
+
expect(r.error).to.be.undefined
|
|
100
|
+
expect(r.params.settings.secret).to.equal('topsecret')
|
|
101
|
+
return done()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('Nested object field with iamPermissions - user lacks permission and omitFields true -> nested field is omitted', (done) => {
|
|
105
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
106
|
+
params: { settings: { level: 5, secret: 'topsecret' } },
|
|
107
|
+
fields: [{
|
|
108
|
+
field: 'settings',
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: [
|
|
111
|
+
{ field: 'level', type: 'integer' },
|
|
112
|
+
{ field: 'secret', type: 'string', iamPermissions: ['settings.admin'] }
|
|
113
|
+
]
|
|
114
|
+
}],
|
|
115
|
+
userPermissions: ['media.read'],
|
|
116
|
+
omitFields: true
|
|
117
|
+
})
|
|
118
|
+
expect(r.error).to.be.undefined
|
|
119
|
+
expect(r.params.settings.level).to.equal(5)
|
|
120
|
+
expect(r.params.settings.secret).to.be.undefined
|
|
121
|
+
return done()
|
|
122
|
+
})
|
|
123
|
+
it('Deeply nested object field with iamPermissions - user has permission -> field is included', (done) => {
|
|
124
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
125
|
+
params: { settings: { nested: { level: 5, secret: 'topsecret' } } },
|
|
126
|
+
fields: [{
|
|
127
|
+
field: 'settings',
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: [{
|
|
130
|
+
field: 'nested',
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: [
|
|
133
|
+
{ field: 'level', type: 'integer' },
|
|
134
|
+
{ field: 'secret', type: 'string', iamPermissions: ['settings.admin'] }
|
|
135
|
+
]
|
|
136
|
+
}]
|
|
137
|
+
}],
|
|
138
|
+
userPermissions: ['settings.admin']
|
|
139
|
+
})
|
|
140
|
+
expect(r.error).to.be.undefined
|
|
141
|
+
expect(r.params.settings.nested.secret).to.equal('topsecret')
|
|
142
|
+
return done()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('Deeply nested object field with iamPermissions - user lacks permission and omitFields true -> field is omitted', (done) => {
|
|
146
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
147
|
+
params: { settings: { nested: { level: 5, secret: 'topsecret' } } },
|
|
148
|
+
fields: [{
|
|
149
|
+
field: 'settings',
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: [{
|
|
152
|
+
field: 'nested',
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: [
|
|
155
|
+
{ field: 'level', type: 'integer' },
|
|
156
|
+
{ field: 'secret', type: 'string', iamPermissions: ['settings.admin'] }
|
|
157
|
+
]
|
|
158
|
+
}]
|
|
159
|
+
}],
|
|
160
|
+
userPermissions: ['media.read'],
|
|
161
|
+
omitFields: true
|
|
162
|
+
})
|
|
163
|
+
expect(r.error).to.be.undefined
|
|
164
|
+
expect(r.params.settings.nested.level).to.equal(5)
|
|
165
|
+
expect(r.params.settings.nested.secret).to.be.undefined
|
|
166
|
+
return done()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('Array of objects with iamPermissions - user has permission -> field is included in all items', (done) => {
|
|
170
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
171
|
+
params: { items: [{ name: 'foo', secret: 'a' }, { name: 'bar', secret: 'b' }] },
|
|
172
|
+
fields: [{
|
|
173
|
+
field: 'items',
|
|
174
|
+
type: 'array',
|
|
175
|
+
valueType: 'object',
|
|
176
|
+
properties: [
|
|
177
|
+
{ field: 'name', type: 'string' },
|
|
178
|
+
{ field: 'secret', type: 'string', iamPermissions: ['media.admin'] }
|
|
179
|
+
]
|
|
180
|
+
}],
|
|
181
|
+
userPermissions: ['media.admin']
|
|
182
|
+
})
|
|
183
|
+
expect(r.error).to.be.undefined
|
|
184
|
+
expect(r.params.items[0].secret).to.equal('a')
|
|
185
|
+
expect(r.params.items[1].secret).to.equal('b')
|
|
186
|
+
return done()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('Array of objects with iamPermissions - user lacks permission and omitFields true -> field is omitted from all items', (done) => {
|
|
190
|
+
const r = sanitizer.checkAndSanitizeValues({
|
|
191
|
+
params: { items: [{ name: 'foo', secret: 'a' }, { name: 'bar', secret: 'b' }] },
|
|
192
|
+
fields: [{
|
|
193
|
+
field: 'items',
|
|
194
|
+
type: 'array',
|
|
195
|
+
valueType: 'object',
|
|
196
|
+
properties: [
|
|
197
|
+
{ field: 'name', type: 'string' },
|
|
198
|
+
{ field: 'secret', type: 'string', iamPermissions: ['media.admin'] }
|
|
199
|
+
]
|
|
200
|
+
}],
|
|
201
|
+
userPermissions: ['media.read'],
|
|
202
|
+
omitFields: true
|
|
203
|
+
})
|
|
204
|
+
expect(r.error).to.be.undefined
|
|
205
|
+
expect(r.params.items[0].name).to.equal('foo')
|
|
206
|
+
expect(r.params.items[0].secret).to.be.undefined
|
|
207
|
+
expect(r.params.items[1].name).to.equal('bar')
|
|
208
|
+
expect(r.params.items[1].secret).to.be.undefined
|
|
209
|
+
return done()
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|