@adobe/helix-config-storage 1.15.4 → 2.0.1

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,28 @@
1
+ ## [2.0.1](https://github.com/adobe/helix-config-storage/compare/v2.0.0...v2.0.1) (2025-03-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add review environment, popover and badges ([8a85edd](https://github.com/adobe/helix-config-storage/commit/8a85eddd7af9162b533eedefa479b744a792e072))
7
+ * dependentRequired ([88d54ce](https://github.com/adobe/helix-config-storage/commit/88d54ce7c52c2a4ff3c49fd3b3870066f7360ec1))
8
+
9
+ # [2.0.0](https://github.com/adobe/helix-config-storage/compare/v1.15.4...v2.0.0) (2025-03-03)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * tetss ([2d6c782](https://github.com/adobe/helix-config-storage/commit/2d6c782e7685416fe329608254a763b48b2c2102))
15
+
16
+
17
+ ### Features
18
+
19
+ * add access control around features, limits and modifying admin roles ([c3d4a20](https://github.com/adobe/helix-config-storage/commit/c3d4a2005fdc9f580e0149198bdc02ac0af7f17a))
20
+
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ * modifying access control needs 'isAdmin' flag to be set
25
+
1
26
  ## [1.15.4](https://github.com/adobe/helix-config-storage/compare/v1.15.3...v1.15.4) (2025-02-25)
2
27
 
3
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-config-storage",
3
- "version": "1.15.4",
3
+ "version": "2.0.1",
4
4
  "description": "Helix Config Storage",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -23,6 +23,7 @@ import {
23
23
  } from './utils.js';
24
24
  import { validate as validateSchema } from './config-validator.js';
25
25
  import { getMergedConfig } from './config-merge.js';
26
+ import { ValidationError } from './ValidationError.js';
26
27
 
27
28
  const FRAGMENTS_COMMON = {
28
29
  content: 'object',
@@ -300,6 +301,28 @@ export class ConfigStore {
300
301
  ? `/orgs/${org}/${name || 'config'}.json`
301
302
  : `/orgs/${org}/${type}/${name}.json`;
302
303
  this.now = new Date();
304
+ this.isAdmin = false;
305
+ this.isOps = false;
306
+ }
307
+
308
+ /**
309
+ * Controls if setting the admin role is allowed.
310
+ * @param {boolean} v
311
+ * @returns {ConfigStore} this
312
+ */
313
+ withAllowAdmin(v) {
314
+ this.isAdmin = v;
315
+ return this;
316
+ }
317
+
318
+ /**
319
+ * Controls if setting ops related properties, like features and limits are allowed.
320
+ * @param {boolean} v
321
+ * @returns {ConfigStore} this
322
+ */
323
+ withAllowOps(v) {
324
+ this.isOps = v;
325
+ return this;
303
326
  }
304
327
 
305
328
  /**
@@ -370,6 +393,62 @@ export class ConfigStore {
370
393
  return validateSchema(data, this.type);
371
394
  }
372
395
 
396
+ /**
397
+ * Checks if the permissions allow to update the config.
398
+ * @param {AdminConfig} ctx
399
+ * @param {object} oldConfig
400
+ * @param {object} newConfig
401
+ * @returns {Promise<void>}
402
+ */
403
+ async validatePermissions(ctx, oldConfig, newConfig) {
404
+ // prevent setting the ops role
405
+ if (newConfig.access?.admin?.role?.ops) {
406
+ throw new ValidationError('invalid role: ops');
407
+ }
408
+ // ops can do everything
409
+ if (this.isOps) {
410
+ return;
411
+ }
412
+ // required admin to set admin role
413
+ if (!this.isAdmin) {
414
+ if (this.type === 'org') {
415
+ const getAdmins = (users) => users
416
+ .filter((user) => user.roles.includes('admin'))
417
+ .map((user) => user.email)
418
+ .sort((a, b) => a.localeCompare(b));
419
+ // get the users with the admin role
420
+ const oldAdmins = getAdmins(oldConfig?.users ?? []);
421
+ const newAdmins = getAdmins(newConfig?.users ?? []);
422
+ if (oldAdmins.join() !== newAdmins.join()) {
423
+ throw new StatusCodeError(403, 'not allowed to modify admin role');
424
+ }
425
+ } else {
426
+ // check for changed admin roles
427
+ const oldAdmins = (oldConfig?.access?.admin?.role?.admin || [])
428
+ .sort((a, b) => a.localeCompare(b));
429
+ const newAdmins = (newConfig?.access?.admin?.role?.admin || [])
430
+ .sort((a, b) => a.localeCompare(b));
431
+ if (oldAdmins.join() !== newAdmins.join()) {
432
+ throw new StatusCodeError(403, 'not allowed to modify admin role');
433
+ }
434
+ }
435
+ }
436
+
437
+ // check for changed features or limits
438
+ if (this.type !== 'org') {
439
+ const oldFeatures = oldConfig?.features ?? {};
440
+ const newFeatures = newConfig?.features ?? {};
441
+ if (!isDeepStrictEqual(oldFeatures, newFeatures)) {
442
+ throw new StatusCodeError(403, 'not allowed to modify features');
443
+ }
444
+ const oldLimits = oldConfig?.limits ?? {};
445
+ const newLimits = newConfig?.limits ?? {};
446
+ if (!isDeepStrictEqual(oldLimits, newLimits)) {
447
+ throw new StatusCodeError(403, 'not allowed to modify limits');
448
+ }
449
+ }
450
+ }
451
+
373
452
  async create(ctx, data, relPath = '') {
374
453
  if (relPath) {
375
454
  throw new StatusCodeError(409, 'create not supported on substructures.');
@@ -396,6 +475,7 @@ export class ConfigStore {
396
475
  this.#updateTimeStamps(data);
397
476
  const config = await this.getAggregatedConfig(ctx, data);
398
477
  await this.validate(ctx, config);
478
+ await this.validatePermissions(ctx, {}, config);
399
479
  await storage.put(this.key, JSON.stringify(data), 'application/json');
400
480
  await this.purge(ctx, null, config);
401
481
  }
@@ -576,6 +656,7 @@ export class ConfigStore {
576
656
  }
577
657
 
578
658
  await this.validate(ctx, newConfig);
659
+ await this.validatePermissions(ctx, oldConfig, newConfig);
579
660
  await storage.put(this.key, JSON.stringify(config), 'application/json');
580
661
  await this.purge(ctx, oldConfig, newConfig);
581
662
  return ret ?? redact(data, frag);
@@ -26,7 +26,7 @@
26
26
  "type": "array",
27
27
  "items": {
28
28
  "type": "string",
29
- "enum": ["any", "dev", "admin", "edit", "preview", "live", "prod"]
29
+ "enum": ["any", "dev", "admin", "edit", "preview", "live", "prod", "review"]
30
30
  },
31
31
  "description": "The environments to display this plugin in",
32
32
  "default": "any"
@@ -69,6 +69,14 @@
69
69
  "type": "string",
70
70
  "description": "he dimensions and position of a palette box"
71
71
  },
72
+ "isPopover": {
73
+ "type": "boolean",
74
+ "description": "Opens the URL in a popover instead of a new tab"
75
+ },
76
+ "popoverRect": {
77
+ "type": "string",
78
+ "description": "The dimensions of a popover, delimited by a semicolon (width, height)"
79
+ },
72
80
  "titleI18n": {
73
81
  "type": "object",
74
82
  "description": "The button text in other supported languages",
@@ -88,6 +96,30 @@
88
96
  "passReferrer": {
89
97
  "type": "boolean",
90
98
  "description": "Append the referrer URL as a query param on new URL button click"
99
+ },
100
+ "isBadge": {
101
+ "type": "boolean",
102
+ "description": "Opens the URL in a palette instead of a new tab"
103
+ },
104
+ "badgeVariant": {
105
+ "type": "string",
106
+ "description": "The variant of the badge following the Adobe Spectrum badge variants",
107
+ "enum": [
108
+ "gray",
109
+ "red",
110
+ "orange",
111
+ "yellow",
112
+ "chartreuse",
113
+ "celery",
114
+ "green",
115
+ "seafoam",
116
+ "cyan",
117
+ "blue",
118
+ "indigo",
119
+ "purple",
120
+ "fuchsia",
121
+ "magenta"
122
+ ]
91
123
  }
92
124
  },
93
125
  "required": [
@@ -95,7 +127,10 @@
95
127
  ],
96
128
  "dependentRequired": {
97
129
  "isPalette": ["url"],
98
- "paletteRect": ["isPalette"]
130
+ "paletteRect": ["isPalette"],
131
+ "isPopover": ["url"],
132
+ "popoverRect": ["isPopover"],
133
+ "badgeVariant": ["isBadge"]
99
134
  }
100
135
  }
101
136
  },