@adobe/helix-config 3.1.2 → 3.2.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 +7 -0
- package/package.json +2 -2
- package/src/config-view.js +69 -2
- package/src/schemas/org.schema.json +3 -0
- package/src/schemas/users.schema.cjs +12 -0
- package/src/schemas/users.schema.json +52 -0
- package/src/storage/config-store.js +41 -6
- package/src/storage/config-validator.js +2 -0
- package/src/storage/utils.js +54 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [3.2.0](https://github.com/adobe/helix-config/compare/v3.1.2...v3.2.0) (2024-05-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add support for org users storage and view ([0b65a1d](https://github.com/adobe/helix-config/commit/0b65a1df91c4271a6c2ab99fb669381207141a24))
|
|
7
|
+
|
|
1
8
|
## [3.1.2](https://github.com/adobe/helix-config/compare/v3.1.1...v3.1.2) (2024-05-22)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-config",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Helix Config",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"husky": "9.0.11",
|
|
47
47
|
"json-schema-to-typescript": "14.0.4",
|
|
48
48
|
"junit-report-builder": "3.2.1",
|
|
49
|
-
"lint-staged": "15.2.
|
|
49
|
+
"lint-staged": "15.2.5",
|
|
50
50
|
"mocha": "10.4.0",
|
|
51
51
|
"mocha-multi-reporters": "1.5.1",
|
|
52
52
|
"mocha-suppress-logs": "0.5.1",
|
package/src/config-view.js
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from './ConfigContext.js';
|
|
22
22
|
import { resolveLegacyConfig, fetchRobotsTxt, toArray } from './config-legacy.js';
|
|
23
23
|
import { getMergedConfig } from './config-merge.js';
|
|
24
|
+
import { deepGetOrCreate } from './storage/utils.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* @typedef Config
|
|
@@ -55,6 +56,10 @@ const VALID_SCOPES = [
|
|
|
55
56
|
SCOPE_PUBLIC,
|
|
56
57
|
];
|
|
57
58
|
|
|
59
|
+
const VALID_ORG_SCOPES = [
|
|
60
|
+
SCOPE_ADMIN,
|
|
61
|
+
];
|
|
62
|
+
|
|
58
63
|
/**
|
|
59
64
|
* Creates a string representation of the given array that is suitable for substring matching by
|
|
60
65
|
* delimiting each entry with `,` eg: ,foo@adobe.com,bar@adobe.com,
|
|
@@ -253,20 +258,39 @@ async function getSurrogateKey(opts, profile) {
|
|
|
253
258
|
const { site, org } = opts;
|
|
254
259
|
const keys = [
|
|
255
260
|
await computeSurrogateKey(`${org}_config.json`),
|
|
256
|
-
await computeSurrogateKey(`${site}--${org}_config.json`),
|
|
257
261
|
];
|
|
262
|
+
if (site) {
|
|
263
|
+
keys.push(await computeSurrogateKey(`${site}--${org}_config.json`));
|
|
264
|
+
}
|
|
258
265
|
if (profile) {
|
|
259
266
|
keys.push(await computeSurrogateKey(`p:${profile}--${org}_config.json`));
|
|
260
267
|
}
|
|
261
268
|
return keys.join(' ');
|
|
262
269
|
}
|
|
263
270
|
|
|
264
|
-
async function loadOrgConfig(ctx, org) {
|
|
271
|
+
export async function loadOrgConfig(ctx, org) {
|
|
265
272
|
const key = `orgs/${org}/config.json`;
|
|
266
273
|
const res = await ctx.loader.getObject(HELIX_CONFIG_BUS, key);
|
|
267
274
|
return res.body ? res.json() : null;
|
|
268
275
|
}
|
|
269
276
|
|
|
277
|
+
function computeAdminRoles(adminConfig, orgConfig) {
|
|
278
|
+
if (orgConfig?.users) {
|
|
279
|
+
// map users[].roles[] to access.admin.role[].email
|
|
280
|
+
const rolesObj = deepGetOrCreate(adminConfig, ['access', 'admin', 'role'], true);
|
|
281
|
+
for (const user of orgConfig.users) {
|
|
282
|
+
for (const role of user.roles) {
|
|
283
|
+
if (!(role in rolesObj)) {
|
|
284
|
+
rolesObj[role] = [];
|
|
285
|
+
}
|
|
286
|
+
if (!rolesObj[role].includes(user.email)) {
|
|
287
|
+
rolesObj[role].push(user.email);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
270
294
|
export async function getConfigResponse(ctx, opts) {
|
|
271
295
|
const {
|
|
272
296
|
ref, site, org, scope,
|
|
@@ -361,6 +385,7 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
361
385
|
...config.content.source,
|
|
362
386
|
},
|
|
363
387
|
};
|
|
388
|
+
computeAdminRoles(adminConfig, orgConfig);
|
|
364
389
|
delete adminConfig.public;
|
|
365
390
|
delete adminConfig.robots;
|
|
366
391
|
return new PipelineResponse(JSON.stringify(adminConfig, null, 2), {
|
|
@@ -420,3 +445,45 @@ export async function getConfigResponse(ctx, opts) {
|
|
|
420
445
|
},
|
|
421
446
|
});
|
|
422
447
|
}
|
|
448
|
+
|
|
449
|
+
export async function getOrgConfigResponse(ctx, opts) {
|
|
450
|
+
const { org, scope } = opts;
|
|
451
|
+
if (!VALID_ORG_SCOPES.includes(scope)) {
|
|
452
|
+
return new PipelineResponse('', {
|
|
453
|
+
status: 400,
|
|
454
|
+
headers: {
|
|
455
|
+
'x-error': 'invalid scope',
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const surrogateHeaders = {
|
|
460
|
+
'x-surrogate-key': await getSurrogateKey(opts),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const orgConfig = await loadOrgConfig(ctx, org);
|
|
464
|
+
if (!orgConfig) {
|
|
465
|
+
return new PipelineResponse('', {
|
|
466
|
+
status: 404,
|
|
467
|
+
headers: {
|
|
468
|
+
'x-error': 'config not found.',
|
|
469
|
+
...surrogateHeaders,
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// only admin scope is supported and it only needs the users
|
|
475
|
+
const adminConfig = {
|
|
476
|
+
org,
|
|
477
|
+
...orgConfig,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
computeAdminRoles(adminConfig, orgConfig);
|
|
481
|
+
delete adminConfig.tokens;
|
|
482
|
+
delete adminConfig.users;
|
|
483
|
+
return new PipelineResponse(JSON.stringify(adminConfig, null, 2), {
|
|
484
|
+
headers: {
|
|
485
|
+
'content-type': 'application/json',
|
|
486
|
+
...surrogateHeaders,
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
}
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
"description": { "$ref": "https://ns.adobe.com/helix/config/common#/$defs/description" },
|
|
25
25
|
"tokens": {
|
|
26
26
|
"$ref": "https://ns.adobe.com/helix/config/tokens"
|
|
27
|
+
},
|
|
28
|
+
"users": {
|
|
29
|
+
"$ref": "https://ns.adobe.com/helix/config/users"
|
|
27
30
|
}
|
|
28
31
|
},
|
|
29
32
|
"required": [
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
module.exports = require('./users.schema.json');
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta:license": [
|
|
3
|
+
"Copyright 2024 Adobe. All rights reserved.",
|
|
4
|
+
"This file is licensed to you under the Apache License, Version 2.0 (the \"License\");",
|
|
5
|
+
"you may not use this file except in compliance with the License. You may obtain a copy",
|
|
6
|
+
"of the License at http://www.apache.org/licenses/LICENSE-2.0",
|
|
7
|
+
"",
|
|
8
|
+
"Unless required by applicable law or agreed to in writing, software distributed under",
|
|
9
|
+
"the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS",
|
|
10
|
+
"OF ANY KIND, either express or implied. See the License for the specific language",
|
|
11
|
+
"governing permissions and limitations under the License."
|
|
12
|
+
],
|
|
13
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
14
|
+
"$id": "https://ns.adobe.com/helix/config/users",
|
|
15
|
+
"title": "users",
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"id": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"pattern": "^[a-zA-Z0-9-_=]+$"
|
|
23
|
+
},
|
|
24
|
+
"email": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"format": "email"
|
|
27
|
+
},
|
|
28
|
+
"name": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"roles": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"enum": [
|
|
36
|
+
"admin",
|
|
37
|
+
"author",
|
|
38
|
+
"publish",
|
|
39
|
+
"config"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": [
|
|
45
|
+
"id",
|
|
46
|
+
"email",
|
|
47
|
+
"roles"
|
|
48
|
+
],
|
|
49
|
+
"additionalProperties": false
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
}
|
|
@@ -14,9 +14,9 @@ import { isDeepStrictEqual } from 'util';
|
|
|
14
14
|
import { HelixStorage } from '@adobe/helix-shared-storage';
|
|
15
15
|
import { StatusCodeError } from './status-code-error.js';
|
|
16
16
|
import {
|
|
17
|
-
createToken,
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
createToken, createUser,
|
|
18
|
+
deepGetOrCreate,
|
|
19
|
+
deepPut,
|
|
20
20
|
migrateToken,
|
|
21
21
|
updateCodeSource,
|
|
22
22
|
updateContentSource,
|
|
@@ -60,6 +60,13 @@ const FRAGMENTS = {
|
|
|
60
60
|
'.': 'token',
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
|
+
users: {
|
|
64
|
+
'.': 'users',
|
|
65
|
+
'.param': {
|
|
66
|
+
name: 'id',
|
|
67
|
+
'.': 'user',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
63
70
|
},
|
|
64
71
|
};
|
|
65
72
|
|
|
@@ -168,6 +175,9 @@ export class ConfigStore {
|
|
|
168
175
|
if (this.type === 'org' && data.tokens) {
|
|
169
176
|
throw new StatusCodeError(400, 'creating org config with tokens not supported yet.');
|
|
170
177
|
}
|
|
178
|
+
if (this.type === 'org' && data.users) {
|
|
179
|
+
throw new StatusCodeError(400, 'creating org config with users is not supported yet.');
|
|
180
|
+
}
|
|
171
181
|
if (this.type !== 'org') {
|
|
172
182
|
updateContentSource(ctx, data.content);
|
|
173
183
|
updateCodeSource(ctx, data.code);
|
|
@@ -186,7 +196,7 @@ export class ConfigStore {
|
|
|
186
196
|
let obj = JSON.parse(buf);
|
|
187
197
|
const frag = getFragmentInfo(this.type, relPath);
|
|
188
198
|
if (frag) {
|
|
189
|
-
obj =
|
|
199
|
+
obj = deepGetOrCreate(obj, frag.relPath);
|
|
190
200
|
}
|
|
191
201
|
return redact(obj, frag);
|
|
192
202
|
}
|
|
@@ -241,8 +251,33 @@ export class ConfigStore {
|
|
|
241
251
|
ret = token;
|
|
242
252
|
// don't expose hash in return value
|
|
243
253
|
delete ret.hash;
|
|
254
|
+
} else if (frag.type === 'users') {
|
|
255
|
+
const user = createUser();
|
|
256
|
+
data = {
|
|
257
|
+
...data,
|
|
258
|
+
...user,
|
|
259
|
+
};
|
|
260
|
+
relPath = ['users', user.id];
|
|
261
|
+
frag.type = 'user';
|
|
262
|
+
ret = user;
|
|
263
|
+
// todo: define via "schema"
|
|
264
|
+
if (!old.users) {
|
|
265
|
+
old.users = [];
|
|
266
|
+
}
|
|
267
|
+
} else if (frag.type === 'user') {
|
|
268
|
+
const user = deepGetOrCreate(old, relPath);
|
|
269
|
+
if (!user) {
|
|
270
|
+
throw new StatusCodeError(404, 'object not found.');
|
|
271
|
+
}
|
|
272
|
+
if (data.id) {
|
|
273
|
+
if (data.id !== user.id) {
|
|
274
|
+
throw new StatusCodeError(400, 'object id mismatch.');
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
data.id = user.id;
|
|
278
|
+
}
|
|
244
279
|
}
|
|
245
|
-
config =
|
|
280
|
+
config = deepPut(old, relPath, data);
|
|
246
281
|
} else if (this.type !== 'org') {
|
|
247
282
|
updateContentSource(ctx, config.content);
|
|
248
283
|
updateCodeSource(ctx, config.code);
|
|
@@ -261,7 +296,7 @@ export class ConfigStore {
|
|
|
261
296
|
}
|
|
262
297
|
const frag = getFragmentInfo(this.type, relPath);
|
|
263
298
|
if (frag) {
|
|
264
|
-
const data =
|
|
299
|
+
const data = deepPut(JSON.parse(buf), relPath, null);
|
|
265
300
|
await validate(data, this.type);
|
|
266
301
|
await storage.put(this.key, JSON.stringify(data), 'application/json');
|
|
267
302
|
await this.purge(ctx, JSON.parse(buf), data);
|
|
@@ -32,6 +32,7 @@ import robotsSchema from '../schemas/robots.schema.cjs';
|
|
|
32
32
|
import sidekickSchema from '../schemas/sidekick.schema.cjs';
|
|
33
33
|
import siteSchema from '../schemas/site.schema.cjs';
|
|
34
34
|
import tokensSchema from '../schemas/tokens.schema.cjs';
|
|
35
|
+
import usersSchema from '../schemas/users.schema.cjs';
|
|
35
36
|
|
|
36
37
|
export const SCHEMAS = [
|
|
37
38
|
accessSchema,
|
|
@@ -53,6 +54,7 @@ export const SCHEMAS = [
|
|
|
53
54
|
sidekickSchema,
|
|
54
55
|
siteSchema,
|
|
55
56
|
tokensSchema,
|
|
57
|
+
usersSchema,
|
|
56
58
|
];
|
|
57
59
|
|
|
58
60
|
const SCHEMA_TYPES = {
|
package/src/storage/utils.js
CHANGED
|
@@ -15,21 +15,60 @@ import { GitUrl } from '@adobe/helix-shared-git';
|
|
|
15
15
|
import { decodeJwt } from 'jose';
|
|
16
16
|
import { StatusCodeError } from './status-code-error.js';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Returns the property addressed by the given path in the object.
|
|
20
|
+
* If {@code create} is {@code true}, intermediate objects will be created if they do not exist.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} obj the object to search
|
|
23
|
+
* @param {string|string[]} path the path or path segments
|
|
24
|
+
* @param {boolean} create if {@code true} intermediate objects will be created if they do not exist
|
|
25
|
+
* @returns {*}
|
|
26
|
+
*/
|
|
27
|
+
export function deepGetOrCreate(obj, path, create = false) {
|
|
28
|
+
const parts = Array.isArray(path) ? path : path.split('/');
|
|
29
|
+
return parts.reduce((o, prop) => {
|
|
30
|
+
if (Array.isArray(o)) {
|
|
31
|
+
// todo: better support for arrays
|
|
32
|
+
return o.find((e) => e.id === prop);
|
|
33
|
+
} else {
|
|
34
|
+
if (create && !(prop in o)) {
|
|
35
|
+
// eslint-disable-next-line no-param-reassign
|
|
36
|
+
o[prop] = {};
|
|
37
|
+
}
|
|
38
|
+
return o[prop];
|
|
39
|
+
}
|
|
40
|
+
}, obj);
|
|
20
41
|
}
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Sets the property addressed by the given path in the object.
|
|
45
|
+
* @param {object} obj the object to search
|
|
46
|
+
* @param {string|string[]} path the path or path segments
|
|
47
|
+
* @param {*} value the value to set on the object
|
|
48
|
+
* @returns {object} the passed in `obj`
|
|
49
|
+
*/
|
|
50
|
+
export function deepPut(obj, path, value) {
|
|
23
51
|
const parts = Array.isArray(path) ? path : path.split('/');
|
|
24
52
|
const last = parts.pop();
|
|
25
|
-
const parent = parts.reduce((o,
|
|
26
|
-
if (!(
|
|
53
|
+
const parent = parts.reduce((o, prop) => {
|
|
54
|
+
if (!(prop in o)) {
|
|
27
55
|
// eslint-disable-next-line no-param-reassign
|
|
28
|
-
o[
|
|
56
|
+
o[prop] = {};
|
|
29
57
|
}
|
|
30
|
-
return o[
|
|
58
|
+
return o[prop];
|
|
31
59
|
}, obj);
|
|
32
|
-
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(parent)) {
|
|
62
|
+
// todo: better support for non id keys
|
|
63
|
+
const idx = parent.findIndex((e) => e.id === last);
|
|
64
|
+
if (idx >= 0 && value === null) {
|
|
65
|
+
parent.splice(idx, 1);
|
|
66
|
+
} else if (idx >= 0) {
|
|
67
|
+
parent[idx] = value;
|
|
68
|
+
} else if (value !== null) {
|
|
69
|
+
parent.push(value);
|
|
70
|
+
}
|
|
71
|
+
} else if (value === null) {
|
|
33
72
|
delete parent[last];
|
|
34
73
|
} else {
|
|
35
74
|
parent[last] = value;
|
|
@@ -137,6 +176,13 @@ export function createToken(key) {
|
|
|
137
176
|
};
|
|
138
177
|
}
|
|
139
178
|
|
|
179
|
+
export function createUser() {
|
|
180
|
+
const id = crypto.randomBytes(16).toString('base64url');
|
|
181
|
+
return {
|
|
182
|
+
id,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
140
186
|
/**
|
|
141
187
|
* migrates an existing jwt token
|
|
142
188
|
* @param key
|