@aifabrix/builder 2.44.5 → 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 +48 -2
- 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/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.js +20 -2
- package/lib/cli/setup-auth.js +26 -0
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra.js +134 -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 +78 -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 +214 -31
- package/lib/commands/repair-system-alignment.js +246 -0
- package/lib/commands/repair-system-permissions.js +168 -0
- package/lib/commands/repair.js +120 -338
- 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/up-common.js +79 -19
- 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 +6 -5
- package/lib/commands/wizard-dataplane.js +2 -2
- package/lib/commands/wizard-entity-selection.js +4 -3
- package/lib/commands/wizard-headless.js +2 -1
- package/lib/commands/wizard.js +2 -1
- 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/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 +1 -1
- 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 +71 -2
- 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 +23 -12
- 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 +1 -1
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +83 -123
- 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,24 +13,22 @@ 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
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (!
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const rbac = {};
|
|
31
|
-
if (hasRoles) rbac.roles = system.roles;
|
|
32
|
-
if (hasPermissions) rbac.permissions = system.permissions;
|
|
33
|
-
return rbac;
|
|
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, '');
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
/**
|
|
@@ -62,20 +60,122 @@ function getCapabilitiesFromDatasource(parsed) {
|
|
|
62
60
|
function collectPermissionNames(appPath, datasourceFiles) {
|
|
63
61
|
const permissionNames = new Set();
|
|
64
62
|
for (const fileName of datasourceFiles || []) {
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
|
|
68
|
-
const parsed = loadConfigFile(filePath);
|
|
69
|
-
const resourceType = parsed?.resourceType || 'document';
|
|
70
|
-
const caps = getCapabilitiesFromDatasource(parsed);
|
|
71
|
-
for (const cap of caps) permissionNames.add(`${resourceType}:${cap}`);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
logger.log(chalk.yellow(`⚠ Could not load ${fileName} for RBAC: ${err.message}`));
|
|
74
|
-
}
|
|
63
|
+
const parsed = _safeLoadDatasource(appPath, fileName);
|
|
64
|
+
if (!parsed) continue;
|
|
65
|
+
_collectPermissionNamesFromDatasourceParsed(permissionNames, parsed);
|
|
75
66
|
}
|
|
76
67
|
return permissionNames;
|
|
77
68
|
}
|
|
78
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
|
+
|
|
79
179
|
/**
|
|
80
180
|
* Loads existing RBAC file (rbac.yaml, rbac.yml, or rbac.json) or creates empty structure.
|
|
81
181
|
* Uses extractRbacFromSystem when no file exists. New file path respects format (rbac.json when format is 'json').
|
|
@@ -87,7 +187,7 @@ function collectPermissionNames(appPath, datasourceFiles) {
|
|
|
87
187
|
* @returns {{ rbac: Object, rbacPath: string }} rbacPath is resolved path or path.join(appPath, 'rbac.{json|yaml}') for new file
|
|
88
188
|
*/
|
|
89
189
|
function loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, format) {
|
|
90
|
-
const resolvedPath = resolveRbacPath(appPath);
|
|
190
|
+
const resolvedPath = appConfigResolver.resolveRbacPath(appPath);
|
|
91
191
|
let rbac;
|
|
92
192
|
let rbacPath;
|
|
93
193
|
if (resolvedPath) {
|
|
@@ -153,6 +253,82 @@ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
|
|
|
153
253
|
return true;
|
|
154
254
|
}
|
|
155
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
|
+
|
|
156
332
|
/**
|
|
157
333
|
* Merges RBAC from datasources: ensures permission per resourceType:capability, adds Admin/Reader roles if none.
|
|
158
334
|
* When creating a new RBAC file, uses rbac.json if format is 'json', otherwise rbac.yaml.
|
|
@@ -161,27 +337,34 @@ function ensureDefaultRoles(rbac, systemKey, displayName, changes) {
|
|
|
161
337
|
* @param {Object} systemParsed - Parsed system (key, displayName)
|
|
162
338
|
* @param {string[]} datasourceFiles - Datasource file names
|
|
163
339
|
* @param {Function} extractRbacFromSystem - (system) => rbac or null
|
|
164
|
-
* @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
|
|
165
341
|
* @returns {boolean} True if rbac was updated (or would be in dry-run)
|
|
166
342
|
*/
|
|
167
343
|
function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, options) {
|
|
168
|
-
const { format = 'yaml', dryRun, changes } = options;
|
|
344
|
+
const { format = 'yaml', dryRun, changes, backupCtx } = options;
|
|
169
345
|
const rbacFormat = format === 'json' ? 'json' : 'yaml';
|
|
170
|
-
const permissionNames =
|
|
346
|
+
const { desired: permissionNames, managedResourceTypes, aliases } = _buildDesiredPermissionNames(appPath, datasourceFiles);
|
|
171
347
|
if (permissionNames.size === 0) return false;
|
|
172
348
|
const systemKey = systemParsed?.key || 'system';
|
|
173
349
|
const displayName = systemParsed?.displayName || systemKey;
|
|
174
350
|
const { rbac, rbacPath } = loadOrCreateRbac(appPath, systemParsed, extractRbacFromSystem, rbacFormat);
|
|
175
|
-
let updated =
|
|
351
|
+
let updated = _renameExistingPermissionIfAliasMatches(rbac, aliases, changes);
|
|
352
|
+
updated = addMissingPermissions(rbac, permissionNames, changes) || updated;
|
|
353
|
+
updated = _removeExtraAutoRbacPermissions(rbac, permissionNames, managedResourceTypes, changes) || updated;
|
|
176
354
|
updated = ensureDefaultRoles(rbac, systemKey, displayName, changes) || updated;
|
|
355
|
+
updated = ensureNonEmptyPermissionRoles(rbac, systemKey, changes) || updated;
|
|
177
356
|
if (updated && !dryRun) {
|
|
357
|
+
backupIntegrationFile(rbacPath, backupCtx);
|
|
178
358
|
writeConfigFile(rbacPath, rbac);
|
|
179
359
|
}
|
|
180
360
|
return updated;
|
|
181
361
|
}
|
|
182
362
|
|
|
363
|
+
const { migrateSystemRbacIntoRbacFile } = require('./repair-rbac-migrate');
|
|
364
|
+
|
|
183
365
|
module.exports = {
|
|
184
366
|
extractRbacFromSystem,
|
|
185
367
|
getCapabilitiesFromDatasource,
|
|
186
|
-
mergeRbacFromDatasources
|
|
368
|
+
mergeRbacFromDatasources,
|
|
369
|
+
migrateSystemRbacIntoRbacFile
|
|
187
370
|
};
|