@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
|
@@ -12,7 +12,14 @@ const logger = require('../utils/logger');
|
|
|
12
12
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
13
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
14
14
|
const { normalizeControllerUrl } = require('../core/config');
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
createServiceUser,
|
|
17
|
+
listServiceUsers,
|
|
18
|
+
regenerateSecretServiceUser,
|
|
19
|
+
deleteServiceUser,
|
|
20
|
+
updateGroupsServiceUser,
|
|
21
|
+
updateRedirectUrisServiceUser
|
|
22
|
+
} = require('../api/service-users.api');
|
|
16
23
|
|
|
17
24
|
const ONE_TIME_WARNING =
|
|
18
25
|
'Save this secret now; it will not be shown again.';
|
|
@@ -54,6 +61,12 @@ function extractCreateResponse(response) {
|
|
|
54
61
|
return { clientId, clientSecret };
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
const ID_WIDTH = 38;
|
|
65
|
+
const USERNAME_WIDTH = 22;
|
|
66
|
+
const EMAIL_WIDTH = 28;
|
|
67
|
+
const CLIENT_ID_WIDTH = 24;
|
|
68
|
+
const TABLE_SEPARATOR_LENGTH = 130;
|
|
69
|
+
|
|
57
70
|
/**
|
|
58
71
|
* Log error for failed create response and exit
|
|
59
72
|
* @param {Object} response - API response with success: false
|
|
@@ -74,6 +87,86 @@ function handleCreateError(response) {
|
|
|
74
87
|
process.exit(1);
|
|
75
88
|
}
|
|
76
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Log error for service-user API response and exit
|
|
92
|
+
* @param {Object} response - API response with success: false
|
|
93
|
+
* @param {string} permissionScope - Permission hint: 'read' | 'update' | 'delete'
|
|
94
|
+
*/
|
|
95
|
+
function handleServiceUserApiError(response, permissionScope) {
|
|
96
|
+
const status = response.status;
|
|
97
|
+
const msg = response.formattedError || response.error || 'Request failed';
|
|
98
|
+
if (status === 400) {
|
|
99
|
+
logger.error(chalk.red(`❌ Validation error: ${msg}`));
|
|
100
|
+
} else if (status === 401) {
|
|
101
|
+
logger.error(chalk.red('❌ Unauthorized. Run "aifabrix login" and try again.'));
|
|
102
|
+
} else if (status === 403) {
|
|
103
|
+
logger.error(chalk.red(`❌ Missing permission: service-user:${permissionScope}`));
|
|
104
|
+
logger.error(chalk.gray(`Your account needs the service-user:${permissionScope} permission on the controller.`));
|
|
105
|
+
} else if (status === 404) {
|
|
106
|
+
logger.error(chalk.red('❌ Service user not found.'));
|
|
107
|
+
const detail = response.error || '';
|
|
108
|
+
if (detail) {
|
|
109
|
+
logger.error(chalk.gray(detail));
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
logger.error(chalk.red(`❌ Request failed: ${msg}`));
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve controller URL and auth for list/rotate/delete/update (no create-specific validation)
|
|
119
|
+
* @async
|
|
120
|
+
* @param {Object} options - CLI options (controller optional)
|
|
121
|
+
* @returns {Promise<{ controllerUrl: string, authConfig: Object }>}
|
|
122
|
+
*/
|
|
123
|
+
async function resolveControllerAndAuth(options) {
|
|
124
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
125
|
+
if (!controllerUrl) {
|
|
126
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const authResult = await getServiceUserAuth(controllerUrl);
|
|
130
|
+
if (!authResult || !authResult.token) {
|
|
131
|
+
logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
|
|
132
|
+
logger.error(chalk.gray('Run: aifabrix login'));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
controllerUrl: authResult.controllerUrl,
|
|
137
|
+
authConfig: { type: 'bearer', token: authResult.token }
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Display service user list as a table (id, username, email, clientId, active).
|
|
143
|
+
* API shape: items have id, username, email, status, federatedIdentity.keycloakClientId.
|
|
144
|
+
* @param {Array<{ id?: string, username?: string, email?: string, status?: string, active?: boolean, clientId?: string, federatedIdentity?: { keycloakClientId?: string } }>} items - Service users
|
|
145
|
+
*/
|
|
146
|
+
function displayServiceUserList(items) {
|
|
147
|
+
logger.log(chalk.bold('\n📋 Service users:\n'));
|
|
148
|
+
if (!items || items.length === 0) {
|
|
149
|
+
logger.log(chalk.gray(' No service users found.\n'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const idCol = 'Id'.padEnd(ID_WIDTH);
|
|
153
|
+
const usernameCol = 'Username'.padEnd(USERNAME_WIDTH);
|
|
154
|
+
const emailCol = 'Email'.padEnd(EMAIL_WIDTH);
|
|
155
|
+
const clientIdCol = 'ClientId'.padEnd(CLIENT_ID_WIDTH);
|
|
156
|
+
const activeCol = 'Active';
|
|
157
|
+
logger.log(chalk.gray(`${idCol}${usernameCol}${emailCol}${clientIdCol}${activeCol}`));
|
|
158
|
+
logger.log(chalk.gray('-'.repeat(TABLE_SEPARATOR_LENGTH)));
|
|
159
|
+
items.forEach((row) => {
|
|
160
|
+
const id = (row.id ?? '').toString().padEnd(ID_WIDTH);
|
|
161
|
+
const username = (row.username ?? '—').padEnd(USERNAME_WIDTH);
|
|
162
|
+
const email = (row.email ?? '—').padEnd(EMAIL_WIDTH);
|
|
163
|
+
const clientId = (row.federatedIdentity?.keycloakClientId ?? row.clientId ?? '—').padEnd(CLIENT_ID_WIDTH);
|
|
164
|
+
const active = row.status === 'active' ? 'yes' : (row.status ?? (row.active === true ? 'yes' : row.active === false ? 'no' : '—'));
|
|
165
|
+
logger.log(`${id}${username}${email}${clientId}${active}`);
|
|
166
|
+
});
|
|
167
|
+
logger.log('');
|
|
168
|
+
}
|
|
169
|
+
|
|
77
170
|
/**
|
|
78
171
|
* Display success output with clientId, clientSecret and one-time warning
|
|
79
172
|
* @param {string} clientId - Service user client ID
|
|
@@ -194,6 +287,142 @@ async function runServiceUserCreate(options = {}) {
|
|
|
194
287
|
displayCreateSuccess(clientId, clientSecret);
|
|
195
288
|
}
|
|
196
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Require service user id option; exit with message if missing
|
|
292
|
+
* @param {string} [id] - Service user ID from options
|
|
293
|
+
* @returns {string} Trimmed id
|
|
294
|
+
*/
|
|
295
|
+
function requireServiceUserId(id) {
|
|
296
|
+
const trimmed = (id && typeof id === 'string' ? id.trim() : '') || '';
|
|
297
|
+
if (!trimmed) {
|
|
298
|
+
logger.error(chalk.red('❌ Service user ID is required. Use --id <uuid>.'));
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
return trimmed;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Run service-user list: call GET /api/v1/service-users and display table
|
|
306
|
+
* @async
|
|
307
|
+
* @param {Object} options - CLI options (controller, page, pageSize, sort, filter, search)
|
|
308
|
+
* @returns {Promise<void>}
|
|
309
|
+
*/
|
|
310
|
+
async function runServiceUserList(options = {}) {
|
|
311
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
312
|
+
const listOptions = {
|
|
313
|
+
page: options.page,
|
|
314
|
+
pageSize: options.pageSize,
|
|
315
|
+
sort: options.sort,
|
|
316
|
+
filter: options.filter,
|
|
317
|
+
search: options.search
|
|
318
|
+
};
|
|
319
|
+
const response = await listServiceUsers(controllerUrl, authConfig, listOptions);
|
|
320
|
+
if (response && response.success === false) {
|
|
321
|
+
handleServiceUserApiError(response, 'read');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const body = response?.data?.data ?? response?.data ?? response ?? {};
|
|
325
|
+
const items = Array.isArray(body) ? body : (body.data ?? []);
|
|
326
|
+
displayServiceUserList(items);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Run service-user rotate-secret: call POST .../regenerate-secret and print new secret once with warning
|
|
331
|
+
* @async
|
|
332
|
+
* @param {Object} options - CLI options (controller, id required)
|
|
333
|
+
* @returns {Promise<void>}
|
|
334
|
+
*/
|
|
335
|
+
async function runServiceUserRotateSecret(options = {}) {
|
|
336
|
+
const id = requireServiceUserId(options.id);
|
|
337
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
338
|
+
const response = await regenerateSecretServiceUser(controllerUrl, authConfig, id);
|
|
339
|
+
if (response && response.success === false) {
|
|
340
|
+
handleServiceUserApiError(response, 'update');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const payload = response?.data?.data ?? response?.data ?? response ?? {};
|
|
344
|
+
const clientSecret = payload?.clientSecret ?? '';
|
|
345
|
+
if (response && response.success === true) {
|
|
346
|
+
logger.log(chalk.bold('\n✓ Secret rotated\n'));
|
|
347
|
+
logger.log(chalk.cyan(' clientSecret: ') + clientSecret);
|
|
348
|
+
logger.log('');
|
|
349
|
+
logger.log(chalk.yellow('⚠ ' + ONE_TIME_WARNING));
|
|
350
|
+
logger.log('');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Run service-user delete: call DELETE .../service-users/{id} (deactivates the user)
|
|
356
|
+
* @async
|
|
357
|
+
* @param {Object} options - CLI options (controller, id required)
|
|
358
|
+
* @returns {Promise<void>}
|
|
359
|
+
*/
|
|
360
|
+
async function runServiceUserDelete(options = {}) {
|
|
361
|
+
const id = requireServiceUserId(options.id);
|
|
362
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
363
|
+
const response = await deleteServiceUser(controllerUrl, authConfig, id);
|
|
364
|
+
if (response && response.success === false) {
|
|
365
|
+
handleServiceUserApiError(response, 'delete');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (response && response.success === true) {
|
|
369
|
+
logger.log(chalk.green('✓ Service user deactivated.\n'));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Run service-user update-groups: call PUT .../groups with groupNames
|
|
375
|
+
* @async
|
|
376
|
+
* @param {Object} options - CLI options (controller, id, groupNames required)
|
|
377
|
+
* @returns {Promise<void>}
|
|
378
|
+
*/
|
|
379
|
+
async function runServiceUserUpdateGroups(options = {}) {
|
|
380
|
+
const id = requireServiceUserId(options.id);
|
|
381
|
+
const groupNames = parseList(options.groupNames);
|
|
382
|
+
if (groupNames.length === 0) {
|
|
383
|
+
logger.error(chalk.red('❌ At least one group name is required. Use --group-names <name1,name2,...>.'));
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
387
|
+
const response = await updateGroupsServiceUser(controllerUrl, authConfig, id, { groupNames });
|
|
388
|
+
if (response && response.success === false) {
|
|
389
|
+
handleServiceUserApiError(response, 'update');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (response && response.success === true) {
|
|
393
|
+
logger.log(chalk.green('✓ Service user groups updated.\n'));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Run service-user update-redirect-uris: call PUT .../redirect-uris (min 1 URI)
|
|
399
|
+
* @async
|
|
400
|
+
* @param {Object} options - CLI options (controller, id, redirectUris required, min 1)
|
|
401
|
+
* @returns {Promise<void>}
|
|
402
|
+
*/
|
|
403
|
+
async function runServiceUserUpdateRedirectUris(options = {}) {
|
|
404
|
+
const id = requireServiceUserId(options.id);
|
|
405
|
+
const redirectUris = parseList(options.redirectUris);
|
|
406
|
+
if (redirectUris.length === 0) {
|
|
407
|
+
logger.error(chalk.red('❌ At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
const { controllerUrl, authConfig } = await resolveControllerAndAuth(options);
|
|
411
|
+
const response = await updateRedirectUrisServiceUser(controllerUrl, authConfig, id, { redirectUris });
|
|
412
|
+
if (response && response.success === false) {
|
|
413
|
+
handleServiceUserApiError(response, 'update');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (response && response.success === true) {
|
|
417
|
+
logger.log(chalk.green('✓ Service user redirect URIs updated.\n'));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
197
421
|
module.exports = {
|
|
198
|
-
runServiceUserCreate
|
|
422
|
+
runServiceUserCreate,
|
|
423
|
+
runServiceUserList,
|
|
424
|
+
runServiceUserRotateSecret,
|
|
425
|
+
runServiceUserDelete,
|
|
426
|
+
runServiceUserUpdateGroups,
|
|
427
|
+
runServiceUserUpdateRedirectUris
|
|
199
428
|
};
|
|
@@ -165,6 +165,30 @@ function patchEnvOutputPathForDeployOnly(appName) {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Removes builder app directories for the given app names. Only removes paths under the builder root
|
|
170
|
+
* to prevent path traversal. Uses getBuilderPath for each app and validates before removal.
|
|
171
|
+
*
|
|
172
|
+
* @param {string[]} appNames - Application names (e.g. ['keycloak', 'miso-controller', 'dataplane'])
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
* @throws {Error} If any path is outside builder root (path traversal attempt)
|
|
175
|
+
*/
|
|
176
|
+
async function cleanBuilderAppDirs(appNames) {
|
|
177
|
+
if (!Array.isArray(appNames) || appNames.length === 0) return;
|
|
178
|
+
const builderRoot = path.resolve(pathsUtil.getBuilderRoot());
|
|
179
|
+
for (const appName of appNames) {
|
|
180
|
+
if (!appName || typeof appName !== 'string') continue;
|
|
181
|
+
const appPath = path.resolve(pathsUtil.getBuilderPath(appName));
|
|
182
|
+
if (!appPath.startsWith(builderRoot + path.sep) && appPath !== builderRoot) {
|
|
183
|
+
throw new Error(`Path ${appPath} is outside builder root ${builderRoot}; refusing to clean`);
|
|
184
|
+
}
|
|
185
|
+
if (fs.existsSync(appPath)) {
|
|
186
|
+
fs.rmSync(appPath, { recursive: true });
|
|
187
|
+
logger.log(chalk.blue(`Cleaned builder/${appName}`));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
168
192
|
/**
|
|
169
193
|
* Ensures builder app directory exists from template if application config is missing.
|
|
170
194
|
* If builder/<appName>/application config does not exist, copies from templates/applications/<appName>.
|
|
@@ -206,6 +230,7 @@ async function ensureAppFromTemplate(appName) {
|
|
|
206
230
|
}
|
|
207
231
|
|
|
208
232
|
module.exports = {
|
|
233
|
+
cleanBuilderAppDirs,
|
|
209
234
|
ensureAppFromTemplate,
|
|
210
235
|
patchEnvOutputPathForDeployOnly,
|
|
211
236
|
validateEnvOutputPathFolderOrNull,
|
|
@@ -80,7 +80,7 @@ async function resolveControllerUrlWithHealthCheck() {
|
|
|
80
80
|
logger.log(chalk.yellow(`\nController at ${controllerUrl} is not responding (health check failed).\n`));
|
|
81
81
|
const newUrl = await promptForControllerUrl(controllerUrl);
|
|
82
82
|
if (!newUrl) {
|
|
83
|
-
throw new Error('Controller URL is required. Run "aifabrix up-dataplane" again and enter a valid controller URL, or set it with: aifabrix auth
|
|
83
|
+
throw new Error('Controller URL is required. Run "aifabrix up-dataplane" again and enter a valid controller URL, or set it with: aifabrix auth --set-controller <url>');
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
try {
|
|
@@ -179,7 +179,7 @@ async function handleUpDataplane(options = {}) {
|
|
|
179
179
|
const environment = (cfg && cfg.environment) ? cfg.environment : 'dev';
|
|
180
180
|
if (environment !== 'dev') {
|
|
181
181
|
throw new Error(
|
|
182
|
-
'Dataplane is only supported in dev environment. Set with: aifabrix auth
|
|
182
|
+
'Dataplane is only supported in dev environment. Set with: aifabrix auth --set-environment dev.'
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
185
|
logger.log(chalk.green('✓ Logged in and environment is dev'));
|
|
@@ -14,6 +14,7 @@ const path = require('path');
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const config = require('./config');
|
|
16
16
|
const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
|
|
17
|
+
const { ensureSecureFilePermissions } = require('../utils/secure-file-permissions');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Parse .env-style content into key-value map (excludes comments and empty lines).
|
|
@@ -57,6 +58,7 @@ async function readAndDecryptAdminSecrets(adminSecretsPath) {
|
|
|
57
58
|
if (!fs.existsSync(resolvedPath)) {
|
|
58
59
|
throw new Error(`Admin secrets file not found: ${resolvedPath}. Run 'aifabrix up-infra' or ensure admin-secrets.env exists.`);
|
|
59
60
|
}
|
|
61
|
+
ensureSecureFilePermissions(resolvedPath);
|
|
60
62
|
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
61
63
|
const raw = parseAdminEnvContent(content);
|
|
62
64
|
const encryptionKey = await config.getSecretsEncryptionKey();
|
package/lib/core/config.js
CHANGED
|
@@ -13,6 +13,7 @@ const path = require('path');
|
|
|
13
13
|
const yaml = require('js-yaml');
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const { encryptToken, decryptToken, isTokenEncrypted } = require('../utils/token-encryption');
|
|
16
|
+
const { ensureSecureFilePermissions, ensureSecureDirPermissions } = require('../utils/secure-file-permissions');
|
|
16
17
|
// Avoid importing paths here to prevent circular dependency.
|
|
17
18
|
// Config location (first match wins):
|
|
18
19
|
// 1. AIFABRIX_CONFIG env = full path to config.yaml
|
|
@@ -111,7 +112,7 @@ function applyConfigDefaults(config) {
|
|
|
111
112
|
config.device = {};
|
|
112
113
|
}
|
|
113
114
|
// Ensure controller field exists (but don't set defaults)
|
|
114
|
-
// It will be set by login or auth
|
|
115
|
+
// It will be set by login or auth --set-controller
|
|
115
116
|
return config;
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -133,6 +134,8 @@ function getDefaultConfig() {
|
|
|
133
134
|
|
|
134
135
|
async function getConfig() {
|
|
135
136
|
try {
|
|
137
|
+
ensureSecureDirPermissions(RUNTIME_CONFIG_DIR);
|
|
138
|
+
ensureSecureFilePermissions(RUNTIME_CONFIG_FILE);
|
|
136
139
|
const configContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
|
|
137
140
|
let config = yaml.load(configContent);
|
|
138
141
|
|
|
@@ -233,6 +236,7 @@ async function getDeveloperId() {
|
|
|
233
236
|
*/
|
|
234
237
|
async function verifyDeveloperIdSaved(devIdString) {
|
|
235
238
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
239
|
+
ensureSecureFilePermissions(RUNTIME_CONFIG_FILE);
|
|
236
240
|
const savedContent = await fs.readFile(RUNTIME_CONFIG_FILE, 'utf8');
|
|
237
241
|
const savedConfig = yaml.load(savedContent);
|
|
238
242
|
const savedDevIdString = String(savedConfig['developer-id']);
|
|
@@ -427,11 +431,11 @@ async function getSecretsPath() {
|
|
|
427
431
|
}
|
|
428
432
|
|
|
429
433
|
async function setSecretsPath(secretsPath) {
|
|
430
|
-
if (
|
|
434
|
+
if (typeof secretsPath !== 'string') {
|
|
431
435
|
throw new Error('Secrets path is required and must be a string');
|
|
432
436
|
}
|
|
433
437
|
const config = await getConfig();
|
|
434
|
-
config['aifabrix-secrets'] = secretsPath;
|
|
438
|
+
config['aifabrix-secrets'] = secretsPath.trim() || undefined;
|
|
435
439
|
await saveConfig(config);
|
|
436
440
|
}
|
|
437
441
|
|
|
@@ -489,10 +493,8 @@ Object.assign(exportsObj, tokenFunctions);
|
|
|
489
493
|
const { createPathConfigFunctions } = require('../utils/config-paths');
|
|
490
494
|
const pathConfigFunctions = createPathConfigFunctions(getConfig, saveConfig);
|
|
491
495
|
Object.assign(exportsObj, pathConfigFunctions);
|
|
492
|
-
|
|
493
496
|
// Format preference functions
|
|
494
497
|
const { createFormatFunctions } = require('../utils/config-format-preference');
|
|
495
498
|
const formatFunctions = createFormatFunctions(getConfig, saveConfig);
|
|
496
499
|
Object.assign(exportsObj, formatFunctions);
|
|
497
|
-
|
|
498
500
|
module.exports = exportsObj;
|
|
@@ -12,7 +12,6 @@ const fs = require('fs');
|
|
|
12
12
|
const yaml = require('js-yaml');
|
|
13
13
|
const crypto = require('crypto');
|
|
14
14
|
const pathsUtil = require('../utils/paths');
|
|
15
|
-
const { saveLocalSecret } = require('../utils/local-secrets');
|
|
16
15
|
|
|
17
16
|
const ENCRYPTION_KEY = 'secrets-encryptionKeyVault';
|
|
18
17
|
|
|
@@ -30,7 +29,7 @@ function readKeyFromFile(filePath) {
|
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
31
|
* Ensure secrets encryption key exists. If config already has it, do nothing.
|
|
33
|
-
* If key exists in user or project secrets file, set config. Otherwise generate
|
|
32
|
+
* If key exists in user or project secrets file, set config. Otherwise generate and store only in config (not in secrets file).
|
|
34
33
|
* @param {Object} config - Config module (getSecretsEncryptionKey, setSecretsEncryptionKey, getSecretsPath)
|
|
35
34
|
* @returns {Promise<void>}
|
|
36
35
|
*/
|
|
@@ -49,7 +48,6 @@ async function ensureSecretsEncryptionKey(config) {
|
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
const newKey = crypto.randomBytes(32).toString('hex');
|
|
52
|
-
await saveLocalSecret(ENCRYPTION_KEY, newKey);
|
|
53
51
|
await config.setSecretsEncryptionKey(newKey);
|
|
54
52
|
}
|
|
55
53
|
|
package/lib/core/secrets.js
CHANGED
|
@@ -52,6 +52,7 @@ const {
|
|
|
52
52
|
} = require('../utils/secrets-utils');
|
|
53
53
|
const { decryptSecret, isEncrypted } = require('../utils/secrets-encryption');
|
|
54
54
|
const pathsUtil = require('../utils/paths');
|
|
55
|
+
const { ensureSecureFilePermissions } = require('../utils/secure-file-permissions');
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Generates a canonical secret name from an environment variable key.
|
|
@@ -150,6 +151,7 @@ function mergeUserWithConfigFile(userSecrets, resolvedConfigPath) {
|
|
|
150
151
|
if (!fs.existsSync(resolvedConfigPath)) {
|
|
151
152
|
return null;
|
|
152
153
|
}
|
|
154
|
+
ensureSecureFilePermissions(resolvedConfigPath);
|
|
153
155
|
let configSecrets;
|
|
154
156
|
try {
|
|
155
157
|
configSecrets = readYamlAtPath(resolvedConfigPath);
|
|
@@ -225,6 +227,7 @@ async function loadSecretsWithFallbacks() {
|
|
|
225
227
|
if (projectRoot) {
|
|
226
228
|
const builderPath = path.join(projectRoot, 'builder', 'secrets.local.yaml');
|
|
227
229
|
if (fs.existsSync(builderPath)) {
|
|
230
|
+
ensureSecureFilePermissions(builderPath);
|
|
228
231
|
const builderSecrets = mergeUserWithConfigFile(merged || {}, builderPath);
|
|
229
232
|
if (builderSecrets) merged = builderSecrets;
|
|
230
233
|
}
|
|
@@ -244,6 +247,7 @@ async function loadSecrets(secretsPath, _appName) {
|
|
|
244
247
|
if (!fs.existsSync(resolvedPath)) {
|
|
245
248
|
throw new Error(`Secrets file not found: ${resolvedPath}`);
|
|
246
249
|
}
|
|
250
|
+
ensureSecureFilePermissions(resolvedPath);
|
|
247
251
|
const explicitSecrets = readYamlAtPath(resolvedPath);
|
|
248
252
|
if (!explicitSecrets || typeof explicitSecrets !== 'object') {
|
|
249
253
|
throw new Error(`Invalid secrets file format: ${resolvedPath}`);
|
|
@@ -516,6 +520,24 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
|
|
|
516
520
|
return envPath;
|
|
517
521
|
}
|
|
518
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Writes admin env key-value pairs to content; encrypts values when encryption key is set.
|
|
525
|
+
* @async
|
|
526
|
+
* @param {Object.<string, string>} adminObj - Key-value object (e.g. POSTGRES_PASSWORD, ...)
|
|
527
|
+
* @returns {Promise<string>} .env-style content (plaintext or secure:// for secrets)
|
|
528
|
+
*/
|
|
529
|
+
async function formatAdminSecretsContent(adminObj) {
|
|
530
|
+
const encryptionKey = await config.getSecretsEncryptionKey();
|
|
531
|
+
const { encryptSecret } = require('../utils/secrets-encryption');
|
|
532
|
+
const lines = ['# Infrastructure Admin Credentials'];
|
|
533
|
+
for (const [k, v] of Object.entries(adminObj)) {
|
|
534
|
+
const value = (v === null || v === undefined) ? '' : String(v).replace(/\n/g, ' ').trim();
|
|
535
|
+
const valueToWrite = encryptionKey ? encryptSecret(value, encryptionKey) : value;
|
|
536
|
+
lines.push(`${k}=${valueToWrite}`);
|
|
537
|
+
}
|
|
538
|
+
return lines.join('\n');
|
|
539
|
+
}
|
|
540
|
+
|
|
519
541
|
/** Generates admin secrets for infrastructure (~/.aifabrix/admin-secrets.env). Uses admin123 when no postgres password. */
|
|
520
542
|
async function generateAdminSecretsEnv(secretsPath) {
|
|
521
543
|
let secrets;
|
|
@@ -541,15 +563,15 @@ async function generateAdminSecretsEnv(secretsPath) {
|
|
|
541
563
|
const raw = secrets['postgres-passwordKeyVault'];
|
|
542
564
|
const postgresPassword = (raw && String(raw).trim()) || 'admin123';
|
|
543
565
|
|
|
544
|
-
const
|
|
545
|
-
POSTGRES_PASSWORD
|
|
546
|
-
PGADMIN_DEFAULT_EMAIL
|
|
547
|
-
PGADMIN_DEFAULT_PASSWORD
|
|
548
|
-
REDIS_HOST
|
|
549
|
-
REDIS_COMMANDER_USER
|
|
550
|
-
REDIS_COMMANDER_PASSWORD
|
|
551
|
-
|
|
552
|
-
|
|
566
|
+
const adminObj = {
|
|
567
|
+
POSTGRES_PASSWORD: postgresPassword,
|
|
568
|
+
PGADMIN_DEFAULT_EMAIL: 'admin@aifabrix.dev',
|
|
569
|
+
PGADMIN_DEFAULT_PASSWORD: postgresPassword,
|
|
570
|
+
REDIS_HOST: 'local:redis:6379:0:',
|
|
571
|
+
REDIS_COMMANDER_USER: 'admin',
|
|
572
|
+
REDIS_COMMANDER_PASSWORD: postgresPassword
|
|
573
|
+
};
|
|
574
|
+
const adminSecrets = await formatAdminSecretsContent(adminObj);
|
|
553
575
|
fs.writeFileSync(adminEnvPath, adminSecrets, { mode: 0o600 });
|
|
554
576
|
return adminEnvPath;
|
|
555
577
|
}
|
|
@@ -560,6 +582,7 @@ module.exports = {
|
|
|
560
582
|
generateEnvContent,
|
|
561
583
|
generateMissingSecrets,
|
|
562
584
|
generateAdminSecretsEnv,
|
|
585
|
+
formatAdminSecretsContent,
|
|
563
586
|
validateSecrets,
|
|
564
587
|
createDefaultSecrets,
|
|
565
588
|
getCanonicalSecretName,
|
package/lib/core/templates.js
CHANGED
|
@@ -268,7 +268,7 @@ function generateSecretsYaml(config, existingSecrets = {}) {
|
|
|
268
268
|
Object.entries(existingSecrets).forEach(([key, value]) => {
|
|
269
269
|
secrets.data[key] = Buffer.from(value).toString('base64');
|
|
270
270
|
});
|
|
271
|
-
return yaml.dump(secrets, { indent: 2, lineWidth:
|
|
271
|
+
return yaml.dump(secrets, { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false });
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
module.exports = {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABAC (Attribute-Based Access Control) validator for external datasources.
|
|
3
|
+
*
|
|
4
|
+
* Validates config.abac.dimensions (dimension-to-attribute references),
|
|
5
|
+
* config.abac.crossSystemJson (allowed operators, one per path, value types),
|
|
6
|
+
* and errors on legacy config.abac.crossSystem.
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview ABAC validation for AI Fabrix Builder
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const DIMENSION_KEY_PATTERN = /^[a-zA-Z0-9_]+$/;
|
|
14
|
+
const ATTRIBUTE_PATH_PATTERN = /^[a-zA-Z0-9_.]+$/;
|
|
15
|
+
const CROSS_SYSTEM_JSON_PATH_PATTERN = /^[a-zA-Z0-9_.]+$/;
|
|
16
|
+
const ALLOWED_CROSS_SYSTEM_OPERATORS = new Set([
|
|
17
|
+
'eq', 'ne', 'gt', 'lt', 'gte', 'lte', 'in', 'nin', 'contains', 'like', 'isNull', 'isNotNull'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates dimension keys and attribute path values for a dimensions object.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} dimensions - Object mapping dimension keys to attribute paths
|
|
24
|
+
* @param {string} source - Label for error messages (e.g. "config.abac.dimensions")
|
|
25
|
+
* @param {Set<string>} validAttributeNames - Set of valid attribute names (from fieldMappings.attributes)
|
|
26
|
+
* @returns {string[]} Error messages
|
|
27
|
+
*/
|
|
28
|
+
function validateDimensionsObject(dimensions, source, validAttributeNames) {
|
|
29
|
+
const errors = [];
|
|
30
|
+
if (!dimensions || typeof dimensions !== 'object' || Array.isArray(dimensions)) {
|
|
31
|
+
return errors;
|
|
32
|
+
}
|
|
33
|
+
for (const [dimKey, attrPath] of Object.entries(dimensions)) {
|
|
34
|
+
if (!DIMENSION_KEY_PATTERN.test(dimKey)) {
|
|
35
|
+
errors.push(
|
|
36
|
+
`${source}: dimension key '${dimKey}' must contain only letters, numbers, and underscores. Add '${dimKey}' to fieldMappings.attributes or fix the key.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (typeof attrPath !== 'string' || !ATTRIBUTE_PATH_PATTERN.test(attrPath)) {
|
|
40
|
+
errors.push(
|
|
41
|
+
`${source}: attribute path for dimension '${dimKey}' must be a string with letters, numbers, underscores, and dots only.`
|
|
42
|
+
);
|
|
43
|
+
} else if (validAttributeNames && validAttributeNames.size > 0) {
|
|
44
|
+
const normalizedName = attrPath.includes('.') ? attrPath.split('.').pop() : attrPath;
|
|
45
|
+
if (!validAttributeNames.has(attrPath) && !validAttributeNames.has(normalizedName)) {
|
|
46
|
+
errors.push(
|
|
47
|
+
`${source}: dimension '${dimKey}' maps to '${attrPath}' which is not in fieldMappings.attributes. Add the attribute or remove from dimensions.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return errors;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validates crossSystemJson: path format, exactly one operator per path, allowed operators and value types.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} crossSystemJson - Object mapping field paths to operator objects
|
|
59
|
+
* @returns {string[]} Error messages
|
|
60
|
+
*/
|
|
61
|
+
function validateCrossSystemJson(crossSystemJson) {
|
|
62
|
+
const errors = [];
|
|
63
|
+
if (!crossSystemJson || typeof crossSystemJson !== 'object' || Array.isArray(crossSystemJson)) {
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
|
66
|
+
for (const [path, opObj] of Object.entries(crossSystemJson)) {
|
|
67
|
+
if (!CROSS_SYSTEM_JSON_PATH_PATTERN.test(path)) {
|
|
68
|
+
errors.push(
|
|
69
|
+
`config.abac.crossSystemJson: path '${path}' must contain only letters, numbers, underscores, and dots.`
|
|
70
|
+
);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (typeof opObj !== 'object' || opObj === null || Array.isArray(opObj)) {
|
|
74
|
+
errors.push(
|
|
75
|
+
`config.abac.crossSystemJson.${path}: value must be an object with exactly one operator (e.g. { "eq": "user.country" }).`
|
|
76
|
+
);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const keys = Object.keys(opObj);
|
|
80
|
+
if (keys.length === 0) {
|
|
81
|
+
errors.push(
|
|
82
|
+
`config.abac.crossSystemJson.${path}: object must have exactly one operator. Allowed: ${[...ALLOWED_CROSS_SYSTEM_OPERATORS].join(', ')}.`
|
|
83
|
+
);
|
|
84
|
+
} else if (keys.length > 1) {
|
|
85
|
+
errors.push(
|
|
86
|
+
`config.abac.crossSystemJson.${path}: must have exactly one operator per path, got ${keys.join(', ')}. Use one of: ${[...ALLOWED_CROSS_SYSTEM_OPERATORS].join(', ')}.`
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
const op = keys[0];
|
|
90
|
+
if (!ALLOWED_CROSS_SYSTEM_OPERATORS.has(op)) {
|
|
91
|
+
errors.push(
|
|
92
|
+
`config.abac.crossSystemJson.${path}: unknown operator '${op}'. Allowed: ${[...ALLOWED_CROSS_SYSTEM_OPERATORS].join(', ')}.`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validates ABAC configuration for a parsed datasource.
|
|
102
|
+
* Checks dimensions (from config.abac or fieldMappings), crossSystemJson, and rejects legacy crossSystem.
|
|
103
|
+
*
|
|
104
|
+
* @function validateAbac
|
|
105
|
+
* @param {Object} parsed - Parsed datasource object (after JSON parse)
|
|
106
|
+
* @returns {string[]} Array of error messages; empty if valid
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const errors = validateAbac(parsed);
|
|
110
|
+
* if (errors.length > 0) errors.forEach(e => console.error(e));
|
|
111
|
+
*/
|
|
112
|
+
function validateAbac(parsed) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
const abac = parsed?.config?.abac;
|
|
115
|
+
if (!abac || typeof abac !== 'object') {
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if ('crossSystem' in abac) {
|
|
120
|
+
errors.push(
|
|
121
|
+
'config.abac.crossSystem is deprecated. Use config.abac.crossSystemJson or config.abac.crossSystemSql instead.'
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const attributeNames = new Set(
|
|
126
|
+
Object.keys(parsed?.fieldMappings?.attributes ?? {})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (abac.dimensions) {
|
|
130
|
+
errors.push(...validateDimensionsObject(
|
|
131
|
+
abac.dimensions,
|
|
132
|
+
'config.abac.dimensions',
|
|
133
|
+
attributeNames
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fieldMappingsDimensions = parsed?.fieldMappings?.dimensions;
|
|
138
|
+
if (fieldMappingsDimensions && typeof fieldMappingsDimensions === 'object') {
|
|
139
|
+
errors.push(...validateDimensionsObject(
|
|
140
|
+
fieldMappingsDimensions,
|
|
141
|
+
'fieldMappings.dimensions',
|
|
142
|
+
attributeNames
|
|
143
|
+
));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (abac.crossSystemJson) {
|
|
147
|
+
errors.push(...validateCrossSystemJson(abac.crossSystemJson));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return errors;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
validateAbac,
|
|
155
|
+
validateDimensionsObject,
|
|
156
|
+
validateCrossSystemJson
|
|
157
|
+
};
|