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 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
- omitFields | [boolean] | If adminLevel is set and you do not have the proper adminLevel the sanitizer will just omit the field (and not return an error) if omitFields is true
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
@@ -4,7 +4,7 @@
4
4
  "author": "Mark Poepping (https://www.admiralcloud.com)",
5
5
  "license": "MIT",
6
6
  "repository": "admiralcloud/ac-sanitizer",
7
- "version": "6.0.7",
7
+ "version": "6.1.0",
8
8
  "homepage": "https://www.admiralcloud.com",
9
9
  "dependencies": {
10
10
  "ac-countrylist": "^1.0.19",
@@ -12,6 +12,11 @@ module.exports = {
12
12
  tests.error.test()
13
13
  })
14
14
 
15
+ describe('IAM PERMISSIONS', function() {
16
+ this.timeout(timeOut)
17
+ tests.iamPermissions.test()
18
+ })
19
+
15
20
  describe('ANY', function() {
16
21
  this.timeout(timeOut)
17
22
  tests.any.test()
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  module.exports = {
2
2
  any: require('./any'),
3
+ iamPermissions: require('./iamPermissions'),
3
4
  array: require('./array'),
4
5
  base64: require('./base64'),
5
6
  bool: require('./bool'),