@aifabrix/builder 2.44.4 → 2.44.6
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/cli-layout.mdc +1 -1
- package/.cursor/rules/project-rules.mdc +1 -1
- package/.npmrc.token +1 -1
- package/README.md +15 -23
- package/integration/hubspot-test/README.md +2 -0
- package/integration/hubspot-test/test.js +5 -3
- package/jest.projects.js +68 -17
- package/lib/api/controller-health.api.js +49 -0
- package/lib/api/dimension-values.api.js +82 -0
- package/lib/api/dimensions.api.js +114 -0
- package/lib/api/external-systems.api.js +1 -0
- package/lib/api/integration-clients.api.js +168 -0
- package/lib/api/types/dimension-values.types.js +28 -0
- package/lib/api/types/dimensions.types.js +31 -0
- package/lib/api/types/integration-clients.types.js +45 -0
- package/lib/api/types/wizard.types.js +2 -1
- package/lib/api/validation-runner.js +46 -25
- package/lib/app/deploy-config.js +11 -1
- package/lib/app/deploy-status-display.js +3 -3
- package/lib/app/deploy.js +36 -14
- package/lib/app/display.js +15 -11
- package/lib/app/push.js +46 -23
- package/lib/app/register.js +1 -1
- package/lib/app/restart-display.js +95 -0
- package/lib/app/rotate-secret.js +1 -1
- package/lib/app/run-container-start.js +12 -6
- package/lib/app/run-env-compose.js +30 -1
- package/lib/app/run-helpers.js +44 -12
- package/lib/app/run-reload-sync.js +148 -0
- package/lib/app/run-resolve-image.js +51 -1
- package/lib/app/run.js +99 -73
- package/lib/build/index.js +75 -45
- package/lib/cli/doctor-check.js +117 -0
- package/lib/cli/index.js +8 -2
- package/lib/cli/infra-guided.js +445 -0
- package/lib/cli/setup-app.help.js +1 -1
- package/lib/cli/setup-app.js +20 -2
- package/lib/cli/setup-app.test-commands.js +9 -5
- package/lib/cli/setup-auth.js +26 -0
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra.js +138 -61
- package/lib/cli/setup-integration-client.js +182 -0
- package/lib/cli/setup-parameters.js +21 -2
- package/lib/cli/setup-platform.js +102 -0
- package/lib/cli/setup-secrets.js +18 -6
- package/lib/cli/setup-utility.js +97 -33
- package/lib/commands/datasource-capability-dimension-cli.js +128 -0
- package/lib/commands/datasource-capability-output.js +29 -0
- package/lib/commands/datasource-capability-relate-cli.js +140 -0
- package/lib/commands/datasource-capability.js +411 -0
- package/lib/commands/datasource-unified-test-cli.options.js +1 -1
- package/lib/commands/datasource.js +53 -13
- package/lib/commands/dev-down.js +3 -3
- package/lib/commands/dev-infra-gate.js +32 -0
- package/lib/commands/dev-init.js +13 -7
- package/lib/commands/dimension-value.js +179 -0
- package/lib/commands/dimension.js +330 -0
- package/lib/commands/integration-client.js +430 -0
- package/lib/commands/login-device.js +65 -30
- package/lib/commands/login.js +21 -10
- package/lib/commands/parameters-validate.js +78 -13
- package/lib/commands/repair-datasource-auto-rbac.js +166 -0
- package/lib/commands/repair-datasource-keys.js +10 -5
- package/lib/commands/repair-datasource.js +19 -7
- package/lib/commands/repair-env-template.js +4 -1
- package/lib/commands/repair-openapi-sync.js +172 -0
- package/lib/commands/repair-persist.js +102 -0
- package/lib/commands/repair-rbac-extract.js +27 -0
- package/lib/commands/repair-rbac-migrate.js +186 -0
- package/lib/commands/repair-rbac.js +225 -19
- package/lib/commands/repair-system-alignment.js +246 -0
- package/lib/commands/repair-system-permissions.js +168 -0
- package/lib/commands/repair.js +120 -354
- package/lib/commands/secure.js +1 -1
- package/lib/commands/setup-modes.js +455 -0
- package/lib/commands/setup-prompts.js +388 -0
- package/lib/commands/setup.js +149 -0
- package/lib/commands/teardown.js +228 -0
- package/lib/commands/test-e2e-external.js +4 -3
- package/lib/commands/up-common.js +97 -12
- package/lib/commands/up-dataplane.js +33 -11
- package/lib/commands/up-miso.js +7 -11
- package/lib/commands/upload.js +109 -23
- package/lib/commands/wizard-core-helpers.js +14 -11
- package/lib/commands/wizard-core.js +58 -15
- package/lib/commands/wizard-dataplane.js +2 -2
- package/lib/commands/wizard-entity-selection.js +72 -14
- package/lib/commands/wizard-headless.js +7 -3
- package/lib/commands/wizard-helpers.js +13 -1
- package/lib/commands/wizard.js +210 -61
- package/lib/constants/infra-compose-service-names.js +40 -0
- package/lib/core/env-reader.js +16 -3
- package/lib/core/secrets-admin-env.js +101 -0
- package/lib/core/secrets-ensure-infra.js +34 -1
- package/lib/core/secrets-ensure.js +88 -66
- package/lib/core/secrets-env-content.js +432 -0
- package/lib/core/secrets-env-write.js +27 -1
- package/lib/core/secrets-load.js +248 -0
- package/lib/core/secrets-names.js +32 -0
- package/lib/core/secrets.js +17 -757
- package/lib/datasource/capability/basic-exposure.js +76 -0
- package/lib/datasource/capability/capability-diff-slice.js +41 -0
- package/lib/datasource/capability/capability-key.js +34 -0
- package/lib/datasource/capability/capability-resolve.js +172 -0
- package/lib/datasource/capability/capability-storage-keys.js +22 -0
- package/lib/datasource/capability/copy-operations.js +348 -0
- package/lib/datasource/capability/copy-test-payload.js +139 -0
- package/lib/datasource/capability/create-operations.js +235 -0
- package/lib/datasource/capability/dimension-operations.js +151 -0
- package/lib/datasource/capability/dimension-validate.js +219 -0
- package/lib/datasource/capability/json-pointer.js +31 -0
- package/lib/datasource/capability/reference-rewrite.js +51 -0
- package/lib/datasource/capability/relate-operations.js +325 -0
- package/lib/datasource/capability/relate-validate.js +219 -0
- package/lib/datasource/capability/remove-operations.js +275 -0
- package/lib/datasource/capability/run-capability-copy.js +152 -0
- package/lib/datasource/capability/run-capability-diff.js +135 -0
- package/lib/datasource/capability/run-capability-dimension.js +291 -0
- package/lib/datasource/capability/run-capability-edit.js +377 -0
- package/lib/datasource/capability/run-capability-relate.js +193 -0
- package/lib/datasource/capability/run-capability-remove.js +105 -0
- package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
- package/lib/datasource/capability/validate-capability-slice.js +35 -0
- package/lib/datasource/list.js +136 -23
- package/lib/datasource/log-viewer.js +2 -4
- package/lib/datasource/unified-validation-run.js +51 -16
- package/lib/datasource/validate.js +53 -1
- package/lib/deployment/deploy-poll-ui.js +60 -0
- package/lib/deployment/deployer-status.js +29 -3
- package/lib/deployment/deployer.js +48 -30
- package/lib/deployment/environment.js +7 -2
- package/lib/deployment/poll-interval.js +72 -0
- package/lib/deployment/push.js +11 -9
- package/lib/external-system/deploy.js +4 -1
- package/lib/external-system/download.js +61 -32
- package/lib/external-system/sync-deploy-manifest.js +33 -0
- package/lib/generator/wizard-prompts.js +7 -1
- package/lib/generator/wizard.js +34 -0
- package/lib/infrastructure/index.js +49 -19
- package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
- package/lib/parameters/infra-kv-discovery.js +29 -4
- package/lib/parameters/infra-parameter-catalog.js +6 -3
- package/lib/parameters/infra-parameter-validate.js +67 -19
- package/lib/resolvers/datasource-resolver.js +53 -0
- package/lib/resolvers/dimension-file.js +52 -0
- package/lib/resolvers/manifest-resolver.js +133 -0
- package/lib/schema/external-datasource.schema.json +183 -53
- package/lib/schema/external-system.schema.json +23 -10
- package/lib/schema/infra.parameter.yaml +26 -11
- package/lib/schema/wizard-config.schema.json +2 -2
- package/lib/utils/aifabrix-config-dir-walk.js +40 -0
- package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
- package/lib/utils/app-run-containers.js +2 -2
- package/lib/utils/bash-secret-env.js +59 -0
- package/lib/utils/cli-secrets-error-format.js +78 -0
- package/lib/utils/cli-test-layout-chalk.js +31 -9
- package/lib/utils/cli-utils.js +4 -36
- package/lib/utils/datasource-test-run-display.js +8 -0
- package/lib/utils/dev-hosts-helper.js +3 -2
- package/lib/utils/dev-init-ssh-merge.js +2 -1
- package/lib/utils/docker-build.js +17 -9
- package/lib/utils/docker-reload-mount.js +127 -0
- package/lib/utils/external-readme.js +117 -4
- package/lib/utils/external-system-local-test-tty.js +3 -2
- package/lib/utils/external-system-readiness-core.js +45 -12
- package/lib/utils/external-system-readiness-deploy-display.js +3 -3
- package/lib/utils/external-system-readiness-display-internals.js +33 -3
- package/lib/utils/external-system-readiness-display.js +10 -1
- package/lib/utils/file-upload.js +40 -3
- package/lib/utils/health-check-db-init.js +107 -0
- package/lib/utils/health-check-public-warn.js +69 -0
- package/lib/utils/health-check-url.js +19 -4
- package/lib/utils/health-check.js +135 -105
- package/lib/utils/help-builder.js +5 -1
- package/lib/utils/image-name.js +34 -7
- package/lib/utils/integration-file-backup.js +74 -0
- package/lib/utils/mutagen-install.js +30 -3
- package/lib/utils/paths.js +108 -25
- package/lib/utils/postgres-wipe.js +212 -0
- package/lib/utils/register-aifabrix-shell-env.js +15 -0
- package/lib/utils/remote-dev-auth.js +21 -5
- package/lib/utils/remote-docker-env.js +9 -1
- package/lib/utils/remote-secrets-loader.js +42 -3
- package/lib/utils/resolve-docker-image-ref.js +9 -3
- package/lib/utils/secrets-ancestor-paths.js +47 -0
- package/lib/utils/secrets-helpers.js +17 -10
- package/lib/utils/secrets-kv-refs.js +42 -0
- package/lib/utils/secrets-kv-scope.js +19 -2
- package/lib/utils/secrets-materialize-local.js +134 -0
- package/lib/utils/secrets-path.js +24 -10
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/system-builder-root.js +34 -0
- package/lib/utils/url-declarative-resolve-build.js +6 -1
- package/lib/utils/url-declarative-runtime-base-path.js +32 -0
- package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
- package/lib/utils/urls-local-registry.js +73 -20
- package/lib/utils/validation-poll-ui.js +81 -0
- package/lib/utils/validation-run-poll.js +29 -5
- package/lib/utils/with-muted-logger.js +53 -0
- package/package.json +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/env.template +8 -6
- package/templates/applications/miso-controller/application.yaml +7 -0
- package/templates/applications/miso-controller/env.template +7 -7
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +89 -102
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/lib/api/service-users.api.js +0 -150
- package/lib/api/types/service-users.types.js +0 -65
- package/lib/cli/setup-service-user.js +0 -187
- package/lib/commands/service-user.js +0 -429
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared RBAC extraction from external system JSON (repair / merge).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview extractRbacFromSystem for repair-rbac and migrate
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extracts roles and permissions from external system JSON for rbac.yaml (same rules as repair).
|
|
13
|
+
* @param {Object} system - Parsed system config
|
|
14
|
+
* @returns {Object|null} RBAC object or null
|
|
15
|
+
*/
|
|
16
|
+
function extractRbacFromSystem(system) {
|
|
17
|
+
if (!system || typeof system !== 'object') return null;
|
|
18
|
+
const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
|
|
19
|
+
const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
|
|
20
|
+
if (!hasRoles && !hasPermissions) return null;
|
|
21
|
+
const rbac = {};
|
|
22
|
+
if (hasRoles) rbac.roles = system.roles;
|
|
23
|
+
if (hasPermissions) rbac.permissions = system.permissions;
|
|
24
|
+
return rbac;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { extractRbacFromSystem };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moves roles/permissions from *-system.{json,yaml} into rbac.{yaml,json} when both exist.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview migrateSystemRbacIntoRbacFile
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const appConfigResolver = require('../utils/app-config-resolver');
|
|
12
|
+
const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
13
|
+
const { backupIntegrationFile } = require('../utils/integration-file-backup');
|
|
14
|
+
const extractModule = require('./repair-rbac-extract');
|
|
15
|
+
|
|
16
|
+
function _safeString(v) {
|
|
17
|
+
return typeof v === 'string' && v.trim() ? v.trim() : '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Merges roles from source into target; dedupes by role.value.
|
|
22
|
+
* @param {Object[]} targetRoles - rbac.roles (mutated)
|
|
23
|
+
* @param {Object[]} sourceRoles - roles to merge in
|
|
24
|
+
* @returns {boolean} True if targetRoles changed
|
|
25
|
+
*/
|
|
26
|
+
function _mergeRoleObjectsInto(targetRoles, sourceRoles) {
|
|
27
|
+
if (!Array.isArray(sourceRoles) || sourceRoles.length === 0) return false;
|
|
28
|
+
const seen = new Set(
|
|
29
|
+
(targetRoles || []).map(r => _safeString(r?.value)).filter(Boolean)
|
|
30
|
+
);
|
|
31
|
+
let updated = false;
|
|
32
|
+
for (const r of sourceRoles) {
|
|
33
|
+
if (!r || typeof r !== 'object') continue;
|
|
34
|
+
const value = _safeString(r.value);
|
|
35
|
+
if (!value || seen.has(value)) continue;
|
|
36
|
+
targetRoles.push({
|
|
37
|
+
name: r.name,
|
|
38
|
+
value: r.value,
|
|
39
|
+
description: r.description,
|
|
40
|
+
groups: Array.isArray(r.groups) ? [...r.groups] : []
|
|
41
|
+
});
|
|
42
|
+
seen.add(value);
|
|
43
|
+
updated = true;
|
|
44
|
+
}
|
|
45
|
+
return updated;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {unknown[]} existingRoles - roles already on permission
|
|
50
|
+
* @param {unknown[]} incomingRoles - roles from merged permission
|
|
51
|
+
* @returns {string[]|null} New roles array if union added roles
|
|
52
|
+
*/
|
|
53
|
+
function _unionPermissionRoleSets(existingRoles, incomingRoles) {
|
|
54
|
+
const a = new Set(Array.isArray(existingRoles) ? existingRoles : []);
|
|
55
|
+
let unionChanged = false;
|
|
56
|
+
for (const rv of incomingRoles || []) {
|
|
57
|
+
if (typeof rv === 'string' && rv.trim() && !a.has(rv)) {
|
|
58
|
+
a.add(rv);
|
|
59
|
+
unionChanged = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return unionChanged ? [...a] : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Merges permissions from source into target; dedupes by name; unions roles arrays.
|
|
67
|
+
* @param {Object[]} targetPerms - rbac.permissions (mutated)
|
|
68
|
+
* @param {Object[]} sourcePerms - permissions to merge in
|
|
69
|
+
* @returns {boolean} True if targetPerms changed
|
|
70
|
+
*/
|
|
71
|
+
function _mergePermissionObjectsInto(targetPerms, sourcePerms) {
|
|
72
|
+
if (!Array.isArray(sourcePerms) || sourcePerms.length === 0) return false;
|
|
73
|
+
const byName = new Map(
|
|
74
|
+
(targetPerms || []).filter(p => p && typeof p === 'object').map(p => [p.name, p])
|
|
75
|
+
);
|
|
76
|
+
let updated = false;
|
|
77
|
+
for (const p of sourcePerms) {
|
|
78
|
+
if (!p || typeof p !== 'object') continue;
|
|
79
|
+
const name = _safeString(p.name);
|
|
80
|
+
if (!name) continue;
|
|
81
|
+
const existing = byName.get(name);
|
|
82
|
+
if (!existing) {
|
|
83
|
+
targetPerms.push({
|
|
84
|
+
name: p.name,
|
|
85
|
+
description: _safeString(p.description) || `Permission: ${name}`,
|
|
86
|
+
roles: Array.isArray(p.roles) ? [...p.roles] : []
|
|
87
|
+
});
|
|
88
|
+
byName.set(name, targetPerms[targetPerms.length - 1]);
|
|
89
|
+
updated = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const mergedRoles = _unionPermissionRoleSets(existing.roles, Array.isArray(p.roles) ? p.roles : []);
|
|
93
|
+
if (mergedRoles) {
|
|
94
|
+
existing.roles = mergedRoles;
|
|
95
|
+
updated = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return updated;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string[]} changes - Repair changelog lines
|
|
103
|
+
* @param {boolean} rbacContentChanged - Whether rbac JSON/YAML payload changed
|
|
104
|
+
*/
|
|
105
|
+
function _appendMigrateSystemRbacNotes(changes, rbacContentChanged) {
|
|
106
|
+
if (rbacContentChanged) {
|
|
107
|
+
changes.push('Merged roles/permissions from external system file into rbac file');
|
|
108
|
+
}
|
|
109
|
+
changes.push('Removed roles and permissions from external system file (canonical copy in rbac file)');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {string} rbacPath
|
|
114
|
+
* @param {Object} rbac
|
|
115
|
+
* @param {boolean} rbacContentChanged
|
|
116
|
+
* @param {string} systemFilePath
|
|
117
|
+
* @param {Object} systemParsed
|
|
118
|
+
* @param {Object} [backupCtx]
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
121
|
+
function _persistMigrateSystemRbacEdits(rbacPath, rbac, rbacContentChanged, systemFilePath, systemParsed, backupCtx) {
|
|
122
|
+
if (rbacContentChanged) {
|
|
123
|
+
backupIntegrationFile(rbacPath, backupCtx);
|
|
124
|
+
writeConfigFile(rbacPath, rbac);
|
|
125
|
+
}
|
|
126
|
+
delete systemParsed.roles;
|
|
127
|
+
delete systemParsed.permissions;
|
|
128
|
+
backupIntegrationFile(systemFilePath, backupCtx);
|
|
129
|
+
writeConfigFile(systemFilePath, systemParsed);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* When an rbac file already exists, moves roles/permissions out of the external system JSON into that file.
|
|
134
|
+
* (Initial create-from-system is handled by createRbacFromSystemIfNeeded in repair.js.)
|
|
135
|
+
*
|
|
136
|
+
* @param {string} appPath - Integration directory
|
|
137
|
+
* @param {string} systemFilePath - Path to *-system.json (or yaml)
|
|
138
|
+
* @param {Object} systemParsed - Parsed system (mutated: roles/permissions removed when not dryRun)
|
|
139
|
+
* @param {{ dryRun: boolean, changes: string[], backupCtx?: Object }} options
|
|
140
|
+
* @returns {boolean} True if system had RBAC fields to relocate or rbac was merged
|
|
141
|
+
*/
|
|
142
|
+
function migrateSystemRbacIntoRbacFile(appPath, systemFilePath, systemParsed, options) {
|
|
143
|
+
const { dryRun, changes, backupCtx } = options;
|
|
144
|
+
const extracted = extractModule.extractRbacFromSystem(systemParsed);
|
|
145
|
+
if (!extracted) return false;
|
|
146
|
+
|
|
147
|
+
const rbacPath = appConfigResolver.resolveRbacPath(appPath);
|
|
148
|
+
if (!rbacPath) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const loaded = loadConfigFile(rbacPath);
|
|
153
|
+
/** Clone so merges never mutate a shared mock / cached object returned by tests or loaders. */
|
|
154
|
+
const rbac = {
|
|
155
|
+
roles: Array.isArray(loaded.roles)
|
|
156
|
+
? loaded.roles.map(r => ({
|
|
157
|
+
name: r.name,
|
|
158
|
+
value: r.value,
|
|
159
|
+
description: r.description,
|
|
160
|
+
groups: Array.isArray(r.groups) ? [...r.groups] : []
|
|
161
|
+
}))
|
|
162
|
+
: [],
|
|
163
|
+
permissions: Array.isArray(loaded.permissions)
|
|
164
|
+
? loaded.permissions.map(p => ({
|
|
165
|
+
name: p.name,
|
|
166
|
+
description: p.description,
|
|
167
|
+
roles: Array.isArray(p.roles) ? [...p.roles] : []
|
|
168
|
+
}))
|
|
169
|
+
: []
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const rolesMerged = _mergeRoleObjectsInto(rbac.roles, extracted.roles);
|
|
173
|
+
const permsMerged = _mergePermissionObjectsInto(rbac.permissions, extracted.permissions);
|
|
174
|
+
const rbacContentChanged = rolesMerged || permsMerged;
|
|
175
|
+
|
|
176
|
+
_appendMigrateSystemRbacNotes(changes, rbacContentChanged);
|
|
177
|
+
|
|
178
|
+
if (!dryRun) {
|
|
179
|
+
_persistMigrateSystemRbacEdits(rbacPath, rbac, rbacContentChanged, systemFilePath, systemParsed, backupCtx);
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
migrateSystemRbacIntoRbacFile
|
|
186
|
+
};
|
|
@@ -13,10 +13,24 @@ const fs = require('fs');
|
|
|
13
13
|
const chalk = require('chalk');
|
|
14
14
|
const logger = require('../utils/logger');
|
|
15
15
|
const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
16
|
-
const {
|
|
16
|
+
const { backupIntegrationFile } = require('../utils/integration-file-backup');
|
|
17
|
+
const appConfigResolver = require('../utils/app-config-resolver');
|
|
18
|
+
const { extractRbacFromSystem } = require('./repair-rbac-extract');
|
|
17
19
|
|
|
18
20
|
const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
|
|
19
21
|
|
|
22
|
+
function _safeString(v) {
|
|
23
|
+
return typeof v === 'string' && v.trim() ? v.trim() : '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _normalizeOperationKey(opKey) {
|
|
27
|
+
const s = _safeString(opKey);
|
|
28
|
+
if (!s) return '';
|
|
29
|
+
const withHyphens = s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
30
|
+
const cleaned = withHyphens.replace(/[^a-z0-9-]+/g, '-');
|
|
31
|
+
return cleaned.replace(/-+/g, '-').replace(/^-+|-+$/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
/**
|
|
21
35
|
* Resolves capabilities from datasource (array or legacy object).
|
|
22
36
|
* @param {Object} parsed - Parsed datasource
|
|
@@ -24,9 +38,15 @@ const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
|
|
|
24
38
|
*/
|
|
25
39
|
function getCapabilitiesFromDatasource(parsed) {
|
|
26
40
|
const cap = parsed?.capabilities;
|
|
27
|
-
if (Array.isArray(cap))
|
|
41
|
+
if (Array.isArray(cap)) {
|
|
42
|
+
const arr = cap.filter(c => typeof c === 'string');
|
|
43
|
+
if (arr.length > 0) return arr;
|
|
44
|
+
return [...DEFAULT_CAPABILITIES];
|
|
45
|
+
}
|
|
28
46
|
if (cap && typeof cap === 'object') {
|
|
29
|
-
|
|
47
|
+
const keys = Object.keys(cap).filter(k => cap[k] === true);
|
|
48
|
+
if (keys.length > 0) return keys;
|
|
49
|
+
return [...DEFAULT_CAPABILITIES];
|
|
30
50
|
}
|
|
31
51
|
return [...DEFAULT_CAPABILITIES];
|
|
32
52
|
}
|
|
@@ -40,20 +60,122 @@ function getCapabilitiesFromDatasource(parsed) {
|
|
|
40
60
|
function collectPermissionNames(appPath, datasourceFiles) {
|
|
41
61
|
const permissionNames = new Set();
|
|
42
62
|
for (const fileName of datasourceFiles || []) {
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
const parsed = loadConfigFile(filePath);
|
|
47
|
-
const resourceType = parsed?.resourceType || 'document';
|
|
48
|
-
const caps = getCapabilitiesFromDatasource(parsed);
|
|
49
|
-
for (const cap of caps) permissionNames.add(`${resourceType}:${cap}`);
|
|
50
|
-
} catch (err) {
|
|
51
|
-
logger.log(chalk.yellow(`⚠ Could not load ${fileName} for RBAC: ${err.message}`));
|
|
52
|
-
}
|
|
63
|
+
const parsed = _safeLoadDatasource(appPath, fileName);
|
|
64
|
+
if (!parsed) continue;
|
|
65
|
+
_collectPermissionNamesFromDatasourceParsed(permissionNames, parsed);
|
|
53
66
|
}
|
|
54
67
|
return permissionNames;
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
function _safeLoadDatasource(appPath, fileName) {
|
|
71
|
+
const filePath = path.join(appPath, fileName);
|
|
72
|
+
if (!fs.existsSync(filePath)) return null;
|
|
73
|
+
try {
|
|
74
|
+
return loadConfigFile(filePath);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.log(chalk.yellow(`⚠ Could not load ${fileName} for RBAC: ${err.message}`));
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _hasAutoRbacOps(parsed) {
|
|
82
|
+
const openapi = parsed?.openapi;
|
|
83
|
+
return (
|
|
84
|
+
openapi &&
|
|
85
|
+
typeof openapi === 'object' &&
|
|
86
|
+
openapi.autoRbac === true &&
|
|
87
|
+
openapi.operations &&
|
|
88
|
+
typeof openapi.operations === 'object' &&
|
|
89
|
+
!Array.isArray(openapi.operations)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _collectPermissionNamesFromDatasourceParsed(permissionNames, parsed) {
|
|
94
|
+
const resourceType = parsed?.resourceType || 'document';
|
|
95
|
+
|
|
96
|
+
if (_hasAutoRbacOps(parsed)) {
|
|
97
|
+
const ops = parsed.openapi.operations;
|
|
98
|
+
for (const opKey of Object.keys(ops)) {
|
|
99
|
+
const normOpKey = _normalizeOperationKey(opKey) || _safeString(opKey);
|
|
100
|
+
if (!normOpKey) continue;
|
|
101
|
+
permissionNames.add(`${resourceType}:${normOpKey}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// When autoRbac is false/missing, RBAC is considered manual; do not auto-generate permissions.
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _collectManagedResourceTypesFromDatasources(appPath, datasourceFiles) {
|
|
108
|
+
const out = new Set();
|
|
109
|
+
for (const fileName of datasourceFiles || []) {
|
|
110
|
+
const parsed = _safeLoadDatasource(appPath, fileName);
|
|
111
|
+
if (!parsed) continue;
|
|
112
|
+
if (!_hasAutoRbacOps(parsed)) continue;
|
|
113
|
+
const rt = _safeString(parsed.resourceType) || 'document';
|
|
114
|
+
out.add(rt);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _buildDesiredPermissionNames(appPath, datasourceFiles) {
|
|
120
|
+
const desired = collectPermissionNames(appPath, datasourceFiles);
|
|
121
|
+
if (desired.size === 0) return { desired, managedResourceTypes: new Set(), aliases: new Map() };
|
|
122
|
+
|
|
123
|
+
const managedResourceTypes = _collectManagedResourceTypesFromDatasources(appPath, datasourceFiles);
|
|
124
|
+
|
|
125
|
+
// Build alias map for rename support (kebab-case or case variants -> canonical).
|
|
126
|
+
const aliases = new Map();
|
|
127
|
+
for (const name of desired) {
|
|
128
|
+
const [rt, op] = String(name).split(':');
|
|
129
|
+
if (!rt || !op) continue;
|
|
130
|
+
const kebab = _normalizeOperationKey(op);
|
|
131
|
+
if (kebab && `${rt}:${kebab}` !== name) {
|
|
132
|
+
aliases.set(`${rt}:${kebab}`, name);
|
|
133
|
+
}
|
|
134
|
+
const lower = `${rt}:${String(op).toLowerCase()}`;
|
|
135
|
+
if (lower !== name) {
|
|
136
|
+
aliases.set(lower, name);
|
|
137
|
+
}
|
|
138
|
+
const dehyphen = `${rt}:${String(op).replace(/-/g, '').toLowerCase()}`;
|
|
139
|
+
if (dehyphen !== name) {
|
|
140
|
+
aliases.set(dehyphen, name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { desired, managedResourceTypes, aliases };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _renameExistingPermissionIfAliasMatches(rbac, aliases, changes) {
|
|
147
|
+
if (!rbac || !Array.isArray(rbac.permissions) || aliases.size === 0) return false;
|
|
148
|
+
let updated = false;
|
|
149
|
+
for (const p of rbac.permissions) {
|
|
150
|
+
if (!p || typeof p !== 'object') continue;
|
|
151
|
+
const name = _safeString(p.name);
|
|
152
|
+
if (!name) continue;
|
|
153
|
+
const canonical = aliases.get(name);
|
|
154
|
+
if (!canonical || canonical === name) continue;
|
|
155
|
+
p.name = canonical;
|
|
156
|
+
changes.push(`Renamed RBAC permission: ${name} → ${canonical}`);
|
|
157
|
+
updated = true;
|
|
158
|
+
}
|
|
159
|
+
return updated;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function _removeExtraAutoRbacPermissions(rbac, desired, managedResourceTypes, changes) {
|
|
163
|
+
if (!rbac || !Array.isArray(rbac.permissions) || desired.size === 0) return false;
|
|
164
|
+
if (!managedResourceTypes || managedResourceTypes.size === 0) return false;
|
|
165
|
+
const before = rbac.permissions.length;
|
|
166
|
+
rbac.permissions = rbac.permissions.filter((p) => {
|
|
167
|
+
const name = _safeString(p?.name);
|
|
168
|
+
if (!name || !name.includes(':')) return true;
|
|
169
|
+
const [rt] = name.split(':');
|
|
170
|
+
if (!managedResourceTypes.has(rt)) return true;
|
|
171
|
+
return desired.has(name);
|
|
172
|
+
});
|
|
173
|
+
const after = rbac.permissions.length;
|
|
174
|
+
if (after === before) return false;
|
|
175
|
+
changes.push(`Removed ${before - after} extra autoRbac permission(s) not present in operations`);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
57
179
|
/**
|
|
58
180
|
* Loads existing RBAC file (rbac.yaml, rbac.yml, or rbac.json) or creates empty structure.
|
|
59
181
|
* Uses extractRbacFromSystem when no file exists. New file path respects format (rbac.json when format is 'json').
|
|
@@ -65,7 +187,7 @@ function collectPermissionNames(appPath, datasourceFiles) {
|
|
|
65
187
|
* @returns {{ rbac: Object, rbacPath: string }} rbacPath is resolved path or path.join(appPath, 'rbac.{json|yaml}') for new file
|
|
66
188
|
*/
|
|
67
189
|
function loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, format) {
|
|
68
|
-
const resolvedPath = resolveRbacPath(appPath);
|
|
190
|
+
const resolvedPath = appConfigResolver.resolveRbacPath(appPath);
|
|
69
191
|
let rbac;
|
|
70
192
|
let rbacPath;
|
|
71
193
|
if (resolvedPath) {
|
|
@@ -131,6 +253,82 @@ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
|
|
|
131
253
|
return true;
|
|
132
254
|
}
|
|
133
255
|
|
|
256
|
+
function _roleValues(rbac) {
|
|
257
|
+
const roles = Array.isArray(rbac.roles) ? rbac.roles : [];
|
|
258
|
+
return roles
|
|
259
|
+
.map(r => r?.value)
|
|
260
|
+
.filter(v => typeof v === 'string' && v.trim());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function _pickAdminRoleValue(roleValues, systemKey) {
|
|
264
|
+
return (
|
|
265
|
+
roleValues.find(v => v === `${systemKey}-admin`) ||
|
|
266
|
+
roleValues.find(v => /-admin$/.test(v)) ||
|
|
267
|
+
roleValues[0] ||
|
|
268
|
+
null
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function _pickReaderRoleValue(roleValues, systemKey) {
|
|
273
|
+
return (
|
|
274
|
+
roleValues.find(v => v === `${systemKey}-reader`) ||
|
|
275
|
+
roleValues.find(v => /-reader$/.test(v)) ||
|
|
276
|
+
null
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function _capabilityFromPermissionName(name) {
|
|
281
|
+
if (typeof name !== 'string' || !name.includes(':')) return null;
|
|
282
|
+
return name.split(':')[1] || null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function _defaultRolesForCapability(cap, { adminValue, readerValue }) {
|
|
286
|
+
const roles = [];
|
|
287
|
+
if ((cap === 'list' || cap === 'get') && readerValue) roles.push(readerValue);
|
|
288
|
+
if (adminValue) roles.push(adminValue);
|
|
289
|
+
return roles;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Ensures every permission has at least one role assigned.
|
|
294
|
+
*
|
|
295
|
+
* When roles already exist, new permissions may be added with empty roles (invalid for manifest).
|
|
296
|
+
* Apply a safe default:
|
|
297
|
+
* - list/get -> reader + admin when those roles exist
|
|
298
|
+
* - create/update/delete -> admin when it exists (otherwise first available role)
|
|
299
|
+
*
|
|
300
|
+
* Mutates rbac.
|
|
301
|
+
* @param {Object} rbac - RBAC object (mutated)
|
|
302
|
+
* @param {string} systemKey - System key
|
|
303
|
+
* @param {string[]} changes - Array to append to
|
|
304
|
+
* @returns {boolean}
|
|
305
|
+
*/
|
|
306
|
+
function ensureNonEmptyPermissionRoles(rbac, systemKey, changes) {
|
|
307
|
+
const roleValues = _roleValues(rbac);
|
|
308
|
+
if (!Array.isArray(rbac.permissions) || roleValues.length === 0) return false;
|
|
309
|
+
|
|
310
|
+
const adminValue = _pickAdminRoleValue(roleValues, systemKey);
|
|
311
|
+
const readerValue = _pickReaderRoleValue(roleValues, systemKey);
|
|
312
|
+
|
|
313
|
+
let updated = false;
|
|
314
|
+
for (const p of rbac.permissions) {
|
|
315
|
+
if (!p || typeof p !== 'object') continue;
|
|
316
|
+
if (!Array.isArray(p.roles)) p.roles = [];
|
|
317
|
+
if (p.roles.length > 0) continue;
|
|
318
|
+
|
|
319
|
+
const cap = _capabilityFromPermissionName(p.name);
|
|
320
|
+
const defaults = _defaultRolesForCapability(cap, { adminValue, readerValue });
|
|
321
|
+
for (const rv of defaults) {
|
|
322
|
+
if (!p.roles.includes(rv)) p.roles.push(rv);
|
|
323
|
+
}
|
|
324
|
+
if (p.roles.length > 0) {
|
|
325
|
+
changes.push(`RBAC: defaulted empty roles for ${p.name}`);
|
|
326
|
+
updated = true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return updated;
|
|
330
|
+
}
|
|
331
|
+
|
|
134
332
|
/**
|
|
135
333
|
* Merges RBAC from datasources: ensures permission per resourceType:capability, adds Admin/Reader roles if none.
|
|
136
334
|
* When creating a new RBAC file, uses rbac.json if format is 'json', otherwise rbac.yaml.
|
|
@@ -139,26 +337,34 @@ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
|
|
|
139
337
|
* @param {Object} systemParsed - Parsed system (key, displayName)
|
|
140
338
|
* @param {string[]} datasourceFiles - Datasource file names
|
|
141
339
|
* @param {Function} extractRbacFromSystem - (system) => rbac or null
|
|
142
|
-
* @param {{ format?: string, dryRun: boolean, changes: string[] }} options - format ('json'|'yaml'), dryRun, changes array
|
|
340
|
+
* @param {{ format?: string, dryRun: boolean, changes: string[], backupCtx?: Object }} options - format ('json'|'yaml'), dryRun, changes array
|
|
143
341
|
* @returns {boolean} True if rbac was updated (or would be in dry-run)
|
|
144
342
|
*/
|
|
145
343
|
function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, options) {
|
|
146
|
-
const { format = 'yaml', dryRun, changes } = options;
|
|
344
|
+
const { format = 'yaml', dryRun, changes, backupCtx } = options;
|
|
147
345
|
const rbacFormat = format === 'json' ? 'json' : 'yaml';
|
|
148
|
-
const permissionNames =
|
|
346
|
+
const { desired: permissionNames, managedResourceTypes, aliases } = _buildDesiredPermissionNames(appPath, datasourceFiles);
|
|
149
347
|
if (permissionNames.size === 0) return false;
|
|
150
348
|
const systemKey = systemParsed?.key || 'system';
|
|
151
349
|
const displayName = systemParsed?.displayName || systemKey;
|
|
152
350
|
const { rbac, rbacPath } = loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, rbacFormat);
|
|
153
|
-
let updated =
|
|
351
|
+
let updated = _renameExistingPermissionIfAliasMatches(rbac, aliases, changes);
|
|
352
|
+
updated = addMissingPermissions(rbac, permissionNames, changes) || updated;
|
|
353
|
+
updated = _removeExtraAutoRbacPermissions(rbac, permissionNames, managedResourceTypes, changes) || updated;
|
|
154
354
|
updated = ensureDefaultRoles(rbac, systemKey, displayName, changes) || updated;
|
|
355
|
+
updated = ensureNonEmptyPermissionRoles(rbac, systemKey, changes) || updated;
|
|
155
356
|
if (updated && !dryRun) {
|
|
357
|
+
backupIntegrationFile(rbacPath, backupCtx);
|
|
156
358
|
writeConfigFile(rbacPath, rbac);
|
|
157
359
|
}
|
|
158
360
|
return updated;
|
|
159
361
|
}
|
|
160
362
|
|
|
363
|
+
const { migrateSystemRbacIntoRbacFile } = require('./repair-rbac-migrate');
|
|
364
|
+
|
|
161
365
|
module.exports = {
|
|
366
|
+
extractRbacFromSystem,
|
|
162
367
|
getCapabilitiesFromDatasource,
|
|
163
|
-
mergeRbacFromDatasources
|
|
368
|
+
mergeRbacFromDatasources,
|
|
369
|
+
migrateSystemRbacIntoRbacFile
|
|
164
370
|
};
|