@aifabrix/builder 2.62.0 → 2.63.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/lib/api/onboarding-status.api.js +117 -0
- package/lib/app/register.js +24 -2
- package/lib/app/run-env-compose.js +39 -2
- package/lib/app/run-env-recovery.js +1 -0
- package/lib/app/run-helpers.js +1 -1
- package/lib/cli/infra-guided.js +30 -2
- package/lib/cli/setup-platform.js +18 -3
- package/lib/commands/setup-modes.js +2 -1
- package/lib/commands/setup-onboarding-readiness.js +83 -0
- package/lib/commands/setup.js +32 -10
- package/lib/commands/up-dataplane-bootstrap.js +115 -38
- package/lib/commands/up-dataplane.js +4 -31
- package/lib/core/local-env-overrides.js +23 -2
- package/lib/deployment/installation/index.js +4 -1
- package/lib/deployment/installation/infra-catalog.js +0 -2
- package/lib/deployment/installation/local-environment-authorize.js +221 -0
- package/lib/deployment/installation/local-environment-snapshot.js +124 -0
- package/lib/deployment/installation/local-environment-stage.js +43 -66
- package/lib/generator/index.js +32 -9
- package/lib/parameters/rsa-secret-values.js +14 -31
- package/lib/schema/infra.parameter.yaml +2 -45
- package/lib/schema/infrastructure-schema.json +0 -58
- package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
- package/lib/utils/log-redaction.js +4 -1
- package/package.json +2 -1
- package/templates/README.md +6 -0
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/mirrored-templates.json +2 -2
- package/templates/applications/miso-controller/application.yaml +18 -1
- package/templates/applications/miso-controller/env.template +3 -21
- package/templates/applications/miso-controller/rbac.yaml +10 -6
- package/templates/marketplace/main.json +5 -69
|
@@ -19,6 +19,10 @@ const logger = require('../../utils/logger');
|
|
|
19
19
|
const { resolveDatabaseNamesWithAdoption } = require('../../parameters/database-adoption');
|
|
20
20
|
const { parseDatabaseSecretKey } = require('../../parameters/database-secret-values');
|
|
21
21
|
const { resolveLocalInstallationName } = require('./local-installation');
|
|
22
|
+
const {
|
|
23
|
+
authorizeEnvironmentWithController
|
|
24
|
+
} = require('./local-environment-authorize');
|
|
25
|
+
const { loadEnvironmentBootstrapSnapshot } = require('./local-environment-snapshot');
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* Applications that exist **once per environment**.
|
|
@@ -127,65 +131,6 @@ async function ensureEnvironmentDatabases(envKey, deps) {
|
|
|
127
131
|
return ensured;
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
/**
|
|
131
|
-
* Ask Miso whether this environment may exist, before anything is created.
|
|
132
|
-
*
|
|
133
|
-
* **Editions are Miso's, not Builder's.** Whether a licence permits `tst` or `pro` is not a
|
|
134
|
-
* question this repository answers or caches: it asks, and Miso accepts or declines. Builder
|
|
135
|
-
* holding its own copy of the entitlement rules would be a second answer to drift from the
|
|
136
|
-
* licence, and a local one at that — the easiest possible thing to edit.
|
|
137
|
-
*
|
|
138
|
-
* This runs **first**. A decline after the databases exist would leave an environment's
|
|
139
|
-
* objects behind for an environment that is not licensed, and nothing would later remove
|
|
140
|
-
* them.
|
|
141
|
-
*
|
|
142
|
-
* @async
|
|
143
|
-
* @param {string} envKey - Environment key
|
|
144
|
-
* @param {Object} deps - Injected collaborators
|
|
145
|
-
* @returns {Promise<void>}
|
|
146
|
-
* @throws {Error} When Miso declines, or cannot be reached to ask
|
|
147
|
-
*/
|
|
148
|
-
async function authorizeEnvironmentWithController(envKey, deps) {
|
|
149
|
-
if (typeof deps.authorizeEnvironment === 'function') {
|
|
150
|
-
await deps.authorizeEnvironment(envKey);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const { resolveControllerUrl } = require('../../utils/controller-url');
|
|
154
|
-
const { getOrRefreshDeviceToken } = require('../../utils/token-manager');
|
|
155
|
-
const { createEnvironment } = require('../../api/environments.api');
|
|
156
|
-
|
|
157
|
-
const controllerUrl = await resolveControllerUrl();
|
|
158
|
-
if (!controllerUrl) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`Cannot create ${envKey}: no controller is configured, so whether your licence ` +
|
|
161
|
-
'permits this environment cannot be checked. Run "aifabrix login" first.'
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
|
|
165
|
-
if (!deviceToken || !deviceToken.token) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
`Cannot create ${envKey}: not signed in, so whether your licence permits this ` +
|
|
168
|
-
'environment cannot be checked. Run "aifabrix login" first.'
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
const authConfig = {
|
|
172
|
-
type: 'device',
|
|
173
|
-
token: deviceToken.token,
|
|
174
|
-
controller: deviceToken.controller || controllerUrl
|
|
175
|
-
};
|
|
176
|
-
try {
|
|
177
|
-
await createEnvironment(controllerUrl, authConfig, { key: envKey, environment: envKey });
|
|
178
|
-
} catch (error) {
|
|
179
|
-
// Miso owns the edition rules, so its refusal is reported as given rather than
|
|
180
|
-
// reinterpreted here. Guessing at the reason would put a second, stale explanation of
|
|
181
|
-
// the licence in front of the operator.
|
|
182
|
-
throw new Error(
|
|
183
|
-
`Miso declined environment ${envKey}: ${error.message}\n` +
|
|
184
|
-
'Editions are licensed in Miso. Nothing was created locally.'
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
134
|
/**
|
|
190
135
|
* Workloads `env deploy` promotes into a promotion-only environment.
|
|
191
136
|
*
|
|
@@ -241,24 +186,32 @@ async function promoteWorkloadsForEnvironment(envKey, deps) {
|
|
|
241
186
|
* @param {string} installationKey - Positional installation key (`local` or `local-…`)
|
|
242
187
|
* @param {string} envKey - Validated stage, one of dev|tst|pro
|
|
243
188
|
* @param {Object} [deps] - Injected collaborators
|
|
244
|
-
* @returns {Promise<{ stage: string, vendor: string, installation: string, databases: string[], promoted: string[] }>}
|
|
189
|
+
* @returns {Promise<{ stage: string, vendor: string, installation: string, databases: string[], promoted: string[], snapshot: { environmentId?: string, applicationId?: string }|null }>}
|
|
245
190
|
*/
|
|
246
191
|
async function runLocalEnvironmentStage(installationKey, envKey, deps = {}) {
|
|
247
192
|
const config = deps.config || require('../../core/config');
|
|
248
193
|
const policy = deps.policy || require('../../core/environment-mode-policy');
|
|
194
|
+
const authorize = deps.authorizeEnvironmentWithController || authorizeEnvironmentWithController;
|
|
195
|
+
const loadSnapshot = deps.loadEnvironmentBootstrapSnapshot || loadEnvironmentBootstrapSnapshot;
|
|
196
|
+
|
|
197
|
+
const developerId = await config.getDeveloperId();
|
|
198
|
+
const installation = resolveLocalInstallationName(installationKey, developerId);
|
|
249
199
|
|
|
250
200
|
// Miso first: it owns editions, and it accepts or declines this environment. Everything
|
|
251
|
-
// below creates something, so the licence answer has to come before any of it.
|
|
252
|
-
|
|
201
|
+
// below creates something, so the licence answer has to come before any of it. Local
|
|
202
|
+
// create uses the same configuration fields as Azure (preset, serviceName, location).
|
|
203
|
+
await authorize(envKey, {
|
|
204
|
+
...deps,
|
|
205
|
+
installationKey,
|
|
206
|
+
serviceName: installation,
|
|
207
|
+
preset: deps.preset
|
|
208
|
+
});
|
|
253
209
|
|
|
254
210
|
// Then the local precondition. tst and pro are unreachable outside the front-door mode,
|
|
255
211
|
// so creating their databases would leave resources behind for an environment that cannot
|
|
256
212
|
// serve a request.
|
|
257
213
|
await policy.assertEnvironmentAllowedForCurrentMode(envKey);
|
|
258
214
|
|
|
259
|
-
const developerId = await config.getDeveloperId();
|
|
260
|
-
const installation = resolveLocalInstallationName(installationKey, developerId);
|
|
261
|
-
|
|
262
215
|
logger.log(`Deploying environment ${envKey} into local installation ${installation}`);
|
|
263
216
|
|
|
264
217
|
// Databases before containers: a workload that starts without its database fails on its
|
|
@@ -271,7 +224,30 @@ async function runLocalEnvironmentStage(installationKey, envKey, deps = {}) {
|
|
|
271
224
|
logger.log(`Promoted into ${envKey}: ${promoted.join(', ')}`);
|
|
272
225
|
}
|
|
273
226
|
|
|
274
|
-
|
|
227
|
+
const snapshot = await summarizeEnvironmentSnapshot(envKey, loadSnapshot, deps);
|
|
228
|
+
return { stage: envKey, vendor: 'DOCKER', installation, databases, promoted, snapshot };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Load this environment's own bootstrap snapshot after the dataplane can register.
|
|
233
|
+
*
|
|
234
|
+
* @async
|
|
235
|
+
* @param {string} envKey
|
|
236
|
+
* @param {Function} loadSnapshot
|
|
237
|
+
* @param {Object} deps
|
|
238
|
+
* @returns {Promise<{ environmentId?: string, applicationId?: string }>}
|
|
239
|
+
*/
|
|
240
|
+
async function summarizeEnvironmentSnapshot(envKey, loadSnapshot, deps) {
|
|
241
|
+
const snapshotPayload = await loadSnapshot(envKey, deps);
|
|
242
|
+
const context = (snapshotPayload && snapshotPayload.context) || {};
|
|
243
|
+
logger.log(
|
|
244
|
+
`Loaded configuration snapshot for ${envKey}` +
|
|
245
|
+
(context.environmentId ? ` (environment ${context.environmentId})` : '')
|
|
246
|
+
);
|
|
247
|
+
return {
|
|
248
|
+
environmentId: context.environmentId,
|
|
249
|
+
applicationId: context.applicationId
|
|
250
|
+
};
|
|
275
251
|
}
|
|
276
252
|
|
|
277
253
|
module.exports = {
|
|
@@ -281,5 +257,6 @@ module.exports = {
|
|
|
281
257
|
resolveEnvironmentDatabasePlan,
|
|
282
258
|
ensureEnvironmentDatabases,
|
|
283
259
|
promoteWorkloadsForEnvironment,
|
|
260
|
+
loadEnvironmentBootstrapSnapshot,
|
|
284
261
|
runLocalEnvironmentStage
|
|
285
262
|
};
|
package/lib/generator/index.js
CHANGED
|
@@ -22,6 +22,22 @@ const { generateControllerManifest } = require('./external-controller-manifest')
|
|
|
22
22
|
const { resolveVersionForApp } = require('../utils/image-version');
|
|
23
23
|
const { getContainerPort } = require('../utils/port-resolver');
|
|
24
24
|
const { buildEnvVarMap } = require('../utils/env-map');
|
|
25
|
+
const config = require('../core/config');
|
|
26
|
+
const {
|
|
27
|
+
resolveDimensionAbacSetting,
|
|
28
|
+
applyDimensionAbacToConfiguration
|
|
29
|
+
} = require('../utils/dataplane-dimension-abac-setting');
|
|
30
|
+
|
|
31
|
+
async function resolveDimensionAbacValue(appName) {
|
|
32
|
+
if (appName !== 'dataplane') return undefined;
|
|
33
|
+
const userConfig = await config.getConfig();
|
|
34
|
+
return resolveDimensionAbacSetting(userConfig).value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function applyResolvedDimensionAbacToConfiguration(appName, configuration) {
|
|
38
|
+
const value = await resolveDimensionAbacValue(appName);
|
|
39
|
+
if (value !== undefined) applyDimensionAbacToConfiguration(appName, configuration, value);
|
|
40
|
+
}
|
|
25
41
|
const { mergeAdditionalPublishedPorts } = require('../utils/docker-manifest-public-port');
|
|
26
42
|
|
|
27
43
|
/**
|
|
@@ -115,6 +131,13 @@ function substitutePortInDeployment(obj, portNumber) {
|
|
|
115
131
|
}
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
function normalizeDeploymentPort(deployment, effectivePort) {
|
|
135
|
+
if (deployment.port === undefined) return;
|
|
136
|
+
deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
|
|
137
|
+
? parseInt(deployment.port, 10)
|
|
138
|
+
: (typeof deployment.port === 'number' ? deployment.port : effectivePort);
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
/** Regex to find ${VAR} placeholders for env substitution */
|
|
119
142
|
const ENV_VAR_PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
|
|
120
143
|
|
|
@@ -179,6 +202,9 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, optio
|
|
|
179
202
|
// Parse environment variables from template and merge portalInput from application config.
|
|
180
203
|
// A known deployment target resolves manifest {NAME} values; otherwise they stay for the Controller.
|
|
181
204
|
const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
|
|
205
|
+
if (options && options.dimensionAbacValue !== undefined) {
|
|
206
|
+
applyDimensionAbacToConfiguration(appName, configuration, options.dimensionAbacValue);
|
|
207
|
+
}
|
|
182
208
|
|
|
183
209
|
// Build deployment manifest (Controller computes deploymentKey from schema)
|
|
184
210
|
const deployment = builders.buildManifestStructure(appName, variables, configuration, rbac);
|
|
@@ -193,10 +219,7 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, optio
|
|
|
193
219
|
// Replace ${PORT} with port from application.yaml so manifest deploys correctly
|
|
194
220
|
const effectivePort = getEffectivePortForSubstitution(variables, 3000);
|
|
195
221
|
substitutePortInDeployment(deployment, effectivePort);
|
|
196
|
-
|
|
197
|
-
deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
|
|
198
|
-
? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
|
|
199
|
-
}
|
|
222
|
+
normalizeDeploymentPort(deployment, effectivePort);
|
|
200
223
|
|
|
201
224
|
// Resolve ${REDIS_HOST}, ${DB_HOST}, etc. from env-config.yaml so manifest has no unresolved vars
|
|
202
225
|
const envVarMap = options && options.envVarMap;
|
|
@@ -239,14 +262,12 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
|
|
|
239
262
|
app: { ...variables.app, version: resolved.version }
|
|
240
263
|
};
|
|
241
264
|
const configuration = parseEnvironmentVariables(envTemplate, variables, options && options.target);
|
|
265
|
+
await applyResolvedDimensionAbacToConfiguration(appName, configuration);
|
|
242
266
|
const deployment = builders.buildManifestStructure(appName, variablesWithVersion, configuration, rbac);
|
|
243
267
|
|
|
244
268
|
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
245
269
|
substitutePortInDeployment(deployment, effectivePort);
|
|
246
|
-
|
|
247
|
-
deployment.port = typeof deployment.port === 'string' && /^\d+$/.test(deployment.port)
|
|
248
|
-
? parseInt(deployment.port, 10) : (typeof deployment.port === 'number' ? deployment.port : effectivePort);
|
|
249
|
-
}
|
|
270
|
+
normalizeDeploymentPort(deployment, effectivePort);
|
|
250
271
|
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
251
272
|
await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
|
|
252
273
|
substituteEnvVarsInDeployment(deployment, envVarMap);
|
|
@@ -302,10 +323,12 @@ async function writeRegularDeployJson(appName, appPath, appType, options = {}) {
|
|
|
302
323
|
};
|
|
303
324
|
const effectivePort = getEffectivePortForSubstitution(variablesWithVersion, 3000);
|
|
304
325
|
const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
|
|
326
|
+
const dimensionAbacValue = await resolveDimensionAbacValue(appName);
|
|
305
327
|
await mergeAdditionalPublishedPorts(envVarMap, variablesWithVersion);
|
|
306
328
|
const deployment = buildAndValidateDeployment(appName, variablesWithVersion, envTemplate, rbac, {
|
|
307
329
|
envVarMap,
|
|
308
|
-
target: options.target
|
|
330
|
+
target: options.target,
|
|
331
|
+
dimensionAbacValue
|
|
309
332
|
});
|
|
310
333
|
const jsonContent = JSON.stringify(deployment, null, 2);
|
|
311
334
|
fs.writeFileSync(jsonPath, jsonContent, { mode: 0o644 });
|
|
@@ -30,30 +30,14 @@ function readPrivateKey(value, encryptionKey, pairId) {
|
|
|
30
30
|
/**
|
|
31
31
|
* Pairs whose private half belongs to Mori, not to an installation.
|
|
32
32
|
*
|
|
33
|
-
* Builder must never mint these.
|
|
34
|
-
*
|
|
33
|
+
* Builder must never mint these. Generating either half would hand an installation the
|
|
34
|
+
* private key of material it only verifies against, so it could sign a licence it then
|
|
35
|
+
* accepts. 227.1 pinned the verifying key "precisely because a key travelling inside the
|
|
36
|
+
* token it verifies proves nothing"; a self-generated key proves exactly as little.
|
|
35
37
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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.
|
|
38
|
+
* How Miso pins, stores, or cross-checks that public key (Key Vault, Docker secrets file,
|
|
39
|
+
* online Mori check) is Miso's contract — Builder has no code path that inspects a vault
|
|
40
|
+
* or deployment mode. Builder's job stops at: refuse to mint, resolve what was supplied.
|
|
57
41
|
*
|
|
58
42
|
* The public half reaches an installation through the `moriLicensePublicKey` deployment
|
|
59
43
|
* parameter, which the installation contract already classifies as registration-supplied.
|
|
@@ -120,8 +104,8 @@ function generateRsaSecretValues(keys, existing = {}, encryptionKey) {
|
|
|
120
104
|
if (populated(existing[key]) || populated(values[key])) continue;
|
|
121
105
|
const entry = catalog.findEntryForKey(key);
|
|
122
106
|
if (entry?.generator?.type !== 'rsaKeyPair') continue;
|
|
123
|
-
// Supplied by Mori
|
|
124
|
-
//
|
|
107
|
+
// Supplied by Mori; never generated here. Minting it would hand the installation the
|
|
108
|
+
// private half of the key it verifies licences against.
|
|
125
109
|
if (isIssuerSuppliedPair(entry)) continue;
|
|
126
110
|
const entries = catalog.data.parameters.filter(e => e.key &&
|
|
127
111
|
e.generator?.type === 'rsaKeyPair' && e.generator.pairId === entry.generator.pairId);
|
|
@@ -146,12 +130,11 @@ function generateRsaSecretValues(keys, existing = {}, encryptionKey) {
|
|
|
146
130
|
function generateRsaValueFromCatalog(key, catalogEntry) {
|
|
147
131
|
if (isIssuerSuppliedPair(catalogEntry)) {
|
|
148
132
|
throw new Error(
|
|
149
|
-
`Secret "${key}" is supplied by Mori and is never generated here.
|
|
150
|
-
'
|
|
151
|
-
'public
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
'unpinned and refuses every licence, which is the intended failure.'
|
|
133
|
+
`Secret "${key}" is supplied by Mori and is never generated here. An installation that ` +
|
|
134
|
+
'minted it would hold the private half of the key it verifies licences against. The ' +
|
|
135
|
+
'public half reaches an installation through the moriLicensePublicKey deployment ' +
|
|
136
|
+
'parameter. An installation with neither stays unpinned and refuses every licence, ' +
|
|
137
|
+
'which is the intended failure.'
|
|
155
138
|
);
|
|
156
139
|
}
|
|
157
140
|
return generateRsaSecretValues([key])[key];
|
|
@@ -28,10 +28,6 @@ standardUpInfraEnsureKeys:
|
|
|
28
28
|
- databases-dataplane-2-passwordKeyVault
|
|
29
29
|
- databases-dataplane-3-urlKeyVault
|
|
30
30
|
- databases-dataplane-3-passwordKeyVault
|
|
31
|
-
# Miso-controller Entra onboarding (vendor app); required for aifabrix resolve miso-controller
|
|
32
|
-
- entra-callback-uri
|
|
33
|
-
- entra-client-idKeyVault
|
|
34
|
-
- entra-client-secretKeyVault
|
|
35
31
|
parameters:
|
|
36
32
|
- key: postgres-passwordKeyVault
|
|
37
33
|
scope: infra
|
|
@@ -351,47 +347,8 @@ parameters:
|
|
|
351
347
|
|
|
352
348
|
# Miso-controller Entra ID onboarding (vendor multi-tenant app). Empty client/callback values
|
|
353
349
|
# allow local resolve and start-dev; set real values before Entra admin-consent flows.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
sensitive: false
|
|
357
|
-
valueSource: optionalInput
|
|
358
|
-
requiredOnSystemOnboarding: false
|
|
359
|
-
scope: system
|
|
360
|
-
generator:
|
|
361
|
-
type: emptyString
|
|
362
|
-
ensureOn: [upInfra, resolveApp]
|
|
363
|
-
azure:
|
|
364
|
-
notes: >-
|
|
365
|
-
ENTRA_ID_CENTRAL_CALLBACK_URI — Mori web redirect (e.g. …/entra-id-concent/callback);
|
|
366
|
-
empty locally until Mori URL is configured.
|
|
367
|
-
|
|
368
|
-
- key: entra-client-idKeyVault
|
|
369
|
-
category: Microsoft Entra ID
|
|
370
|
-
sensitive: false
|
|
371
|
-
valueSource: optionalInput
|
|
372
|
-
requiredOnSystemOnboarding: false
|
|
373
|
-
scope: system
|
|
374
|
-
generator:
|
|
375
|
-
type: emptyString
|
|
376
|
-
ensureOn: [upInfra, resolveApp]
|
|
377
|
-
azure:
|
|
378
|
-
notes: >-
|
|
379
|
-
Vendor Entra app client id (ENTRA_ID_CLIENT_ID); empty for local dev without Entra flows.
|
|
380
|
-
|
|
381
|
-
- key: entra-client-secretKeyVault
|
|
382
|
-
category: Microsoft Entra ID
|
|
383
|
-
sensitive: true
|
|
384
|
-
valueSource: optionalInput
|
|
385
|
-
requiredOnSystemOnboarding: false
|
|
386
|
-
scope: system
|
|
387
|
-
generator:
|
|
388
|
-
type: emptyString
|
|
389
|
-
ensureOn: [upInfra, resolveApp]
|
|
390
|
-
azure:
|
|
391
|
-
legacyVaultSecretNames:
|
|
392
|
-
- entra-vendor-client-secret
|
|
393
|
-
notes: >-
|
|
394
|
-
Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET); empty for local dev without Entra flows.
|
|
350
|
+
# No entra-callback-uri, entra-client-idKeyVault or entra-client-secretKeyVault: Miso holds no
|
|
351
|
+
# Entra client credentials; Mori owns the registration and every sign-in (plan 248.0).
|
|
395
352
|
|
|
396
353
|
# Keycloak events: webhook signature secret used by miso-controller (KEYCLOAK_EVENTS_SECRET).
|
|
397
354
|
# This must exist on first boot because up-platform runs containers from templates (no prior resolve).
|
|
@@ -509,8 +509,6 @@
|
|
|
509
509
|
"acrResourceId",
|
|
510
510
|
"deployRoleAssignments",
|
|
511
511
|
"moriBaseUrl",
|
|
512
|
-
"entraIdClientId",
|
|
513
|
-
"entraIdClientSecret",
|
|
514
512
|
"entraIdTenantId",
|
|
515
513
|
"licenseEdition",
|
|
516
514
|
"capacityProfile",
|
|
@@ -970,62 +968,6 @@
|
|
|
970
968
|
"encryptionKey": {
|
|
971
969
|
"$ref": "#/definitions/envReference"
|
|
972
970
|
},
|
|
973
|
-
"entraIdClientId": {
|
|
974
|
-
"type": "object",
|
|
975
|
-
"required": [
|
|
976
|
-
"value",
|
|
977
|
-
"source",
|
|
978
|
-
"description"
|
|
979
|
-
],
|
|
980
|
-
"additionalProperties": false,
|
|
981
|
-
"properties": {
|
|
982
|
-
"value": {
|
|
983
|
-
"type": "null"
|
|
984
|
-
},
|
|
985
|
-
"source": {
|
|
986
|
-
"type": "string",
|
|
987
|
-
"minLength": 1
|
|
988
|
-
},
|
|
989
|
-
"description": {
|
|
990
|
-
"type": "string",
|
|
991
|
-
"minLength": 1
|
|
992
|
-
},
|
|
993
|
-
"required": {
|
|
994
|
-
"type": "boolean"
|
|
995
|
-
},
|
|
996
|
-
"requiredAt": {
|
|
997
|
-
"const": "tenantActivation"
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
},
|
|
1001
|
-
"entraIdClientSecret": {
|
|
1002
|
-
"type": "object",
|
|
1003
|
-
"required": [
|
|
1004
|
-
"value",
|
|
1005
|
-
"source",
|
|
1006
|
-
"description"
|
|
1007
|
-
],
|
|
1008
|
-
"additionalProperties": false,
|
|
1009
|
-
"properties": {
|
|
1010
|
-
"value": {
|
|
1011
|
-
"type": "null"
|
|
1012
|
-
},
|
|
1013
|
-
"source": {
|
|
1014
|
-
"type": "string",
|
|
1015
|
-
"minLength": 1
|
|
1016
|
-
},
|
|
1017
|
-
"description": {
|
|
1018
|
-
"type": "string",
|
|
1019
|
-
"minLength": 1
|
|
1020
|
-
},
|
|
1021
|
-
"required": {
|
|
1022
|
-
"type": "boolean"
|
|
1023
|
-
},
|
|
1024
|
-
"requiredAt": {
|
|
1025
|
-
"const": "tenantActivation"
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
},
|
|
1029
971
|
"entraIdTenantId": {
|
|
1030
972
|
"type": "object",
|
|
1031
973
|
"required": [
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve and materialize the Dataplane Dimension ABAC installer setting.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Strict non-secret Dimension ABAC setting helpers
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const SETTING_NAME = 'DIMENSION_ABAC_ENFORCEMENT_ENABLED';
|
|
12
|
+
const CONFIG_KEY = 'dimensionAbacEnforcementEnabled';
|
|
13
|
+
const DATAPLANE_APP = 'dataplane';
|
|
14
|
+
|
|
15
|
+
function settingError(message) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.code = 'DIMENSION_ABAC_SETTING_INVALID';
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseDimensionAbacOption(raw) {
|
|
22
|
+
if (raw === undefined || raw === null) return undefined;
|
|
23
|
+
if (raw === true || raw === false) return raw;
|
|
24
|
+
const value = String(raw).trim().toLowerCase();
|
|
25
|
+
if (value === 'on') return true;
|
|
26
|
+
if (value === 'off') return false;
|
|
27
|
+
throw settingError('Invalid --dimension-abac value. Use on or off.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readStoredValue(userConfig) {
|
|
31
|
+
const applications = userConfig && userConfig.applications;
|
|
32
|
+
if (applications === undefined || applications === null) return undefined;
|
|
33
|
+
if (typeof applications !== 'object' || Array.isArray(applications)) {
|
|
34
|
+
throw settingError('Invalid applications configuration; expected an object.');
|
|
35
|
+
}
|
|
36
|
+
const entry = applications[DATAPLANE_APP];
|
|
37
|
+
if (entry === undefined || entry === null) return undefined;
|
|
38
|
+
if (typeof entry !== 'object' || Array.isArray(entry)) {
|
|
39
|
+
throw settingError('Invalid applications.dataplane configuration; expected an object.');
|
|
40
|
+
}
|
|
41
|
+
if (!Object.prototype.hasOwnProperty.call(entry, CONFIG_KEY)) return undefined;
|
|
42
|
+
if (typeof entry[CONFIG_KEY] !== 'boolean') {
|
|
43
|
+
throw settingError(`Invalid applications.dataplane.${CONFIG_KEY}; expected a boolean.`);
|
|
44
|
+
}
|
|
45
|
+
return entry[CONFIG_KEY];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveDimensionAbacSetting(userConfig, explicitValue) {
|
|
49
|
+
const stored = readStoredValue(userConfig);
|
|
50
|
+
if (explicitValue !== undefined && typeof explicitValue !== 'boolean') {
|
|
51
|
+
throw settingError('Explicit Dimension ABAC selection must be a boolean.');
|
|
52
|
+
}
|
|
53
|
+
if (explicitValue !== undefined) return { value: explicitValue, source: 'explicit' };
|
|
54
|
+
if (stored !== undefined) return { value: stored, source: 'stored' };
|
|
55
|
+
return { value: false, source: 'default' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function persistExplicitDimensionAbacSetting(configApi, value) {
|
|
59
|
+
if (value === undefined) return undefined;
|
|
60
|
+
if (!configApi || typeof configApi.getConfig !== 'function' || typeof configApi.saveConfig !== 'function') {
|
|
61
|
+
throw settingError('Configuration API is unavailable.');
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== 'boolean') {
|
|
64
|
+
throw settingError('Persisted Dimension ABAC selection must be a boolean.');
|
|
65
|
+
}
|
|
66
|
+
const userConfig = await configApi.getConfig();
|
|
67
|
+
readStoredValue(userConfig);
|
|
68
|
+
const applications = { ...(userConfig.applications || {}) };
|
|
69
|
+
const dataplane = { ...(applications[DATAPLANE_APP] || {}), [CONFIG_KEY]: value };
|
|
70
|
+
const nextConfig = { ...userConfig, applications: { ...applications, [DATAPLANE_APP]: dataplane } };
|
|
71
|
+
await configApi.saveConfig(nextConfig);
|
|
72
|
+
return nextConfig;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toDimensionAbacEnvValue(value) {
|
|
76
|
+
if (typeof value !== 'boolean') {
|
|
77
|
+
throw settingError('Dimension ABAC value must be a boolean.');
|
|
78
|
+
}
|
|
79
|
+
return value ? 'true' : 'false';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function applyDimensionAbacToEnvMap(appName, envMap, value) {
|
|
83
|
+
if (appName !== DATAPLANE_APP) return envMap;
|
|
84
|
+
if (!envMap || typeof envMap !== 'object' || Array.isArray(envMap)) {
|
|
85
|
+
throw settingError('Dataplane environment map must be an object.');
|
|
86
|
+
}
|
|
87
|
+
envMap[SETTING_NAME] = toDimensionAbacEnvValue(value);
|
|
88
|
+
return envMap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function applyDimensionAbacToConfiguration(appName, configuration, value) {
|
|
92
|
+
if (appName !== DATAPLANE_APP) return configuration;
|
|
93
|
+
if (!Array.isArray(configuration)) {
|
|
94
|
+
throw settingError('Dataplane deployment configuration must be an array.');
|
|
95
|
+
}
|
|
96
|
+
const matches = configuration.filter(item => item && item.name === SETTING_NAME);
|
|
97
|
+
if (matches.length > 1) {
|
|
98
|
+
throw settingError(`Dataplane deployment configuration contains duplicate ${SETTING_NAME} entries.`);
|
|
99
|
+
}
|
|
100
|
+
const entry = {
|
|
101
|
+
name: SETTING_NAME,
|
|
102
|
+
value: toDimensionAbacEnvValue(value),
|
|
103
|
+
location: 'variable',
|
|
104
|
+
required: false
|
|
105
|
+
};
|
|
106
|
+
if (matches.length === 0) configuration.push(entry);
|
|
107
|
+
else Object.assign(matches[0], entry);
|
|
108
|
+
return configuration;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
SETTING_NAME,
|
|
113
|
+
CONFIG_KEY,
|
|
114
|
+
parseDimensionAbacOption,
|
|
115
|
+
resolveDimensionAbacSetting,
|
|
116
|
+
persistExplicitDimensionAbacSetting,
|
|
117
|
+
toDimensionAbacEnvValue,
|
|
118
|
+
applyDimensionAbacToEnvMap,
|
|
119
|
+
applyDimensionAbacToConfiguration
|
|
120
|
+
};
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
/** Env key patterns that indicate a secret (mask value) — same rules as aifabrix logs <app>. */
|
|
14
14
|
const SECRET_KEY_PATTERN = /password|secret|token|credential|api[_-]?key/i;
|
|
15
15
|
|
|
16
|
+
/** Bounded encryption/master-key names; avoids treating every `key` variable as secret. */
|
|
17
|
+
const ENCRYPTION_KEY_PATTERN = /(?:^|[_-])(?:encryption|master)[_-]?key(?:$|[_-])/i;
|
|
18
|
+
|
|
16
19
|
/** Prefixes to strip before checking key (avoids masking KEYCLOAK_SERVER_URL etc.) */
|
|
17
20
|
const KEY_PREFIXES_TO_STRIP = /^KEYCLOAK_|^KEY_VAULT_/;
|
|
18
21
|
|
|
@@ -50,7 +53,7 @@ function maskEnvLine(line) {
|
|
|
50
53
|
const value = line.slice(eq + 1);
|
|
51
54
|
|
|
52
55
|
const keyForCheck = key.replace(KEY_PREFIXES_TO_STRIP, '');
|
|
53
|
-
const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck);
|
|
56
|
+
const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck) || ENCRYPTION_KEY_PATTERN.test(keyForCheck);
|
|
54
57
|
|
|
55
58
|
const maskedValue = maskUrlEmbeddedCredentials(value);
|
|
56
59
|
const hasUrlCredentials = maskedValue !== value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aifabrix/builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.63.1",
|
|
4
4
|
"description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"check:cli-help": "node scripts/check-cli-help-examples.js",
|
|
43
43
|
"check:mirrored-templates": "node scripts/check-mirrored-templates.js",
|
|
44
44
|
"test:manual": "jest --config jest.config.manual.js --runInBand",
|
|
45
|
+
"test:manual:public": "jest --config jest.config.manual-public.js --runInBand",
|
|
45
46
|
"test:integration": "jest --config jest.config.integration.js --runInBand",
|
|
46
47
|
"test:integration:fixtures": "jest --config jest.config.integration.fixtures.js --runInBand",
|
|
47
48
|
"test:hubspot-wizard": "node integration/hubspot-test/test.js",
|
package/templates/README.md
CHANGED
|
@@ -26,6 +26,12 @@ Application templates are folder-based and located under `templates/applications
|
|
|
26
26
|
| `aifabrix-miso/packages/miso-controller/configs/infra.parameter.yaml` | `lib/schema/infra.parameter.yaml` |
|
|
27
27
|
| `aifabrix-dataplane/builder/dataplane/` (`env.template`, `rbac.yaml`, `application.yaml`) | `templates/applications/dataplane/` |
|
|
28
28
|
|
|
29
|
+
The Dataplane `env.template` is the source of truth for
|
|
30
|
+
`DIMENSION_ABAC_ENFORCEMENT_ENABLED` and its default `false`. Do not hand-edit
|
|
31
|
+
the bundled mirror. `aifabrix setup --dimension-abac on|off` stores an operator
|
|
32
|
+
selection outside the template and overlays the resolved lowercase value during
|
|
33
|
+
local runtime and deployment-manifest materialization.
|
|
34
|
+
|
|
29
35
|
**Schema master (dataplane):** Also copies dataplane `app/schemas/json/` core integration schemas + `type/*` and `configs/deployment-rules.yaml` into `lib/schema/` (same contract as dataplane `make sync-json`). Do not hand-edit those mirrors — edit dataplane SSOT, then sync. `lib/schema/infra.parameter.yaml` is the Miso catalog mirror above; other Builder-owned schemas in `lib/schema/` (wizard and the rest) are left alone.
|
|
30
36
|
|
|
31
37
|
Overrides: `AIFABRIX_MISO_SRC`, `AIFABRIX_DATAPLANE_SRC` / `AIFABRIX_DATAPLANE_ROOT` (repo roots). Install with `aifabrix up-platform` / `up-builder-api` — do not hand-edit the template copies as primary.
|
|
@@ -5,7 +5,7 @@ app:
|
|
|
5
5
|
description: 'Business Transformation services — HTTP orchestration for connected-system workspace lifecycle via Dataplane Enterprise MCP and governance APIs.'
|
|
6
6
|
type: webapp
|
|
7
7
|
language: typescript
|
|
8
|
-
version: 2.
|
|
8
|
+
version: 2.63.1
|
|
9
9
|
|
|
10
10
|
# Image Configuration
|
|
11
11
|
image:
|
|
@@ -5,7 +5,7 @@ app:
|
|
|
5
5
|
description: "AI Fabrix Dataplane is a secure, in-tenant integration and automation layer that supplies governed, normalized, and explainable enterprise data to AI agents. Using CIP as a declarative standard, it enforces RBAC and ABAC, executes integrations, and exposes trusted data via MCP and OpenAPI."
|
|
6
6
|
type: webapp
|
|
7
7
|
language: python # Explicitly specify Python language
|
|
8
|
-
version: 2.0.
|
|
8
|
+
version: 2.0.84
|
|
9
9
|
# Image Configuration
|
|
10
10
|
# Set tag to match your build (example: aifabrix build dataplane -t 1.0.0).
|
|
11
11
|
# Registry is required so the controller can pull the image (avoids docker-not-found on the controller host).
|
|
@@ -44,7 +44,7 @@ API_KEY_BYPASS_ROLES=full-access
|
|
|
44
44
|
|
|
45
45
|
# API Configuration
|
|
46
46
|
API_V1_STR=/api/v1
|
|
47
|
-
VERSION=2.0.
|
|
47
|
+
VERSION=2.0.84
|
|
48
48
|
# Minimum supported aifabrix-builder CLI semver (advertised on /health and /api/v1/health; empty = no gate)
|
|
49
49
|
# MIN_BUILDER_CLI_VERSION=2.45.0
|
|
50
50
|
MIN_BUILDER_CLI_VERSION=2.45.5
|