@aifabrix/builder 2.61.2 → 2.62.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/lib/api/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/run-env-compose.js +5 -6
- package/lib/app/run.js +6 -4
- package/lib/build/standard-docker-build.js +4 -0
- package/lib/channels/channel-artifact-generator.js +13 -3
- package/lib/cli/infra-guided-footers.js +10 -8
- package/lib/cli/infra-guided.js +30 -10
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +2 -0
- package/lib/commands/auth-config.js +2 -0
- package/lib/commands/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +3 -1
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +7 -1
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup.js +3 -0
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +70 -14
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +37 -18
- package/lib/commands/up-integration-server.js +112 -0
- package/lib/commands/wizard-config-normalizer.js +5 -1
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/environment-access-policy.js +121 -0
- package/lib/core/environment-mode-policy.js +177 -0
- package/lib/core/local-env-overrides.js +149 -0
- package/lib/core/secrets-ensure.js +4 -2
- package/lib/core/secrets-env-content.js +12 -9
- package/lib/core/secrets-env-write.js +1 -1
- package/lib/datasource/capability/capability-manifest.js +1 -0
- package/lib/deployment/installation/index.js +14 -1
- package/lib/deployment/installation/infra-catalog.js +3 -0
- package/lib/deployment/installation/local-environment-stage.js +285 -0
- package/lib/deployment/installation/local-installation.js +76 -0
- package/lib/deployment/installation/stage-input.js +5 -1
- package/lib/generator/builders.js +3 -1
- package/lib/generator/split-variables.js +2 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/physical-database-naming.js +136 -0
- package/lib/parameters/physical-secret-name.js +194 -0
- package/lib/parameters/rsa-secret-values.js +165 -0
- package/lib/programmatic/run-channel-install-artifacts.js +28 -2
- package/lib/programmatic/run-channel-package.js +3 -1
- package/lib/programmatic/workspace-context.js +37 -2
- package/lib/schema/infra.parameter.yaml +61 -27
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +2 -2
- package/lib/utils/compose-generator.js +4 -7
- package/lib/utils/compose-miso-env.js +2 -2
- package/lib/utils/compose-traefik-ingress-base.js +3 -4
- package/lib/utils/declarative-url-ports.js +3 -0
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/redis-env-scope.js +21 -1
- package/lib/utils/registry-auth-sources.js +154 -0
- package/lib/utils/registry-credentials.js +131 -14
- package/lib/utils/secrets-generator.js +30 -21
- package/lib/utils/secrets-helpers.js +5 -7
- package/lib/utils/secrets-kv-scope.js +127 -28
- package/lib/utils/secrets-materialize-local.js +9 -0
- package/lib/utils/secrets-missing-error.js +35 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- package/package.json +3 -2
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +2 -2
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +43 -1
- package/templates/applications/miso-controller/env.template +45 -39
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +85 -7
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The physical secret name for a catalog key (plan 210.0, mirroring Miso 227.4).
|
|
3
|
+
*
|
|
4
|
+
* Builder resolved `tst` and `pro` secrets by prefixing the logical key: `pro-<key>`. That is
|
|
5
|
+
* right for an environment-scoped parameter and **wrong for the other two cases**, which
|
|
6
|
+
* Miso's naming rule (`services/parameters/parameter-naming.ts`) states as:
|
|
7
|
+
*
|
|
8
|
+
* ```text
|
|
9
|
+
* system <key> no prefix, deliberately
|
|
10
|
+
* environment-scoped dev-<key>
|
|
11
|
+
* application-scoped dev-<app>-<key>
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* So the database password an installation actually holds is
|
|
15
|
+
* `pro-dataplane-databases-0-passwordKeyVault`, not `pro-databases-dataplane-0-passwordKeyVault`
|
|
16
|
+
* — the application segment sits before the key, not after the environment alone. And system
|
|
17
|
+
* names such as `keycloak-*` and `acr-*` must not move at all, because the Marketplace
|
|
18
|
+
* template writes them and every installation-level reader expects them where they are.
|
|
19
|
+
*
|
|
20
|
+
* Rather than restate that rule, this resolves the name the way Miso's `computeVaultSecretName`
|
|
21
|
+
* does: from the catalog entry's own `azure.vaultSecretName` or `azure.vaultSecretNamePattern`.
|
|
22
|
+
* The catalog is the Miso master and Builder mirrors it byte for byte, so the pattern is the
|
|
23
|
+
* contract. A key with no pattern is installation-wide by declaration, not by omission.
|
|
24
|
+
*
|
|
25
|
+
* This is why no caller may build a prefix itself: the three cases are not distinguishable
|
|
26
|
+
* from the key alone.
|
|
27
|
+
*
|
|
28
|
+
* @fileoverview Catalog-driven physical secret names, mirroring Miso's computeVaultSecretName
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
/** Database catalog keys carry both the owning application and the index in the key. */
|
|
34
|
+
const DATABASE_KEY_PARTS = /^databases-([a-z0-9-]+)-(\d+)-(?:url|password)KeyVault$/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} key - Logical catalog key
|
|
38
|
+
* @returns {string} The index, or empty when the key is not a database key
|
|
39
|
+
*/
|
|
40
|
+
function extractDatabaseIndex(key) {
|
|
41
|
+
const match = String(key ?? '').match(DATABASE_KEY_PARTS);
|
|
42
|
+
return match ? match[2] : '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The application a database key belongs to.
|
|
47
|
+
*
|
|
48
|
+
* Most database entries match the catalog's generic `keyPattern` rather than an exact key, so
|
|
49
|
+
* they carry no `ownerAppKey` to read — the owner is in the key. Without this the `{appKey}`
|
|
50
|
+
* placeholder resolves empty and the name collapses to `pro--databases-0-…`, a name nothing
|
|
51
|
+
* writes and nothing reads.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} key - Logical catalog key
|
|
54
|
+
* @returns {string} The application key, or empty when the key is not a database key
|
|
55
|
+
*/
|
|
56
|
+
function extractDatabaseAppKey(key) {
|
|
57
|
+
const match = String(key ?? '').match(DATABASE_KEY_PARTS);
|
|
58
|
+
return match ? match[1] : '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Substitute the placeholders a vault name pattern may carry.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} pattern - From `azure.vaultSecretNamePattern`
|
|
65
|
+
* @param {string} key - Logical catalog key
|
|
66
|
+
* @param {{ environment?: string, serviceName?: string, appKey?: string }} context
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function applyVaultNamePattern(pattern, key, context = {}) {
|
|
70
|
+
return String(pattern)
|
|
71
|
+
.replace(/\{key\}/g, key)
|
|
72
|
+
.replace(/\{environment\}/g, context.environment ?? '')
|
|
73
|
+
.replace(/\{serviceName\}/g, context.serviceName ?? '')
|
|
74
|
+
.replace(/\{appKey\}/g, context.appKey ?? '')
|
|
75
|
+
.replace(/\{index\}/g, extractDatabaseIndex(key));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The physical name of the secret holding a logical key's value.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} key - Logical catalog key
|
|
82
|
+
* @param {Object} [context] - Resolution context
|
|
83
|
+
* @param {string} [context.environment] - Environment key
|
|
84
|
+
* @param {string} [context.serviceName] - Installation service name
|
|
85
|
+
* @param {Object} [context.catalog] - Catalog override, for tests
|
|
86
|
+
* @returns {string} Physical secret name, or the key unchanged when the catalog says so
|
|
87
|
+
*/
|
|
88
|
+
function computePhysicalSecretName(key, context = {}) {
|
|
89
|
+
const logicalKey = String(key ?? '').trim();
|
|
90
|
+
if (!logicalKey) {
|
|
91
|
+
return logicalKey;
|
|
92
|
+
}
|
|
93
|
+
const entry = findCatalogEntry(logicalKey, context.catalog);
|
|
94
|
+
const azure = entry && entry.azure ? entry.azure : null;
|
|
95
|
+
if (!azure) {
|
|
96
|
+
return logicalKey;
|
|
97
|
+
}
|
|
98
|
+
if (azure.vaultSecretName) {
|
|
99
|
+
return String(azure.vaultSecretName);
|
|
100
|
+
}
|
|
101
|
+
if (!azure.vaultSecretNamePattern) {
|
|
102
|
+
return logicalKey;
|
|
103
|
+
}
|
|
104
|
+
// The owning application comes from the entry or from the key, never from the caller: an
|
|
105
|
+
// application-scoped parameter names its own owner, and a caller passing a different one
|
|
106
|
+
// would silently address another application's secret.
|
|
107
|
+
return applyVaultNamePattern(azure.vaultSecretNamePattern, logicalKey, {
|
|
108
|
+
environment: context.environment,
|
|
109
|
+
serviceName: context.serviceName,
|
|
110
|
+
appKey: entry.ownerAppKey || extractDatabaseAppKey(logicalKey)
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} key - Logical catalog key
|
|
116
|
+
* @param {Object} [override] - Catalog override, for tests
|
|
117
|
+
* @returns {Object|null}
|
|
118
|
+
*/
|
|
119
|
+
function findCatalogEntry(key, override) {
|
|
120
|
+
const catalog = override || loadCatalogQuietly();
|
|
121
|
+
if (!catalog || typeof catalog.findEntryForKey !== 'function') {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return catalog.findEntryForKey(key) || null;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The catalog, or nothing.
|
|
133
|
+
*
|
|
134
|
+
* A key resolves to itself when the catalog cannot be read, which is the same answer an
|
|
135
|
+
* absent entry gives. Failing here instead would turn an unreadable catalog into "no secret
|
|
136
|
+
* resolves", and the catalog is already validated by its own checks.
|
|
137
|
+
*
|
|
138
|
+
* @returns {Object|null}
|
|
139
|
+
*/
|
|
140
|
+
function loadCatalogQuietly() {
|
|
141
|
+
try {
|
|
142
|
+
return require('./infra-parameter-catalog').getInfraParameterCatalog();
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The name an environment-isolated installation must read.
|
|
150
|
+
*
|
|
151
|
+
* `computePhysicalSecretName` answers what the catalog declares. This answers what an
|
|
152
|
+
* environment may actually use, and the two differ in one place that matters.
|
|
153
|
+
*
|
|
154
|
+
* **An application-scoped secret is never shared between environments.** Each environment's
|
|
155
|
+
* application holds its own credential. Some catalog entries still pin a literal from before
|
|
156
|
+
* environments existed — `dataplane-client-secretKeyVault` names itself, with no
|
|
157
|
+
* `{environment}` anywhere — and honouring that in `pro` would hand `pro` the credential
|
|
158
|
+
* `dev` is using. One leaked client secret would then authenticate as the other environment's
|
|
159
|
+
* application, which is the whole isolation gone.
|
|
160
|
+
*
|
|
161
|
+
* So an app-scoped name that does not carry its environment gets one. System names
|
|
162
|
+
* (`keycloak-*`, `acr-*`) are left exactly as they are: there is one Keycloak and one
|
|
163
|
+
* registry per platform, and those secrets are shared on purpose.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} key - Logical catalog key
|
|
166
|
+
* @param {string} environment - Environment key
|
|
167
|
+
* @returns {string} The secret name this environment must read
|
|
168
|
+
*/
|
|
169
|
+
function resolveStrictSecretName(key, environment) {
|
|
170
|
+
const logicalKey = String(key ?? '').trim();
|
|
171
|
+
const env = String(environment ?? '').trim().toLowerCase();
|
|
172
|
+
if (!logicalKey || !env) {
|
|
173
|
+
return logicalKey;
|
|
174
|
+
}
|
|
175
|
+
const physical = computePhysicalSecretName(logicalKey, { environment: env });
|
|
176
|
+
if (physical.startsWith(`${env}-`)) {
|
|
177
|
+
return physical;
|
|
178
|
+
}
|
|
179
|
+
const entry = findCatalogEntry(logicalKey);
|
|
180
|
+
// Unplaced keys are this installation's own and are scoped by environment; `system`,
|
|
181
|
+
// `infra` and `shared` entries are platform-wide and keep their names.
|
|
182
|
+
if (!entry || entry.scope === 'app') {
|
|
183
|
+
return `${env}-${physical}`;
|
|
184
|
+
}
|
|
185
|
+
return physical;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
extractDatabaseIndex,
|
|
190
|
+
extractDatabaseAppKey,
|
|
191
|
+
applyVaultNamePattern,
|
|
192
|
+
computePhysicalSecretName,
|
|
193
|
+
resolveStrictSecretName
|
|
194
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generate linked catalog RSA secrets without rotating existing identities.
|
|
3
|
+
*/
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
|
|
6
|
+
|
|
7
|
+
/** @param {*} value @returns {boolean} Whether a stored value is populated. */
|
|
8
|
+
function populated(value) {
|
|
9
|
+
return value !== undefined && value !== null && String(value).trim() !== '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read a stored key without exposing crypto errors or key material.
|
|
14
|
+
* @param {string} value
|
|
15
|
+
* @param {string} encryptionKey
|
|
16
|
+
* @param {string} pairId
|
|
17
|
+
* @returns {import('crypto').KeyObject}
|
|
18
|
+
*/
|
|
19
|
+
function readPrivateKey(value, encryptionKey, pairId) {
|
|
20
|
+
try {
|
|
21
|
+
const pem = isEncrypted(value) ? decryptSecret(value, encryptionKey) : value;
|
|
22
|
+
const key = crypto.createPrivateKey(pem);
|
|
23
|
+
if (key.asymmetricKeyType !== 'rsa') throw new Error('Not RSA');
|
|
24
|
+
return key;
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(`Cannot generate RSA pair "${pairId}": existing private key cannot be read as RSA. Restore a valid private key before retrying.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pairs whose private half belongs to Mori, not to an installation.
|
|
32
|
+
*
|
|
33
|
+
* Builder must never mint these. Licence trust is a two-signature chain, and both links
|
|
34
|
+
* exist so that substituting a key fails rather than silently succeeding:
|
|
35
|
+
*
|
|
36
|
+
* 1. Mori signs the licence. The installation verifies it against the public key held on
|
|
37
|
+
* disk, so a licence from anywhere else does not verify.
|
|
38
|
+
* 2. That on-disk material is itself validated against a key in Key Vault. The Key Vault is
|
|
39
|
+
* provisioned by the Marketplace deployment and readable only by Miso's managed
|
|
40
|
+
* identity — nobody can change it, including us — so swapping the on-disk public key for
|
|
41
|
+
* one whose private half an attacker holds fails at the second check.
|
|
42
|
+
*
|
|
43
|
+
* The second link is an **Azure** guarantee. A Docker installation has no Key Vault and no
|
|
44
|
+
* managed identity, so locally the chain is the pinned public key alone. Closing that is
|
|
45
|
+
* online validation against Mori, which is a known gap deferred on purpose: the delivery
|
|
46
|
+
* target is Azure, and Docker exists to validate the shape of that stack.
|
|
47
|
+
*
|
|
48
|
+
* It does not soften this rule. The one environment lacking the second check is the last
|
|
49
|
+
* one that should mint its own keys, and a locally generated pair would make local
|
|
50
|
+
* behaviour diverge from the Azure stack it exists to mirror.
|
|
51
|
+
*
|
|
52
|
+
* Generating the pair locally breaks both links at once: the installation would hold the
|
|
53
|
+
* private half of the key it verifies against, so it could sign a licence it then accepts,
|
|
54
|
+
* and the material would no longer correspond to what Key Vault validates. 227.1 pinned
|
|
55
|
+
* this key "precisely because a key travelling inside the token it verifies proves
|
|
56
|
+
* nothing"; a self-generated key proves exactly as little.
|
|
57
|
+
*
|
|
58
|
+
* The public half reaches an installation through the `moriLicensePublicKey` deployment
|
|
59
|
+
* parameter, which the installation contract already classifies as registration-supplied.
|
|
60
|
+
* An installation with neither half stays unpinned and refuses every licence, which is the
|
|
61
|
+
* documented and correct failure.
|
|
62
|
+
*/
|
|
63
|
+
const ISSUER_SUPPLIED_PAIR_IDS = Object.freeze(['mori-license-signing', 'mori-policy-signing']);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {Object} entry - Catalog entry
|
|
67
|
+
* @returns {boolean} True when the pair is supplied by Mori rather than generated here
|
|
68
|
+
*/
|
|
69
|
+
function isIssuerSuppliedPair(entry) {
|
|
70
|
+
return ISSUER_SUPPLIED_PAIR_IDS.includes(entry?.generator?.pairId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate missing parts for one catalog pair, preserving existing key material.
|
|
75
|
+
* @param {Object[]} entries
|
|
76
|
+
* @param {Object} existing
|
|
77
|
+
* @param {string} encryptionKey
|
|
78
|
+
* @returns {Object} Missing values, including the linked counterpart when declared.
|
|
79
|
+
*/
|
|
80
|
+
function generatePair(entries, existing, encryptionKey) {
|
|
81
|
+
const pairId = entries[0].generator.pairId;
|
|
82
|
+
const privateEntry = entries.find(e => e.generator.part === 'private');
|
|
83
|
+
const publicEntries = entries.filter(e => e.generator.part === 'public');
|
|
84
|
+
if (!privateEntry) throw new Error(`Cannot generate RSA pair "${pairId}": catalog has no private key entry.`);
|
|
85
|
+
const storedPrivate = existing[privateEntry.key];
|
|
86
|
+
const hasPublic = publicEntries.some(e => populated(existing[e.key]));
|
|
87
|
+
if (!populated(storedPrivate) && hasPublic) {
|
|
88
|
+
throw new Error(`Cannot generate RSA pair "${pairId}": public key exists but private key is missing. Restore the matching private key before retrying.`);
|
|
89
|
+
}
|
|
90
|
+
const privateKey = populated(storedPrivate)
|
|
91
|
+
? readPrivateKey(storedPrivate, encryptionKey, pairId)
|
|
92
|
+
: crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }).privateKey;
|
|
93
|
+
const publicKey = crypto.createPublicKey(privateKey).export({ type: 'spki', format: 'pem' });
|
|
94
|
+
const values = {};
|
|
95
|
+
if (!populated(storedPrivate)) {
|
|
96
|
+
values[privateEntry.key] = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
97
|
+
}
|
|
98
|
+
for (const entry of publicEntries) {
|
|
99
|
+
if (!populated(existing[entry.key])) values[entry.key] = publicKey;
|
|
100
|
+
}
|
|
101
|
+
return values;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Prepare linked RSA values for a single write operation; never cache identities globally.
|
|
106
|
+
* @param {string[]} keys - Missing keys requested by the caller
|
|
107
|
+
* @param {Object} [existing] - Existing or explicitly supplied values (plain or encrypted)
|
|
108
|
+
* @param {string} [encryptionKey] - Local encryption key
|
|
109
|
+
* @returns {Object} Newly generated values only
|
|
110
|
+
*/
|
|
111
|
+
function generateRsaSecretValues(keys, existing = {}, encryptionKey) {
|
|
112
|
+
let catalog;
|
|
113
|
+
try {
|
|
114
|
+
catalog = require('./infra-parameter-catalog').getInfraParameterCatalog();
|
|
115
|
+
} catch {
|
|
116
|
+
return {}; // Ordinary generation retains its existing catalog-unavailable handling.
|
|
117
|
+
}
|
|
118
|
+
const values = {};
|
|
119
|
+
for (const key of keys) {
|
|
120
|
+
if (populated(existing[key]) || populated(values[key])) continue;
|
|
121
|
+
const entry = catalog.findEntryForKey(key);
|
|
122
|
+
if (entry?.generator?.type !== 'rsaKeyPair') continue;
|
|
123
|
+
// Supplied by Mori at deployment and validated against Key Vault. Generating it here
|
|
124
|
+
// would hand the installation the private half of the key it verifies licences against.
|
|
125
|
+
if (isIssuerSuppliedPair(entry)) continue;
|
|
126
|
+
const entries = catalog.data.parameters.filter(e => e.key &&
|
|
127
|
+
e.generator?.type === 'rsaKeyPair' && e.generator.pairId === entry.generator.pairId);
|
|
128
|
+
Object.assign(values, generatePair(entries, { ...existing, ...values }, encryptionKey));
|
|
129
|
+
}
|
|
130
|
+
return values;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The rsaKeyPair branch, which is the only one that can refuse.
|
|
135
|
+
*
|
|
136
|
+
* Builder never mints a pair whose private half belongs to the issuer: an installation
|
|
137
|
+
* holding it could sign a licence it would then accept. Miso's catalog has since changed
|
|
138
|
+
* these three entries to `emptyAllowed`, so this is now a guard against the declaration
|
|
139
|
+
* coming back rather than a branch the shipped catalog reaches.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} key - Secret key
|
|
142
|
+
* @param {Object} catalogEntry - Catalog entry
|
|
143
|
+
* @returns {string|undefined}
|
|
144
|
+
* @throws {Error} When the pair is issuer-supplied
|
|
145
|
+
*/
|
|
146
|
+
function generateRsaValueFromCatalog(key, catalogEntry) {
|
|
147
|
+
if (isIssuerSuppliedPair(catalogEntry)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Secret "${key}" is supplied by Mori and is never generated here. Licence trust is a ` +
|
|
150
|
+
'two-signature chain: Mori signs the licence, which is verified against the on-disk ' +
|
|
151
|
+
'public key, and that material is validated against a Key Vault key readable only by ' +
|
|
152
|
+
'Miso\'s managed identity. The public half reaches an installation through the ' +
|
|
153
|
+
'moriLicensePublicKey deployment parameter. An installation with neither stays ' +
|
|
154
|
+
'unpinned and refuses every licence, which is the intended failure.'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return generateRsaSecretValues([key])[key];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
generateRsaSecretValues,
|
|
162
|
+
generateRsaValueFromCatalog,
|
|
163
|
+
isIssuerSuppliedPair,
|
|
164
|
+
ISSUER_SUPPLIED_PAIR_IDS
|
|
165
|
+
};
|
|
@@ -45,6 +45,30 @@ function listChannelArtifacts(ctx) {
|
|
|
45
45
|
return { artifacts };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Locate the rendered setup document.
|
|
50
|
+
*
|
|
51
|
+
* `dist/channels/<target>/` is build output and is gitignored, so anything served only from
|
|
52
|
+
* there disappears on a clean, a rebuild, or in a fresh workspace — which is what made the
|
|
53
|
+
* portal report "Setup document not available" after an install that had succeeded. The
|
|
54
|
+
* durable copy lives beside the channel's other committed files under
|
|
55
|
+
* `integration/<systemKey>/`, so that is preferred and `dist/` is only a fallback for a
|
|
56
|
+
* package built but not yet persisted.
|
|
57
|
+
*
|
|
58
|
+
* @param {import('./workspace-context').WorkspaceContext} ctx - Workspace context
|
|
59
|
+
* @param {string} root - User root
|
|
60
|
+
* @param {string} target - Channel target / system key
|
|
61
|
+
* @returns {string} Absolute path, or '' when no copy exists
|
|
62
|
+
*/
|
|
63
|
+
function resolveInstallGuidePath(ctx, root, target) {
|
|
64
|
+
const candidates = [];
|
|
65
|
+
if (typeof ctx.getIntegrationPath === 'function') {
|
|
66
|
+
candidates.push(path.join(ctx.getIntegrationPath(), 'INSTALL.md'));
|
|
67
|
+
}
|
|
68
|
+
candidates.push(path.join(path.dirname(getArtifactOutputPath(root, target)), 'INSTALL.md'));
|
|
69
|
+
return candidates.find(candidate => fs.existsSync(candidate)) || '';
|
|
70
|
+
}
|
|
71
|
+
|
|
48
72
|
/**
|
|
49
73
|
* @param {import('./workspace-context').WorkspaceContext} ctx
|
|
50
74
|
* @param {string} artifactId
|
|
@@ -68,8 +92,8 @@ function resolveArtifactFile(ctx, artifactId) {
|
|
|
68
92
|
}
|
|
69
93
|
}
|
|
70
94
|
if (id === 'installMd') {
|
|
71
|
-
const absPath =
|
|
72
|
-
if (
|
|
95
|
+
const absPath = resolveInstallGuidePath(ctx, root, target);
|
|
96
|
+
if (absPath) {
|
|
73
97
|
return { absPath, contentType: 'text/markdown; charset=utf-8', fileName: 'INSTALL.md' };
|
|
74
98
|
}
|
|
75
99
|
}
|
|
@@ -131,6 +155,8 @@ async function handleChannelCallback(ctx, provider, query, body, deps = {}) {
|
|
|
131
155
|
|
|
132
156
|
module.exports = {
|
|
133
157
|
listChannelArtifacts,
|
|
158
|
+
resolveArtifactFile,
|
|
159
|
+
resolveInstallGuidePath,
|
|
134
160
|
getChannelArtifact,
|
|
135
161
|
handleChannelCallback,
|
|
136
162
|
storeChannelCallbackCredential
|
|
@@ -119,7 +119,9 @@ function generatePackageArtifact(ctx, body, channelTarget, branding, dataplaneUr
|
|
|
119
119
|
projectRoot: ctx.getUserRoot(),
|
|
120
120
|
channelTarget,
|
|
121
121
|
runtimeValues,
|
|
122
|
-
dryRun: body.dryRun === true
|
|
122
|
+
dryRun: body.dryRun === true,
|
|
123
|
+
// Persist the setup document beside the channel's committed files; dist/ is disposable.
|
|
124
|
+
durableDir: workspaceSystemDir(ctx)
|
|
123
125
|
});
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -27,6 +27,38 @@ const WORKSPACE_INTEGRATION_FORMAT = 'json';
|
|
|
27
27
|
* @property {string} bearerToken
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The root a given environment's work lives under.
|
|
32
|
+
*
|
|
33
|
+
* `tst` and `pro` get a path segment of their own so an environment's work is separable and
|
|
34
|
+
* can be deleted on its own; `dev` keeps the existing layout, so no in-flight workspace moves
|
|
35
|
+
* and nothing that already holds a path stops resolving.
|
|
36
|
+
*
|
|
37
|
+
* Because `dev` is unprefixed, a user id equal to a promotion-only environment name would
|
|
38
|
+
* make `<root>/pro` mean two things — that environment, or that user's dev work. `userId`
|
|
39
|
+
* comes from a token and only has to avoid traversal to pass validation, so the collision is
|
|
40
|
+
* refused here rather than assumed away.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} workspaceRoot - Resolved WORKSPACE_ROOT
|
|
43
|
+
* @param {string} userId - Validated user id
|
|
44
|
+
* @param {string} [environment] - Environment key
|
|
45
|
+
* @returns {string} Root for this environment
|
|
46
|
+
* @throws {Error} When the user id would collide with an environment segment
|
|
47
|
+
*/
|
|
48
|
+
function resolveEnvironmentWorkspaceRoot(workspaceRoot, userId, environment) {
|
|
49
|
+
const { isPromotionOnlyEnvironment } = require('../core/environment-mode-policy');
|
|
50
|
+
if (isPromotionOnlyEnvironment(userId)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`userId "${userId}" collides with an environment workspace segment; it cannot be an ` +
|
|
53
|
+
'environment name'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (!isPromotionOnlyEnvironment(environment)) {
|
|
57
|
+
return workspaceRoot;
|
|
58
|
+
}
|
|
59
|
+
return path.join(workspaceRoot, String(environment).trim().toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
|
|
30
62
|
/**
|
|
31
63
|
* @param {string} segment
|
|
32
64
|
* @returns {string}
|
|
@@ -63,7 +95,9 @@ function createWorkspaceContext(params) {
|
|
|
63
95
|
}
|
|
64
96
|
|
|
65
97
|
const workspaceRoot = resolveWorkspaceRoot(params.workspaceRoot);
|
|
66
|
-
const
|
|
98
|
+
const environment = params.environment || process.env.DEFAULT_ENVIRONMENT || 'dev';
|
|
99
|
+
const environmentRoot = resolveEnvironmentWorkspaceRoot(workspaceRoot, userId, environment);
|
|
100
|
+
const userRoot = path.join(environmentRoot, userId);
|
|
67
101
|
const integrationRoot = path.join(userRoot, 'integration');
|
|
68
102
|
const useExplicitIntegration = Boolean(params.integrationPath);
|
|
69
103
|
const integrationPath = useExplicitIntegration
|
|
@@ -82,7 +116,7 @@ function createWorkspaceContext(params) {
|
|
|
82
116
|
userId,
|
|
83
117
|
workspaceRoot,
|
|
84
118
|
systemKey,
|
|
85
|
-
environment
|
|
119
|
+
environment,
|
|
86
120
|
controllerUrl:
|
|
87
121
|
params.controllerUrl ||
|
|
88
122
|
process.env.MISO_CONTROLLER_URL ||
|
|
@@ -149,5 +183,6 @@ module.exports = {
|
|
|
149
183
|
runWithWorkspaceContext,
|
|
150
184
|
ensureIntegrationDirectory,
|
|
151
185
|
resolveWorkspaceRoot,
|
|
186
|
+
resolveEnvironmentWorkspaceRoot,
|
|
152
187
|
assertPathContained
|
|
153
188
|
};
|
|
@@ -593,31 +593,31 @@ parameters:
|
|
|
593
593
|
azure:
|
|
594
594
|
notes: Not a typical Key Vault name; local optional pypi token.
|
|
595
595
|
|
|
596
|
-
#
|
|
596
|
+
# Miso Controller ↔ controller OAuth. The Controller issues both values at registration;
|
|
597
|
+
# local infrastructure must leave them empty until application resolution retrieves them.
|
|
597
598
|
- key: miso-controller-client-idKeyVault
|
|
598
599
|
ownerAppKey: miso-controller
|
|
599
600
|
scope: app
|
|
600
601
|
sensitive: false
|
|
601
602
|
generator:
|
|
602
|
-
type:
|
|
603
|
-
|
|
604
|
-
ensureOn: [upInfra, resolveApp]
|
|
603
|
+
type: emptyString
|
|
604
|
+
ensureOn: [resolveApp]
|
|
605
605
|
azure:
|
|
606
606
|
legacyVaultSecretNames:
|
|
607
607
|
- '{serviceName}-miso-clientIdKeyVault'
|
|
608
608
|
notes: >-
|
|
609
|
-
Per-app OAuth client id
|
|
609
|
+
Per-app OAuth client id issued when miso-controller is registered with Miso.
|
|
610
610
|
|
|
611
611
|
- key: miso-controller-client-secretKeyVault
|
|
612
612
|
ownerAppKey: miso-controller
|
|
613
613
|
scope: app
|
|
614
614
|
generator:
|
|
615
|
-
type:
|
|
616
|
-
ensureOn: [
|
|
615
|
+
type: emptyString
|
|
616
|
+
ensureOn: [resolveApp]
|
|
617
617
|
azure:
|
|
618
618
|
notes: >-
|
|
619
|
-
Per-app OAuth client secret
|
|
620
|
-
|
|
619
|
+
Per-app OAuth client secret issued when miso-controller is registered with Miso and used
|
|
620
|
+
for service-to-service auth (MISO_CLIENTSECRET). Never generated by local infrastructure.
|
|
621
621
|
|
|
622
622
|
# Builder API ↔ controller OAuth. These are explicit declarations rather than catch-all
|
|
623
623
|
# matches so the backup catalog records the owner and canonical names unambiguously.
|
|
@@ -644,28 +644,41 @@ parameters:
|
|
|
644
644
|
notes: Per-app OAuth client secret issued when builder-api is registered with Miso.
|
|
645
645
|
|
|
646
646
|
# Dataplane ↔ controller OAuth (builder/dataplane env.template MISO_CLIENTID / MISO_CLIENTSECRET).
|
|
647
|
+
# Plan 265.0: issued by the Controller at registration, so the catalog must not produce a
|
|
648
|
+
# value for it. The previous `literal` on the `upInfra` hook meant Builder generated an id
|
|
649
|
+
# the Controller owns — and the literal was `miso-controller-dev-dataplane`, with `dev`
|
|
650
|
+
# baked into an entry that is not dev-specific. `emptyString` on `resolveApp` is the shape
|
|
651
|
+
# `builder-api-client-idKeyVault` already uses for the same relationship.
|
|
647
652
|
- key: dataplane-client-idKeyVault
|
|
648
653
|
ownerAppKey: dataplane
|
|
649
654
|
scope: app
|
|
650
655
|
sensitive: false
|
|
651
656
|
generator:
|
|
652
|
-
type:
|
|
653
|
-
|
|
654
|
-
ensureOn: [upInfra, resolveApp]
|
|
657
|
+
type: emptyString
|
|
658
|
+
ensureOn: [resolveApp]
|
|
655
659
|
azure:
|
|
656
660
|
notes: >-
|
|
657
|
-
Per-app OAuth client id
|
|
658
|
-
|
|
659
|
-
|
|
661
|
+
Per-app OAuth client id issued when dataplane is registered with Miso. Not generated
|
|
662
|
+
locally: the Controller owns it, and a locally invented id is one the Controller has
|
|
663
|
+
never seen.
|
|
664
|
+
|
|
665
|
+
# Plan 265.0: the Controller issues this secret, so a `randomBytes32` generator on the
|
|
666
|
+
# `upInfra` hook had Builder inventing an OAuth secret the Controller owns. That invented
|
|
667
|
+
# value is what defeated the "credentials missing locally" guard and surfaced as a
|
|
668
|
+
# misleading auth error rather than a clear one. Matches
|
|
669
|
+
# `builder-api-client-secretKeyVault`.
|
|
660
670
|
- key: dataplane-client-secretKeyVault
|
|
661
671
|
ownerAppKey: dataplane
|
|
662
672
|
scope: app
|
|
663
673
|
generator:
|
|
664
|
-
type:
|
|
665
|
-
ensureOn: [
|
|
674
|
+
type: emptyString
|
|
675
|
+
ensureOn: [resolveApp]
|
|
666
676
|
azure:
|
|
667
677
|
vaultSecretName: dataplane-client-secretKeyVault
|
|
668
|
-
notes:
|
|
678
|
+
notes: >-
|
|
679
|
+
Per-app OAuth client secret issued when dataplane is registered with Miso. Never
|
|
680
|
+
generated locally; it is fetched from the Controller, which is the only party that
|
|
681
|
+
can issue one it will accept.
|
|
669
682
|
|
|
670
683
|
# Mori / telemetry is optional for an unlicensed Community installation.
|
|
671
684
|
- key: mori-controller-api-keyKeyVault
|
|
@@ -706,10 +719,17 @@ parameters:
|
|
|
706
719
|
#
|
|
707
720
|
# Rotating an installation that already holds a value here means clearing BOTH halves
|
|
708
721
|
# together — the generator is get-or-create, so replacing one alone leaves a mismatched pair.
|
|
722
|
+
# Supplied, never generated. Mori owns this pair and keeps the private half; an installation
|
|
723
|
+
# that minted it would hold the private half of the key it verifies against and could sign a
|
|
724
|
+
# licence it would then accept. Builder refuses these pairs (aifabrix-builder 211.0 /
|
|
725
|
+
# 5bfeb06d); `rsaKeyPair` here made Miso mint them anyway, because catalog-resolver runs the
|
|
726
|
+
# generator unconditionally and never consults valueSource. `emptyAllowed` is what stops it:
|
|
727
|
+
# the materializer skips isEmptyByDesign rows, so Miso no longer writes this secret at all.
|
|
728
|
+
# It arrives instead as a deploy parameter -- packages.json -> main.bicep ->
|
|
729
|
+
# 08_keyvault-secrets.bicep -> the installation vault -- which is also the only writer that
|
|
730
|
+
# can hold Mori's real key rather than a locally invented one.
|
|
709
731
|
generator:
|
|
710
|
-
type:
|
|
711
|
-
pairId: mori-license-signing
|
|
712
|
-
part: public
|
|
732
|
+
type: emptyAllowed
|
|
713
733
|
ensureOn: [upInfra, resolveApp]
|
|
714
734
|
azure:
|
|
715
735
|
vaultSecretName: mori-license-public-keyKeyVault
|
|
@@ -817,10 +837,17 @@ parameters:
|
|
|
817
837
|
ownerAppKey: mori-controller
|
|
818
838
|
scope: app
|
|
819
839
|
sensitive: true
|
|
840
|
+
# Supplied, never generated. Mori owns this pair and keeps the private half; an installation
|
|
841
|
+
# that minted it would hold the private half of the key it verifies against and could sign a
|
|
842
|
+
# licence it would then accept. Builder refuses these pairs (aifabrix-builder 211.0 /
|
|
843
|
+
# 5bfeb06d); `rsaKeyPair` here made Miso mint them anyway, because catalog-resolver runs the
|
|
844
|
+
# generator unconditionally and never consults valueSource. `emptyAllowed` is what stops it:
|
|
845
|
+
# the materializer skips isEmptyByDesign rows, so Miso no longer writes this secret at all.
|
|
846
|
+
# It arrives instead as a deploy parameter -- packages.json -> main.bicep ->
|
|
847
|
+
# 08_keyvault-secrets.bicep -> the installation vault -- which is also the only writer that
|
|
848
|
+
# can hold Mori's real key rather than a locally invented one.
|
|
820
849
|
generator:
|
|
821
|
-
type:
|
|
822
|
-
pairId: mori-policy-signing
|
|
823
|
-
part: private
|
|
850
|
+
type: emptyAllowed
|
|
824
851
|
ensureOn: [resolveApp]
|
|
825
852
|
azure:
|
|
826
853
|
vaultSecretName: secrets-policyPrivateKeyVault
|
|
@@ -857,10 +884,17 @@ parameters:
|
|
|
857
884
|
# Mori and the dataplane must not share a signing identity. Sharing one means the dataplane
|
|
858
885
|
# holds a key that mints licences, and the two services have completely different blast
|
|
859
886
|
# radii. Mori is the licence server; the dataplane runs customer workloads.
|
|
887
|
+
# Supplied, never generated. Mori owns this pair and keeps the private half; an installation
|
|
888
|
+
# that minted it would hold the private half of the key it verifies against and could sign a
|
|
889
|
+
# licence it would then accept. Builder refuses these pairs (aifabrix-builder 211.0 /
|
|
890
|
+
# 5bfeb06d); `rsaKeyPair` here made Miso mint them anyway, because catalog-resolver runs the
|
|
891
|
+
# generator unconditionally and never consults valueSource. `emptyAllowed` is what stops it:
|
|
892
|
+
# the materializer skips isEmptyByDesign rows, so Miso no longer writes this secret at all.
|
|
893
|
+
# It arrives instead as a deploy parameter -- packages.json -> main.bicep ->
|
|
894
|
+
# 08_keyvault-secrets.bicep -> the installation vault -- which is also the only writer that
|
|
895
|
+
# can hold Mori's real key rather than a locally invented one.
|
|
860
896
|
generator:
|
|
861
|
-
type:
|
|
862
|
-
pairId: mori-license-signing
|
|
863
|
-
part: private
|
|
897
|
+
type: emptyAllowed
|
|
864
898
|
ensureOn: [resolveApp]
|
|
865
899
|
azure:
|
|
866
900
|
vaultSecretName: secrets-licensePrivateKeyVault
|