@aifabrix/builder 2.42.1 → 2.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/aifabrix.js +1 -1
- package/integration/hubspot-test/README.md +126 -0
- package/integration/{hubspot → hubspot-test}/application.json +6 -6
- package/integration/{hubspot → hubspot-test}/create-hubspot.js +5 -5
- package/integration/hubspot-test/env.template +4 -0
- package/integration/{hubspot/hubspot-datasource-company.json → hubspot-test/hubspot-test-datasource-company.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-contact.json → hubspot-test/hubspot-test-datasource-contact.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-deal.json → hubspot-test/hubspot-test-datasource-deal.json} +3 -2
- package/integration/{hubspot/hubspot-datasource-users.json → hubspot-test/hubspot-test-datasource-users.json} +3 -2
- package/integration/{hubspot/hubspot-deploy.json → hubspot-test/hubspot-test-deploy.json} +198 -21
- package/integration/{hubspot/hubspot-system.json → hubspot-test/hubspot-test-system.json} +8 -7
- package/integration/hubspot-test/rbac.json +166 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-credential-real.yaml +3 -3
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-hubspot-env-vars.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-add-datasource.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-create.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-credential-select.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-known-platform.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-mode.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-file.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-openapi-url.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-source.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-array-test.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot-test/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-dimension-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +1 -1
- package/integration/{hubspot → hubspot-test}/test.js +102 -59
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-e2e.yaml +2 -2
- package/integration/{hubspot → hubspot-test}/wizard-hubspot-platform.yaml +1 -1
- package/lib/api/external-test.api.js +1 -1
- package/lib/api/service-users.api.js +111 -2
- package/lib/api/types/service-users.types.js +41 -0
- package/lib/app/register.js +3 -1
- package/lib/app/rotate-secret.js +3 -0
- package/lib/cli/setup-app.js +2 -2
- package/lib/cli/setup-auth.js +19 -11
- package/lib/cli/setup-dev.js +62 -32
- package/lib/cli/setup-environment.js +6 -21
- package/lib/cli/setup-infra.js +13 -0
- package/lib/cli/setup-secrets.js +45 -6
- package/lib/cli/setup-service-user.js +146 -20
- package/lib/cli/setup-utility.js +12 -0
- package/lib/commands/auth-config.js +4 -8
- package/lib/commands/datasource.js +46 -1
- package/lib/commands/dev-init.js +1 -1
- package/lib/commands/repair-env-template.js +14 -8
- package/lib/commands/repair-rbac.js +25 -19
- package/lib/commands/repair.js +96 -30
- package/lib/commands/secrets-remove.js +1 -1
- package/lib/commands/secrets-validate.js +17 -4
- package/lib/commands/service-user.js +231 -2
- package/lib/commands/up-common.js +25 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/core/admin-secrets.js +2 -0
- package/lib/core/config.js +7 -5
- package/lib/core/ensure-encryption-key.js +1 -3
- package/lib/core/secrets.js +32 -9
- package/lib/core/templates.js +1 -1
- package/lib/datasource/abac-validator.js +157 -0
- package/lib/datasource/field-reference-validator.js +74 -36
- package/lib/datasource/log-viewer.js +221 -0
- package/lib/datasource/resolve-app.js +109 -0
- package/lib/datasource/test-e2e.js +11 -20
- package/lib/datasource/test-integration.js +42 -22
- package/lib/datasource/validate.js +5 -2
- package/lib/external-system/generator.js +12 -8
- package/lib/external-system/test-system-level.js +1 -1
- package/lib/generator/external-controller-manifest.js +3 -3
- package/lib/generator/external.js +7 -7
- package/lib/generator/helpers.js +13 -9
- package/lib/generator/index.js +4 -4
- package/lib/generator/split.js +45 -10
- package/lib/generator/wizard.js +9 -6
- package/lib/infrastructure/helpers.js +50 -35
- package/lib/infrastructure/index.js +39 -23
- package/lib/schema/env-config.yaml +19 -2
- package/lib/schema/external-datasource.schema.json +11 -1
- package/lib/utils/app-config-resolver.js +23 -1
- package/lib/utils/config-paths.js +48 -4
- package/lib/utils/credential-secrets-env.js +16 -1
- package/lib/utils/env-map.js +7 -3
- package/lib/utils/error-formatter.js +37 -0
- package/lib/utils/external-env-template.js +180 -0
- package/lib/utils/external-system-display.js +43 -0
- package/lib/utils/external-system-validators.js +2 -2
- package/lib/utils/help-builder.js +3 -5
- package/lib/utils/local-secrets.js +26 -3
- package/lib/utils/paths.js +2 -1
- package/lib/utils/secrets-generator.js +2 -2
- package/lib/utils/secrets-utils.js +4 -0
- package/lib/utils/secure-file-permissions.js +91 -0
- package/lib/utils/token-manager.js +36 -3
- package/lib/utils/yaml-preserve.js +59 -1
- package/lib/validation/env-template-auth.js +50 -2
- package/lib/validation/external-manifest-validator.js +8 -0
- package/lib/validation/validate.js +8 -0
- package/lib/validation/validator.js +10 -13
- package/package.json +5 -1
- package/templates/applications/dataplane/env.template +5 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +13 -2
- package/templates/external-system/env.template.hbs +22 -0
- package/integration/hubspot/README.md +0 -102
- package/integration/hubspot/env.template +0 -4
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +0 -2
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +0 -5
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +0 -5
- /package/integration/{hubspot → hubspot-test}/companies.json +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-app-name.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-artifacts/wizard-invalid-missing-app.yaml +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-helpers.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down-tests.js +0 -0
- /package/integration/{hubspot → hubspot-test}/test-dataplane-down.js +0 -0
|
@@ -198,7 +198,7 @@ function saveSecretsFile(resolvedPath, secrets) {
|
|
|
198
198
|
|
|
199
199
|
const yamlContent = yaml.dump(secrets, {
|
|
200
200
|
indent: 2,
|
|
201
|
-
lineWidth:
|
|
201
|
+
lineWidth: -1,
|
|
202
202
|
noRefs: true,
|
|
203
203
|
sortKeys: false
|
|
204
204
|
});
|
|
@@ -206,7 +206,7 @@ function saveSecretsFile(resolvedPath, secrets) {
|
|
|
206
206
|
fs.writeFileSync(resolvedPath, yamlContent, { mode: 0o600 });
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
const YAML_DUMP_OPTS = { indent: 2, lineWidth:
|
|
209
|
+
const YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false };
|
|
210
210
|
|
|
211
211
|
/**
|
|
212
212
|
* Merges secret keys into the secrets file (load existing, merge, overwrite file).
|
|
@@ -14,6 +14,7 @@ const path = require('path');
|
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const logger = require('./logger');
|
|
16
16
|
const pathsUtil = require('./paths');
|
|
17
|
+
const { ensureSecureFilePermissions } = require('./secure-file-permissions');
|
|
17
18
|
const { getContainerPort } = require('./port-resolver');
|
|
18
19
|
const { loadYamlTolerantOfDuplicateKeys } = require('./secrets-generator');
|
|
19
20
|
|
|
@@ -78,6 +79,7 @@ function loadPrimaryUserSecrets() {
|
|
|
78
79
|
if (!fs.existsSync(userSecretsPath)) {
|
|
79
80
|
return {};
|
|
80
81
|
}
|
|
82
|
+
ensureSecureFilePermissions(userSecretsPath);
|
|
81
83
|
|
|
82
84
|
try {
|
|
83
85
|
const content = fs.readFileSync(userSecretsPath, 'utf8');
|
|
@@ -106,6 +108,7 @@ function loadUserSecrets() {
|
|
|
106
108
|
if (!fs.existsSync(userSecretsPath)) {
|
|
107
109
|
return {};
|
|
108
110
|
}
|
|
111
|
+
ensureSecureFilePermissions(userSecretsPath);
|
|
109
112
|
|
|
110
113
|
try {
|
|
111
114
|
const content = fs.readFileSync(userSecretsPath, 'utf8');
|
|
@@ -134,6 +137,7 @@ function loadDefaultSecrets() {
|
|
|
134
137
|
if (!fs.existsSync(defaultPath)) {
|
|
135
138
|
return {};
|
|
136
139
|
}
|
|
140
|
+
ensureSecureFilePermissions(defaultPath);
|
|
137
141
|
|
|
138
142
|
try {
|
|
139
143
|
const content = fs.readFileSync(defaultPath, 'utf8');
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure file permissions for secrets and config (ISO 27001).
|
|
3
|
+
* Ensures sensitive files are restricted to owner-only (0o600) when read or written.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Enforce restrictive permissions on secrets and config files
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/** Mode for secrets and admin files: owner read/write only (no group/other). */
|
|
16
|
+
const SECRET_FILE_MODE = 0o600;
|
|
17
|
+
|
|
18
|
+
/** Mode for config file (may contain tokens): owner read/write only. */
|
|
19
|
+
const CONFIG_FILE_MODE = 0o600;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ensures a file has restrictive permissions (0o600) when it exists.
|
|
23
|
+
* If the file has group or other read/write/execute bits set, chmods to owner-only.
|
|
24
|
+
* Safe to call on every read path; no-op when file is missing or already 0o600.
|
|
25
|
+
* On Windows, chmod restricts write access; mode bits are not fully supported.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} filePath - Absolute or relative path to the file
|
|
28
|
+
* @param {number} [mode=0o600] - Desired mode (default SECRET_FILE_MODE)
|
|
29
|
+
* @returns {boolean} True if file existed and permissions were (or are now) secure
|
|
30
|
+
*/
|
|
31
|
+
function ensureSecureFilePermissions(filePath, mode = SECRET_FILE_MODE) {
|
|
32
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(resolved)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const stat = fs.statSync(resolved);
|
|
41
|
+
if (!stat.isFile()) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const currentMode = stat.mode & 0o777;
|
|
45
|
+
if ((currentMode & 0o77) === 0) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
fs.chmodSync(resolved, mode);
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ensures a directory has restrictive permissions (0o700) when it exists.
|
|
57
|
+
* No-op when directory is missing or already 0o700 (no group/other).
|
|
58
|
+
*
|
|
59
|
+
* @param {string} dirPath - Absolute or relative path to the directory
|
|
60
|
+
* @returns {boolean} True if directory existed and permissions were (or are now) secure
|
|
61
|
+
*/
|
|
62
|
+
function ensureSecureDirPermissions(dirPath) {
|
|
63
|
+
if (!dirPath || typeof dirPath !== 'string') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const resolved = path.isAbsolute(dirPath) ? dirPath : path.resolve(dirPath);
|
|
67
|
+
try {
|
|
68
|
+
if (!fs.existsSync(resolved)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const stat = fs.statSync(resolved);
|
|
72
|
+
if (!stat.isDirectory()) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const currentMode = stat.mode & 0o777;
|
|
76
|
+
if ((currentMode & 0o77) === 0) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
fs.chmodSync(resolved, 0o700);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
ensureSecureFilePermissions,
|
|
88
|
+
ensureSecureDirPermissions,
|
|
89
|
+
SECRET_FILE_MODE,
|
|
90
|
+
CONFIG_FILE_MODE
|
|
91
|
+
};
|
|
@@ -21,12 +21,43 @@ const {
|
|
|
21
21
|
} = require('./token-manager-refresh');
|
|
22
22
|
const { warnRefreshFailureOnce, warnRefreshTokenExpiredOnce } = require('./token-manager-messages');
|
|
23
23
|
|
|
24
|
+
/** App key used for dataplane client credentials in secrets.local.yaml */
|
|
25
|
+
const DATAPLANE_APP_KEY = 'dataplane';
|
|
26
|
+
|
|
24
27
|
function getSecretsFilePath() {
|
|
25
28
|
return path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
|
-
*
|
|
32
|
+
* Validate that secrets.local.yaml contains dataplane client credentials.
|
|
33
|
+
* If missing, the developer should run: aifabrix app rotate-secret dataplane
|
|
34
|
+
* @param {string} [secretsFilePath] - Path to secrets file; defaults to ~/.aifabrix/secrets.local.yaml
|
|
35
|
+
* @returns {{ valid: boolean, hint?: string }}
|
|
36
|
+
*/
|
|
37
|
+
function validateDataplaneSecrets(secretsFilePath) {
|
|
38
|
+
const filePath = secretsFilePath || getSecretsFilePath();
|
|
39
|
+
const hint = 'Dataplane credentials are missing. Run: aifabrix app rotate-secret dataplane';
|
|
40
|
+
try {
|
|
41
|
+
if (!fs.existsSync(filePath)) {
|
|
42
|
+
return { valid: false, hint };
|
|
43
|
+
}
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
const secrets = yaml.load(content) || {};
|
|
46
|
+
const clientIdKey = `${DATAPLANE_APP_KEY}-client-idKeyVault`;
|
|
47
|
+
const clientSecretKey = `${DATAPLANE_APP_KEY}-client-secretKeyVault`;
|
|
48
|
+
const hasId = secrets[clientIdKey] !== null && secrets[clientIdKey] !== undefined && String(secrets[clientIdKey]).trim() !== '';
|
|
49
|
+
const hasSecret = secrets[clientSecretKey] !== null && secrets[clientSecretKey] !== undefined && String(secrets[clientSecretKey]).trim() !== '';
|
|
50
|
+
if (hasId && hasSecret) {
|
|
51
|
+
return { valid: true };
|
|
52
|
+
}
|
|
53
|
+
return { valid: false, hint };
|
|
54
|
+
} catch {
|
|
55
|
+
return { valid: false, hint };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load client credentials from secrets.local.yaml or process.env (e.g. integration/hubspot-test/.env).
|
|
30
61
|
* Reads secrets file using pattern: <app-name>-client-idKeyVault and <app-name>-client-secretKeyVault.
|
|
31
62
|
* If not found, checks process.env.CLIENTID and process.env.CLIENTSECRET (set when .env is loaded).
|
|
32
63
|
* @param {string} appName - Application name
|
|
@@ -60,7 +91,7 @@ async function loadClientCredentials(appName) {
|
|
|
60
91
|
logger.warn(`Failed to load credentials from secrets.local.yaml: ${error.message}`);
|
|
61
92
|
}
|
|
62
93
|
|
|
63
|
-
// Fallback: use CLIENTID/CLIENTSECRET from process.env (e.g. from integration/hubspot/.env)
|
|
94
|
+
// Fallback: use CLIENTID/CLIENTSECRET from process.env (e.g. from integration/hubspot-test/.env)
|
|
64
95
|
const envClientId = process.env.CLIENTID || process.env.CLIENT_ID;
|
|
65
96
|
const envClientSecret = process.env.CLIENTSECRET || process.env.CLIENT_SECRET;
|
|
66
97
|
if (envClientId && envClientSecret) {
|
|
@@ -439,6 +470,7 @@ function requireBearerForDataplanePipeline(authConfig) {
|
|
|
439
470
|
}
|
|
440
471
|
|
|
441
472
|
module.exports = {
|
|
473
|
+
DATAPLANE_APP_KEY,
|
|
442
474
|
getDeviceToken,
|
|
443
475
|
getClientToken,
|
|
444
476
|
isTokenExpired,
|
|
@@ -452,5 +484,6 @@ module.exports = {
|
|
|
452
484
|
getDeploymentAuth,
|
|
453
485
|
getDeviceOnlyAuth,
|
|
454
486
|
extractClientCredentials,
|
|
455
|
-
requireBearerForDataplanePipeline
|
|
487
|
+
requireBearerForDataplanePipeline,
|
|
488
|
+
validateDataplaneSecrets
|
|
456
489
|
};
|
|
@@ -185,6 +185,9 @@ function formatValue(value, quoted, quoteChar) {
|
|
|
185
185
|
* const result = encryptYamlValues(yamlContent, encryptionKey);
|
|
186
186
|
* // Returns: { content: '...', encrypted: 5, total: 10 }
|
|
187
187
|
*/
|
|
188
|
+
/** Pattern for YAML block scalar indicator (e.g. key: >- or key: |) */
|
|
189
|
+
const BLOCK_SCALAR_PATTERN = /^(\s*)([^#:\n]+?):\s*(\|[-+]?|>[-+]?)(\s*)(#.*)?$/;
|
|
190
|
+
|
|
188
191
|
/**
|
|
189
192
|
* Processes a single line for encryption
|
|
190
193
|
* @function processLineForEncryption
|
|
@@ -221,13 +224,68 @@ function processLineForEncryption(line, encryptionKey, stats) {
|
|
|
221
224
|
return line;
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Collects continuation lines for a YAML block scalar (value on next lines).
|
|
229
|
+
* @param {string[]} lines - All lines
|
|
230
|
+
* @param {number} startIndex - Index of line after the key: >- line
|
|
231
|
+
* @param {number} keyIndentLen - Number of leading spaces on the key line
|
|
232
|
+
* @returns {{ value: string, endIndex: number }} Combined value and index after last continuation line
|
|
233
|
+
*/
|
|
234
|
+
function collectBlockScalarLines(lines, startIndex, keyIndentLen) {
|
|
235
|
+
const parts = [];
|
|
236
|
+
let i = startIndex;
|
|
237
|
+
while (i < lines.length) {
|
|
238
|
+
const line = lines[i];
|
|
239
|
+
const contentTrimmed = line.trim();
|
|
240
|
+
if (contentTrimmed === '' || contentTrimmed.startsWith('#')) {
|
|
241
|
+
i++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const indentLen = line.length - line.trimStart().length;
|
|
245
|
+
if (indentLen <= keyIndentLen) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
parts.push(contentTrimmed);
|
|
249
|
+
i++;
|
|
250
|
+
}
|
|
251
|
+
return { value: parts.join(' '), endIndex: i };
|
|
252
|
+
}
|
|
253
|
+
|
|
224
254
|
function encryptYamlValues(content, encryptionKey) {
|
|
225
255
|
const lines = content.split(/\r?\n/);
|
|
226
256
|
const encryptedLines = [];
|
|
227
257
|
const stats = { encrypted: 0, total: 0 };
|
|
258
|
+
let i = 0;
|
|
259
|
+
|
|
260
|
+
while (i < lines.length) {
|
|
261
|
+
const line = lines[i];
|
|
262
|
+
const trimmed = line.trim();
|
|
263
|
+
|
|
264
|
+
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
265
|
+
encryptedLines.push(line);
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const blockMatch = line.match(BLOCK_SCALAR_PATTERN);
|
|
271
|
+
if (blockMatch) {
|
|
272
|
+
const [, indent, key, blockIndicator, trailingWhitespace, comment] = blockMatch;
|
|
273
|
+
const keyIndentLen = indent.length;
|
|
274
|
+
const folded = blockIndicator.startsWith('>');
|
|
275
|
+
const { value: rawValue, endIndex } = collectBlockScalarLines(lines, i + 1, keyIndentLen);
|
|
276
|
+
const value = folded ? rawValue.replace(/\s+/g, ' ').trim() : rawValue;
|
|
277
|
+
stats.total++;
|
|
278
|
+
const outValue = shouldEncryptValue(value) ? encryptSecret(value, encryptionKey) : value;
|
|
279
|
+
if (shouldEncryptValue(value)) {
|
|
280
|
+
stats.encrypted++;
|
|
281
|
+
}
|
|
282
|
+
encryptedLines.push(`${indent}${key}: ${outValue}${trailingWhitespace || ''}${comment || ''}`);
|
|
283
|
+
i = endIndex;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
228
286
|
|
|
229
|
-
for (const line of lines) {
|
|
230
287
|
encryptedLines.push(processLineForEncryption(line, encryptionKey, stats));
|
|
288
|
+
i++;
|
|
231
289
|
}
|
|
232
290
|
|
|
233
291
|
return {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { loadExternalIntegrationConfig, loadSystemFile } = require('../generator/external');
|
|
13
|
+
const { getKvPathSegmentForSecurityKey } = require('../utils/credential-secrets-env');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Extracts all kv:// paths from env.template content (RHS of VAR=value lines).
|
|
@@ -93,7 +94,7 @@ function setHasPathIgnoreCase(pathSet, requiredPath) {
|
|
|
93
94
|
* @returns {Promise<{ requiredPaths: Set<string>, warning?: string }>} Required kv paths and optional warning
|
|
94
95
|
*
|
|
95
96
|
* @example
|
|
96
|
-
* const { requiredPaths } = await collectRequiredAuthKvPaths('/path/to/integration/hubspot');
|
|
97
|
+
* const { requiredPaths } = await collectRequiredAuthKvPaths('/path/to/integration/hubspot-test');
|
|
97
98
|
* // requiredPaths has kv:// paths from authentication.security
|
|
98
99
|
*/
|
|
99
100
|
async function collectRequiredAuthKvPaths(appPath, _options = {}) {
|
|
@@ -148,10 +149,57 @@ async function validateAuthKvCoverage(appPath, content, errors, warnings, option
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Derives system key from system file name (e.g. hubspot-system.yaml -> hubspot).
|
|
154
|
+
* @param {string} systemFileName - System file name
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
function systemKeyFromFileName(systemFileName) {
|
|
158
|
+
if (!systemFileName || typeof systemFileName !== 'string') return '';
|
|
159
|
+
return systemFileName.replace(/-system\.(yaml|yml|json)$/i, '');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validates that authentication.security paths in system files match the canonical path
|
|
164
|
+
* (kv://<systemKey>/<getKvPathSegmentForSecurityKey(securityKey)>). Pushes errors when they differ.
|
|
165
|
+
*
|
|
166
|
+
* @async
|
|
167
|
+
* @function validateAuthSecurityPathConsistency
|
|
168
|
+
* @param {string} appPath - Application path (integration or builder dir)
|
|
169
|
+
* @param {string[]} errors - Errors array to push to
|
|
170
|
+
* @param {string[]} warnings - Warnings array to push to (unused; for API consistency)
|
|
171
|
+
*/
|
|
172
|
+
async function validateAuthSecurityPathConsistency(appPath, errors, _warnings) {
|
|
173
|
+
try {
|
|
174
|
+
const { schemaBasePath, systemFiles } = await loadExternalIntegrationConfig(appPath);
|
|
175
|
+
for (const systemFileName of systemFiles) {
|
|
176
|
+
const systemKey = systemKeyFromFileName(systemFileName);
|
|
177
|
+
if (!systemKey) continue;
|
|
178
|
+
const systemJson = await loadSystemFile(appPath, schemaBasePath, systemFileName);
|
|
179
|
+
const security = systemJson.authentication?.security;
|
|
180
|
+
if (!security || typeof security !== 'object') continue;
|
|
181
|
+
for (const [key, value] of Object.entries(security)) {
|
|
182
|
+
if (typeof value !== 'string' || !/^kv:\/\/.+/.test(value)) continue;
|
|
183
|
+
const canonicalSegment = getKvPathSegmentForSecurityKey(key);
|
|
184
|
+
const canonicalPath = canonicalSegment ? `kv://${systemKey}/${canonicalSegment}` : null;
|
|
185
|
+
if (canonicalPath && value !== canonicalPath) {
|
|
186
|
+
errors.push(
|
|
187
|
+
`authentication.security.${key} has path ${value}; canonical path is ${canonicalPath}. Run \`aifabrix repair <app>\` to normalize.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
_warnings.push(`Could not validate auth path consistency: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
151
197
|
module.exports = {
|
|
152
198
|
extractKvPathsFromEnvTemplate,
|
|
153
199
|
extractKvPathsFromCommentedLines,
|
|
154
200
|
setHasPathIgnoreCase,
|
|
155
201
|
collectRequiredAuthKvPaths,
|
|
156
|
-
validateAuthKvCoverage
|
|
202
|
+
validateAuthKvCoverage,
|
|
203
|
+
validateAuthSecurityPathConsistency,
|
|
204
|
+
systemKeyFromFileName
|
|
157
205
|
};
|
|
@@ -13,6 +13,8 @@ const Ajv = require('ajv');
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
16
|
+
const { validateFieldReferences } = require('../datasource/field-reference-validator');
|
|
17
|
+
const { validateAbac } = require('../datasource/abac-validator');
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Sets up AJV validator with external schemas
|
|
@@ -113,6 +115,12 @@ function validateDatasources(manifest, ajv, externalDatasourceSchema, errors, wa
|
|
|
113
115
|
if (!datasourceValid) {
|
|
114
116
|
const datasourceErrors = formatValidationErrors(validateDatasource.errors);
|
|
115
117
|
errors.push(...datasourceErrors.map(err => `Datasource ${index + 1} (${datasource.key || 'unknown'}): ${err}`));
|
|
118
|
+
} else {
|
|
119
|
+
const fieldRefErrors = validateFieldReferences(datasource);
|
|
120
|
+
const abacErrors = validateAbac(datasource);
|
|
121
|
+
const prefix = `Datasource ${index + 1} (${datasource.key || 'unknown'}): `;
|
|
122
|
+
fieldRefErrors.forEach(e => errors.push(prefix + e));
|
|
123
|
+
abacErrors.forEach(e => errors.push(prefix + e));
|
|
116
124
|
}
|
|
117
125
|
});
|
|
118
126
|
}
|
|
@@ -15,6 +15,8 @@ const validator = require('./validator');
|
|
|
15
15
|
const { resolveExternalFiles } = require('../utils/schema-resolver');
|
|
16
16
|
const { loadExternalSystemSchema, loadExternalDataSourceSchema, detectSchemaType } = require('../utils/schema-loader');
|
|
17
17
|
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
18
|
+
const { validateFieldReferences } = require('../datasource/field-reference-validator');
|
|
19
|
+
const { validateAbac } = require('../datasource/abac-validator');
|
|
18
20
|
const { detectAppType } = require('../utils/paths');
|
|
19
21
|
const batch = require('./validate-batch');
|
|
20
22
|
const { logOfflinePathWhenType } = require('../utils/cli-utils');
|
|
@@ -200,6 +202,12 @@ async function validateExternalFile(filePath, type) {
|
|
|
200
202
|
validateConfigurationNoStandardAuthVariables(parseResult.parsed, errors);
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
if (normalizedType === 'datasource') {
|
|
206
|
+
const fieldRefErrors = validateFieldReferences(parseResult.parsed);
|
|
207
|
+
const abacErrors = validateAbac(parseResult.parsed);
|
|
208
|
+
errors.push(...fieldRefErrors, ...abacErrors);
|
|
209
|
+
}
|
|
210
|
+
|
|
203
211
|
return {
|
|
204
212
|
valid: errors.length === 0,
|
|
205
213
|
errors,
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
-
const yaml = require('js-yaml');
|
|
15
14
|
const Ajv = require('ajv');
|
|
16
15
|
const applicationSchema = require('../schema/application-schema.json');
|
|
17
16
|
const externalSystemSchema = require('../schema/external-system.schema.json');
|
|
@@ -19,9 +18,9 @@ const externalDataSourceSchema = require('../schema/external-datasource.schema.j
|
|
|
19
18
|
const { transformVariablesForValidation } = require('../utils/variable-transformer');
|
|
20
19
|
const { checkEnvironment } = require('../utils/environment-checker');
|
|
21
20
|
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
22
|
-
const { detectAppType, resolveApplicationConfigPath } = require('../utils/paths');
|
|
21
|
+
const { detectAppType, resolveApplicationConfigPath, resolveRbacPath } = require('../utils/paths');
|
|
23
22
|
const { loadConfigFile } = require('../utils/config-format');
|
|
24
|
-
const { validateAuthKvCoverage } = require('./env-template-auth');
|
|
23
|
+
const { validateAuthKvCoverage, validateAuthSecurityPathConsistency } = require('./env-template-auth');
|
|
25
24
|
const { validateKvReferencesInLines } = require('./env-template-kv');
|
|
26
25
|
|
|
27
26
|
/**
|
|
@@ -155,7 +154,7 @@ async function validateVariables(appName, options = {}) {
|
|
|
155
154
|
function validateRoles(roles) {
|
|
156
155
|
const errors = [];
|
|
157
156
|
if (!roles || !Array.isArray(roles)) {
|
|
158
|
-
errors.push('rbac
|
|
157
|
+
errors.push('rbac file must contain a "roles" array');
|
|
159
158
|
return errors;
|
|
160
159
|
}
|
|
161
160
|
|
|
@@ -179,7 +178,7 @@ function validateRoles(roles) {
|
|
|
179
178
|
function validatePermissions(permissions) {
|
|
180
179
|
const errors = [];
|
|
181
180
|
if (!permissions || !Array.isArray(permissions)) {
|
|
182
|
-
errors.push('rbac
|
|
181
|
+
errors.push('rbac file must contain a "permissions" array');
|
|
183
182
|
return errors;
|
|
184
183
|
}
|
|
185
184
|
|
|
@@ -203,21 +202,18 @@ async function validateRbac(appName, options = {}) {
|
|
|
203
202
|
|
|
204
203
|
// Support both builder/ and integration/ directories using detectAppType
|
|
205
204
|
const { appPath } = await detectAppType(appName, options);
|
|
206
|
-
const
|
|
207
|
-
const rbacYml = path.join(appPath, 'rbac.yml');
|
|
208
|
-
const rbacPath = fs.existsSync(rbacYaml) ? rbacYaml : (fs.existsSync(rbacYml) ? rbacYml : null);
|
|
205
|
+
const rbacPath = resolveRbacPath(appPath);
|
|
209
206
|
|
|
210
207
|
if (!rbacPath) {
|
|
211
|
-
return { valid: true, errors: [], warnings: ['rbac
|
|
208
|
+
return { valid: true, errors: [], warnings: ['rbac file not found - authentication disabled'] };
|
|
212
209
|
}
|
|
213
210
|
|
|
214
|
-
const content = fs.readFileSync(rbacPath, 'utf8');
|
|
215
211
|
let rbac;
|
|
216
|
-
|
|
217
212
|
try {
|
|
218
|
-
rbac =
|
|
213
|
+
rbac = loadConfigFile(rbacPath);
|
|
219
214
|
} catch (error) {
|
|
220
|
-
|
|
215
|
+
const basename = path.basename(rbacPath);
|
|
216
|
+
throw new Error(`Invalid syntax in ${basename}: ${error.message}`);
|
|
221
217
|
}
|
|
222
218
|
|
|
223
219
|
const errors = [
|
|
@@ -287,6 +283,7 @@ async function validateEnvTemplate(appName, options = {}) {
|
|
|
287
283
|
|
|
288
284
|
if (isExternal) {
|
|
289
285
|
await validateAuthKvCoverage(appPath, content, errors, warnings, options);
|
|
286
|
+
await validateAuthSecurityPathConsistency(appPath, errors, warnings);
|
|
290
287
|
}
|
|
291
288
|
|
|
292
289
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aifabrix/builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.43.0",
|
|
4
4
|
"description": "AI Fabrix Local Fabric & Deployment SDK",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"test:integration": "jest --config jest.config.integration.js --runInBand",
|
|
18
18
|
"test:integration:python": "cross-env TEST_LANGUAGE=python jest --config jest.config.integration.js --runInBand",
|
|
19
19
|
"test:integration:typescript": "cross-env TEST_LANGUAGE=typescript jest --config jest.config.integration.js --runInBand",
|
|
20
|
+
"test:hubspot-wizard": "node integration/hubspot-test/test.js",
|
|
21
|
+
"test:hubspot-wizard:negative": "node integration/hubspot-test/test.js --type negative",
|
|
22
|
+
"test:hubspot-wizard:positive": "node integration/hubspot-test/test.js --type positive",
|
|
23
|
+
"test:hubspot-dataplane-down": "node integration/hubspot-test/test-dataplane-down.js",
|
|
20
24
|
"test:manual": "jest --config jest.config.manual.js --runInBand",
|
|
21
25
|
"lint": "eslint . --ext .js",
|
|
22
26
|
"lint:fix": "eslint . --ext .js --fix",
|
|
@@ -54,10 +54,14 @@ DATABASE_URL=kv://databases-dataplane-0-urlKeyVault
|
|
|
54
54
|
DB_0_PASSWORD=kv://databases-dataplane-0-passwordKeyVault
|
|
55
55
|
|
|
56
56
|
# Vector and document store DB: chunks, embeddings, vector indexes (pgvector).
|
|
57
|
-
# Binaries path: config.processing.fileStoragePath or /data/documents.
|
|
58
57
|
VECTOR_DATABASE_URL=kv://databases-dataplane-1-urlKeyVault
|
|
59
58
|
DB_1_PASSWORD=kv://databases-dataplane-1-passwordKeyVault
|
|
60
59
|
|
|
60
|
+
# Base path for document binary storage (used when datasource config has no processing.fileStoragePath).
|
|
61
|
+
# Dataplane creates subdirs per datasource key (e.g. DOCUMENT_STORAGE_BASE_PATH/test-e2e-sharepoint-documents).
|
|
62
|
+
# Production: use a writable path (e.g. /data/documents) and mount a volume. Local/Docker: use /tmp/documents or /app/data/documents.
|
|
63
|
+
DOCUMENT_STORAGE_BASE_PATH=/mnt/data/documents
|
|
64
|
+
|
|
61
65
|
# Logs Database Configuration (for execution, audit, ABAC traces)
|
|
62
66
|
LOGS_DATABASE_URL=kv://databases-dataplane-2-urlKeyVault
|
|
63
67
|
DB_2_PASSWORD=kv://databases-dataplane-2-passwordKeyVault
|
|
@@ -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: '1.
|
|
7
|
+
version: '1.9.0'
|
|
8
8
|
|
|
9
9
|
# Image Configuration
|
|
10
10
|
image:
|
|
@@ -111,6 +111,10 @@ REDIS_PERMISSIONS_TTL=900
|
|
|
111
111
|
KEYCLOAK_REALM=aifabrix
|
|
112
112
|
KEYCLOAK_SERVER_URL=kv://keycloak-server-url
|
|
113
113
|
KEYCLOAK_INTERNAL_SERVER_URL=kv://keycloak-internal-server-url
|
|
114
|
+
# Docker/internal host and port: used when config from DB has localhost (getDockerKeycloakInternalUrl).
|
|
115
|
+
# Resolved from env-config (e.g. KEYCLOAK_HOST=keycloak, KEYCLOAK_PORT=8080 for docker).
|
|
116
|
+
KEYCLOAK_HOST=${KEYCLOAK_HOST}
|
|
117
|
+
KEYCLOAK_PORT=${KEYCLOAK_PORT}
|
|
114
118
|
KEYCLOAK_CLIENT_ID=miso-controller
|
|
115
119
|
KEYCLOAK_CLIENT_SECRET=kv://keycloak-client-secretKeyVault
|
|
116
120
|
KEYCLOAK_ADMIN_USERNAME=admin
|
|
@@ -306,8 +310,14 @@ MISO_ALLOWED_ORIGINS=http://localhost:*
|
|
|
306
310
|
# =============================================================================
|
|
307
311
|
# LICENSE CONFIGURATION
|
|
308
312
|
# =============================================================================
|
|
309
|
-
#
|
|
310
|
-
#
|
|
313
|
+
# Offline JWT license (optional):
|
|
314
|
+
# - If set, controller validates license offline (RS256) without Mori subscription status call.
|
|
315
|
+
# - Value can be literal JWT or kv:// reference.
|
|
316
|
+
# - If not set, controller falls back to existing Mori subscription validation flow.
|
|
317
|
+
#
|
|
318
|
+
# Development: set to DEVELOPMENT to disable license validation (no Mori/JWT required):
|
|
319
|
+
# LICENSE_JWT=DEVELOPMENT
|
|
320
|
+
# - Use only for local development; do not use in production.
|
|
311
321
|
LICENSE_JWT=DEVELOPMENT
|
|
312
322
|
|
|
313
323
|
# =============================================================================
|
|
@@ -315,6 +325,7 @@ LICENSE_JWT=DEVELOPMENT
|
|
|
315
325
|
# =============================================================================
|
|
316
326
|
|
|
317
327
|
MORI_BASE_URL=kv://mori-controller-url
|
|
328
|
+
MORI_AUTH_METHOD=apiKey
|
|
318
329
|
MORI_API_KEY=kv://mori-controller-api-keyKeyVault
|
|
319
330
|
MORI_USERNAME=kv://mori-controller-basic-usernameKeyVault
|
|
320
331
|
MORI_PASSWORD=kv://mori-controller-basic-passwordKeyVault
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Environment variables for external system integration
|
|
2
|
+
# Use kv:// (or aifabrix secret set) for sensitive values; plain values for non-sensitive configuration.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
{{#if authMethod}}
|
|
6
|
+
# Authentication
|
|
7
|
+
# Type: {{authMethod}}
|
|
8
|
+
{{#each authSecureVars}}
|
|
9
|
+
{{name}}={{value}}
|
|
10
|
+
{{/each}}
|
|
11
|
+
{{#if authNonSecureVarNames}}
|
|
12
|
+
# Non-secure (e.g. URLs): {{#each authNonSecureVarNames}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
|
|
13
|
+
{{/if}}
|
|
14
|
+
|
|
15
|
+
{{/if}}
|
|
16
|
+
{{#if configuration.length}}
|
|
17
|
+
# Configuration
|
|
18
|
+
{{#each configuration}}
|
|
19
|
+
# {{comment}}
|
|
20
|
+
{{name}}={{value}}
|
|
21
|
+
{{/each}}
|
|
22
|
+
{{/if}}
|