@aifabrix/builder 2.63.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/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/parameters/rsa-secret-values.js +14 -31
- package/lib/schema/infra.parameter.yaml +2 -45
- package/lib/schema/infrastructure-schema.json +0 -58
- package/package.json +1 -1
- 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
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
|
+
const { buildDevUsernameForFrontDoorHost } = require('../utils/compose-traefik-ingress-base');
|
|
16
|
+
|
|
15
17
|
/** Applications the CLI registers itself through `.../applications/bootstrap`. */
|
|
16
18
|
const CLI_BOOTSTRAPPED_APP_KEYS = Object.freeze(['dataplane']);
|
|
17
19
|
|
|
@@ -43,12 +45,30 @@ const ONBOARDING_APPS_CONFIG_KEY = 'onboardingApps';
|
|
|
43
45
|
* to everybody, which is what happened the last time it carried a value.
|
|
44
46
|
*/
|
|
45
47
|
const MISO_CONTROLLER_LOCAL_ONLY_ENV = Object.freeze({
|
|
46
|
-
INFRASTRUCTURE_NAME: '${DEV_USERNAME}.${DEV_ROOT_DOMAIN}',
|
|
47
48
|
KEYCLOAK_PUBLIC_KEY: '',
|
|
48
49
|
OTEL_SERVICE_VERSION: '',
|
|
49
50
|
ACR_LOGIN_SERVER: ''
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Miso's infrastructure name on a local install: `local-devNN`, the installation name the licence
|
|
55
|
+
* binds to (Miso plan 266.0). Miso names the customer's Entra SSO and SCIM applications from it
|
|
56
|
+
* (`AIF-local-dev03-SSO`) and also looks them up by that name.
|
|
57
|
+
*
|
|
58
|
+
* A concrete value, not a placeholder. These overrides are merged after the template is
|
|
59
|
+
* resolved, so the previous `${DEV_USERNAME}.${DEV_ROOT_DOMAIN}` was never interpolated by
|
|
60
|
+
* Builder; Docker substituted the unset variables with empty strings and Miso received `.`,
|
|
61
|
+
* creating `AIF-.-SSO` / `AIF-.-SCIM` in the customer's directory (2026-10-06). Developer id 0
|
|
62
|
+
* has no `devNN`, so nothing is injected there rather than an invalid `local-`.
|
|
63
|
+
*
|
|
64
|
+
* @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
|
|
65
|
+
* @returns {Object.<string, string>} `{ INFRASTRUCTURE_NAME }` or empty
|
|
66
|
+
*/
|
|
67
|
+
function localInfrastructureNameEnv(cfg) {
|
|
68
|
+
const devUsername = buildDevUsernameForFrontDoorHost(cfg ? cfg['developer-id'] : undefined);
|
|
69
|
+
return devUsername ? { INFRASTRUCTURE_NAME: `local-${devUsername}` } : {};
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
function normalizeAppKeyList(value) {
|
|
53
73
|
const raw = Array.isArray(value) ? value : String(value ?? '').split(',');
|
|
54
74
|
const seen = new Set();
|
|
@@ -91,7 +111,8 @@ function localEnvOverridesForApp(appName, cfg) {
|
|
|
91
111
|
if (key === 'miso-controller') {
|
|
92
112
|
return {
|
|
93
113
|
ONBOARDING_APPS: resolveLocalOnboardingApps(cfg).join(','),
|
|
94
|
-
...MISO_CONTROLLER_LOCAL_ONLY_ENV
|
|
114
|
+
...MISO_CONTROLLER_LOCAL_ONLY_ENV,
|
|
115
|
+
...localInfrastructureNameEnv(cfg)
|
|
95
116
|
};
|
|
96
117
|
}
|
|
97
118
|
if (key === 'builder-api' || key === 'integration-server') {
|
|
@@ -29,7 +29,10 @@ async function deployInstallationStage(resourceGroupKey, options, deps = {}) {
|
|
|
29
29
|
// install registers with Mori exactly as any other does.
|
|
30
30
|
if (isLocalInstallationKey(resourceGroup) && ENVIRONMENT_STAGES.includes(validateStage(options.stage))) {
|
|
31
31
|
const { runLocalEnvironmentStage } = require('./local-environment-stage');
|
|
32
|
-
return runLocalEnvironmentStage(resourceGroup, validateStage(options.stage),
|
|
32
|
+
return runLocalEnvironmentStage(resourceGroup, validateStage(options.stage), {
|
|
33
|
+
...deps,
|
|
34
|
+
preset: options.preset
|
|
35
|
+
});
|
|
33
36
|
}
|
|
34
37
|
const workspace = openWorkspace(resourceGroup, { root: deps.root });
|
|
35
38
|
const input = resolveStageInput(resourceGroup, options, workspace.infra?.vendor?.value);
|
|
@@ -102,8 +102,6 @@ const PARAMETER_CATALOG = Object.freeze({
|
|
|
102
102
|
moriLicensePublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori licence issuer public key (SPKI PEM); empty leaves the installation unpinned and it then refuses every licence.' },
|
|
103
103
|
moriPolicyPublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori policy signing anchor (SPKI PEM), a different pair from the licence key; empty leaves the policy bundle checkable only against the key it carries, which proves nothing.' },
|
|
104
104
|
moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
|
|
105
|
-
entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
106
|
-
entraIdClientSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
|
|
107
105
|
entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
|
|
108
106
|
licenseEdition: { kind: 'license', source: 'cliOrFile', description: 'License requested from Mori during tenant activation: COMMUNITY, STANDARD or ENTERPRISE, or an Azure Marketplace subscription ID. Standard/Enterprise by name is a 30-day evaluation.' },
|
|
109
107
|
installationResourceModel: { kind: 'file', source: 'templateDefault', description: 'Initial installation topology: shared for Community and Standard, dedicated for Enterprise DEV. Defaults to shared and is protected by Azure application configuration. It records the topology the installation starts in and grants no later licence entitlement, so it is not a licence input.' },
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask Miso whether a local environment may exist, with honest failure labels.
|
|
3
|
+
*
|
|
4
|
+
* Local create uses the same request shape as Azure: key, environment, and
|
|
5
|
+
* configuration.preset / serviceName / location. Docker values are real
|
|
6
|
+
* placement for that system (location local, serviceName local-devNN), not a
|
|
7
|
+
* stripped body. Schema rejection is not a licence decline.
|
|
8
|
+
*
|
|
9
|
+
* @fileoverview Local env deploy — Miso authorize with contract vs licence split
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { resolveLocalInstallationName } = require('./local-installation');
|
|
15
|
+
const { normalizePreset } = require('../environment-config');
|
|
16
|
+
|
|
17
|
+
/** Placement location stored for Docker / local / database installs. */
|
|
18
|
+
const LOCAL_LOCATION = 'local';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object|Error|null|undefined} errorOrResponse
|
|
22
|
+
* @returns {number}
|
|
23
|
+
*/
|
|
24
|
+
function statusOf(errorOrResponse) {
|
|
25
|
+
if (!errorOrResponse || typeof errorOrResponse !== 'object') {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const raw =
|
|
29
|
+
errorOrResponse.status ??
|
|
30
|
+
errorOrResponse.statusCode ??
|
|
31
|
+
(errorOrResponse.errorData && errorOrResponse.errorData.statusCode);
|
|
32
|
+
const n = Number(raw);
|
|
33
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {Object|Error|null|undefined} errorOrResponse
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function detailOf(errorOrResponse) {
|
|
41
|
+
if (!errorOrResponse) {
|
|
42
|
+
return 'request failed';
|
|
43
|
+
}
|
|
44
|
+
if (typeof errorOrResponse === 'string') {
|
|
45
|
+
return errorOrResponse;
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
48
|
+
errorOrResponse.formattedError ||
|
|
49
|
+
errorOrResponse.detail ||
|
|
50
|
+
(typeof errorOrResponse.error === 'string' ? errorOrResponse.error : null) ||
|
|
51
|
+
errorOrResponse.message ||
|
|
52
|
+
'request failed'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True when the controller refused the body as invalid, before any licence decision.
|
|
58
|
+
*
|
|
59
|
+
* @param {Object|Error|null|undefined} errorOrResponse
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
function isContractValidationFailure(errorOrResponse) {
|
|
63
|
+
const status = statusOf(errorOrResponse);
|
|
64
|
+
if (status === 400 || status === 422) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const text = [
|
|
68
|
+
detailOf(errorOrResponse),
|
|
69
|
+
errorOrResponse && errorOrResponse.title,
|
|
70
|
+
errorOrResponse && errorOrResponse.type,
|
|
71
|
+
JSON.stringify((errorOrResponse && errorOrResponse.errorData) || {})
|
|
72
|
+
]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(' ')
|
|
75
|
+
.toLowerCase();
|
|
76
|
+
return /configuration\.|["']?preset["']?|servicename|service.?name|["']?location["']?/.test(
|
|
77
|
+
text
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} envKey
|
|
83
|
+
* @param {Object|Error} errorOrResponse
|
|
84
|
+
* @returns {Error}
|
|
85
|
+
*/
|
|
86
|
+
function formatAuthorizeFailure(envKey, errorOrResponse) {
|
|
87
|
+
const detail = detailOf(errorOrResponse);
|
|
88
|
+
if (isContractValidationFailure(errorOrResponse)) {
|
|
89
|
+
return new Error(
|
|
90
|
+
`Cannot create environment ${envKey}: the controller rejected the request as invalid ` +
|
|
91
|
+
`(not a licence decision).\n${detail}\n` +
|
|
92
|
+
'Local create uses the same configuration fields as Azure (preset, serviceName, ' +
|
|
93
|
+
'location). Nothing was created locally.'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return new Error(
|
|
97
|
+
`Miso declined environment ${envKey}: ${detail}\n` +
|
|
98
|
+
'Editions are licensed in Miso. Nothing was created locally.'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Same create body shape as Azure environment create.
|
|
104
|
+
*
|
|
105
|
+
* Docker still sends a licensed size preset (default `s`). `location` is `local`
|
|
106
|
+
* and `serviceName` is the installation name. Optional `--preset` overrides size.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} envKey
|
|
109
|
+
* @param {{ serviceName: string, preset?: string, location?: string }} placement
|
|
110
|
+
* @returns {{ key: string, environment: string, configuration: Object }}
|
|
111
|
+
*/
|
|
112
|
+
function buildLocalEnvironmentCreateBody(envKey, placement) {
|
|
113
|
+
const key = String(envKey || '').trim();
|
|
114
|
+
return {
|
|
115
|
+
key,
|
|
116
|
+
environment: key,
|
|
117
|
+
configuration: {
|
|
118
|
+
preset: normalizePreset(placement.preset),
|
|
119
|
+
serviceName: String(placement.serviceName || '').trim(),
|
|
120
|
+
location: String(placement.location || LOCAL_LOCATION).trim() || LOCAL_LOCATION
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @async
|
|
127
|
+
* @param {Object} deps
|
|
128
|
+
* @param {string} installationKey
|
|
129
|
+
* @returns {Promise<string>}
|
|
130
|
+
*/
|
|
131
|
+
async function resolveAuthorizeServiceName(deps, installationKey) {
|
|
132
|
+
if (deps.serviceName) {
|
|
133
|
+
return String(deps.serviceName).trim();
|
|
134
|
+
}
|
|
135
|
+
const config = deps.config || require('../../core/config');
|
|
136
|
+
const developerId = await config.getDeveloperId();
|
|
137
|
+
return resolveLocalInstallationName(installationKey || 'local', developerId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @async
|
|
142
|
+
* @param {string} envKey
|
|
143
|
+
* @param {Object} deps
|
|
144
|
+
* @returns {Promise<{ controllerUrl: string, authConfig: Object }>}
|
|
145
|
+
*/
|
|
146
|
+
async function resolveAuthorizeSession(envKey, deps) {
|
|
147
|
+
const resolveControllerUrl =
|
|
148
|
+
deps.resolveControllerUrl || require('../../utils/controller-url').resolveControllerUrl;
|
|
149
|
+
const getOrRefreshDeviceToken =
|
|
150
|
+
deps.getOrRefreshDeviceToken ||
|
|
151
|
+
require('../../utils/token-manager').getOrRefreshDeviceToken;
|
|
152
|
+
const controllerUrl = await resolveControllerUrl();
|
|
153
|
+
if (!controllerUrl) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Cannot create ${envKey}: no controller is configured, so whether your licence ` +
|
|
156
|
+
'permits this environment cannot be checked. Run "aifabrix login" first.'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
|
|
160
|
+
if (!deviceToken || !deviceToken.token) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Cannot create ${envKey}: not signed in, so whether your licence permits this ` +
|
|
163
|
+
'environment cannot be checked. Run "aifabrix login" first.'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
controllerUrl,
|
|
168
|
+
authConfig: {
|
|
169
|
+
type: 'device',
|
|
170
|
+
token: deviceToken.token,
|
|
171
|
+
controller: deviceToken.controller || controllerUrl
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @async
|
|
178
|
+
* @param {string} envKey
|
|
179
|
+
* @param {Object} [deps]
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
*/
|
|
182
|
+
async function authorizeEnvironmentWithController(envKey, deps = {}) {
|
|
183
|
+
if (typeof deps.authorizeEnvironment === 'function') {
|
|
184
|
+
await deps.authorizeEnvironment(envKey);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const createEnvironment =
|
|
188
|
+
deps.createEnvironment || require('../../api/environments.api').createEnvironment;
|
|
189
|
+
const { controllerUrl, authConfig } = await resolveAuthorizeSession(envKey, deps);
|
|
190
|
+
const serviceName = await resolveAuthorizeServiceName(deps, deps.installationKey);
|
|
191
|
+
if (!serviceName) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Cannot create ${envKey}: local serviceName is empty. Name the installation ` +
|
|
194
|
+
'(for example local-dev03) or set a developer id with "aifabrix setup".'
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const body = buildLocalEnvironmentCreateBody(envKey, {
|
|
198
|
+
serviceName,
|
|
199
|
+
preset: deps.preset,
|
|
200
|
+
location: deps.location
|
|
201
|
+
});
|
|
202
|
+
let response;
|
|
203
|
+
try {
|
|
204
|
+
response = await createEnvironment(controllerUrl, authConfig, body);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
throw formatAuthorizeFailure(envKey, error);
|
|
207
|
+
}
|
|
208
|
+
if (response && response.success === false) {
|
|
209
|
+
throw formatAuthorizeFailure(envKey, response);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
LOCAL_LOCATION,
|
|
215
|
+
statusOf,
|
|
216
|
+
detailOf,
|
|
217
|
+
isContractValidationFailure,
|
|
218
|
+
formatAuthorizeFailure,
|
|
219
|
+
buildLocalEnvironmentCreateBody,
|
|
220
|
+
authorizeEnvironmentWithController
|
|
221
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load an environment's bootstrap snapshot after its dataplane credential exists.
|
|
3
|
+
*
|
|
4
|
+
* Each environment holds its own application credential. `pro` must never authenticate
|
|
5
|
+
* with `dev`'s client secret. The snapshot is the settings source; this module does not
|
|
6
|
+
* write a parallel env file.
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Local env deploy — per-environment bootstrap snapshot
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { resolveStrictSecretName } = require('../../parameters/physical-secret-name');
|
|
14
|
+
|
|
15
|
+
const DATAPLANE_CLIENT_ID_KEY = 'dataplane-client-idKeyVault';
|
|
16
|
+
const DATAPLANE_CLIENT_SECRET_KEY = 'dataplane-client-secretKeyVault';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {Object} secrets
|
|
20
|
+
* @param {string} key
|
|
21
|
+
* @returns {string|null}
|
|
22
|
+
*/
|
|
23
|
+
function nonEmptySecret(secrets, key) {
|
|
24
|
+
if (!secrets || typeof secrets !== 'object') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const value = secrets[key];
|
|
28
|
+
if (value === null || value === undefined) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const text = String(value).trim();
|
|
32
|
+
return text ? text : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve this environment's dataplane client credentials.
|
|
37
|
+
*
|
|
38
|
+
* Uses `resolveStrictSecretName` so an application-scoped secret is never shared between
|
|
39
|
+
* environments, even when the catalog still pins an unprefixed literal.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} envKey
|
|
42
|
+
* @param {Object} secrets
|
|
43
|
+
* @returns {{ clientId: string, clientSecret: string, clientIdKey: string, clientSecretKey: string }}
|
|
44
|
+
* @throws {Error} When either credential is missing for this environment
|
|
45
|
+
*/
|
|
46
|
+
function resolveEnvironmentDataplaneCredentials(envKey, secrets) {
|
|
47
|
+
const env = String(envKey || '').trim().toLowerCase();
|
|
48
|
+
const clientIdKey = resolveStrictSecretName(DATAPLANE_CLIENT_ID_KEY, env);
|
|
49
|
+
const clientSecretKey = resolveStrictSecretName(DATAPLANE_CLIENT_SECRET_KEY, env);
|
|
50
|
+
let clientId = nonEmptySecret(secrets, clientIdKey);
|
|
51
|
+
let clientSecret = nonEmptySecret(secrets, clientSecretKey);
|
|
52
|
+
let resolvedIdKey = clientIdKey;
|
|
53
|
+
let resolvedSecretKey = clientSecretKey;
|
|
54
|
+
// Same adoption rule as databases: a pre-environment install's unprefixed credential
|
|
55
|
+
// belongs to `dev`. tst and pro never adopt — that would share isolation away.
|
|
56
|
+
if (env === 'dev' && (!clientId || !clientSecret)) {
|
|
57
|
+
const adoptedId = nonEmptySecret(secrets, DATAPLANE_CLIENT_ID_KEY);
|
|
58
|
+
const adoptedSecret = nonEmptySecret(secrets, DATAPLANE_CLIENT_SECRET_KEY);
|
|
59
|
+
if (adoptedId && adoptedSecret) {
|
|
60
|
+
clientId = adoptedId;
|
|
61
|
+
clientSecret = adoptedSecret;
|
|
62
|
+
resolvedIdKey = DATAPLANE_CLIENT_ID_KEY;
|
|
63
|
+
resolvedSecretKey = DATAPLANE_CLIENT_SECRET_KEY;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!clientId || !clientSecret) {
|
|
67
|
+
const missing = [
|
|
68
|
+
!clientId ? clientIdKey : null,
|
|
69
|
+
!clientSecret ? clientSecretKey : null
|
|
70
|
+
].filter(Boolean);
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Cannot load a configuration snapshot for ${env}: missing ${missing.join(' and ')}. ` +
|
|
73
|
+
'Each environment uses its own dataplane credential. Add ' +
|
|
74
|
+
`aifabrix secret set ${missing[0]} <value>` +
|
|
75
|
+
(missing[1] ? ` (and ${missing[1]})` : '') +
|
|
76
|
+
'.'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
clientId,
|
|
81
|
+
clientSecret,
|
|
82
|
+
clientIdKey: resolvedIdKey,
|
|
83
|
+
clientSecretKey: resolvedSecretKey
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Fetch the registered dataplane's bootstrap snapshot for this environment.
|
|
89
|
+
*
|
|
90
|
+
* @async
|
|
91
|
+
* @param {string} envKey
|
|
92
|
+
* @param {Object} [deps]
|
|
93
|
+
* @returns {Promise<{ configuration: Object, context: Object, issuedAt?: string }>}
|
|
94
|
+
*/
|
|
95
|
+
async function loadEnvironmentBootstrapSnapshot(envKey, deps = {}) {
|
|
96
|
+
const resolveControllerUrl =
|
|
97
|
+
deps.resolveControllerUrl || require('../../utils/controller-url').resolveControllerUrl;
|
|
98
|
+
const loadSecrets = deps.loadSecrets || require('../../core/secrets').loadSecrets;
|
|
99
|
+
const fetchBootstrapSnapshot =
|
|
100
|
+
deps.fetchBootstrapSnapshot ||
|
|
101
|
+
require('../../api/bootstrap-snapshot.api').fetchBootstrapSnapshot;
|
|
102
|
+
|
|
103
|
+
const controllerUrl = await resolveControllerUrl();
|
|
104
|
+
if (!controllerUrl) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Cannot load a configuration snapshot for ${envKey}: no controller is configured. ` +
|
|
107
|
+
'Run "aifabrix login" first.'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const secrets = deps.secrets || (await loadSecrets(undefined));
|
|
111
|
+
const credentials = resolveEnvironmentDataplaneCredentials(envKey, secrets);
|
|
112
|
+
return fetchBootstrapSnapshot(
|
|
113
|
+
controllerUrl,
|
|
114
|
+
{ clientId: credentials.clientId, clientSecret: credentials.clientSecret },
|
|
115
|
+
deps
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
DATAPLANE_CLIENT_ID_KEY,
|
|
121
|
+
DATAPLANE_CLIENT_SECRET_KEY,
|
|
122
|
+
resolveEnvironmentDataplaneCredentials,
|
|
123
|
+
loadEnvironmentBootstrapSnapshot
|
|
124
|
+
};
|
|
@@ -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
|
};
|
|
@@ -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": [
|
package/package.json
CHANGED
|
@@ -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.63.
|
|
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
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"repo": "aifabrix-miso",
|
|
7
7
|
"path": "builder/miso-controller/env.template"
|
|
8
8
|
},
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "c14aba1bd4588e334b89f3714714521e659d29f96bb76fc28ef17cacec47c80f"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"path": "templates/applications/builder-api/env.template",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"repo": "aifabrix-dataplane",
|
|
31
31
|
"path": "builder/dataplane/env.template"
|
|
32
32
|
},
|
|
33
|
-
"sha256": "
|
|
33
|
+
"sha256": "3f4abcd6e3b7a8cb8a6029c8841b63c2384b863ef168a2ed0128bd5856119e3f"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"path": "templates/applications/integration-server/env.template",
|
|
@@ -4,7 +4,7 @@ app:
|
|
|
4
4
|
displayName: 'Miso Controller'
|
|
5
5
|
description: 'Miso is the AI Fabrix in-tenant controller and portal layer for securely operating enterprise AI apps inside a customer’s Azure tenant. It provides Entra ID SSO, RBAC, audit logs, environment/app configuration via schemas, and safe secret handling via Key Vault references—ensuring governance, traceability, and predictable UX across portal, SDK, and CLI.'
|
|
6
6
|
type: webapp
|
|
7
|
-
version: '2.0.
|
|
7
|
+
version: '2.0.95'
|
|
8
8
|
# Image Configuration
|
|
9
9
|
# Set tag to match your build (e.g. aifabrix build miso-controller -t 1.0.0 then tag: 1.0.0)
|
|
10
10
|
# Registry is required so the controller can pull the image (avoids "docker: not found" on the controller host).
|
|
@@ -96,6 +96,11 @@ platforms:
|
|
|
96
96
|
value: 'azure'
|
|
97
97
|
- name: MORI_BASE_URL
|
|
98
98
|
value: 'https://platform.aifabrix.dev/mori'
|
|
99
|
+
# Names the Mori policy-signing pair the licence bundle is checked against. Must match
|
|
100
|
+
# Mori's own POLICY_KEY_ID (aifabrix-mori builder/mori-controller/application.yaml).
|
|
101
|
+
# Names the Mori policy-signing pair (MORI_POLICY_KEY_ID); must match Mori's manifest.
|
|
102
|
+
- name: POLICY_KEY_ID
|
|
103
|
+
value: 'mori-policy-key-1'
|
|
99
104
|
|
|
100
105
|
# Local Docker runs. No resource group is supplied, so `matchLocalPlatform` takes the FIRST entry
|
|
101
106
|
# declaring the local platform (`dev`) -- this one -- and the literal `aifabrix-local` can never
|
|
@@ -135,6 +140,10 @@ platforms:
|
|
|
135
140
|
value: 'swedencentral'
|
|
136
141
|
- name: MORI_BASE_URL
|
|
137
142
|
value: 'url://mori-controller-internal'
|
|
143
|
+
# Names the Mori policy-signing pair the licence bundle is checked against. Must match
|
|
144
|
+
# Mori's own POLICY_KEY_ID (aifabrix-mori builder/mori-controller/application.yaml).
|
|
145
|
+
- name: POLICY_KEY_ID
|
|
146
|
+
value: 'mori-policy-key-1'
|
|
138
147
|
# Declared, because the fallback infers it and infers it wrongly here:
|
|
139
148
|
# `resolveCloudVendor` returns 'azure' whenever AZURE_SUBSCRIPTION_ID is set, and every
|
|
140
149
|
# developer box has it from the shared development service principal. A local stack was
|
|
@@ -186,6 +195,10 @@ platforms:
|
|
|
186
195
|
# consumer of it was pointed at a loopback address that does not exist in the cluster.
|
|
187
196
|
- name: MORI_BASE_URL
|
|
188
197
|
value: 'https://platform-{platform}.aifabrix.dev/mori'
|
|
198
|
+
# Names the Mori policy-signing pair the licence bundle is checked against. Must match
|
|
199
|
+
# Mori's own POLICY_KEY_ID (aifabrix-mori builder/mori-controller/application.yaml).
|
|
200
|
+
- name: POLICY_KEY_ID
|
|
201
|
+
value: 'mori-policy-key-1'
|
|
189
202
|
|
|
190
203
|
- resource: aifabrix-ce-*
|
|
191
204
|
name: '*'
|
|
@@ -212,6 +225,10 @@ platforms:
|
|
|
212
225
|
# (`platform-dev.aifabrix.dev/mori`) this entry can follow the pattern used above.
|
|
213
226
|
- name: MORI_BASE_URL
|
|
214
227
|
value: 'https://{resourceGroup}-miso-mori-controller.azurewebsites.net/mori'
|
|
228
|
+
# Names the Mori policy-signing pair the licence bundle is checked against. Must match
|
|
229
|
+
# Mori's own POLICY_KEY_ID (aifabrix-mori builder/mori-controller/application.yaml).
|
|
230
|
+
- name: POLICY_KEY_ID
|
|
231
|
+
value: 'mori-policy-key-1'
|
|
215
232
|
|
|
216
233
|
configuration:
|
|
217
234
|
# -------------------------------------------------------------------------
|
|
@@ -148,8 +148,7 @@ REDIS_PERMISSIONS_TTL=900
|
|
|
148
148
|
# sync-application-urls-from-env.service.
|
|
149
149
|
#
|
|
150
150
|
# After POST /api/ide/onboarding/entra-id/configure succeeds, controller adds Microsoft Entra IdP to stock Keycloak
|
|
151
|
-
# using the customer enterprise app (AI Fabrix Miso)
|
|
152
|
-
# After configure: Keycloak broker redirect is on the customer enterprise app (AI Fabrix Miso), not ENTRA_ID_CLIENT_ID.
|
|
151
|
+
# using the customer enterprise app (AI Fabrix Miso); its broker redirect is on that app.
|
|
153
152
|
|
|
154
153
|
KEYCLOAK_REALM=aifabrix
|
|
155
154
|
# Public issuer / browser URL (includes /auth when Keycloak uses KC_HTTP_RELATIVE_PATH=/auth).
|
|
@@ -231,16 +230,8 @@ MISO_CLIENTSECRET=kv://miso-controller-client-secretKeyVault
|
|
|
231
230
|
# Kept in the shared block because EITHER mode may install Entra later from the plan 219
|
|
232
231
|
# provider catalogue: Azure hosting does not force Entra, and SaaS may choose it.
|
|
233
232
|
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
# Customer SSO after configure uses a separate enterprise app (AI Fabrix Miso);
|
|
237
|
-
# Keycloak broker redirect is on that customer app, not ENTRA_ID_CLIENT_ID.
|
|
238
|
-
#
|
|
239
|
-
# ENTRA_ID_CENTRAL_CALLBACK_URI — Web redirect on the vendor app; must point at
|
|
240
|
-
# Mori (e.g. …/entra-id-concent/callback), not the SPA or controller.
|
|
241
|
-
ENTRA_ID_CENTRAL_CALLBACK_URI=url://mori-controller-public/entra-id-concent/callback
|
|
242
|
-
ENTRA_ID_CLIENT_ID=kv://entra-client-idKeyVault
|
|
243
|
-
ENTRA_ID_CLIENT_SECRET=kv://entra-client-secretKeyVault
|
|
233
|
+
# No Entra client id, secret or callback here. Mori owns the app registration and every
|
|
234
|
+
# sign-in; Miso only says which scopes it needs (plan 248.0, remove-vendor-app-secret-from-miso).
|
|
244
235
|
|
|
245
236
|
# =============================================================================
|
|
246
237
|
# TENANT ACTIVATION (TA-3) — EXISTING LLM CATALOG — [customerOwned]
|
|
@@ -378,15 +369,6 @@ MORI_LICENSE_PUBLIC_KEY=kv://mori-license-public-keyKeyVault
|
|
|
378
369
|
# MORI_LICENSE_PUBLIC_KEY_FILE=/run/secrets/mori-license-public-key.pem
|
|
379
370
|
MORI_LICENSE_ISSUER=mori-controller
|
|
380
371
|
|
|
381
|
-
# =============================================================================
|
|
382
|
-
# ENTRA ID ONBOARDING (delegated Graph via Mori)
|
|
383
|
-
# =============================================================================
|
|
384
|
-
# Must match a Web redirect URI on the vendor multi-tenant app and Mori’s route
|
|
385
|
-
# …/entra-graph-delegated/callback. Shared pro Mori addon (CICD *-miso-rg):
|
|
386
|
-
ENTRA_GRAPH_DELEGATED_REDIRECT_URI=url://mori-controller-public/entra-graph-delegated/callback
|
|
387
|
-
|
|
388
|
-
# Optional delegated Graph scopes for GET …/graph-auth-url (space- or comma-separated).
|
|
389
|
-
# ENTRA_GRAPH_DELEGATED_SCOPES=https://graph.microsoft.com/Application.ReadWrite.All,https://graph.microsoft.com/Group.ReadWrite.All,https://graph.microsoft.com/Synchronization.ReadWrite.All,openid,profile,offline_access
|
|
390
372
|
|
|
391
373
|
# Dev only: log full delegated Graph JWT in general_log_* as delegatedGraphJwt (default off).
|
|
392
374
|
# LOG_GRAPH_TOKEN=true
|
|
@@ -91,6 +91,10 @@ permissions:
|
|
|
91
91
|
roles: ['aifabrix-platform-admin', 'aifabrix-security-admin']
|
|
92
92
|
description: 'Delete groups'
|
|
93
93
|
|
|
94
|
+
- name: 'user-groups:update'
|
|
95
|
+
roles: ['aifabrix-security-admin']
|
|
96
|
+
description: 'Manage user-group memberships'
|
|
97
|
+
|
|
94
98
|
# Administrative Permissions
|
|
95
99
|
- name: 'admin:read'
|
|
96
100
|
roles: ['aifabrix-platform-admin']
|
|
@@ -315,15 +319,15 @@ permissions:
|
|
|
315
319
|
description: 'View dimensions and dimension values'
|
|
316
320
|
|
|
317
321
|
- name: 'dimensions:create'
|
|
318
|
-
roles: ['aifabrix-
|
|
322
|
+
roles: ['aifabrix-security-admin']
|
|
319
323
|
description: 'Create dimensions and dimension values'
|
|
320
324
|
|
|
321
325
|
- name: 'dimensions:update'
|
|
322
|
-
roles: ['aifabrix-
|
|
326
|
+
roles: ['aifabrix-security-admin']
|
|
323
327
|
description: 'Update dimensions and dimension values'
|
|
324
328
|
|
|
325
329
|
- name: 'dimensions:delete'
|
|
326
|
-
roles: ['aifabrix-
|
|
330
|
+
roles: ['aifabrix-security-admin']
|
|
327
331
|
description: 'Delete dimensions and dimension values'
|
|
328
332
|
|
|
329
333
|
# Resource Type Management
|
|
@@ -363,15 +367,15 @@ permissions:
|
|
|
363
367
|
description: 'View business roles'
|
|
364
368
|
|
|
365
369
|
- name: 'roles:create'
|
|
366
|
-
roles: ['aifabrix-
|
|
370
|
+
roles: ['aifabrix-security-admin']
|
|
367
371
|
description: 'Create business roles'
|
|
368
372
|
|
|
369
373
|
- name: 'roles:update'
|
|
370
|
-
roles: ['aifabrix-
|
|
374
|
+
roles: ['aifabrix-security-admin']
|
|
371
375
|
description: 'Update business roles'
|
|
372
376
|
|
|
373
377
|
- name: 'roles:delete'
|
|
374
|
-
roles: ['aifabrix-
|
|
378
|
+
roles: ['aifabrix-security-admin']
|
|
375
379
|
description: 'Delete business roles'
|
|
376
380
|
|
|
377
381
|
# Evidence Kind Management
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"_generator": {
|
|
6
6
|
"name": "bicep",
|
|
7
7
|
"version": "0.47.16.16243",
|
|
8
|
-
"templateHash": "
|
|
8
|
+
"templateHash": "17889093247943830402"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"parameters": {
|
|
@@ -179,20 +179,6 @@
|
|
|
179
179
|
"description": "Expected `iss` claim on a Mori licence."
|
|
180
180
|
}
|
|
181
181
|
},
|
|
182
|
-
"entraIdClientId": {
|
|
183
|
-
"type": "string",
|
|
184
|
-
"defaultValue": "481251f2-a318-47ba-9ad8-40c36f790df1",
|
|
185
|
-
"metadata": {
|
|
186
|
-
"description": "Vendor Entra multi-tenant app client ID. Default matches vendor miso-controller .env (not in createUiDefinition)."
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
"entraIdClientSecret": {
|
|
190
|
-
"type": "securestring",
|
|
191
|
-
"defaultValue": "",
|
|
192
|
-
"metadata": {
|
|
193
|
-
"description": "Optional vendor Entra app client secret. Configure through the Miso installation contract."
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
182
|
"entraIdTenantId": {
|
|
197
183
|
"type": "string",
|
|
198
184
|
"defaultValue": "09aff594-ba37-4af2-bf40-b773b654c563",
|
|
@@ -2540,9 +2526,6 @@
|
|
|
2540
2526
|
"encryptionKey": {
|
|
2541
2527
|
"value": "[reference(resourceId('Microsoft.Resources/deployments', 'bootstrapSecrets'), '2025-04-01').outputs.encryptionKey.value]"
|
|
2542
2528
|
},
|
|
2543
|
-
"entraIdClientSecret": {
|
|
2544
|
-
"value": "[parameters('entraIdClientSecret')]"
|
|
2545
|
-
},
|
|
2546
2529
|
"moriLicensePublicKey": {
|
|
2547
2530
|
"value": "[parameters('moriLicensePublicKey')]"
|
|
2548
2531
|
},
|
|
@@ -2560,7 +2543,7 @@
|
|
|
2560
2543
|
"_generator": {
|
|
2561
2544
|
"name": "bicep",
|
|
2562
2545
|
"version": "0.47.16.16243",
|
|
2563
|
-
"templateHash": "
|
|
2546
|
+
"templateHash": "6681447246114830480"
|
|
2564
2547
|
}
|
|
2565
2548
|
},
|
|
2566
2549
|
"parameters": {
|
|
@@ -2629,13 +2612,6 @@
|
|
|
2629
2612
|
"description": "MISO Client Secret (auto-generated if not provided)"
|
|
2630
2613
|
}
|
|
2631
2614
|
},
|
|
2632
|
-
"entraIdClientSecret": {
|
|
2633
|
-
"type": "securestring",
|
|
2634
|
-
"defaultValue": "",
|
|
2635
|
-
"metadata": {
|
|
2636
|
-
"description": "Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET). Placeholder when empty — replace for live Entra flows."
|
|
2637
|
-
}
|
|
2638
|
-
},
|
|
2639
2615
|
"moriLicensePublicKey": {
|
|
2640
2616
|
"type": "string",
|
|
2641
2617
|
"defaultValue": "",
|
|
@@ -2659,7 +2635,6 @@
|
|
|
2659
2635
|
}
|
|
2660
2636
|
},
|
|
2661
2637
|
"variables": {
|
|
2662
|
-
"resolvedEntraIdClientSecret": "[if(not(empty(parameters('entraIdClientSecret'))), parameters('entraIdClientSecret'), format('PLACEHOLDER_ENTRA_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]",
|
|
2663
2638
|
"misoWebAppOrigin": "[if(endsWith(parameters('misoWebAppUrl'), '/'), substring(parameters('misoWebAppUrl'), 0, max(0, sub(length(parameters('misoWebAppUrl')), 1))), parameters('misoWebAppUrl'))]",
|
|
2664
2639
|
"misoVdirSegment": "[if(or(equals(parameters('misoVdirPublic'), ''), equals(parameters('misoVdirPublic'), '/')), '', if(startsWith(parameters('misoVdirPublic'), '/'), parameters('misoVdirPublic'), format('/{0}', parameters('misoVdirPublic'))))]",
|
|
2665
2640
|
"misoWebServerPublicUrl": "[if(empty(variables('misoVdirSegment')), variables('misoWebAppOrigin'), format('{0}{1}', variables('misoWebAppOrigin'), variables('misoVdirSegment')))]",
|
|
@@ -2750,14 +2725,6 @@
|
|
|
2750
2725
|
"value": "[if(not(empty(parameters('misoClientSecret'))), parameters('misoClientSecret'), format('PLACEHOLDER_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]"
|
|
2751
2726
|
}
|
|
2752
2727
|
},
|
|
2753
|
-
{
|
|
2754
|
-
"type": "Microsoft.KeyVault/vaults/secrets",
|
|
2755
|
-
"apiVersion": "2023-07-01",
|
|
2756
|
-
"name": "[format('{0}/{1}', parameters('keyVaultName'), 'entra-client-secretKeyVault')]",
|
|
2757
|
-
"properties": {
|
|
2758
|
-
"value": "[variables('resolvedEntraIdClientSecret')]"
|
|
2759
|
-
}
|
|
2760
|
-
},
|
|
2761
2728
|
{
|
|
2762
2729
|
"condition": "[not(empty(parameters('moriLicensePublicKey')))]",
|
|
2763
2730
|
"type": "Microsoft.KeyVault/vaults/secrets",
|
|
@@ -2891,9 +2858,6 @@
|
|
|
2891
2858
|
"moriLicenseIssuer": {
|
|
2892
2859
|
"value": "[parameters('moriLicenseIssuer')]"
|
|
2893
2860
|
},
|
|
2894
|
-
"entraIdClientId": {
|
|
2895
|
-
"value": "[parameters('entraIdClientId')]"
|
|
2896
|
-
},
|
|
2897
2861
|
"entraIdTenantId": {
|
|
2898
2862
|
"value": "[variables('effectiveEntraIdTenantId')]"
|
|
2899
2863
|
},
|
|
@@ -2926,7 +2890,7 @@
|
|
|
2926
2890
|
"_generator": {
|
|
2927
2891
|
"name": "bicep",
|
|
2928
2892
|
"version": "0.47.16.16243",
|
|
2929
|
-
"templateHash": "
|
|
2893
|
+
"templateHash": "16391707537665698614"
|
|
2930
2894
|
}
|
|
2931
2895
|
},
|
|
2932
2896
|
"parameters": {
|
|
@@ -3136,13 +3100,6 @@
|
|
|
3136
3100
|
"description": "Expected `iss` claim on a Mori licence."
|
|
3137
3101
|
}
|
|
3138
3102
|
},
|
|
3139
|
-
"entraIdClientId": {
|
|
3140
|
-
"type": "string",
|
|
3141
|
-
"defaultValue": "481251f2-a318-47ba-9ad8-40c36f790df1",
|
|
3142
|
-
"metadata": {
|
|
3143
|
-
"description": "Vendor multi-tenant Entra app client ID. No longer emitted as an app setting — Miso does not hold provider credentials; Mori does. Retained so the marketplace parameter surface is unchanged."
|
|
3144
|
-
}
|
|
3145
|
-
},
|
|
3146
3103
|
"entraIdTenantId": {
|
|
3147
3104
|
"type": "string",
|
|
3148
3105
|
"defaultValue": "09aff594-ba37-4af2-bf40-b773b654c563",
|
|
@@ -3224,8 +3181,6 @@
|
|
|
3224
3181
|
"databaseUrlAppSettingValue": "[if(not(empty(parameters('databaseUrlSecretUriWithVersion'))), format('@Microsoft.KeyVault(SecretUri={0})', parameters('databaseUrlSecretUriWithVersion')), format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-databases-0-urlKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix')))]",
|
|
3225
3182
|
"databaseLogUrlAppSettingValue": "[if(not(empty(parameters('databaseLogUrlSecretUriWithVersion'))), format('@Microsoft.KeyVault(SecretUri={0})', parameters('databaseLogUrlSecretUriWithVersion')), format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-databases-1-urlKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix')))]",
|
|
3226
3183
|
"moriBaseNormalized": "[if(and(greater(length(parameters('moriBaseUrl')), 1), endsWith(parameters('moriBaseUrl'), '/')), substring(parameters('moriBaseUrl'), 0, max(0, sub(length(parameters('moriBaseUrl')), 1))), parameters('moriBaseUrl'))]",
|
|
3227
|
-
"moriEntraGraphDelegatedRedirectUri": "[format('{0}/entra-graph-delegated/callback', variables('moriBaseNormalized'))]",
|
|
3228
|
-
"moriEntraCentralCallbackUri": "[format('{0}/entra-id-concent/callback', variables('moriBaseNormalized'))]",
|
|
3229
3184
|
"registryCredentialAppSettings": "[if(and(parameters('registryPasswordConfigured'), not(empty(parameters('acrLoginServer')))), createArray(createObject('name', 'DOCKER_REGISTRY_SERVER_USERNAME', 'value', parameters('acrAdminUsername')), createObject('name', 'DOCKER_REGISTRY_SERVER_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-acr-registry-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix')))), createArray())]"
|
|
3230
3185
|
},
|
|
3231
3186
|
"resources": [
|
|
@@ -3268,7 +3223,7 @@
|
|
|
3268
3223
|
"acrUseManagedIdentityCreds": "[not(parameters('registryPasswordConfigured'))]",
|
|
3269
3224
|
"healthCheckPath": "/health",
|
|
3270
3225
|
"alwaysOn": true,
|
|
3271
|
-
"appSettings": "[concat(createArray(createObject('name', 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'value', 'false'), createObject('name', 'WEBSITES_CONTAINER_START_TIME_LIMIT', 'value', '1800'), createObject('name', 'DATABASE_URL', 'value', variables('databaseUrlAppSettingValue')), createObject('name', 'JWT_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-secrets-jwtKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'NODE_ENV', 'value', 'production'), createObject('name', 'LOCATION', 'value', parameters('location')), createObject('name', 'PORT', 'value', '3000'), createObject('name', '
|
|
3226
|
+
"appSettings": "[concat(createArray(createObject('name', 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'value', 'false'), createObject('name', 'WEBSITES_CONTAINER_START_TIME_LIMIT', 'value', '1800'), createObject('name', 'DATABASE_URL', 'value', variables('databaseUrlAppSettingValue')), createObject('name', 'JWT_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-secrets-jwtKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'NODE_ENV', 'value', 'production'), createObject('name', 'LOCATION', 'value', parameters('location')), createObject('name', 'PORT', 'value', '3000'), createObject('name', 'WEBSITE_VNET_ROUTE_ALL', 'value', if(parameters('enablePostgresPublicAccess'), '0', '1')), createObject('name', 'WEBSITE_DNS_SERVER', 'value', '168.63.129.16'), createObject('name', 'DATABASELOG_URL', 'value', variables('databaseLogUrlAppSettingValue')), createObject('name', 'KEY_VAULT_ENABLED', 'value', 'true'), createObject('name', 'SECURITY_STORAGE_MODE', 'value', 'keyvault'), createObject('name', 'KEY_VAULT_URL', 'value', format('https://{0}.vault.azure.net/', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'ENCRYPTION_KEY', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-encryption-keyKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'AUTO_CREATE_TABLES', 'value', if(parameters('autoCreateTables'), 'true', 'false')), createObject('name', 'KEYCLOAK_REALM', 'value', parameters('keycloakRealm')), createObject('name', 'KEYCLOAK_CLIENT_ID', 'value', parameters('keycloakClientId')), createObject('name', 'KEYCLOAK_ADMIN_USERNAME', 'value', parameters('keycloakAdminUsername')), createObject('name', 'KEYCLOAK_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-keycloak-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-onboarding-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_EMAIL', 'value', parameters('adminEmail')), createObject('name', 'KEYCLOAK_SERVER_URL', 'value', variables('keycloakPublicUrl')), createObject('name', 'KEYCLOAK_INTERNAL_SERVER_URL', 'value', variables('keycloakInternalUrl')), createObject('name', 'KEYCLOAK_TOKEN_TIMEOUT', 'value', '5000'), createObject('name', 'KEYCLOAK_VERIFY_AUDIENCE', 'value', if(parameters('keycloakVerifyAudience'), 'true', 'false')), createObject('name', 'DISABLE_RATE_LIMIT', 'value', variables('shouldDisableRateLimit')), createObject('name', 'SKIP_DB_REDEPLOY', 'value', 'true'), createObject('name', 'MISO_WEB_SERVER_URL', 'value', variables('misoPublicUrl')), createObject('name', 'MISO_CONTROLLER_URL', 'value', variables('misoInternalUrl')), createObject('name', 'MISO_RELATIVE_PATH', 'value', variables('misoVdirValue')), createObject('name', 'CONTROLLER_BASE_URL', 'value', if(not(empty(parameters('misoControllerUrl'))), parameters('misoControllerUrl'), format('https://{0}.azurewebsites.net', parameters('name')))), createObject('name', 'AZURE_SUBSCRIPTION_ID', 'value', subscription().subscriptionId), createObject('name', 'TENANT_ID', 'value', subscription().tenantId), createObject('name', 'DEPLOYMENT', 'value', 'azure'), createObject('name', 'AZURE_RESOURCE_GROUP_NAME', 'value', parameters('resourceGroupName')), createObject('name', 'INFRASTRUCTURE_NAME', 'value', parameters('prefix')), createObject('name', 'ENTRA_ID_TENANT_ID', 'value', parameters('entraIdTenantId')), createObject('name', 'MISO_CLIENTID', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-miso-clientIdKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'MISO_CLIENTSECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-client-secretKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_BASE_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-base-urlKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_LICENSE_PUBLIC_KEY', 'value', if(empty(parameters('moriLicensePublicKey')), '', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-license-public-keyKeyVault)', last(split(parameters('keyVaultId'), '/'))))), createObject('name', 'MORI_LICENSE_ISSUER', 'value', parameters('moriLicenseIssuer')), createObject('name', 'DOCKER_ENABLE_CI', 'value', 'true'), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', if(not(empty(parameters('acrLoginServer'))), format('https://{0}', parameters('acrLoginServer')), '')), createObject('name', 'ONBOARDING_CREATE_PIPELINE_CREDENTIALS', 'value', 'true'), createObject('name', 'BOOTSTRAP_APPKEYS', 'value', parameters('bootstrapAppKeys')), createObject('name', 'LICENSE_JWT', 'value', parameters('licenseJwt')), createObject('name', 'INSTALLATION_RESOURCE_MODEL', 'value', parameters('installationResourceModel')), createObject('name', 'LICENSE_EDITION', 'value', parameters('licenseEdition')), createObject('name', 'ONBOARDING_MODE', 'value', parameters('onboardingMode')), createObject('name', 'INSTALLATION_CAPACITY_PROFILE', 'value', parameters('capacityProfile'))), variables('registryCredentialAppSettings'))]",
|
|
3272
3227
|
"vnetRouteAllEnabled": "[not(parameters('enablePostgresPublicAccess'))]"
|
|
3273
3228
|
},
|
|
3274
3229
|
"vnetRouteAllEnabled": "[not(parameters('enablePostgresPublicAccess'))]"
|
|
@@ -3663,9 +3618,6 @@
|
|
|
3663
3618
|
},
|
|
3664
3619
|
"encryptionKey": {
|
|
3665
3620
|
"value": "[reference(resourceId('Microsoft.Resources/deployments', 'bootstrapSecrets'), '2025-04-01').outputs.encryptionKey.value]"
|
|
3666
|
-
},
|
|
3667
|
-
"entraIdClientSecret": {
|
|
3668
|
-
"value": "[parameters('entraIdClientSecret')]"
|
|
3669
3621
|
}
|
|
3670
3622
|
},
|
|
3671
3623
|
"template": {
|
|
@@ -3675,7 +3627,7 @@
|
|
|
3675
3627
|
"_generator": {
|
|
3676
3628
|
"name": "bicep",
|
|
3677
3629
|
"version": "0.47.16.16243",
|
|
3678
|
-
"templateHash": "
|
|
3630
|
+
"templateHash": "6681447246114830480"
|
|
3679
3631
|
}
|
|
3680
3632
|
},
|
|
3681
3633
|
"parameters": {
|
|
@@ -3744,13 +3696,6 @@
|
|
|
3744
3696
|
"description": "MISO Client Secret (auto-generated if not provided)"
|
|
3745
3697
|
}
|
|
3746
3698
|
},
|
|
3747
|
-
"entraIdClientSecret": {
|
|
3748
|
-
"type": "securestring",
|
|
3749
|
-
"defaultValue": "",
|
|
3750
|
-
"metadata": {
|
|
3751
|
-
"description": "Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET). Placeholder when empty — replace for live Entra flows."
|
|
3752
|
-
}
|
|
3753
|
-
},
|
|
3754
3699
|
"moriLicensePublicKey": {
|
|
3755
3700
|
"type": "string",
|
|
3756
3701
|
"defaultValue": "",
|
|
@@ -3774,7 +3719,6 @@
|
|
|
3774
3719
|
}
|
|
3775
3720
|
},
|
|
3776
3721
|
"variables": {
|
|
3777
|
-
"resolvedEntraIdClientSecret": "[if(not(empty(parameters('entraIdClientSecret'))), parameters('entraIdClientSecret'), format('PLACEHOLDER_ENTRA_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]",
|
|
3778
3722
|
"misoWebAppOrigin": "[if(endsWith(parameters('misoWebAppUrl'), '/'), substring(parameters('misoWebAppUrl'), 0, max(0, sub(length(parameters('misoWebAppUrl')), 1))), parameters('misoWebAppUrl'))]",
|
|
3779
3723
|
"misoVdirSegment": "[if(or(equals(parameters('misoVdirPublic'), ''), equals(parameters('misoVdirPublic'), '/')), '', if(startsWith(parameters('misoVdirPublic'), '/'), parameters('misoVdirPublic'), format('/{0}', parameters('misoVdirPublic'))))]",
|
|
3780
3724
|
"misoWebServerPublicUrl": "[if(empty(variables('misoVdirSegment')), variables('misoWebAppOrigin'), format('{0}{1}', variables('misoWebAppOrigin'), variables('misoVdirSegment')))]",
|
|
@@ -3865,14 +3809,6 @@
|
|
|
3865
3809
|
"value": "[if(not(empty(parameters('misoClientSecret'))), parameters('misoClientSecret'), format('PLACEHOLDER_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]"
|
|
3866
3810
|
}
|
|
3867
3811
|
},
|
|
3868
|
-
{
|
|
3869
|
-
"type": "Microsoft.KeyVault/vaults/secrets",
|
|
3870
|
-
"apiVersion": "2023-07-01",
|
|
3871
|
-
"name": "[format('{0}/{1}', parameters('keyVaultName'), 'entra-client-secretKeyVault')]",
|
|
3872
|
-
"properties": {
|
|
3873
|
-
"value": "[variables('resolvedEntraIdClientSecret')]"
|
|
3874
|
-
}
|
|
3875
|
-
},
|
|
3876
3812
|
{
|
|
3877
3813
|
"condition": "[not(empty(parameters('moriLicensePublicKey')))]",
|
|
3878
3814
|
"type": "Microsoft.KeyVault/vaults/secrets",
|