@adobe/helix-config-storage 1.0.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/.eslintrc.cjs +16 -0
- package/.husky/pre-commit +1 -0
- package/.mocha-multi.json +6 -0
- package/.nycrc.json +10 -0
- package/.releaserc.cjs +16 -0
- package/CHANGELOG.md +877 -0
- package/CODE_OF_CONDUCT.md +74 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE.txt +264 -0
- package/README.md +40 -0
- package/package.json +71 -0
- package/src/ValidationError.js +95 -0
- package/src/config-merge.js +145 -0
- package/src/config-store.js +359 -0
- package/src/config-validator.js +112 -0
- package/src/index.js +15 -0
- package/src/schemas/access-admin.schema.cjs +12 -0
- package/src/schemas/access-admin.schema.json +57 -0
- package/src/schemas/access-site.schema.cjs +12 -0
- package/src/schemas/access-site.schema.json +24 -0
- package/src/schemas/access.schema.cjs +12 -0
- package/src/schemas/access.schema.json +22 -0
- package/src/schemas/cdn-prod-akamai.schema.cjs +12 -0
- package/src/schemas/cdn-prod-akamai.schema.json +45 -0
- package/src/schemas/cdn-prod-cloudflare.schema.cjs +12 -0
- package/src/schemas/cdn-prod-cloudflare.schema.json +41 -0
- package/src/schemas/cdn-prod-cloudfront.schema.cjs +12 -0
- package/src/schemas/cdn-prod-cloudfront.schema.json +41 -0
- package/src/schemas/cdn-prod-fastly.schema.cjs +12 -0
- package/src/schemas/cdn-prod-fastly.schema.json +40 -0
- package/src/schemas/cdn-prod-managed.schema.cjs +12 -0
- package/src/schemas/cdn-prod-managed.schema.json +29 -0
- package/src/schemas/cdn.schema.cjs +12 -0
- package/src/schemas/cdn.schema.json +75 -0
- package/src/schemas/code.schema.cjs +12 -0
- package/src/schemas/code.schema.json +43 -0
- package/src/schemas/common.schema.cjs +12 -0
- package/src/schemas/common.schema.json +27 -0
- package/src/schemas/content-source-google.schema.cjs +12 -0
- package/src/schemas/content-source-google.schema.json +26 -0
- package/src/schemas/content-source-markup.schema.cjs +12 -0
- package/src/schemas/content-source-markup.schema.json +24 -0
- package/src/schemas/content-source-onedrive.schema.cjs +12 -0
- package/src/schemas/content-source-onedrive.schema.json +28 -0
- package/src/schemas/content.schema.cjs +12 -0
- package/src/schemas/content.schema.json +30 -0
- package/src/schemas/folders.schema.cjs +12 -0
- package/src/schemas/folders.schema.json +13 -0
- package/src/schemas/groups.schema.cjs +12 -0
- package/src/schemas/groups.schema.json +39 -0
- package/src/schemas/headers.schema.cjs +12 -0
- package/src/schemas/headers.schema.json +16 -0
- package/src/schemas/metadata-source.schema.cjs +12 -0
- package/src/schemas/metadata-source.schema.json +16 -0
- package/src/schemas/org.schema.cjs +12 -0
- package/src/schemas/org.schema.json +26 -0
- package/src/schemas/profile.schema.cjs +12 -0
- package/src/schemas/profile.schema.json +57 -0
- package/src/schemas/profiles.schema.cjs +12 -0
- package/src/schemas/profiles.schema.json +28 -0
- package/src/schemas/public.schema.cjs +12 -0
- package/src/schemas/public.schema.json +8 -0
- package/src/schemas/robots.schema.cjs +12 -0
- package/src/schemas/robots.schema.json +13 -0
- package/src/schemas/sidekick.schema.cjs +12 -0
- package/src/schemas/sidekick.schema.json +112 -0
- package/src/schemas/site.schema.cjs +12 -0
- package/src/schemas/site.schema.json +74 -0
- package/src/schemas/sites.schema.cjs +12 -0
- package/src/schemas/sites.schema.json +28 -0
- package/src/schemas/tokens.schema.cjs +12 -0
- package/src/schemas/tokens.schema.json +28 -0
- package/src/schemas/user.schema.cjs +12 -0
- package/src/schemas/user.schema.json +42 -0
- package/src/schemas/users.schema.cjs +12 -0
- package/src/schemas/users.schema.json +10 -0
- package/src/status-code-error.js +22 -0
- package/src/utils.js +223 -0
- package/types/org-config.d.ts +51 -0
- package/types/profile-config.d.ts +368 -0
- package/types/site-config.d.ts +375 -0
- package/validate-json-schemas.sh +32 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 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
|
+
/* eslint-disable no-continue, no-param-reassign */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef RootProperty
|
|
16
|
+
* @property {boolean} atomic whether the property should be overwritten atomically
|
|
17
|
+
* @property {boolean} isModified whether the property need special merging
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const ROOT_PROPERTIES = {
|
|
21
|
+
name: { atomic: true },
|
|
22
|
+
title: { atomic: true },
|
|
23
|
+
description: { atomic: true },
|
|
24
|
+
content: { atomic: true },
|
|
25
|
+
code: { atomic: true },
|
|
26
|
+
folders: {},
|
|
27
|
+
headers: { isModifier: true },
|
|
28
|
+
cdn: {},
|
|
29
|
+
access: {},
|
|
30
|
+
sidekick: {},
|
|
31
|
+
metadata: { isModifier: true },
|
|
32
|
+
robots: {},
|
|
33
|
+
extends: {},
|
|
34
|
+
tokens: {},
|
|
35
|
+
public: {},
|
|
36
|
+
groups: {},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const FORCED_TYPES = {
|
|
40
|
+
'.cdn.prod.route': 'array',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Merges the `src` object into the `dst` object.
|
|
45
|
+
* - if the dst property is a scalar, it will be overwritten by the dst property
|
|
46
|
+
* - if the src property is an empty string, it will delete the dst property
|
|
47
|
+
* - if the dst property is an array, it will be expanded by the src property uniquely
|
|
48
|
+
* - some well known paths are forced to be arrays
|
|
49
|
+
* @param {object} dst
|
|
50
|
+
* @param {object} src
|
|
51
|
+
* @param {boolean} isModifier specifies that the objects are modifiers sheets and need special
|
|
52
|
+
* merging
|
|
53
|
+
*/
|
|
54
|
+
function merge(dst, src, path, isModifier) {
|
|
55
|
+
for (const [key, value] of Object.entries(src)) {
|
|
56
|
+
const itemPath = `${path}.${key}`;
|
|
57
|
+
if (value === '') {
|
|
58
|
+
// remove key if value is empty string (keep false, 0)
|
|
59
|
+
delete dst[key];
|
|
60
|
+
} else if (typeof value !== 'object') {
|
|
61
|
+
// handle non object
|
|
62
|
+
if (key in dst) {
|
|
63
|
+
let dstValue = dst[key];
|
|
64
|
+
if (!Array.isArray(dstValue) && FORCED_TYPES[itemPath] === 'array') {
|
|
65
|
+
dstValue = [dst[key]];
|
|
66
|
+
dst[key] = dstValue;
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(dstValue)) {
|
|
69
|
+
dst[key] = value;
|
|
70
|
+
} else if (!dstValue.includes(value)) {
|
|
71
|
+
// only add to array if not exist yet
|
|
72
|
+
dstValue.push(value);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
dst[key] = value;
|
|
76
|
+
}
|
|
77
|
+
} else if (Array.isArray(value)) {
|
|
78
|
+
// handle array
|
|
79
|
+
if (key in dst) {
|
|
80
|
+
if (!Array.isArray(dst[key])) {
|
|
81
|
+
dst[key] = [dst[key]];
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
dst[key] = [];
|
|
85
|
+
}
|
|
86
|
+
const dstArray = dst[key];
|
|
87
|
+
for (const item of value) {
|
|
88
|
+
if (typeof item === 'object') {
|
|
89
|
+
// todo: handle arrays in arrays eventually
|
|
90
|
+
// special case for modifier sheets
|
|
91
|
+
if (isModifier && item.key) {
|
|
92
|
+
const idx = dstArray.findIndex((i) => i.key === item.key);
|
|
93
|
+
if (idx >= 0) {
|
|
94
|
+
dstArray.splice(idx, 1);
|
|
95
|
+
}
|
|
96
|
+
if (!item.value) {
|
|
97
|
+
// don't set empty modifier
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const dstObj = Object.create(null);
|
|
102
|
+
dstArray.push(dstObj);
|
|
103
|
+
merge(dstObj, item, itemPath, isModifier);
|
|
104
|
+
} else if (!dstArray.includes(item)) {
|
|
105
|
+
dstArray.push(item);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// handle plain object
|
|
110
|
+
// eslint-disable-next-line no-use-before-define
|
|
111
|
+
mergeObject(dst, key, value, `${path}.${key}`, isModifier);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function mergeObject(dst, key, value, path, isModifier) {
|
|
117
|
+
const dstObj = dst[key] ?? Object.create(null);
|
|
118
|
+
if (!dst[key]) {
|
|
119
|
+
dst[key] = dstObj;
|
|
120
|
+
}
|
|
121
|
+
merge(dstObj, value, path, isModifier);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mergeConfig(dst, src) {
|
|
125
|
+
Object.entries(ROOT_PROPERTIES).forEach(([key, { atomic, isModifier }]) => {
|
|
126
|
+
if (src[key]) {
|
|
127
|
+
if (atomic) {
|
|
128
|
+
dst[key] = src[key];
|
|
129
|
+
} else {
|
|
130
|
+
mergeObject(dst, key, src[key], `.${key}`, isModifier);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getMergedConfig(site, profile) {
|
|
137
|
+
if (!profile) {
|
|
138
|
+
return site;
|
|
139
|
+
}
|
|
140
|
+
const ret = Object.create(null);
|
|
141
|
+
ret.version = 1;
|
|
142
|
+
mergeConfig(ret, profile);
|
|
143
|
+
mergeConfig(ret, site);
|
|
144
|
+
return ret;
|
|
145
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 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
|
+
/* eslint-disable no-param-reassign */
|
|
13
|
+
import { isDeepStrictEqual } from 'util';
|
|
14
|
+
import { HelixStorage } from '@adobe/helix-shared-storage';
|
|
15
|
+
import { StatusCodeError } from './status-code-error.js';
|
|
16
|
+
import {
|
|
17
|
+
createToken, createUser,
|
|
18
|
+
migrateToken,
|
|
19
|
+
updateCodeSource,
|
|
20
|
+
updateContentSource,
|
|
21
|
+
deepGetOrCreate, deepPut,
|
|
22
|
+
} from './utils.js';
|
|
23
|
+
import { validate as validateSchema } from './config-validator.js';
|
|
24
|
+
|
|
25
|
+
const FRAGMENTS_COMMON = {
|
|
26
|
+
content: 'object',
|
|
27
|
+
code: 'object',
|
|
28
|
+
folders: 'object',
|
|
29
|
+
headers: 'object',
|
|
30
|
+
metadata: 'object',
|
|
31
|
+
sidekick: 'object',
|
|
32
|
+
cdn: {
|
|
33
|
+
'.': 'object',
|
|
34
|
+
prod: 'object',
|
|
35
|
+
preview: 'object',
|
|
36
|
+
live: 'object',
|
|
37
|
+
},
|
|
38
|
+
access: {
|
|
39
|
+
'.': 'object',
|
|
40
|
+
admin: 'object',
|
|
41
|
+
preview: 'object',
|
|
42
|
+
live: 'object',
|
|
43
|
+
},
|
|
44
|
+
tokens: {
|
|
45
|
+
'.': 'tokens',
|
|
46
|
+
'.param': {
|
|
47
|
+
name: 'id',
|
|
48
|
+
'.': 'token',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
groups: {
|
|
52
|
+
'.': 'object',
|
|
53
|
+
'.param': {
|
|
54
|
+
'.': 'object',
|
|
55
|
+
members: 'object',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
public: 'object',
|
|
59
|
+
robots: 'object',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const FRAGMENTS = {
|
|
63
|
+
sites: {
|
|
64
|
+
...FRAGMENTS_COMMON,
|
|
65
|
+
extends: 'object',
|
|
66
|
+
},
|
|
67
|
+
profiles: FRAGMENTS_COMMON,
|
|
68
|
+
org: {
|
|
69
|
+
users: {
|
|
70
|
+
'.': 'users',
|
|
71
|
+
'.param': {
|
|
72
|
+
name: 'id',
|
|
73
|
+
'.': 'user',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
groups: {
|
|
77
|
+
'.': 'object',
|
|
78
|
+
'.param': {
|
|
79
|
+
'.': 'object',
|
|
80
|
+
members: 'object',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function getFragmentInfo(type, relPath) {
|
|
87
|
+
if (!relPath) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const parts = relPath.split('/');
|
|
91
|
+
let fragment = FRAGMENTS[type];
|
|
92
|
+
const info = {
|
|
93
|
+
relPath,
|
|
94
|
+
};
|
|
95
|
+
for (const part of parts) {
|
|
96
|
+
let next = fragment[part];
|
|
97
|
+
if (!next) {
|
|
98
|
+
next = fragment['.param'];
|
|
99
|
+
if (!next) {
|
|
100
|
+
throw new StatusCodeError(400, 'invalid object path.');
|
|
101
|
+
}
|
|
102
|
+
info[next.name] = part;
|
|
103
|
+
}
|
|
104
|
+
fragment = next;
|
|
105
|
+
}
|
|
106
|
+
if (typeof fragment === 'string') {
|
|
107
|
+
info.type = fragment;
|
|
108
|
+
} else {
|
|
109
|
+
info.type = fragment['.'];
|
|
110
|
+
}
|
|
111
|
+
return info;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Redact / transform the token config
|
|
116
|
+
* @param token
|
|
117
|
+
*/
|
|
118
|
+
function redactToken(token) {
|
|
119
|
+
return {
|
|
120
|
+
id: token.id,
|
|
121
|
+
created: token.created,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Redact / transform the tokens config
|
|
127
|
+
* @param tokens
|
|
128
|
+
*/
|
|
129
|
+
function redactTokens(tokens) {
|
|
130
|
+
return Object.values(tokens).map(redactToken);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* redact information from the config
|
|
135
|
+
* @param {object} config
|
|
136
|
+
* @param {object} frag
|
|
137
|
+
*/
|
|
138
|
+
function redact(config, frag) {
|
|
139
|
+
if (!config) {
|
|
140
|
+
return config;
|
|
141
|
+
}
|
|
142
|
+
let ret = config;
|
|
143
|
+
if (frag?.type === 'tokens') {
|
|
144
|
+
ret = redactTokens(config);
|
|
145
|
+
}
|
|
146
|
+
if (frag?.type === 'token') {
|
|
147
|
+
ret = redactToken(config);
|
|
148
|
+
}
|
|
149
|
+
if (ret.tokens) {
|
|
150
|
+
// eslint-disable-next-line no-param-reassign
|
|
151
|
+
ret.tokens = redactTokens(ret.tokens);
|
|
152
|
+
}
|
|
153
|
+
return ret;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* General purpose config store.
|
|
158
|
+
*/
|
|
159
|
+
export class ConfigStore {
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} org the org id
|
|
162
|
+
* @param {string} type store type (org, sites, profiles, secrets, users)
|
|
163
|
+
* @param {string} name config name
|
|
164
|
+
*/
|
|
165
|
+
constructor(org, type = 'org', name = '') {
|
|
166
|
+
if (!org) {
|
|
167
|
+
throw new Error('org required');
|
|
168
|
+
}
|
|
169
|
+
if (org.includes('/') || type.includes('/') || name.includes('/')) {
|
|
170
|
+
throw new Error('orgId, type and name must not contain slashes');
|
|
171
|
+
}
|
|
172
|
+
this.org = org;
|
|
173
|
+
this.type = type;
|
|
174
|
+
this.name = name;
|
|
175
|
+
this.key = type === 'org'
|
|
176
|
+
? `/orgs/${this.org}/${this.name || 'config'}.json`
|
|
177
|
+
: `/orgs/${this.org}/${this.type}/${this.name}.json`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validates the config and throws an error if not valid.
|
|
182
|
+
* @param {AdminContext} ctx
|
|
183
|
+
* @param {HelixSiteConfig|HelixProfileConfig} data the data of the config to be updated
|
|
184
|
+
* @returns {Promise<boolean|undefined>}
|
|
185
|
+
*/
|
|
186
|
+
async validate(ctx, data) {
|
|
187
|
+
return validateSchema(data, this.type);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async create(ctx, data, relPath = '') {
|
|
191
|
+
if (relPath) {
|
|
192
|
+
throw new StatusCodeError(409, 'create not supported on substructures.');
|
|
193
|
+
}
|
|
194
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
195
|
+
if (await storage.head(this.key)) {
|
|
196
|
+
throw new StatusCodeError(409, 'config already exists.');
|
|
197
|
+
}
|
|
198
|
+
if (data.tokens) {
|
|
199
|
+
throw new StatusCodeError(400, 'creating config with tokens not supported yet.');
|
|
200
|
+
}
|
|
201
|
+
if (this.type === 'org' && data.users) {
|
|
202
|
+
throw new StatusCodeError(400, 'creating org config with users is not supported yet.');
|
|
203
|
+
}
|
|
204
|
+
if (this.type !== 'org') {
|
|
205
|
+
updateContentSource(ctx, data.content);
|
|
206
|
+
updateCodeSource(ctx, data.code);
|
|
207
|
+
}
|
|
208
|
+
await this.validate(ctx, data);
|
|
209
|
+
await storage.put(this.key, JSON.stringify(data), 'application/json');
|
|
210
|
+
await this.purge(ctx, null, data);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async #list(ctx) {
|
|
214
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
215
|
+
const key = `orgs/${this.org}/${this.type}/`;
|
|
216
|
+
const list = await storage.list(key);
|
|
217
|
+
return {
|
|
218
|
+
[this.type]: list.map((entry) => {
|
|
219
|
+
const siteKey = entry.key;
|
|
220
|
+
if (siteKey.endsWith('.json')) {
|
|
221
|
+
const name = siteKey.split('/').pop();
|
|
222
|
+
return {
|
|
223
|
+
path: `/config/${this.org}/${this.type}/${name}`,
|
|
224
|
+
name: name.substring(0, name.length - 5),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}).filter((entry) => entry),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async read(ctx, relPath = '') {
|
|
233
|
+
if (this.name === '' && (this.type === 'sites' || this.type === 'profiles')) {
|
|
234
|
+
return this.#list(ctx);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
238
|
+
const buf = await storage.get(this.key);
|
|
239
|
+
if (!buf) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
let obj = JSON.parse(buf);
|
|
243
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
244
|
+
if (frag) {
|
|
245
|
+
obj = deepGetOrCreate(obj, frag.relPath);
|
|
246
|
+
}
|
|
247
|
+
return redact(obj, frag) ?? null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async update(ctx, data, relPath = '') {
|
|
251
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
252
|
+
const buf = await storage.get(this.key);
|
|
253
|
+
let old = buf ? JSON.parse(buf) : null;
|
|
254
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
255
|
+
let config = data;
|
|
256
|
+
// set config to null if empty object
|
|
257
|
+
if (isDeepStrictEqual(config, {})) {
|
|
258
|
+
config = null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let ret = null;
|
|
262
|
+
if (!config && frag?.type !== 'tokens') {
|
|
263
|
+
throw new StatusCodeError(400, 'no config in body.');
|
|
264
|
+
}
|
|
265
|
+
if (frag) {
|
|
266
|
+
if (!old) {
|
|
267
|
+
if (this.type === 'profiles' && frag.type === 'tokens') {
|
|
268
|
+
old = {};
|
|
269
|
+
} else {
|
|
270
|
+
throw new StatusCodeError(404, 'config not found.');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (relPath === 'code') {
|
|
274
|
+
updateCodeSource(ctx, data);
|
|
275
|
+
} else if (relPath === 'content') {
|
|
276
|
+
updateContentSource(ctx, data);
|
|
277
|
+
} else if (frag.type === 'token') {
|
|
278
|
+
// don't allow to update individual token
|
|
279
|
+
throw new StatusCodeError(400, 'invalid object path.');
|
|
280
|
+
} else if (frag.type === 'tokens') {
|
|
281
|
+
// TODO: remove support after all helix4 projects are migrated
|
|
282
|
+
let token;
|
|
283
|
+
if (data.jwt) {
|
|
284
|
+
token = await migrateToken(this.org, data.jwt);
|
|
285
|
+
} else {
|
|
286
|
+
// create new token
|
|
287
|
+
token = createToken(this.org);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
data = {
|
|
291
|
+
...token,
|
|
292
|
+
};
|
|
293
|
+
// don't store token value in the config
|
|
294
|
+
delete data.value;
|
|
295
|
+
relPath = ['tokens', token.id];
|
|
296
|
+
frag.type = 'token';
|
|
297
|
+
ret = token;
|
|
298
|
+
// don't expose hash in return value
|
|
299
|
+
delete ret.hash;
|
|
300
|
+
} else if (frag.type === 'users') {
|
|
301
|
+
const user = createUser();
|
|
302
|
+
data = {
|
|
303
|
+
...data,
|
|
304
|
+
...user,
|
|
305
|
+
};
|
|
306
|
+
relPath = ['users', user.id];
|
|
307
|
+
frag.type = 'user';
|
|
308
|
+
// todo: define via "schema"
|
|
309
|
+
if (!old.users) {
|
|
310
|
+
old.users = [];
|
|
311
|
+
}
|
|
312
|
+
} else if (frag.type === 'user') {
|
|
313
|
+
const user = deepGetOrCreate(old, relPath);
|
|
314
|
+
if (!user) {
|
|
315
|
+
throw new StatusCodeError(404, 'object not found.');
|
|
316
|
+
}
|
|
317
|
+
if (data.id) {
|
|
318
|
+
if (data.id !== user.id) {
|
|
319
|
+
throw new StatusCodeError(400, 'object id mismatch.');
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
data.id = user.id;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
config = deepPut(old, relPath, data);
|
|
326
|
+
} else if (this.type !== 'org') {
|
|
327
|
+
updateContentSource(ctx, config.content);
|
|
328
|
+
updateCodeSource(ctx, config.code);
|
|
329
|
+
}
|
|
330
|
+
await this.validate(ctx, config);
|
|
331
|
+
await storage.put(this.key, JSON.stringify(config), 'application/json');
|
|
332
|
+
await this.purge(ctx, buf ? JSON.parse(buf) : null, config);
|
|
333
|
+
return ret ?? redact(data, frag);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async remove(ctx, relPath) {
|
|
337
|
+
const storage = HelixStorage.fromContext(ctx).configBus();
|
|
338
|
+
const buf = await storage.get(this.key);
|
|
339
|
+
if (!buf) {
|
|
340
|
+
throw new StatusCodeError(404, 'config not found.');
|
|
341
|
+
}
|
|
342
|
+
const frag = getFragmentInfo(this.type, relPath);
|
|
343
|
+
if (frag) {
|
|
344
|
+
const data = deepPut(JSON.parse(buf), relPath, null);
|
|
345
|
+
await this.validate(ctx, data);
|
|
346
|
+
await storage.put(this.key, JSON.stringify(data), 'application/json');
|
|
347
|
+
await this.purge(ctx, JSON.parse(buf), data);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await storage.remove(this.key);
|
|
352
|
+
await this.purge(ctx, JSON.parse(buf), null);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// eslint-disable-next-line class-methods-use-this,no-unused-vars
|
|
356
|
+
async purge(ctx, oldConfig, newConfig) {
|
|
357
|
+
// override in subclass
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
import Ajv2019 from 'ajv/dist/2019.js';
|
|
13
|
+
import ajvFormats from 'ajv-formats';
|
|
14
|
+
import { ValidationError } from './ValidationError.js';
|
|
15
|
+
|
|
16
|
+
import accessAdminSchema from './schemas/access-admin.schema.cjs';
|
|
17
|
+
import accessSchema from './schemas/access.schema.cjs';
|
|
18
|
+
import accessSiteSchema from './schemas/access-site.schema.cjs';
|
|
19
|
+
import cdnSchema from './schemas/cdn.schema.cjs';
|
|
20
|
+
import cdnProdFastlySchema from './schemas/cdn-prod-fastly.schema.cjs';
|
|
21
|
+
import cdnProdCloudflareSchema from './schemas/cdn-prod-cloudflare.schema.cjs';
|
|
22
|
+
import cdnProdAkamaiSchema from './schemas/cdn-prod-akamai.schema.cjs';
|
|
23
|
+
import cdnProdManagedSchema from './schemas/cdn-prod-managed.schema.cjs';
|
|
24
|
+
import cdnProdCloudfrontSchema from './schemas/cdn-prod-cloudfront.schema.cjs';
|
|
25
|
+
import commonSchema from './schemas/common.schema.cjs';
|
|
26
|
+
import codeSchema from './schemas/code.schema.cjs';
|
|
27
|
+
import contentSchema from './schemas/content.schema.cjs';
|
|
28
|
+
import foldersSchema from './schemas/folders.schema.cjs';
|
|
29
|
+
import groupsSchema from './schemas/groups.schema.cjs';
|
|
30
|
+
import googleSchema from './schemas/content-source-google.schema.cjs';
|
|
31
|
+
import headersSchema from './schemas/headers.schema.cjs';
|
|
32
|
+
import markupSchema from './schemas/content-source-markup.schema.cjs';
|
|
33
|
+
import metadataSchema from './schemas/metadata-source.schema.cjs';
|
|
34
|
+
import orgSchema from './schemas/org.schema.cjs';
|
|
35
|
+
import onedriveSchema from './schemas/content-source-onedrive.schema.cjs';
|
|
36
|
+
import publicSchema from './schemas/public.schema.cjs';
|
|
37
|
+
import profileSchema from './schemas/profile.schema.cjs';
|
|
38
|
+
import profilesSchema from './schemas/profiles.schema.cjs';
|
|
39
|
+
import robotsSchema from './schemas/robots.schema.cjs';
|
|
40
|
+
import sidekickSchema from './schemas/sidekick.schema.cjs';
|
|
41
|
+
import siteSchema from './schemas/site.schema.cjs';
|
|
42
|
+
import sitesSchema from './schemas/sites.schema.cjs';
|
|
43
|
+
import tokensSchema from './schemas/tokens.schema.cjs';
|
|
44
|
+
import userSchema from './schemas/user.schema.cjs';
|
|
45
|
+
import usersSchema from './schemas/users.schema.cjs';
|
|
46
|
+
|
|
47
|
+
export const SCHEMAS = [
|
|
48
|
+
accessSchema,
|
|
49
|
+
accessAdminSchema,
|
|
50
|
+
accessSiteSchema,
|
|
51
|
+
cdnSchema,
|
|
52
|
+
commonSchema,
|
|
53
|
+
contentSchema,
|
|
54
|
+
codeSchema,
|
|
55
|
+
foldersSchema,
|
|
56
|
+
googleSchema,
|
|
57
|
+
groupsSchema,
|
|
58
|
+
headersSchema,
|
|
59
|
+
markupSchema,
|
|
60
|
+
metadataSchema,
|
|
61
|
+
orgSchema,
|
|
62
|
+
onedriveSchema,
|
|
63
|
+
publicSchema,
|
|
64
|
+
profileSchema,
|
|
65
|
+
profilesSchema,
|
|
66
|
+
robotsSchema,
|
|
67
|
+
sidekickSchema,
|
|
68
|
+
siteSchema,
|
|
69
|
+
sitesSchema,
|
|
70
|
+
tokensSchema,
|
|
71
|
+
userSchema,
|
|
72
|
+
usersSchema,
|
|
73
|
+
cdnProdFastlySchema,
|
|
74
|
+
cdnProdCloudflareSchema,
|
|
75
|
+
cdnProdAkamaiSchema,
|
|
76
|
+
cdnProdManagedSchema,
|
|
77
|
+
cdnProdCloudfrontSchema,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const SCHEMA_TYPES = {
|
|
81
|
+
profiles: profileSchema,
|
|
82
|
+
sites: siteSchema,
|
|
83
|
+
org: orgSchema,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validates the loaded configuration and coerces types and sets defaults
|
|
88
|
+
* @param {object} config The configuration to validate
|
|
89
|
+
* @param {string} type the config type
|
|
90
|
+
* @returns {object} The validated configuration
|
|
91
|
+
*/
|
|
92
|
+
export async function validate(config, type) {
|
|
93
|
+
const schema = SCHEMA_TYPES[type];
|
|
94
|
+
if (!schema) {
|
|
95
|
+
throw new Error(`no such type: ${type}`);
|
|
96
|
+
}
|
|
97
|
+
const ajv = new Ajv2019({
|
|
98
|
+
allErrors: true,
|
|
99
|
+
verbose: true,
|
|
100
|
+
useDefaults: true,
|
|
101
|
+
coerceTypes: 'array',
|
|
102
|
+
strict: false,
|
|
103
|
+
});
|
|
104
|
+
ajvFormats(ajv);
|
|
105
|
+
|
|
106
|
+
ajv.addSchema(SCHEMAS);
|
|
107
|
+
const res = ajv.validate(schema, config);
|
|
108
|
+
if (res) {
|
|
109
|
+
return res;
|
|
110
|
+
}
|
|
111
|
+
throw new ValidationError(ajv.errorsText(), ajv.errors);
|
|
112
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export { ConfigStore } from './config-store.js';
|
|
13
|
+
export { SCHEMAS } from './config-validator.js';
|
|
14
|
+
export * from './ValidationError.js';
|
|
15
|
+
export * from './config-merge.js';
|
|
@@ -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('./access-admin.schema.json');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "https://github.com/adobe/helix-config/blob/main/LICENSE.txt",
|
|
3
|
+
"$id": "https://ns.adobe.com/helix/config/access/admin",
|
|
4
|
+
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
5
|
+
"title": "Admin Access Config",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"role": {
|
|
9
|
+
"title": "Role",
|
|
10
|
+
"type": "object",
|
|
11
|
+
"patternProperties": {
|
|
12
|
+
"^[a-z-_]+$": {
|
|
13
|
+
"description": "The email glob of the users or a group reference for the respective role.",
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": {"type": "string"}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
},
|
|
20
|
+
"requireAuth": {
|
|
21
|
+
"description": "Enforce authentication if set to true. If set to 'auto' it will enforce authentication if a role mapping is defined. defaults to 'auto'.",
|
|
22
|
+
"default": "auto",
|
|
23
|
+
"oneOf": [
|
|
24
|
+
{
|
|
25
|
+
"type": "boolean"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "string",
|
|
29
|
+
"enum": ["auto"]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"defaultRole": {
|
|
34
|
+
"description": "the default roles assigned to the users. defaults to `basic_publish` for unauthenticated setups.",
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": [
|
|
39
|
+
"admin",
|
|
40
|
+
"author",
|
|
41
|
+
"publish",
|
|
42
|
+
"develop",
|
|
43
|
+
"basic_author",
|
|
44
|
+
"basic_publish",
|
|
45
|
+
"config",
|
|
46
|
+
"config_admin"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"apiKeyId": {
|
|
51
|
+
"description": "the id of the API key(s). this is used to validate the API KEYS and allows to invalidate them.",
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {"type": "string"}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"additionalProperties": false
|
|
57
|
+
}
|