@aifabrix/builder 2.45.0 → 2.45.5
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/.cursor/rules/docs-rules.mdc +3 -1
- package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +1 -1
- package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
- package/integration/hubspot-test/wizard-hubspot-e2e.yaml +1 -1
- package/jest.projects.js +44 -1
- package/lib/api/agent-metadata-validation.api.js +93 -0
- package/lib/api/audit-api-helpers.js +68 -0
- package/lib/api/audit.api.js +119 -0
- package/lib/api/auth-cache.api.js +44 -0
- package/lib/api/certificates.api.js +4 -4
- package/lib/api/credential.api.js +2 -2
- package/lib/api/dataplane-health.api.js +137 -0
- package/lib/api/dataplane-sync.api.js +27 -0
- package/lib/api/datasources-core.api.js +21 -18
- package/lib/api/datasources-extended.api.js +22 -22
- package/lib/api/dimensions.api.js +3 -0
- package/lib/api/external-systems.api.js +13 -13
- package/lib/api/governance-scenarios.api.js +39 -0
- package/lib/api/groups.api.js +79 -0
- package/lib/api/index.js +45 -2
- package/lib/api/pipeline.api.js +4 -4
- package/lib/api/protection.api.js +157 -0
- package/lib/api/records-bulk.api.js +30 -0
- package/lib/api/records-search-parse.js +49 -0
- package/lib/api/records-search.api.js +29 -0
- package/lib/api/types/agent-metadata-validation.types.js +67 -0
- package/lib/api/types/audit.types.js +72 -0
- package/lib/api/types/dataplane-health.types.js +27 -0
- package/lib/api/types/dimensions.types.js +2 -0
- package/lib/api/types/governance-scenarios.types.js +39 -0
- package/lib/api/types/protection.types.js +69 -0
- package/lib/api/types/records-bulk.types.js +35 -0
- package/lib/api/types/records-search.types.js +28 -0
- package/lib/api/user-groups.api.js +50 -0
- package/lib/api/users.api.js +95 -0
- package/lib/api/validation-run.api.js +3 -3
- package/lib/api/validation-runner.js +3 -3
- package/lib/api/wizard-platform.api.js +3 -3
- package/lib/api/wizard.api.js +21 -21
- package/lib/app/deploy.js +34 -19
- package/lib/cli/index.js +4 -2
- package/lib/cli/infra-guided.js +22 -3
- package/lib/cli/setup-app.help.js +50 -2
- package/lib/cli/setup-app.js +33 -24
- package/lib/cli/setup-app.test-commands.js +80 -21
- package/lib/cli/setup-auth.js +30 -3
- package/lib/cli/setup-external-system.js +37 -3
- package/lib/cli/setup-utility-help-after.js +45 -0
- package/lib/cli/setup-utility-resolve.js +2 -2
- package/lib/cli/setup-utility.js +99 -81
- package/lib/commands/auth-status-dataplane-version.js +231 -0
- package/lib/commands/auth-status-display.js +94 -0
- package/lib/commands/auth-status.js +70 -6
- package/lib/commands/credential-push.js +10 -5
- package/lib/commands/datasource-load-export-cli.js +178 -0
- package/lib/commands/datasource-test-trust-cli.js +114 -0
- package/lib/commands/datasource-unified-test-cli.js +25 -17
- package/lib/commands/datasource-unified-test-cli.options.js +18 -3
- package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +5 -5
- package/lib/commands/datasource-verify-audit-cli.js +96 -0
- package/lib/commands/datasource.js +68 -1
- package/lib/commands/dimension-value.js +43 -13
- package/lib/commands/dimension.js +38 -7
- package/lib/commands/identity-apply-sync.js +131 -0
- package/lib/commands/identity-group-cli.js +146 -0
- package/lib/commands/identity-membership-role-cache.js +175 -0
- package/lib/commands/identity-shared.js +94 -0
- package/lib/commands/identity-user-cli.js +158 -0
- package/lib/commands/identity.js +43 -0
- package/lib/commands/login.js +46 -16
- package/lib/commands/protection-cli-leaves.js +286 -0
- package/lib/commands/protection.js +50 -0
- package/lib/commands/repair.js +13 -3
- package/lib/commands/test-e2e-external.js +40 -9
- package/lib/commands/test-governance-cli-options.js +47 -0
- package/lib/commands/test-governance-command-action.js +55 -0
- package/lib/commands/test-governance-external.js +88 -0
- package/lib/commands/test-trust-cli-options.js +32 -0
- package/lib/commands/test-trust-command-action.js +118 -0
- package/lib/commands/test-trust-external.js +127 -0
- package/lib/commands/upload.js +34 -13
- package/lib/commands/wizard-core.js +3 -0
- package/lib/core/config-device-dataplane.js +185 -0
- package/lib/datasource/agent-trust-debug-log.js +45 -0
- package/lib/datasource/agent-trust-fetch.js +115 -0
- package/lib/datasource/agent-trust-map.js +101 -0
- package/lib/datasource/agent-trust-mock.js +44 -0
- package/lib/datasource/agent-trust-run.js +93 -0
- package/lib/datasource/agent-trust-timeout.js +25 -0
- package/lib/datasource/audit-evidence-display.js +80 -0
- package/lib/datasource/audit-evidence-extract-debug.js +103 -0
- package/lib/datasource/audit-evidence-extract.js +177 -0
- package/lib/datasource/audit-evidence-matrix-rows.js +158 -0
- package/lib/datasource/audit-evidence-operation-aliases.js +63 -0
- package/lib/datasource/audit-evidence-poll.js +109 -0
- package/lib/datasource/audit-evidence-run.js +177 -0
- package/lib/datasource/audit-evidence-verify-helpers.js +167 -0
- package/lib/datasource/audit-evidence-verify-matrix.js +177 -0
- package/lib/datasource/audit-evidence-verify.js +108 -0
- package/lib/datasource/bulk-loader-run-batches.js +88 -0
- package/lib/datasource/bulk-loader-service-retry.js +52 -0
- package/lib/datasource/bulk-loader-service.js +127 -0
- package/lib/datasource/capability/run-capability-dimension.js +2 -1
- package/lib/datasource/datasource-exporter-resolve.js +82 -0
- package/lib/datasource/datasource-exporter-service.js +144 -0
- package/lib/datasource/datasource-identifier.js +148 -0
- package/lib/datasource/datasource-load-export-context.js +92 -0
- package/lib/datasource/integration-context.js +6 -4
- package/lib/datasource/local-data-paths.js +145 -0
- package/lib/datasource/log-cleaner.js +241 -0
- package/lib/datasource/log-viewer.js +111 -6
- package/lib/datasource/record-file-parser.js +85 -0
- package/lib/datasource/record-mapper.js +129 -0
- package/lib/datasource/resolve-app.js +74 -55
- package/lib/datasource/test-e2e.js +26 -2
- package/lib/datasource/unified-validation-run-resolve.js +45 -12
- package/lib/datasource/unified-validation-run.js +43 -17
- package/lib/datasource/validate.js +37 -20
- package/lib/external-system/test-system-level.js +25 -4
- package/lib/external-system/test.js +3 -1
- package/lib/governance/governance-pack-loader.js +70 -0
- package/lib/governance/governance-run-display.js +119 -0
- package/lib/identity/identity-apply-core.js +108 -0
- package/lib/identity/identity-apply-phases.js +244 -0
- package/lib/identity/identity-apply-service.js +77 -0
- package/lib/identity/identity-csv-parser.js +114 -0
- package/lib/internal/fs-real-sync.js +29 -1
- package/lib/protection/auth-context.js +45 -0
- package/lib/protection/convert-batch.js +90 -0
- package/lib/protection/load.js +41 -0
- package/lib/protection/paths.js +158 -0
- package/lib/protection/preflight-datasource-ready.js +46 -0
- package/lib/protection/protection-create-probe-lines.js +142 -0
- package/lib/protection/protection-create-probes.js +95 -0
- package/lib/protection/protection-create-scaffold.js +281 -0
- package/lib/protection/protection-display-helpers.js +445 -0
- package/lib/protection/protection-display.js +215 -0
- package/lib/protection/protection-preset-registry.js +97 -0
- package/lib/protection/report-exit.js +62 -0
- package/lib/protection/resolve.js +202 -0
- package/lib/protection/run-commands-validate.js +130 -0
- package/lib/protection/run-commands.js +216 -0
- package/lib/protection/run-protection-create-helpers.js +134 -0
- package/lib/protection/run-protection-create-probes-flow.js +43 -0
- package/lib/protection/run-protection-create.js +191 -0
- package/lib/protection/scope.js +22 -0
- package/lib/protection/sync-after-upload.js +64 -0
- package/lib/protection/upload-batch.js +127 -0
- package/lib/protection/validate-batch.js +113 -0
- package/lib/protection/validate-local.js +65 -0
- package/lib/resolvers/dimension-file.js +29 -4
- package/lib/schema/external-datasource.schema.json +47 -13
- package/lib/schema/flag-map-validation-run.json +2 -2
- package/lib/schema/protection.schema.json +374 -0
- package/lib/schema/type/credential-field-registry.json +636 -0
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/agent-trust-run-display.js +231 -0
- package/lib/utils/agent-trust-run-exit.js +59 -0
- package/lib/utils/agent-trust-wait-ui.js +94 -0
- package/lib/utils/cli-exit-codes.js +30 -0
- package/lib/utils/cli-sync-options.js +33 -0
- package/lib/utils/credential-push-ui.js +90 -0
- package/lib/utils/dataplane-cli-version-gate.js +202 -0
- package/lib/utils/dataplane-cli-version-help.js +74 -0
- package/lib/utils/datasource-export-display.js +137 -0
- package/lib/utils/datasource-load-display.js +174 -0
- package/lib/utils/datasource-load-export-ui.js +120 -0
- package/lib/utils/datasource-test-run-capability-scope.js +3 -3
- package/lib/utils/datasource-test-run-display.js +3 -3
- package/lib/utils/datasource-test-run-tty-log.js +5 -1
- package/lib/utils/help-builder.js +30 -24
- package/lib/utils/manifest-source-emit.js +22 -0
- package/lib/utils/semver-compare.js +127 -0
- package/lib/utils/upload-pipeline-ui.js +91 -0
- package/lib/utils/validation-poll-ui.js +14 -7
- package/lib/validation/wizard-config-validator.js +58 -19
- package/package.json +1 -1
- package/templates/applications/dataplane/env.template +13 -1
- package/templates/applications/dataplane/rbac.yaml +4 -0
- package/templates/external-system/README.md.hbs +56 -8
|
@@ -16,7 +16,7 @@ const { getIntegrationPath } = require('../utils/paths');
|
|
|
16
16
|
const { runUnifiedDatasourceValidation } = require('../datasource/unified-validation-run');
|
|
17
17
|
|
|
18
18
|
async function runOneDatasourceKey(appName, datasourceKey, opts) {
|
|
19
|
-
const { environment, runType, debug, timeoutMs } = opts;
|
|
19
|
+
const { environment, runType, debug, timeoutMs, authConfig, dataplaneUrl } = opts;
|
|
20
20
|
try {
|
|
21
21
|
const uni = await runUnifiedDatasourceValidation(datasourceKey, {
|
|
22
22
|
app: appName,
|
|
@@ -26,7 +26,9 @@ async function runOneDatasourceKey(appName, datasourceKey, opts) {
|
|
|
26
26
|
verbose: false,
|
|
27
27
|
timeout: timeoutMs,
|
|
28
28
|
async: true,
|
|
29
|
-
noAsync: false
|
|
29
|
+
noAsync: false,
|
|
30
|
+
authConfig,
|
|
31
|
+
dataplaneUrl
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
if (uni.apiError) {
|
|
@@ -89,7 +91,17 @@ async function maybeWriteSystemRunLog(appName, systemKey, datasourceKeys, runTyp
|
|
|
89
91
|
* @param {number} [params.timeout] - Request timeout
|
|
90
92
|
* @returns {Promise<{success: boolean, datasourceResults: Object[]}>}
|
|
91
93
|
*/
|
|
92
|
-
async function runSystemLevelTest({
|
|
94
|
+
async function runSystemLevelTest({
|
|
95
|
+
appName,
|
|
96
|
+
systemKey,
|
|
97
|
+
datasourceKeys,
|
|
98
|
+
environment,
|
|
99
|
+
runType,
|
|
100
|
+
debug,
|
|
101
|
+
timeout,
|
|
102
|
+
authConfig,
|
|
103
|
+
dataplaneUrl
|
|
104
|
+
}) {
|
|
93
105
|
if (!Array.isArray(datasourceKeys) || datasourceKeys.length === 0) {
|
|
94
106
|
return { success: true, datasourceResults: [] };
|
|
95
107
|
}
|
|
@@ -100,7 +112,16 @@ async function runSystemLevelTest({ appName, systemKey, datasourceKeys, environm
|
|
|
100
112
|
// Keep sequential to avoid spiking external APIs / dataplane concurrency in CLI runs.
|
|
101
113
|
// (Plan allows parallel later, but UX needs stable logs first.)
|
|
102
114
|
// eslint-disable-next-line no-await-in-loop -- intentional sequential execution
|
|
103
|
-
results.push(
|
|
115
|
+
results.push(
|
|
116
|
+
await runOneDatasourceKey(appName, key, {
|
|
117
|
+
environment,
|
|
118
|
+
runType,
|
|
119
|
+
debug,
|
|
120
|
+
timeoutMs,
|
|
121
|
+
authConfig,
|
|
122
|
+
dataplaneUrl
|
|
123
|
+
})
|
|
124
|
+
);
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
const success = results.every(r => r.success);
|
|
@@ -486,7 +486,9 @@ async function testExternalSystemIntegration(appName, options = {}) {
|
|
|
486
486
|
environment: options.environment,
|
|
487
487
|
runType: 'integration',
|
|
488
488
|
debug: options.debug,
|
|
489
|
-
timeout
|
|
489
|
+
timeout,
|
|
490
|
+
authConfig,
|
|
491
|
+
dataplaneUrl
|
|
490
492
|
});
|
|
491
493
|
results.success = systemResult.success;
|
|
492
494
|
results.datasourceResults = systemResult.datasourceResults;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Load governance scenario pack YAML from integration folder
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const yaml = require('js-yaml');
|
|
12
|
+
const { getIntegrationPath } = require('../utils/paths');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve pack file path for test-governance.
|
|
16
|
+
* @param {string} systemKey
|
|
17
|
+
* @param {Object} options
|
|
18
|
+
* @param {string} [options.pack] - Explicit path (absolute or relative to integration root)
|
|
19
|
+
* @param {string} [options.app] - Integration folder override
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function resolveGovernancePackPath(systemKey, options = {}) {
|
|
23
|
+
const integrationPath = options.app
|
|
24
|
+
? path.isAbsolute(options.app)
|
|
25
|
+
? options.app
|
|
26
|
+
: path.resolve(process.cwd(), options.app)
|
|
27
|
+
: getIntegrationPath(systemKey);
|
|
28
|
+
|
|
29
|
+
if (options.pack) {
|
|
30
|
+
const packPath = path.isAbsolute(options.pack)
|
|
31
|
+
? options.pack
|
|
32
|
+
: path.join(integrationPath, options.pack);
|
|
33
|
+
if (!fs.existsSync(packPath)) {
|
|
34
|
+
throw new Error(`Scenario pack not found: ${packPath}`);
|
|
35
|
+
}
|
|
36
|
+
return packPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const candidates = [
|
|
40
|
+
path.join(integrationPath, 'scenarios', 'default.yaml'),
|
|
41
|
+
path.join(integrationPath, 'scenarios', `${systemKey}-v1.yaml`)
|
|
42
|
+
];
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
if (fs.existsSync(candidate)) {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error(
|
|
49
|
+
`No scenario pack found under ${path.join(integrationPath, 'scenarios')}. ` +
|
|
50
|
+
'Use --pack <path> (e.g. scenarios/test-protection-v1.yaml).'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} packPath
|
|
56
|
+
* @returns {Object}
|
|
57
|
+
*/
|
|
58
|
+
function loadGovernancePackYaml(packPath) {
|
|
59
|
+
const raw = fs.readFileSync(packPath, 'utf8');
|
|
60
|
+
const doc = yaml.load(raw);
|
|
61
|
+
if (!doc || typeof doc !== 'object') {
|
|
62
|
+
throw new Error(`Invalid scenario pack YAML: ${packPath}`);
|
|
63
|
+
}
|
|
64
|
+
return doc;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
resolveGovernancePackPath,
|
|
69
|
+
loadGovernancePackYaml
|
|
70
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview TTY display for governance scenario acceptance runs
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
const {
|
|
12
|
+
sectionTitle,
|
|
13
|
+
headerKeyValue,
|
|
14
|
+
metadata: metaGray,
|
|
15
|
+
formatSuccessParagraph,
|
|
16
|
+
formatBlockingError
|
|
17
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
18
|
+
|
|
19
|
+
const SEP = '────────────────────────────────';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {'pass'|'fail'} status
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function scenarioGlyph(status) {
|
|
26
|
+
return status === 'pass' ? chalk.green('✔') : chalk.red('✖');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {import('../api/types/governance-scenarios.types').GovernanceScenarioResult} scenario
|
|
31
|
+
* @param {boolean} verbose
|
|
32
|
+
*/
|
|
33
|
+
function logScenarioVerdict(scenario, verbose) {
|
|
34
|
+
logger.log(` ${scenarioGlyph(scenario.status)} ${scenario.verdict}`);
|
|
35
|
+
if (!verbose) return;
|
|
36
|
+
logger.log(
|
|
37
|
+
metaGray(
|
|
38
|
+
` Subject: ${scenario.subjectDisplayName} (${scenario.subjectUserId})`
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
if (scenario.subjectGroups && scenario.subjectGroups.length > 0) {
|
|
42
|
+
logger.log(metaGray(` Groups: ${scenario.subjectGroups.join(', ')}`));
|
|
43
|
+
}
|
|
44
|
+
logger.log(
|
|
45
|
+
metaGray(
|
|
46
|
+
` Visible keys: ${scenario.visibleKeyCount} · excluded ABAC: ${scenario.excludedAbac}` +
|
|
47
|
+
(scenario.auditRef ? ` · audit: ${scenario.auditRef}` : '')
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
if (scenario.unexpectedVisibleKeys && scenario.unexpectedVisibleKeys.length > 0) {
|
|
51
|
+
logger.log(
|
|
52
|
+
metaGray(` Unexpected: ${scenario.unexpectedVisibleKeys.join(', ')}`)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (scenario.missingRequiredKeys && scenario.missingRequiredKeys.length > 0) {
|
|
56
|
+
logger.log(metaGray(` Missing: ${scenario.missingRequiredKeys.join(', ')}`));
|
|
57
|
+
}
|
|
58
|
+
if (scenario.fixHint) {
|
|
59
|
+
logger.log(metaGray(` Fix: ${scenario.fixHint}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} systemKey
|
|
65
|
+
* @param {import('../api/types/governance-scenarios.types').GovernanceScenariosRunResponse} result
|
|
66
|
+
* @param {{ verbose?: boolean, packPath?: string }} opts
|
|
67
|
+
*/
|
|
68
|
+
function displayGovernanceRunTTY(systemKey, result, opts = {}) {
|
|
69
|
+
const agg = result.summary.failed > 0 ? 'fail' : 'ok';
|
|
70
|
+
logger.log(SEP);
|
|
71
|
+
logger.log(sectionTitle('Governed search acceptance (key sets only — no record payload)'));
|
|
72
|
+
logger.log(headerKeyValue('System:', systemKey));
|
|
73
|
+
logger.log(headerKeyValue('Pack:', result.packKey));
|
|
74
|
+
if (opts.packPath) {
|
|
75
|
+
logger.log(metaGray(` File: ${opts.packPath}`));
|
|
76
|
+
}
|
|
77
|
+
logger.log(
|
|
78
|
+
headerKeyValue(
|
|
79
|
+
'Summary:',
|
|
80
|
+
`${result.summary.passed}/${result.summary.total} passed`
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
logger.log(metaGray(' Does not call vendor APIs (use test-e2e for connectivity).'));
|
|
84
|
+
logger.log('');
|
|
85
|
+
for (const scenario of result.scenarios || []) {
|
|
86
|
+
logScenarioVerdict(scenario, opts.verbose === true);
|
|
87
|
+
}
|
|
88
|
+
logger.log('');
|
|
89
|
+
if (agg === 'ok') {
|
|
90
|
+
logger.log(formatSuccessParagraph('All governance scenarios passed'));
|
|
91
|
+
} else {
|
|
92
|
+
logger.log(formatBlockingError(`${result.summary.failed} scenario(s) failed`));
|
|
93
|
+
}
|
|
94
|
+
logger.log(SEP);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {import('../api/types/governance-scenarios.types').GovernanceScenariosRunResponse} result
|
|
99
|
+
*/
|
|
100
|
+
function printGovernanceRunJson(result) {
|
|
101
|
+
logger.log(JSON.stringify(result, null, 2));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {{ total: number, passed: number, failed: number }} summary
|
|
106
|
+
* @returns {number}
|
|
107
|
+
*/
|
|
108
|
+
function exitCodeFromGovernanceSummary(summary) {
|
|
109
|
+
if (!summary || summary.failed > 0) {
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
displayGovernanceRunTTY,
|
|
117
|
+
printGovernanceRunJson,
|
|
118
|
+
exitCodeFromGovernanceSummary
|
|
119
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared helpers for identity apply
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { getUser, listUsers } = require('../api/users.api');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} response
|
|
13
|
+
* @returns {any}
|
|
14
|
+
*/
|
|
15
|
+
function unwrapControllerData(response) {
|
|
16
|
+
if (!response || response.success === false) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return response?.data?.data ?? response?.data ?? response;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} response
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function apiErrorMessage(response) {
|
|
27
|
+
if (!response) {
|
|
28
|
+
return 'Request failed';
|
|
29
|
+
}
|
|
30
|
+
const err = response.error;
|
|
31
|
+
if (typeof err === 'string') {
|
|
32
|
+
return err;
|
|
33
|
+
}
|
|
34
|
+
if (err && typeof err === 'object') {
|
|
35
|
+
return err.formattedError || err.message || err.error || 'Request failed';
|
|
36
|
+
}
|
|
37
|
+
return response.formattedError || response.message || 'Request failed';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {Object} response
|
|
42
|
+
* @throws {Error}
|
|
43
|
+
*/
|
|
44
|
+
function throwIfApiFailed(response) {
|
|
45
|
+
if (!response || response.success === false) {
|
|
46
|
+
throw new Error(apiErrorMessage(response));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {Object|null|undefined} stats
|
|
52
|
+
* @param {{ allowEmptySync?: boolean }} [opts]
|
|
53
|
+
* @throws {Error}
|
|
54
|
+
*/
|
|
55
|
+
function assertSyncStatsOk(stats, opts = {}) {
|
|
56
|
+
const syncErrors = Number(stats?.errors ?? 0);
|
|
57
|
+
if (syncErrors > 0) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Dataplane full sync reported ${syncErrors} error(s). ` +
|
|
60
|
+
`usersProcessed=${stats?.usersProcessed ?? 0} groupsProcessed=${stats?.groupsProcessed ?? 0}. ` +
|
|
61
|
+
'Check miso-controller logs for the sync request.'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const processed = Number(stats?.usersProcessed ?? 0);
|
|
65
|
+
if (processed === 0 && !opts.allowEmptySync) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'Dataplane full sync completed with usersProcessed=0. Use --allow-empty-sync to accept, or fix identity on the controller.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @async
|
|
74
|
+
* @param {string} controllerUrl
|
|
75
|
+
* @param {Object} authConfig
|
|
76
|
+
* @param {{ csvId?: string, email: string }} userSpec
|
|
77
|
+
* @returns {Promise<Object|null>}
|
|
78
|
+
*/
|
|
79
|
+
async function findExistingUser(controllerUrl, authConfig, userSpec) {
|
|
80
|
+
const csvId = String(userSpec.csvId || '').trim();
|
|
81
|
+
if (csvId) {
|
|
82
|
+
const byId = await getUser(controllerUrl, authConfig, csvId);
|
|
83
|
+
const row = unwrapControllerData(byId);
|
|
84
|
+
if (row && row.id) {
|
|
85
|
+
return row;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const email = String(userSpec.email || '').trim();
|
|
89
|
+
const listed = await listUsers(controllerUrl, authConfig, {
|
|
90
|
+
search: email,
|
|
91
|
+
pageSize: 10,
|
|
92
|
+
page: 1
|
|
93
|
+
});
|
|
94
|
+
const items = unwrapControllerData(listed);
|
|
95
|
+
const list = Array.isArray(items) ? items : items?.data;
|
|
96
|
+
if (!Array.isArray(list)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return list.find((u) => String(u.email || '').toLowerCase() === email.toLowerCase()) || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
unwrapControllerData,
|
|
104
|
+
apiErrorMessage,
|
|
105
|
+
throwIfApiFailed,
|
|
106
|
+
assertSyncStatsOk,
|
|
107
|
+
findExistingUser
|
|
108
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Phased execution for identity apply
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { getGroup, createGroup } = require('../api/groups.api');
|
|
10
|
+
const { createUser } = require('../api/users.api');
|
|
11
|
+
const { addUserToGroup } = require('../api/user-groups.api');
|
|
12
|
+
const { clearAuthCache } = require('../api/auth-cache.api');
|
|
13
|
+
const { fullSyncToDataplane } = require('../api/dataplane-sync.api');
|
|
14
|
+
const {
|
|
15
|
+
unwrapControllerData,
|
|
16
|
+
throwIfApiFailed,
|
|
17
|
+
assertSyncStatsOk,
|
|
18
|
+
apiErrorMessage,
|
|
19
|
+
findExistingUser
|
|
20
|
+
} = require('./identity-apply-core');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} msg
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isAlreadyMemberError(msg) {
|
|
27
|
+
return /already a member|409|Conflict/i.test(String(msg || ''));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @async
|
|
32
|
+
* @param {string} controllerUrl
|
|
33
|
+
* @param {Object} authConfig
|
|
34
|
+
* @param {{ name: string, displayName: string }} spec
|
|
35
|
+
* @param {boolean} dryRun
|
|
36
|
+
* @returns {Promise<{ id: string, created: boolean }>}
|
|
37
|
+
*/
|
|
38
|
+
async function ensureGroup(controllerUrl, authConfig, spec, dryRun) {
|
|
39
|
+
const name = spec.name;
|
|
40
|
+
const existingRes = await getGroup(controllerUrl, authConfig, name);
|
|
41
|
+
const existing = unwrapControllerData(existingRes);
|
|
42
|
+
if (existing && existing.id) {
|
|
43
|
+
return { id: existing.id, created: false };
|
|
44
|
+
}
|
|
45
|
+
if (dryRun) {
|
|
46
|
+
return { id: `(dry-run:${name})`, created: true };
|
|
47
|
+
}
|
|
48
|
+
const createdRes = await createGroup(controllerUrl, authConfig, {
|
|
49
|
+
name,
|
|
50
|
+
displayName: spec.displayName || name
|
|
51
|
+
});
|
|
52
|
+
throwIfApiFailed(createdRes);
|
|
53
|
+
const created = unwrapControllerData(createdRes);
|
|
54
|
+
return { id: created.id, created: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @async
|
|
59
|
+
* @param {string} controllerUrl
|
|
60
|
+
* @param {Object} authConfig
|
|
61
|
+
* @param {Object} userSpec
|
|
62
|
+
* @param {boolean} dryRun
|
|
63
|
+
* @returns {Promise<{ id: string, created: boolean }>}
|
|
64
|
+
*/
|
|
65
|
+
async function ensureUser(controllerUrl, authConfig, userSpec, dryRun) {
|
|
66
|
+
const existing = await findExistingUser(controllerUrl, authConfig, userSpec);
|
|
67
|
+
if (existing && existing.id) {
|
|
68
|
+
return { id: existing.id, created: false };
|
|
69
|
+
}
|
|
70
|
+
if (dryRun) {
|
|
71
|
+
return { id: `(dry-run:${userSpec.email})`, created: true };
|
|
72
|
+
}
|
|
73
|
+
const body = {
|
|
74
|
+
email: userSpec.email,
|
|
75
|
+
firstName: userSpec.firstName,
|
|
76
|
+
lastName: userSpec.lastName,
|
|
77
|
+
displayName: userSpec.displayName,
|
|
78
|
+
username: userSpec.username,
|
|
79
|
+
status: 'active'
|
|
80
|
+
};
|
|
81
|
+
Object.keys(body).forEach((k) => {
|
|
82
|
+
if (body[k] === undefined || body[k] === '') {
|
|
83
|
+
delete body[k];
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const createdRes = await createUser(controllerUrl, authConfig, body);
|
|
87
|
+
throwIfApiFailed(createdRes);
|
|
88
|
+
const created = unwrapControllerData(createdRes);
|
|
89
|
+
return { id: created.id, created: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @async
|
|
94
|
+
* @param {string} controllerUrl
|
|
95
|
+
* @param {Object} authConfig
|
|
96
|
+
* @param {Map} groups
|
|
97
|
+
* @param {boolean} dryRun
|
|
98
|
+
* @returns {Promise<{ map: Map<string, string>, created: number, processed: number }>}
|
|
99
|
+
*/
|
|
100
|
+
async function applyGroupsPhase(controllerUrl, authConfig, groups, dryRun) {
|
|
101
|
+
const map = new Map();
|
|
102
|
+
let created = 0;
|
|
103
|
+
let processed = 0;
|
|
104
|
+
for (const spec of groups.values()) {
|
|
105
|
+
const out = await ensureGroup(controllerUrl, authConfig, spec, dryRun);
|
|
106
|
+
map.set(spec.name, out.id);
|
|
107
|
+
processed += 1;
|
|
108
|
+
if (out.created) {
|
|
109
|
+
created += 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { map, created, processed };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @async
|
|
117
|
+
* @param {string} controllerUrl
|
|
118
|
+
* @param {Object} authConfig
|
|
119
|
+
* @param {Map} users
|
|
120
|
+
* @param {boolean} dryRun
|
|
121
|
+
* @returns {Promise<{ map: Map<string, string>, created: number, processed: number }>}
|
|
122
|
+
*/
|
|
123
|
+
async function applyUsersPhase(controllerUrl, authConfig, users, dryRun) {
|
|
124
|
+
const map = new Map();
|
|
125
|
+
let created = 0;
|
|
126
|
+
let processed = 0;
|
|
127
|
+
for (const [userKey, userSpec] of users.entries()) {
|
|
128
|
+
const out = await ensureUser(controllerUrl, authConfig, userSpec, dryRun);
|
|
129
|
+
map.set(userKey, out.id);
|
|
130
|
+
processed += 1;
|
|
131
|
+
if (out.created) {
|
|
132
|
+
created += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { map, created, processed };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @async
|
|
140
|
+
* @param {string} controllerUrl
|
|
141
|
+
* @param {Object} authConfig
|
|
142
|
+
* @param {Object[]} memberships
|
|
143
|
+
* @param {Map<string, string>} userIdByKey
|
|
144
|
+
* @param {Map<string, string>} groupIdByName
|
|
145
|
+
* @param {boolean} dryRun
|
|
146
|
+
* @returns {Promise<{ created: number, processed: number, skipped: number }>}
|
|
147
|
+
*/
|
|
148
|
+
/**
|
|
149
|
+
* @async
|
|
150
|
+
* @param {string} controllerUrl
|
|
151
|
+
* @param {Object} authConfig
|
|
152
|
+
* @param {Object} m
|
|
153
|
+
* @param {Map<string, string>} userIdByKey
|
|
154
|
+
* @param {Map<string, string>} groupIdByName
|
|
155
|
+
* @param {boolean} dryRun
|
|
156
|
+
* @returns {Promise<'created'|'skipped'|'dry'>}
|
|
157
|
+
*/
|
|
158
|
+
async function applyOneMembership(controllerUrl, authConfig, m, userIdByKey, groupIdByName, dryRun) {
|
|
159
|
+
const userId = userIdByKey.get(m.userKey);
|
|
160
|
+
const groupId = groupIdByName.get(m.groupName);
|
|
161
|
+
if (!userId || !groupId) {
|
|
162
|
+
return 'skipped';
|
|
163
|
+
}
|
|
164
|
+
if (dryRun) {
|
|
165
|
+
return 'dry';
|
|
166
|
+
}
|
|
167
|
+
const res = await addUserToGroup(controllerUrl, authConfig, userId, groupId, {});
|
|
168
|
+
if (res && res.success !== false) {
|
|
169
|
+
return 'created';
|
|
170
|
+
}
|
|
171
|
+
if (isAlreadyMemberError(apiErrorMessage(res))) {
|
|
172
|
+
return 'skipped';
|
|
173
|
+
}
|
|
174
|
+
throw new Error(apiErrorMessage(res));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function applyMembershipsPhase(
|
|
178
|
+
controllerUrl,
|
|
179
|
+
authConfig,
|
|
180
|
+
memberships,
|
|
181
|
+
userIdByKey,
|
|
182
|
+
groupIdByName,
|
|
183
|
+
dryRun
|
|
184
|
+
) {
|
|
185
|
+
let created = 0;
|
|
186
|
+
let processed = 0;
|
|
187
|
+
let skipped = 0;
|
|
188
|
+
for (const m of memberships) {
|
|
189
|
+
const userId = userIdByKey.get(m.userKey);
|
|
190
|
+
const groupId = groupIdByName.get(m.groupName);
|
|
191
|
+
if (!userId || !groupId) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
processed += 1;
|
|
195
|
+
const outcome = await applyOneMembership(
|
|
196
|
+
controllerUrl,
|
|
197
|
+
authConfig,
|
|
198
|
+
m,
|
|
199
|
+
userIdByKey,
|
|
200
|
+
groupIdByName,
|
|
201
|
+
dryRun
|
|
202
|
+
);
|
|
203
|
+
if (outcome === 'created' || outcome === 'dry') {
|
|
204
|
+
created += 1;
|
|
205
|
+
} else if (outcome === 'skipped') {
|
|
206
|
+
skipped += 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { created, processed, skipped };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @async
|
|
214
|
+
* @param {string} controllerUrl
|
|
215
|
+
* @param {Object} authConfig
|
|
216
|
+
* @param {Object} opts
|
|
217
|
+
* @returns {Promise<Object|null>}
|
|
218
|
+
*/
|
|
219
|
+
async function maybePurgeAndSync(controllerUrl, authConfig, opts) {
|
|
220
|
+
if (opts.dryRun) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (opts.purgeCache) {
|
|
224
|
+
const cacheRes = await clearAuthCache(controllerUrl, authConfig);
|
|
225
|
+
throwIfApiFailed(cacheRes);
|
|
226
|
+
}
|
|
227
|
+
if (!opts.sync || !opts.envKey) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const syncRes = await fullSyncToDataplane(controllerUrl, authConfig, opts.envKey);
|
|
231
|
+
throwIfApiFailed(syncRes);
|
|
232
|
+
const stats = unwrapControllerData(syncRes);
|
|
233
|
+
assertSyncStatsOk(stats, { allowEmptySync: opts.allowEmptySync });
|
|
234
|
+
return stats;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
ensureGroup,
|
|
239
|
+
ensureUser,
|
|
240
|
+
applyGroupsPhase,
|
|
241
|
+
applyUsersPhase,
|
|
242
|
+
applyMembershipsPhase,
|
|
243
|
+
maybePurgeAndSync
|
|
244
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Bulk identity apply (CSV) against miso-controller
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { parseUsersCsvFile, buildApplyPlanFromRows } = require('./identity-csv-parser');
|
|
10
|
+
const {
|
|
11
|
+
applyGroupsPhase,
|
|
12
|
+
applyUsersPhase,
|
|
13
|
+
applyMembershipsPhase,
|
|
14
|
+
maybePurgeAndSync
|
|
15
|
+
} = require('./identity-apply-phases');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
unwrapControllerData,
|
|
19
|
+
apiErrorMessage,
|
|
20
|
+
throwIfApiFailed,
|
|
21
|
+
findExistingUser
|
|
22
|
+
} = require('./identity-apply-core');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @async
|
|
26
|
+
* @param {string} controllerUrl
|
|
27
|
+
* @param {Object} authConfig
|
|
28
|
+
* @param {Object} options
|
|
29
|
+
* @returns {Promise<Object>}
|
|
30
|
+
*/
|
|
31
|
+
async function runIdentityApply(controllerUrl, authConfig, options) {
|
|
32
|
+
const { rows } = parseUsersCsvFile(options.filePath, options.filterPrefix);
|
|
33
|
+
if (!rows.length) {
|
|
34
|
+
throw new Error('No data rows to apply after parsing CSV');
|
|
35
|
+
}
|
|
36
|
+
const plan = buildApplyPlanFromRows(rows);
|
|
37
|
+
const dryRun = Boolean(options.dryRun);
|
|
38
|
+
|
|
39
|
+
const groupPhase = await applyGroupsPhase(controllerUrl, authConfig, plan.groups, dryRun);
|
|
40
|
+
const userPhase = await applyUsersPhase(controllerUrl, authConfig, plan.users, dryRun);
|
|
41
|
+
const memPhase = await applyMembershipsPhase(
|
|
42
|
+
controllerUrl,
|
|
43
|
+
authConfig,
|
|
44
|
+
plan.memberships,
|
|
45
|
+
userPhase.map,
|
|
46
|
+
groupPhase.map,
|
|
47
|
+
dryRun
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const syncStats = await maybePurgeAndSync(controllerUrl, authConfig, {
|
|
51
|
+
dryRun,
|
|
52
|
+
purgeCache: Boolean(options.purgeCache),
|
|
53
|
+
sync: Boolean(options.sync),
|
|
54
|
+
envKey: options.envKey,
|
|
55
|
+
allowEmptySync: Boolean(options.allowEmptySync)
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
groupsProcessed: groupPhase.processed,
|
|
60
|
+
groupsCreated: groupPhase.created,
|
|
61
|
+
usersProcessed: userPhase.processed,
|
|
62
|
+
usersCreated: userPhase.created,
|
|
63
|
+
membershipsProcessed: memPhase.processed,
|
|
64
|
+
membershipsCreated: memPhase.created,
|
|
65
|
+
membershipsSkipped: memPhase.skipped,
|
|
66
|
+
dryRun,
|
|
67
|
+
syncStats
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
unwrapControllerData,
|
|
73
|
+
apiErrorMessage,
|
|
74
|
+
throwIfApiFailed,
|
|
75
|
+
runIdentityApply,
|
|
76
|
+
findExistingUser
|
|
77
|
+
};
|