@catladder/cli 0.0.0-main-2355fd7c → 0.0.0-main-018e2dee

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.
Files changed (83) hide show
  1. package/dist/apps/cli/src/apps/catenv/catenv.js +3 -0
  2. package/dist/apps/cli/src/apps/catenv/catenv.js.map +1 -1
  3. package/dist/apps/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.d.ts +4 -0
  4. package/dist/apps/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.js +12 -0
  5. package/dist/apps/cli/src/apps/cli/commands/project/doctor/GcloudProjectInspector.js.map +1 -1
  6. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkCloudRun.d.ts +2 -2
  7. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkCloudRun.js +3 -1
  8. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkCloudRun.js.map +1 -1
  9. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.d.ts +10 -0
  10. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.js +98 -0
  11. package/dist/apps/cli/src/apps/cli/commands/project/doctor/checkLocalAccess.js.map +1 -0
  12. package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js +1 -1
  13. package/dist/apps/cli/src/apps/cli/commands/project/doctor/index.js.map +1 -1
  14. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupContext.js +2 -0
  15. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupContext.js.map +1 -1
  16. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.d.ts +9 -0
  17. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.js +31 -0
  18. package/dist/apps/cli/src/apps/cli/commands/project/setup/setupLocalAccess.js.map +1 -0
  19. package/dist/apps/cli/src/config/getProjectConfig.js +9 -1
  20. package/dist/apps/cli/src/config/getProjectConfig.js.map +1 -1
  21. package/dist/apps/cli/src/gcloud/localAccess.d.ts +53 -0
  22. package/dist/apps/cli/src/gcloud/localAccess.js +176 -0
  23. package/dist/apps/cli/src/gcloud/localAccess.js.map +1 -0
  24. package/dist/apps/cli/src/gcloud/serviceAccounts.js +9 -2
  25. package/dist/apps/cli/src/gcloud/serviceAccounts.js.map +1 -1
  26. package/dist/apps/cli/src/localAccess/declared.d.ts +14 -0
  27. package/dist/apps/cli/src/localAccess/declared.js +34 -0
  28. package/dist/apps/cli/src/localAccess/declared.js.map +1 -0
  29. package/dist/apps/cli/src/localAccess/keyFiles.d.ts +28 -0
  30. package/dist/apps/cli/src/localAccess/keyFiles.js +91 -0
  31. package/dist/apps/cli/src/localAccess/keyFiles.js.map +1 -0
  32. package/dist/apps/cli/src/utils/writePrivateFile.d.ts +7 -0
  33. package/dist/apps/cli/src/utils/writePrivateFile.js +25 -0
  34. package/dist/apps/cli/src/utils/writePrivateFile.js.map +1 -0
  35. package/dist/apps/cli/src/vault/BitwardenVault.js +13 -22
  36. package/dist/apps/cli/src/vault/BitwardenVault.js.map +1 -1
  37. package/dist/apps/cli/src/vault/VaultManager.d.ts +6 -0
  38. package/dist/apps/cli/src/vault/VaultManager.js +18 -0
  39. package/dist/apps/cli/src/vault/VaultManager.js.map +1 -1
  40. package/dist/apps/cli/src/vault/bw.d.ts +6 -2
  41. package/dist/apps/cli/src/vault/bw.js +32 -6
  42. package/dist/apps/cli/src/vault/bw.js.map +1 -1
  43. package/dist/apps/cli/src/vault/cache.js +5 -2
  44. package/dist/apps/cli/src/vault/cache.js.map +1 -1
  45. package/dist/bundles/catenv/index.js +436 -31
  46. package/dist/bundles/cli/index.js +786 -35
  47. package/dist/bundles/skills/catladder-secrets/SKILL.md +35 -3
  48. package/dist/packages/pipeline/src/index.d.ts +1 -0
  49. package/dist/packages/pipeline/src/index.js +1 -0
  50. package/dist/packages/pipeline/src/index.js.map +1 -1
  51. package/dist/packages/pipeline/src/localAccess/index.d.ts +53 -0
  52. package/dist/packages/pipeline/src/localAccess/index.js +177 -0
  53. package/dist/packages/pipeline/src/localAccess/index.js.map +1 -0
  54. package/dist/packages/pipeline/src/types/config.d.ts +75 -0
  55. package/dist/packages/pipeline/src/types/config.js.map +1 -1
  56. package/dist/skills/catladder-secrets/SKILL.md +35 -3
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +1 -1
  59. package/src/apps/catenv/catenv.ts +4 -0
  60. package/src/apps/cli/commands/project/doctor/GcloudProjectInspector.ts +25 -1
  61. package/src/apps/cli/commands/project/doctor/__tests__/checkLocalAccess.test.ts +127 -0
  62. package/src/apps/cli/commands/project/doctor/checkCloudRun.ts +5 -1
  63. package/src/apps/cli/commands/project/doctor/checkLocalAccess.ts +146 -0
  64. package/src/apps/cli/commands/project/doctor/index.ts +1 -1
  65. package/src/apps/cli/commands/project/setup/setupContext.ts +2 -0
  66. package/src/apps/cli/commands/project/setup/setupLocalAccess.ts +44 -0
  67. package/src/config/__tests__/getEnvVarsResolved.test.ts +45 -0
  68. package/src/config/getProjectConfig.ts +11 -1
  69. package/src/gcloud/__tests__/localAccess.test.ts +82 -0
  70. package/src/gcloud/__tests__/syncLocalAccess.test.ts +196 -0
  71. package/src/gcloud/localAccess.ts +257 -0
  72. package/src/gcloud/serviceAccounts.ts +8 -2
  73. package/src/localAccess/__tests__/keyFiles.test.ts +166 -0
  74. package/src/localAccess/declared.ts +38 -0
  75. package/src/localAccess/keyFiles.ts +126 -0
  76. package/src/utils/__tests__/writePrivateFile.test.ts +40 -0
  77. package/src/utils/writePrivateFile.ts +20 -0
  78. package/src/vault/BitwardenVault.ts +8 -9
  79. package/src/vault/VaultManager.ts +28 -0
  80. package/src/vault/__tests__/BitwardenVault.test.ts +4 -4
  81. package/src/vault/__tests__/cache.test.ts +37 -0
  82. package/src/vault/bw.ts +34 -9
  83. package/src/vault/cache.ts +5 -2
@@ -475,6 +475,18 @@ class GcloudProjectInspector {
475
475
  .filter((binding) => { var _a; return (_a = binding.members) === null || _a === void 0 ? void 0 : _a.includes(member); })
476
476
  .map((binding) => binding.role);
477
477
  }
478
+ /** roles bound to the member without an iam condition */
479
+ async getUnconditionalRolesOfMember(projectId, member) {
480
+ return (await this.getIamBindings(projectId))
481
+ .filter((b) => { var _a; return !b.condition && ((_a = b.members) === null || _a === void 0 ? void 0 : _a.includes(member)); })
482
+ .map((b) => b.role);
483
+ }
484
+ /** roles bound to the member under an iam condition */
485
+ async getConditionalRolesOfMember(projectId, member) {
486
+ return (await this.getIamBindings(projectId))
487
+ .filter((b) => { var _a; return b.condition && ((_a = b.members) === null || _a === void 0 ? void 0 : _a.includes(member)); })
488
+ .map((b) => b.role);
489
+ }
478
490
  getEnabledServices(projectId) {
479
491
  if (!this.enabledServices.has(projectId)) {
480
492
  this.enabledServices.set(projectId, (0, child_process_promise_1.exec)(`gcloud services list --enabled --project=${projectId} --format="value(config.name)"`).then((result) => new Set(result.stdout
@@ -512,6 +524,7 @@ const serviceAccountNames_1 = __nccwpck_require__(67851);
512
524
  const serviceAccounts_1 = __nccwpck_require__(18190);
513
525
  const concurrency_1 = __nccwpck_require__(59723);
514
526
  const setupCloudRun_1 = __nccwpck_require__(92526);
527
+ const checkLocalAccess_1 = __nccwpck_require__(80206);
515
528
  const GcloudProjectInspector_1 = __nccwpck_require__(50379);
516
529
  const getCloudRunConfig = (context) => {
517
530
  var _a;
@@ -531,7 +544,7 @@ const getCloudRunConfig = (context) => {
531
544
  * setup ran — deploys keep working and only the review teardown hits
532
545
  * the missing `roles/cloudsql.admin` as a 403 in ci.
533
546
  */
534
- const checkCloudRun = async (report, contexts) => {
547
+ const checkCloudRun = async (report, contexts, config) => {
535
548
  var _a;
536
549
  const cloudRunContexts = contexts.filter((context) => { var _a; return (0, pipeline_1.isOfDeployType)((_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config, "google-cloudrun"); });
537
550
  if (cloudRunContexts.length === 0)
@@ -602,6 +615,7 @@ const checkCloudRun = async (report, contexts) => {
602
615
  report.ok(`${label}: service account exists with all config-implied roles`);
603
616
  }
604
617
  }
618
+ await (0, checkLocalAccess_1.checkLocalAccess)(report, config, cloudRunContexts, inspector);
605
619
  };
606
620
  exports.checkCloudRun = checkCloudRun;
607
621
  //# sourceMappingURL=checkCloudRun.js.map
@@ -1052,6 +1066,111 @@ exports.checkGitlab = checkGitlab;
1052
1066
 
1053
1067
  /***/ }),
1054
1068
 
1069
+ /***/ 80206:
1070
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
1071
+
1072
+ "use strict";
1073
+
1074
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1075
+ exports.checkLocalAccess = void 0;
1076
+ const pipeline_1 = __nccwpck_require__(53695);
1077
+ const localAccess_1 = __nccwpck_require__(94216);
1078
+ const serviceAccounts_1 = __nccwpck_require__(18190);
1079
+ const concurrency_1 = __nccwpck_require__(59723);
1080
+ /**
1081
+ * compares the read-only local access service accounts (`localAccess`
1082
+ * in catladder.ts) with their declaration: missing account, missing and
1083
+ * extra bindings, and an account that exists although nothing is
1084
+ * declared any more. Expects an authenticated inspector.
1085
+ */
1086
+ const checkLocalAccess = async (report, config, contexts, inspector) => {
1087
+ const results = await (0, concurrency_1.mapWithConcurrency)(contexts, 5, async (context) => {
1088
+ var _a;
1089
+ const deployConfig = (_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config;
1090
+ if (!(0, pipeline_1.isOfDeployType)(deployConfig, "google-cloudrun"))
1091
+ return null;
1092
+ const label = `${context.env}:${context.name}`;
1093
+ let declared;
1094
+ try {
1095
+ declared = (0, pipeline_1.getLocalAccessGcpRoles)(config, context.env);
1096
+ (0, pipeline_1.getLocalAccessGcpCredentialsEnvVars)(config, context.name);
1097
+ }
1098
+ catch (e) {
1099
+ return { label, invalid: e.message };
1100
+ }
1101
+ const { fullIdentifier } = (0, localAccess_1.getLocalAccessServiceAccountNames)(context, deployConfig.projectId);
1102
+ const exists = await (0, serviceAccounts_1.accountExists)(fullIdentifier);
1103
+ const member = `serviceAccount:${fullIdentifier}`;
1104
+ const bound = exists
1105
+ ? await inspector.getUnconditionalRolesOfMember(deployConfig.projectId, member)
1106
+ : [];
1107
+ const conditional = exists
1108
+ ? await inspector.getConditionalRolesOfMember(deployConfig.projectId, member)
1109
+ : [];
1110
+ const keyCount = exists
1111
+ ? (await (0, localAccess_1.listUserManagedKeyIds)(fullIdentifier)).length
1112
+ : 0;
1113
+ return {
1114
+ label,
1115
+ declared,
1116
+ fullIdentifier,
1117
+ exists,
1118
+ bound,
1119
+ conditional,
1120
+ keyCount,
1121
+ };
1122
+ });
1123
+ const relevant = results.filter((r) => r && ("invalid" in r || r.declared.length > 0 || r.exists));
1124
+ if (relevant.length === 0)
1125
+ return;
1126
+ report.section("local access (read-only service accounts)");
1127
+ for (const result of relevant) {
1128
+ if (!result)
1129
+ continue;
1130
+ if ("invalid" in result) {
1131
+ report.fail(`${result.label}: ${result.invalid}`);
1132
+ continue;
1133
+ }
1134
+ const { label, declared, fullIdentifier, exists, bound, conditional, keyCount, } = result;
1135
+ if (declared.length > 0 && (0, pipeline_1.getVaultConfig)(config).type === "gitlab") {
1136
+ report.fail(`${label}: localAccess needs a vault that is not a CI variable store (the gitlab vault would expose the key to every CI job)`);
1137
+ continue;
1138
+ }
1139
+ const heal = `run: catladder project setup`;
1140
+ if (declared.length === 0) {
1141
+ report.warn(`${label}: ${fullIdentifier} exists although no local access is declared`, heal);
1142
+ }
1143
+ else if (!exists) {
1144
+ report.fail(`${label}: ${fullIdentifier} does not exist`, heal);
1145
+ }
1146
+ else {
1147
+ const { missing, extra } = (0, pipeline_1.diffRoleBindings)(declared, bound);
1148
+ if (missing.length > 0) {
1149
+ report.fail(`${label}: local access account is missing roles: ${missing.join(", ")}`, heal);
1150
+ }
1151
+ if (extra.length > 0) {
1152
+ report.fail(`${label}: local access account has undeclared roles: ${extra.join(", ")}`, heal);
1153
+ }
1154
+ if (conditional.length > 0) {
1155
+ report.fail(`${label}: local access account has conditional roles (${conditional.join(", ")}) that setup does not remove`, "remove them by hand");
1156
+ }
1157
+ if (keyCount !== 1) {
1158
+ report.fail(`${label}: local access account has ${keyCount} user-managed keys, expected exactly 1`, heal);
1159
+ }
1160
+ if (missing.length === 0 &&
1161
+ extra.length === 0 &&
1162
+ conditional.length === 0 &&
1163
+ keyCount === 1) {
1164
+ report.ok(`${label}: local access account has exactly the declared roles`);
1165
+ }
1166
+ }
1167
+ }
1168
+ };
1169
+ exports.checkLocalAccess = checkLocalAccess;
1170
+ //# sourceMappingURL=checkLocalAccess.js.map
1171
+
1172
+ /***/ }),
1173
+
1055
1174
  /***/ 62878:
1056
1175
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
1057
1176
 
@@ -1145,7 +1264,7 @@ const doctorProject = async (instance, onlyComponents) => {
1145
1264
  if ((0, pipeline_1.getEnabledPipelineTypes)(config).includes("gitlab")) {
1146
1265
  await (0, checkGitlab_1.checkGitlab)(instance, report, contexts);
1147
1266
  }
1148
- await (0, checkCloudRun_1.checkCloudRun)(report, contexts);
1267
+ await (0, checkCloudRun_1.checkCloudRun)(report, contexts, config);
1149
1268
  await (0, checkGithubImageCasing_1.checkGithubImageCasing)(report, config);
1150
1269
  await (0, checkGithubReleaseWorkflow_1.checkGithubReleaseWorkflow)(report, config);
1151
1270
  await (0, checkGithub_1.checkGithub)(report, config);
@@ -1704,6 +1823,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
1704
1823
  exports.setupContext = void 0;
1705
1824
  const pipeline_1 = __nccwpck_require__(53695);
1706
1825
  const setupCloudRun_1 = __nccwpck_require__(92526);
1826
+ const setupLocalAccess_1 = __nccwpck_require__(3299);
1707
1827
  const setupKubernetes_1 = __nccwpck_require__(51796);
1708
1828
  const logSection_1 = __nccwpck_require__(74120);
1709
1829
  const setupContext = async (instance, context) => {
@@ -1711,6 +1831,7 @@ const setupContext = async (instance, context) => {
1711
1831
  var _a, _b;
1712
1832
  if ((0, pipeline_1.isOfDeployType)((_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config, "google-cloudrun")) {
1713
1833
  await (0, setupCloudRun_1.setupCloudRun)(instance, context);
1834
+ await (0, setupLocalAccess_1.setupLocalAccess)(instance, context);
1714
1835
  }
1715
1836
  const deployConfig = (_b = context.deploy) === null || _b === void 0 ? void 0 : _b.config;
1716
1837
  if ((0, pipeline_1.isOfDeployType)(deployConfig, "kubernetes")) {
@@ -2067,6 +2188,44 @@ exports.setupKubernetes = setupKubernetes;
2067
2188
 
2068
2189
  /***/ }),
2069
2190
 
2191
+ /***/ 3299:
2192
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
2193
+
2194
+ "use strict";
2195
+
2196
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2197
+ exports.setupLocalAccess = void 0;
2198
+ const pipeline_1 = __nccwpck_require__(53695);
2199
+ const getProjectConfig_1 = __nccwpck_require__(70478);
2200
+ const localAccess_1 = __nccwpck_require__(94216);
2201
+ /**
2202
+ * provisions the local access (`localAccess` in catladder.ts)
2203
+ * of a google cloud run component/env: a dedicated service account with
2204
+ * exactly the declared bindings and its key in the secret store. Also
2205
+ * removes what is no longer declared.
2206
+ */
2207
+ const setupLocalAccess = async (instance, context) => {
2208
+ var _a;
2209
+ const config = (_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config;
2210
+ if (!(0, pipeline_1.isOfDeployType)(config, "google-cloudrun"))
2211
+ return;
2212
+ const projectConfig = await (0, getProjectConfig_1.getProjectConfig)();
2213
+ const roles = (0, pipeline_1.getLocalAccessGcpRoles)(projectConfig, context.env);
2214
+ // validates `credentialsEnvVar` (catenv silently skips invalid ones)
2215
+ (0, pipeline_1.getLocalAccessGcpCredentialsEnvVars)(projectConfig, context.name);
2216
+ if (roles.length > 0 && (0, pipeline_1.getVaultConfig)(projectConfig).type === "gitlab") {
2217
+ // the gitlab vault stores secrets as CI variables available to every
2218
+ // job on every branch: the key would leak to anyone who can push
2219
+ throw new Error(`localAccess (${context.env}:${context.name}) needs a vault that is not a CI variable store (e.g. bitwarden): the gitlab vault would expose the service account key to every CI job. Configure \`vault\` or remove the localAccess declaration.`);
2220
+ }
2221
+ instance.log("sync local access service account...");
2222
+ await (0, localAccess_1.syncLocalAccessServiceAccount)(instance, context, config.projectId, roles);
2223
+ };
2224
+ exports.setupLocalAccess = setupLocalAccess;
2225
+ //# sourceMappingURL=setupLocalAccess.js.map
2226
+
2227
+ /***/ }),
2228
+
2070
2229
  /***/ 63361:
2071
2230
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
2072
2231
 
@@ -5790,6 +5949,7 @@ exports.uninstallCompletions = uninstallCompletions;
5790
5949
  Object.defineProperty(exports, "__esModule", ({ value: true }));
5791
5950
  exports.getJobOnlyEnvVarsResolved = exports.getEnvVarsResolved = exports.getGitlabVar = exports.getEnvironment = exports.getAllPipelineContexts = exports.getAllComponentsWithAllEnvsHierarchical = exports.getAllComponentsWithAllEnvsFlat = exports.getPipelineContextByChoice = exports.getProjectComponents = exports.invalidateProjectConfig = exports.getProjectConfig = exports.parseChoice = void 0;
5792
5951
  const pipeline_1 = __nccwpck_require__(53695);
5952
+ const keyFiles_1 = __nccwpck_require__(70180);
5793
5953
  const gitlab_1 = __nccwpck_require__(17697);
5794
5954
  const projects_1 = __nccwpck_require__(20575);
5795
5955
  var parseChoice_1 = __nccwpck_require__(70769);
@@ -5910,12 +6070,19 @@ const getEnvVarsResolved = async (io, env, componentName) => {
5910
6070
  const envionment = await (0, exports.getEnvironment)(env, componentName);
5911
6071
  // in the pipeline the secrets alreadyy exists and bash will expand them
5912
6072
  // but here we need to manually load them
5913
- return resolveSecrets(io, [
6073
+ const resolved = await resolveSecrets(io, [
5914
6074
  {
5915
6075
  envVars: envionment.envVars,
5916
6076
  secretEnvVarKeys: envionment.secretEnvVarKeys,
5917
6077
  },
5918
6078
  ]);
6079
+ if (env !== "local")
6080
+ return resolved;
6081
+ // paths of the local access key files (written by catenv)
6082
+ return {
6083
+ ...resolved,
6084
+ ...(await (0, keyFiles_1.getLocalAccessKeyFileVars)(await (0, exports.getProjectConfig)(), componentName)),
6085
+ };
5919
6086
  };
5920
6087
  exports.getEnvVarsResolved = getEnvVarsResolved;
5921
6088
  /**
@@ -6230,6 +6397,189 @@ exports.fetchGcloudProjectNumber = fetchGcloudProjectNumber;
6230
6397
 
6231
6398
  /***/ }),
6232
6399
 
6400
+ /***/ 94216:
6401
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
6402
+
6403
+ "use strict";
6404
+
6405
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
6406
+ exports.syncLocalAccessServiceAccount = exports.listUserManagedKeyIds = exports.parseServiceAccountKey = exports.getConditionalRolesOfMemberInBindings = exports.getRolesOfMemberInBindings = exports.getLocalAccessServiceAccountNames = void 0;
6407
+ const pipeline_1 = __nccwpck_require__(53695);
6408
+ const child_process_promise_1 = __nccwpck_require__(11542);
6409
+ const promises_1 = __nccwpck_require__(91943);
6410
+ const os_1 = __nccwpck_require__(70857);
6411
+ const path_1 = __nccwpck_require__(16928);
6412
+ const serviceAccountNames_1 = __nccwpck_require__(67851);
6413
+ const serviceAccounts_1 = __nccwpck_require__(18190);
6414
+ /**
6415
+ * the dedicated read-only service account of a component/env that
6416
+ * `project setup` provisions from the `localAccess` declaration.
6417
+ * Shared with `project doctor` and catenv.
6418
+ */
6419
+ const getLocalAccessServiceAccountNames = (context, projectId) => (0, serviceAccountNames_1.getGcloudServiceAccountNames)(context, {
6420
+ name: pipeline_1.LOCAL_ACCESS_GCP_SA_NAME,
6421
+ projectId,
6422
+ });
6423
+ exports.getLocalAccessServiceAccountNames = getLocalAccessServiceAccountNames;
6424
+ /**
6425
+ * all unconditional roles bound to `member` in the given iam policy
6426
+ * bindings. Conditional bindings are reported separately, see
6427
+ * {@link getConditionalRolesOfMemberInBindings}.
6428
+ */
6429
+ const getRolesOfMemberInBindings = (bindings, member) => bindings
6430
+ .filter((binding) => !binding.condition)
6431
+ .filter((binding) => { var _a; return (_a = binding.members) === null || _a === void 0 ? void 0 : _a.includes(member); })
6432
+ .map((binding) => binding.role)
6433
+ .sort();
6434
+ exports.getRolesOfMemberInBindings = getRolesOfMemberInBindings;
6435
+ /**
6436
+ * roles bound to `member` under an iam condition. These cannot be
6437
+ * removed by role name alone, so they are never touched by `project
6438
+ * setup` — only reported.
6439
+ */
6440
+ const getConditionalRolesOfMemberInBindings = (bindings, member) => bindings
6441
+ .filter((binding) => binding.condition)
6442
+ .filter((binding) => { var _a; return (_a = binding.members) === null || _a === void 0 ? void 0 : _a.includes(member); })
6443
+ .map((binding) => binding.role)
6444
+ .sort();
6445
+ exports.getConditionalRolesOfMemberInBindings = getConditionalRolesOfMemberInBindings;
6446
+ /**
6447
+ * parses the output of `gcloud iam service-accounts keys create` and
6448
+ * makes sure it is a usable service account key
6449
+ */
6450
+ const parseServiceAccountKey = (raw) => {
6451
+ let parsed;
6452
+ try {
6453
+ parsed = JSON.parse(raw);
6454
+ }
6455
+ catch (_a) {
6456
+ throw new Error("gcloud did not return a valid service account key");
6457
+ }
6458
+ if (typeof parsed.private_key_id !== "string" ||
6459
+ typeof parsed.private_key !== "string") {
6460
+ throw new Error("gcloud did not return a valid service account key");
6461
+ }
6462
+ return { key: raw, privateKeyId: parsed.private_key_id };
6463
+ };
6464
+ exports.parseServiceAccountKey = parseServiceAccountKey;
6465
+ const getProjectBindings = async (projectId) => {
6466
+ var _a;
6467
+ return (_a = JSON.parse((await (0, child_process_promise_1.exec)(`gcloud projects get-iam-policy ${projectId} --format=json`))
6468
+ .stdout).bindings) !== null && _a !== void 0 ? _a : [];
6469
+ };
6470
+ const listUserManagedKeyIds = async (fullIdentifier) => {
6471
+ const keys = JSON.parse((await (0, child_process_promise_1.exec)(`gcloud iam service-accounts keys list --iam-account=${fullIdentifier} --managed-by=user --format=json`)).stdout);
6472
+ return keys.map((key) => key.name.split("/").pop());
6473
+ };
6474
+ exports.listUserManagedKeyIds = listUserManagedKeyIds;
6475
+ /**
6476
+ * makes the account's bindings equal to `roles`: adds missing, removes
6477
+ * everything else (the account is dedicated, every other binding is
6478
+ * drift). Idempotent — a no-op when nothing differs.
6479
+ */
6480
+ const syncBindings = async (io, projectId, fullIdentifier, roles) => {
6481
+ const member = `serviceAccount:${fullIdentifier}`;
6482
+ const bindings = await getProjectBindings(projectId);
6483
+ const conditional = (0, exports.getConditionalRolesOfMemberInBindings)(bindings, member);
6484
+ if (conditional.length > 0) {
6485
+ io.log(`⚠️ ${fullIdentifier} has conditional bindings (${conditional.join(", ")}) — setup does not touch them, remove them by hand`);
6486
+ }
6487
+ const { missing, extra } = (0, pipeline_1.diffRoleBindings)(roles, (0, exports.getRolesOfMemberInBindings)(bindings, member));
6488
+ for (const role of missing) {
6489
+ io.log(` + ${role}`);
6490
+ await (0, child_process_promise_1.exec)(`gcloud projects add-iam-policy-binding ${projectId} --member=${member} --role=${role} --condition=None`);
6491
+ }
6492
+ for (const role of extra) {
6493
+ io.log(` - ${role}`);
6494
+ await (0, child_process_promise_1.exec)(`gcloud projects remove-iam-policy-binding ${projectId} --member=${member} --role=${role} --condition=None`);
6495
+ }
6496
+ };
6497
+ /**
6498
+ * brings the local access service account of a component/env in line
6499
+ * with the declared `roles`:
6500
+ *
6501
+ * - roles declared: account exists, carries exactly these bindings,
6502
+ * and a key for it is in the secret store (under the `local` env, so
6503
+ * it is never mirrored to ci). The key is only created when the
6504
+ * stored one is missing or no longer valid — re-running is a no-op.
6505
+ * - no roles declared: bindings are removed and the account (with its
6506
+ * keys) deleted if it exists.
6507
+ */
6508
+ const syncLocalAccessServiceAccount = async (io, context, projectId, roles) => {
6509
+ const { fullName, fullIdentifier } = (0, exports.getLocalAccessServiceAccountNames)(context, projectId);
6510
+ const exists = await (0, serviceAccounts_1.accountExists)(fullIdentifier);
6511
+ if (roles.length === 0) {
6512
+ if (exists) {
6513
+ io.log(`local access no longer declared, removing ${fullIdentifier}...`);
6514
+ await syncBindings(io, projectId, fullIdentifier, []);
6515
+ await (0, child_process_promise_1.exec)(`gcloud iam service-accounts delete ${fullIdentifier} --quiet --project="${projectId}"`);
6516
+ }
6517
+ return;
6518
+ }
6519
+ if (!exists) {
6520
+ io.log(`creating local access service account ${fullIdentifier}...`);
6521
+ await (0, child_process_promise_1.exec)(`gcloud iam service-accounts create ${fullName} --display-name="${context.fullConfig.customerName}-${context.fullConfig.appName} ${context.env}:${context.name} | Catladder local access (read-only)" --project="${projectId}" --description="Read-only access for local use (agents, scripts)"`);
6522
+ }
6523
+ await syncBindings(io, projectId, fullIdentifier, roles);
6524
+ const secretName = (0, pipeline_1.getLocalAccessGcpSecretName)(context.env);
6525
+ const vaultManager = await io.getVaultManager();
6526
+ const rawName = (0, pipeline_1.getSecretVarName)("local", context.name, secretName);
6527
+ // straight from the vault, NOT through the local cache: a stale cache
6528
+ // (older than the first setup, or holding an older key) would make us
6529
+ // believe the key is gone, delete the valid one and lock out every
6530
+ // other machine.
6531
+ const stored = (await vaultManager.vault.readAllSecrets(io))[rawName];
6532
+ const validKeyIds = await (0, exports.listUserManagedKeyIds)(fullIdentifier);
6533
+ let storedKeyId;
6534
+ try {
6535
+ storedKeyId = JSON.parse(stored).private_key_id;
6536
+ }
6537
+ catch (_a) {
6538
+ // no (or unparsable) stored key
6539
+ }
6540
+ if (storedKeyId && validKeyIds.includes(storedKeyId)) {
6541
+ // keep the cache in line with the vault
6542
+ await vaultManager.cacheSecrets("local", context.name, {
6543
+ [secretName]: stored,
6544
+ });
6545
+ // at most one key per account: also drop leftovers of a failed run
6546
+ // or keys added by hand
6547
+ await deleteKeys(fullIdentifier, validKeyIds.filter((id) => id !== storedKeyId));
6548
+ return;
6549
+ }
6550
+ io.log("creating local access key...");
6551
+ // create first: a failed create must not leave the vault empty
6552
+ const { key, privateKeyId } = await createServiceAccountKey(fullIdentifier);
6553
+ // straight into the vault, NOT through writeSecretsAndMirror: this
6554
+ // key is for local use only and must never reach ci variables.
6555
+ // A fresh key replaces the old one, a backup would be dead.
6556
+ await vaultManager.writeSecrets("local", context.name, { [secretName]: key }, io, { backup: false });
6557
+ // only now drop the keys that are not the new one
6558
+ await deleteKeys(fullIdentifier, validKeyIds.filter((id) => id !== privateKeyId));
6559
+ };
6560
+ exports.syncLocalAccessServiceAccount = syncLocalAccessServiceAccount;
6561
+ const deleteKeys = async (fullIdentifier, keyIds) => {
6562
+ for (const keyId of keyIds) {
6563
+ await (0, child_process_promise_1.exec)(`gcloud iam service-accounts keys delete ${keyId} --quiet --iam-account=${fullIdentifier}`);
6564
+ }
6565
+ };
6566
+ const createServiceAccountKey = async (fullIdentifier) => {
6567
+ // a private temp file (0700 folder) instead of /dev/stdout | cat:
6568
+ // the pipe hid gcloud's exit code
6569
+ const folder = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), "catladder-local-access-"));
6570
+ try {
6571
+ const file = (0, path_1.join)(folder, "key.json");
6572
+ await (0, child_process_promise_1.exec)(`gcloud iam service-accounts keys create "${file}" --iam-account=${fullIdentifier}`);
6573
+ return (0, exports.parseServiceAccountKey)(await (0, promises_1.readFile)(file, "utf-8"));
6574
+ }
6575
+ finally {
6576
+ await (0, promises_1.rm)(folder, { recursive: true, force: true });
6577
+ }
6578
+ };
6579
+ //# sourceMappingURL=localAccess.js.map
6580
+
6581
+ /***/ }),
6582
+
6233
6583
  /***/ 74241:
6234
6584
  /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
6235
6585
 
@@ -6319,12 +6669,19 @@ const child_process_promise_1 = __nccwpck_require__(11542);
6319
6669
  const secrets_1 = __nccwpck_require__(85200);
6320
6670
  const serviceAccountNames_1 = __nccwpck_require__(67851);
6321
6671
  const accountExists = async (fullIdentifier) => {
6672
+ var _a, _b;
6322
6673
  try {
6323
6674
  await (0, child_process_promise_1.exec)(`gcloud iam service-accounts describe ${fullIdentifier}`);
6324
6675
  return true;
6325
6676
  }
6326
- catch (_a) {
6327
- return false;
6677
+ catch (e) {
6678
+ // only a missing account means "does not exist": an auth or
6679
+ // permission error must not look like one (setup would skip the
6680
+ // deletion, doctor would report it missing)
6681
+ if (`${(_a = e === null || e === void 0 ? void 0 : e.stderr) !== null && _a !== void 0 ? _a : ""}${(_b = e === null || e === void 0 ? void 0 : e.message) !== null && _b !== void 0 ? _b : ""}`.includes("NOT_FOUND")) {
6682
+ return false;
6683
+ }
6684
+ throw e;
6328
6685
  }
6329
6686
  };
6330
6687
  exports.accountExists = accountExists;
@@ -6622,6 +6979,145 @@ exports.startKubePortForward = startKubePortForward;
6622
6979
 
6623
6980
  /***/ }),
6624
6981
 
6982
+ /***/ 83228:
6983
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
6984
+
6985
+ "use strict";
6986
+
6987
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
6988
+ exports.getDeclaredLocalAccessCredentialsEnvVars = exports.getDeclaredLocalAccessEnvs = void 0;
6989
+ const pipeline_1 = __nccwpck_require__(53695);
6990
+ /**
6991
+ * the envs of a component declaring gcp local access. An invalid
6992
+ * declaration yields none instead of throwing: catenv and the vault
6993
+ * cache must keep working (`project doctor` and `project setup` report
6994
+ * the invalid declaration loudly).
6995
+ */
6996
+ const getDeclaredLocalAccessEnvs = (config, componentName) => {
6997
+ try {
6998
+ return (0, pipeline_1.getLocalAccessGcpEnvs)(config, componentName);
6999
+ }
7000
+ catch (_a) {
7001
+ return [];
7002
+ }
7003
+ };
7004
+ exports.getDeclaredLocalAccessEnvs = getDeclaredLocalAccessEnvs;
7005
+ /**
7006
+ * the declared `credentialsEnvVar` names of a component, mapped to their
7007
+ * env. Empty instead of throwing on an invalid declaration, see
7008
+ * {@link getDeclaredLocalAccessEnvs}.
7009
+ */
7010
+ const getDeclaredLocalAccessCredentialsEnvVars = (config, componentName) => {
7011
+ try {
7012
+ return (0, pipeline_1.getLocalAccessGcpCredentialsEnvVars)(config, componentName);
7013
+ }
7014
+ catch (_a) {
7015
+ return {};
7016
+ }
7017
+ };
7018
+ exports.getDeclaredLocalAccessCredentialsEnvVars = getDeclaredLocalAccessCredentialsEnvVars;
7019
+ //# sourceMappingURL=declared.js.map
7020
+
7021
+ /***/ }),
7022
+
7023
+ /***/ 70180:
7024
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
7025
+
7026
+ "use strict";
7027
+
7028
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
7029
+ exports.writeLocalAccessKeyFiles = exports.getLocalAccessKeyFileVars = exports.getLocalAccessKeyFileVarName = exports.getLocalAccessKeyFileName = void 0;
7030
+ const pipeline_1 = __nccwpck_require__(53695);
7031
+ const promises_1 = __nccwpck_require__(91943);
7032
+ const fs_1 = __nccwpck_require__(79896);
7033
+ const path_1 = __nccwpck_require__(16928);
7034
+ const writePrivateFile_1 = __nccwpck_require__(14550);
7035
+ const declared_1 = __nccwpck_require__(83228);
7036
+ const gitProjectInformation_1 = __nccwpck_require__(86884);
7037
+ const sanitize = (s) => s.replace(/[^A-Za-z0-9]+/g, "_");
7038
+ /** file name of a component/env key, relative to the local access folder */
7039
+ const getLocalAccessKeyFileName = (env, componentName) => `gcp-${sanitize(env)}-${sanitize(componentName)}.json`;
7040
+ exports.getLocalAccessKeyFileName = getLocalAccessKeyFileName;
7041
+ /**
7042
+ * env var exposing the key file path. Deliberately NOT
7043
+ * GOOGLE_APPLICATION_CREDENTIALS: the key must only be used where a
7044
+ * command opts in (isolated gcloud config, or a declared
7045
+ * `credentialsEnvVar`), never by ADC implicitly.
7046
+ */
7047
+ const getLocalAccessKeyFileVarName = (env) => `CATLADDER_LOCAL_ACCESS_GCP_${sanitize(env).toUpperCase()}_KEY_FILE`;
7048
+ exports.getLocalAccessKeyFileVarName = getLocalAccessKeyFileVarName;
7049
+ // inside .catladder/, which ignores itself (see vault/cache.ts)
7050
+ const getFolder = async () => (0, path_1.join)(await (0, gitProjectInformation_1.getProjectRootPath)(), ".catladder", "local-access");
7051
+ /**
7052
+ * env vars pointing at the key files that exist for the component
7053
+ * (only paths, no secrets): one per env, plus the declared
7054
+ * `credentialsEnvVar` names
7055
+ */
7056
+ const getLocalAccessKeyFileVars = async (config, componentName) => {
7057
+ const folder = await getFolder();
7058
+ const getPath = (env) => (0, path_1.join)(folder, (0, exports.getLocalAccessKeyFileName)(env, componentName));
7059
+ return Object.fromEntries([
7060
+ ...(0, declared_1.getDeclaredLocalAccessEnvs)(config, componentName).map((env) => [
7061
+ (0, exports.getLocalAccessKeyFileVarName)(env),
7062
+ getPath(env),
7063
+ ]),
7064
+ ...Object.entries((0, declared_1.getDeclaredLocalAccessCredentialsEnvVars)(config, componentName)).map(([name, env]) => [name, getPath(env)]),
7065
+ ].filter(([, path]) => (0, fs_1.existsSync)(path)));
7066
+ };
7067
+ exports.getLocalAccessKeyFileVars = getLocalAccessKeyFileVars;
7068
+ /**
7069
+ * writes the stored local access service account keys to gitignored
7070
+ * files (mode 0600) and removes files of declarations that are gone.
7071
+ * A no-op without any declaration (nothing is created). Never throws:
7072
+ * a vault that cannot be reached or an invalid declaration (reported by
7073
+ * `project doctor`) must not break catenv.
7074
+ *
7075
+ * The key is only read through the cache; if setup rotated it on another
7076
+ * machine, refresh with `catenv --vault-mode refresh`.
7077
+ */
7078
+ const writeLocalAccessKeyFiles = async (io, config) => {
7079
+ const wanted = Object.keys(config.components).flatMap((componentName) => (0, declared_1.getDeclaredLocalAccessEnvs)(config, componentName).map((env) => ({
7080
+ env,
7081
+ componentName,
7082
+ secretName: (0, pipeline_1.getSecretVarName)("local", componentName, (0, pipeline_1.getLocalAccessGcpSecretName)(env)),
7083
+ fileName: (0, exports.getLocalAccessKeyFileName)(env, componentName),
7084
+ })));
7085
+ const folder = await getFolder();
7086
+ if (wanted.length === 0 && !(0, fs_1.existsSync)(folder))
7087
+ return;
7088
+ try {
7089
+ const secrets = wanted.length > 0
7090
+ ? await (await io.getVaultManager()).readSecrets(wanted.map((w) => w.secretName), io)
7091
+ : {};
7092
+ await (0, promises_1.mkdir)(folder, { recursive: true, mode: 0o700 });
7093
+ // the parent folder ignores itself
7094
+ await (0, promises_1.writeFile)((0, path_1.join)(folder, "..", ".gitignore"), "*\n");
7095
+ const written = new Set();
7096
+ for (const { secretName, fileName, env, componentName } of wanted) {
7097
+ const key = secrets[secretName];
7098
+ if (!key) {
7099
+ io.log(`⚠️ no local access key for ${env}:${componentName} in the secret store — run \`catladder project setup\` (or \`catenv --vault-mode refresh\` if it was set up on another machine)`);
7100
+ continue;
7101
+ }
7102
+ const path = (0, path_1.join)(folder, fileName);
7103
+ await (0, writePrivateFile_1.writePrivateFile)(path, key);
7104
+ written.add(fileName);
7105
+ }
7106
+ for (const file of await (0, promises_1.readdir)(folder)) {
7107
+ if (!written.has(file)) {
7108
+ await (0, promises_1.rm)((0, path_1.join)(folder, file), { force: true });
7109
+ }
7110
+ }
7111
+ }
7112
+ catch (e) {
7113
+ io.log(`⚠️ could not write local access keys: ${e.message}`);
7114
+ }
7115
+ };
7116
+ exports.writeLocalAccessKeyFiles = writeLocalAccessKeyFiles;
7117
+ //# sourceMappingURL=keyFiles.js.map
7118
+
7119
+ /***/ }),
7120
+
6625
7121
  /***/ 55689:
6626
7122
  /***/ ((__unused_webpack_module, exports) => {
6627
7123
 
@@ -8034,6 +8530,38 @@ exports.getShell = getShell;
8034
8530
 
8035
8531
  /***/ }),
8036
8532
 
8533
+ /***/ 14550:
8534
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
8535
+
8536
+ "use strict";
8537
+
8538
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
8539
+ exports.writePrivateFile = void 0;
8540
+ const promises_1 = __nccwpck_require__(91943);
8541
+ /**
8542
+ * writes a file readable by the owner only. Goes through a 0600 temp
8543
+ * file and a rename, so the content is never visible with looser
8544
+ * permissions (writing first and chmod-ing afterwards would leave an
8545
+ * existing 0644 file readable until the chmod).
8546
+ */
8547
+ const writePrivateFile = async (path, content) => {
8548
+ const tmp = `${path}.${process.pid}.tmp`;
8549
+ try {
8550
+ await (0, promises_1.writeFile)(tmp, content, { mode: 0o600 });
8551
+ // the mode is only applied when the file is created
8552
+ await (0, promises_1.chmod)(tmp, 0o600);
8553
+ await (0, promises_1.rename)(tmp, path);
8554
+ }
8555
+ catch (e) {
8556
+ await (0, promises_1.rm)(tmp, { force: true });
8557
+ throw e;
8558
+ }
8559
+ };
8560
+ exports.writePrivateFile = writePrivateFile;
8561
+ //# sourceMappingURL=writePrivateFile.js.map
8562
+
8563
+ /***/ }),
8564
+
8037
8565
  /***/ 42827:
8038
8566
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
8039
8567
 
@@ -8140,30 +8668,21 @@ class BitwardenVault {
8140
8668
  : {};
8141
8669
  const notes = (0, yaml_1.stringify)({ ...previous, ...secrets });
8142
8670
  if (existing) {
8143
- await (0, bw_1.runBw)(bwSession, [
8144
- "edit",
8145
- "item",
8146
- existing.id,
8147
- encodePayload({ ...existing, notes }),
8148
- ]);
8671
+ await (0, bw_1.runBw)(bwSession, ["edit", "item", existing.id], encodePayload({ ...existing, notes }));
8149
8672
  }
8150
8673
  else {
8151
- await (0, bw_1.runBw)(bwSession, [
8152
- "create",
8153
- "item",
8154
- encodePayload({
8155
- type: SECURE_NOTE_TYPE,
8156
- secureNote: { type: 0 },
8157
- name,
8158
- notes,
8159
- ...(collection.organizationId
8160
- ? {
8161
- organizationId: collection.organizationId,
8162
- collectionIds: [collection.id],
8163
- }
8164
- : {}),
8165
- }),
8166
- ]);
8674
+ await (0, bw_1.runBw)(bwSession, ["create", "item"], encodePayload({
8675
+ type: SECURE_NOTE_TYPE,
8676
+ secureNote: { type: 0 },
8677
+ name,
8678
+ notes,
8679
+ ...(collection.organizationId
8680
+ ? {
8681
+ organizationId: collection.organizationId,
8682
+ collectionIds: [collection.id],
8683
+ }
8684
+ : {}),
8685
+ }));
8167
8686
  }
8168
8687
  }
8169
8688
  }
@@ -8210,6 +8729,7 @@ exports.GitlabVault = GitlabVault;
8210
8729
  Object.defineProperty(exports, "__esModule", ({ value: true }));
8211
8730
  exports.VaultManager = void 0;
8212
8731
  const pipeline_1 = __nccwpck_require__(53695);
8732
+ const declared_1 = __nccwpck_require__(83228);
8213
8733
  const BitwardenVault_1 = __nccwpck_require__(42827);
8214
8734
  const cache_1 = __nccwpck_require__(57011);
8215
8735
  const GitlabVault_1 = __nccwpck_require__(6020);
@@ -8292,6 +8812,15 @@ class VaultManager {
8292
8812
  ])));
8293
8813
  }
8294
8814
  }
8815
+ /**
8816
+ * updates the local cache only (the vault already holds the values),
8817
+ * "local" env only
8818
+ */
8819
+ async cacheSecrets(env, componentName, secrets) {
8820
+ if (env !== CACHED_ENV)
8821
+ return;
8822
+ await (0, cache_1.updateSecretsCache)(this.vault.id, env, componentName, secrets);
8823
+ }
8295
8824
  /**
8296
8825
  * the subset of the vault secrets that may be cached: the declared
8297
8826
  * secrets of the "local" env, per component, as a hierarchy
@@ -8315,6 +8844,14 @@ class VaultManager {
8315
8844
  return Object.keys(local).length > 0 ? { [CACHED_ENV]: local } : {};
8316
8845
  }
8317
8846
  async declaredSecretKeys(componentName) {
8847
+ return [
8848
+ ...(await this.declaredEnvSecretKeys(componentName)),
8849
+ // the local access keys `project setup` provisions are not part
8850
+ // of any env's vars, but belong to the cached local secrets
8851
+ ...(0, declared_1.getDeclaredLocalAccessEnvs)(this.config, componentName).map(pipeline_1.getLocalAccessGcpSecretName),
8852
+ ];
8853
+ }
8854
+ async declaredEnvSecretKeys(componentName) {
8318
8855
  try {
8319
8856
  const { secretEnvVarKeys, jobOnlyVars } = await (0, pipeline_1.getEnvironment)({
8320
8857
  config: this.config,
@@ -8424,14 +8961,40 @@ const getBwSession = async (io, { allowPrompt }) => {
8424
8961
  };
8425
8962
  exports.getBwSession = getBwSession;
8426
8963
  /**
8427
- * runs a bw command with the given session and returns stdout
8964
+ * runs a bw command with the given session and returns stdout.
8965
+ *
8966
+ * `input` is written to stdin: use it for payloads (e.g. the secrets of
8967
+ * `bw create item`), which would otherwise be visible in `ps` and be
8968
+ * echoed by node's "Command failed" error.
8428
8969
  */
8429
- const runBw = async (session, args) => {
8430
- const { stdout } = await execFile("bw", [...args, "--raw", "--nointeraction"], {
8431
- env: { ...process.env, BW_SESSION: session },
8432
- maxBuffer: 50 * 1024 * 1024,
8970
+ const runBw = async (session, args, input) => {
8971
+ const fullArgs = [...args, "--raw", "--nointeraction"];
8972
+ const env = { ...process.env, BW_SESSION: session };
8973
+ if (input === undefined) {
8974
+ const { stdout } = await execFile("bw", fullArgs, {
8975
+ env,
8976
+ maxBuffer: 50 * 1024 * 1024,
8977
+ });
8978
+ return stdout;
8979
+ }
8980
+ return new Promise((resolve, reject) => {
8981
+ const child = (0, child_process_1.spawn)("bw", fullArgs, {
8982
+ env,
8983
+ stdio: ["pipe", "pipe", "pipe"],
8984
+ });
8985
+ let stdout = "";
8986
+ let stderr = "";
8987
+ child.stdout.on("data", (data) => (stdout += data.toString("utf-8")));
8988
+ child.stderr.on("data", (data) => (stderr += data.toString("utf-8")));
8989
+ child.on("error", reject);
8990
+ child.on("close", (code) => code === 0
8991
+ ? resolve(stdout)
8992
+ : reject(new Error(`bw ${args[0]} failed: ${stderr.trim()}`)));
8993
+ child.stdin.on("error", () => {
8994
+ // bw exiting early closes the pipe, the close handler reports why
8995
+ });
8996
+ child.stdin.end(input);
8433
8997
  });
8434
- return stdout;
8435
8998
  };
8436
8999
  exports.runBw = runBw;
8437
9000
  const runBwJson = async (session, args) => JSON.parse(await (0, exports.runBw)(session, args));
@@ -8449,6 +9012,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
8449
9012
  exports.updateSecretsCache = exports.writeSecretsCache = exports.readSecretsCache = void 0;
8450
9013
  const promises_1 = __nccwpck_require__(91943);
8451
9014
  const path_1 = __nccwpck_require__(16928);
9015
+ const writePrivateFile_1 = __nccwpck_require__(14550);
8452
9016
  const gitProjectInformation_1 = __nccwpck_require__(86884);
8453
9017
  const getCacheFolder = async () => (0, path_1.join)(await (0, gitProjectInformation_1.getProjectRootPath)(), ".catladder");
8454
9018
  const getCachePath = async () => (0, path_1.join)(await getCacheFolder(), "secrets.json");
@@ -8463,7 +9027,7 @@ const readSecretsCache = async () => {
8463
9027
  exports.readSecretsCache = readSecretsCache;
8464
9028
  const writeSecretsCache = async (vaultId, secrets) => {
8465
9029
  const folder = await getCacheFolder();
8466
- await (0, promises_1.mkdir)(folder, { recursive: true });
9030
+ await (0, promises_1.mkdir)(folder, { recursive: true, mode: 0o700 });
8467
9031
  // the folder ignores itself, so the cache can never be committed
8468
9032
  await (0, promises_1.writeFile)((0, path_1.join)(folder, ".gitignore"), "*\n");
8469
9033
  const cache = {
@@ -8471,7 +9035,9 @@ const writeSecretsCache = async (vaultId, secrets) => {
8471
9035
  updatedAt: new Date().toISOString(),
8472
9036
  secrets,
8473
9037
  };
8474
- await (0, promises_1.writeFile)(await getCachePath(), JSON.stringify(cache, null, 2));
9038
+ const path = await getCachePath();
9039
+ // may hold key material (local access keys): owner only
9040
+ await (0, writePrivateFile_1.writePrivateFile)(path, JSON.stringify(cache, null, 2));
8475
9041
  };
8476
9042
  exports.writeSecretsCache = writeSecretsCache;
8477
9043
  /**
@@ -19364,6 +19930,191 @@ __exportStar(__nccwpck_require__(91804), exports);
19364
19930
  __exportStar(__nccwpck_require__(94143), exports);
19365
19931
  __exportStar(__nccwpck_require__(98011), exports);
19366
19932
  __exportStar(__nccwpck_require__(61459), exports);
19933
+ __exportStar(__nccwpck_require__(54165), exports);
19934
+ //# sourceMappingURL=index.js.map
19935
+
19936
+ /***/ }),
19937
+
19938
+ /***/ 54165:
19939
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
19940
+
19941
+ "use strict";
19942
+
19943
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
19944
+ exports.getLocalAccessGcpCredentialsEnvVars = exports.diffRoleBindings = exports.getLocalAccessGcpSecretName = exports.getLocalAccessGcpEnvs = exports.getLocalAccessGcpRoles = exports.getLocalAccessConfig = exports.getDeniedLocalAccessRoleReason = exports.LOCAL_ACCESS_GCP_SA_NAME = exports.LOCAL_ACCESS_GCP_PRESETS = void 0;
19945
+ const configruedEnvs_1 = __nccwpck_require__(38866);
19946
+ const getEnvConfig_1 = __nccwpck_require__(69578);
19947
+ const types_1 = __nccwpck_require__(79689);
19948
+ exports.LOCAL_ACCESS_GCP_PRESETS = {
19949
+ // no run.viewer on purpose, see LocalAccessGcpPreset
19950
+ logs: ["roles/logging.viewer"],
19951
+ // not read-only, see LocalAccessGcpPreset; only reachable as a
19952
+ // preset, getDeniedLocalAccessRoleReason rejects it in `roles`
19953
+ vertex: ["roles/aiplatform.user"],
19954
+ };
19955
+ /** name prefix of the service account `project setup` creates (see getGcloudServiceAccountNames) */
19956
+ exports.LOCAL_ACCESS_GCP_SA_NAME = "local-diag";
19957
+ const ROLE_FORMAT = /^(roles\/[A-Za-z0-9_.]+|projects\/[a-z0-9-]+\/roles\/[A-Za-z0-9_.]+|organizations\/\d+\/roles\/[A-Za-z0-9_.]+)$/;
19958
+ /**
19959
+ * roles that are read-only on paper but expose secrets: cloud run
19960
+ * services/jobs carry their secrets as plain env vars, so anything that
19961
+ * may read them reads every deployed secret. Denied even with
19962
+ * `allowAnyRole`.
19963
+ */
19964
+ const SECRET_EXPOSING_ROLES = {
19965
+ "roles/viewer": "roles/viewer can read cloud run env vars (secrets)",
19966
+ "roles/run.viewer": "roles/run.viewer can read cloud run env vars (secrets)",
19967
+ "roles/secretmanager.secretAccessor": "this role reads secret values",
19968
+ "roles/secretmanager.admin": "this role reads secret values",
19969
+ };
19970
+ /**
19971
+ * roles granted without `allowAnyRole`. An explicit list instead of a
19972
+ * name pattern: a role called `*.viewer` / `*.reader` says nothing about
19973
+ * its permissions (custom roles can hold anything, and predefined ones
19974
+ * like storage.objectViewer, cloudfunctions.viewer, compute.viewer or
19975
+ * logging.privateLogViewer expose data or secrets).
19976
+ */
19977
+ const SAFE_ROLES = [
19978
+ "roles/logging.viewer",
19979
+ "roles/monitoring.viewer",
19980
+ "roles/errorreporting.viewer",
19981
+ ];
19982
+ /** `roles/foo.bar.v1` -> `foo.bar` (version suffixes must not defeat the checks) */
19983
+ const getRoleName = (role) => { var _a; return ((_a = role.split("/").pop()) !== null && _a !== void 0 ? _a : role).replace(/(\.v\d+)+$/i, ""); };
19984
+ /**
19985
+ * why a role may not be granted for local access, or undefined if it
19986
+ * is acceptable.
19987
+ *
19988
+ * By default only the roles in {@link SAFE_ROLES} pass; custom roles
19989
+ * and everything else need `allowAnyRole`. `allowAnyRole` lifts the
19990
+ * allow-list, but the obvious write/admin/impersonation roles and the
19991
+ * secret exposing roles stay denied.
19992
+ */
19993
+ const getDeniedLocalAccessRoleReason = (role, options = {}) => {
19994
+ var _a;
19995
+ const secretReason = SECRET_EXPOSING_ROLES[role];
19996
+ if (secretReason)
19997
+ return secretReason;
19998
+ const name = getRoleName(role);
19999
+ if (name === "owner" || name === "editor") {
20000
+ return "owner/editor are full write roles";
20001
+ }
20002
+ const last = (_a = name.split(".").pop()) !== null && _a !== void 0 ? _a : name;
20003
+ if (/admin$/i.test(last)) {
20004
+ return "admin roles can change resources";
20005
+ }
20006
+ if (/(editor|developer|writer|creator|deployer|invoker|builder|publisher|client|user|objectUser)$/i.test(last) &&
20007
+ !/^iam\./.test(name)) {
20008
+ return "this role can change resources";
20009
+ }
20010
+ if (/^iam\./.test(name) &&
20011
+ /(serviceAccountUser|serviceAccountTokenCreator|serviceAccountKeyAdmin|workloadIdentityUser)$/.test(name)) {
20012
+ return "this role allows impersonating service accounts";
20013
+ }
20014
+ if (!options.allowAnyRole && !SAFE_ROLES.includes(role)) {
20015
+ return `only these roles are allowed: ${SAFE_ROLES.join(", ")} (set allowAnyRole: true to grant others at your own risk)`;
20016
+ }
20017
+ return undefined;
20018
+ };
20019
+ exports.getDeniedLocalAccessRoleReason = getDeniedLocalAccessRoleReason;
20020
+ /**
20021
+ * the effective local access declaration of an env: the env's own
20022
+ * declaration replaces the project default entirely
20023
+ */
20024
+ const getLocalAccessConfig = (config, env) => {
20025
+ var _a;
20026
+ if (env === "local")
20027
+ return undefined;
20028
+ const envEntry = (_a = config.environments) === null || _a === void 0 ? void 0 : _a[env];
20029
+ const declared = envEntry && envEntry.localAccess !== undefined
20030
+ ? envEntry.localAccess
20031
+ : config.localAccess;
20032
+ return declared || undefined;
20033
+ };
20034
+ exports.getLocalAccessConfig = getLocalAccessConfig;
20035
+ /**
20036
+ * the gcp roles local access grants in `env`: preset roles plus the
20037
+ * explicit ones, deduplicated and sorted. Throws on an unknown preset,
20038
+ * a malformed role or a denied (write/admin) role.
20039
+ */
20040
+ const getLocalAccessGcpRoles = (config, env) => {
20041
+ var _a, _b, _c;
20042
+ const gcp = (_a = (0, exports.getLocalAccessConfig)(config, env)) === null || _a === void 0 ? void 0 : _a.gcp;
20043
+ if (!gcp)
20044
+ return [];
20045
+ const roles = new Set();
20046
+ for (const preset of (_b = gcp.presets) !== null && _b !== void 0 ? _b : []) {
20047
+ const presetRoles = exports.LOCAL_ACCESS_GCP_PRESETS[preset];
20048
+ if (!presetRoles) {
20049
+ throw new Error(`localAccess (${env}): unknown gcp preset '${preset}', known: ${Object.keys(exports.LOCAL_ACCESS_GCP_PRESETS).join(", ")}`);
20050
+ }
20051
+ presetRoles.forEach((role) => roles.add(role));
20052
+ }
20053
+ for (const role of (_c = gcp.roles) !== null && _c !== void 0 ? _c : []) {
20054
+ if (!ROLE_FORMAT.test(role)) {
20055
+ throw new Error(`localAccess (${env}): '${role}' is not a valid role (expected e.g. roles/logging.viewer)`);
20056
+ }
20057
+ const reason = (0, exports.getDeniedLocalAccessRoleReason)(role, {
20058
+ allowAnyRole: gcp.allowAnyRole,
20059
+ });
20060
+ if (reason) {
20061
+ throw new Error(`localAccess (${env}): role '${role}' is not allowed (${reason})`);
20062
+ }
20063
+ roles.add(role);
20064
+ }
20065
+ return [...roles].sort();
20066
+ };
20067
+ exports.getLocalAccessGcpRoles = getLocalAccessGcpRoles;
20068
+ /**
20069
+ * the envs of a component that declare gcp local access. Empty for
20070
+ * components that do not deploy to google cloud run.
20071
+ */
20072
+ const getLocalAccessGcpEnvs = (config, componentName) => (0, configruedEnvs_1.getAllEnvs)(config, componentName).filter((env) =>
20073
+ // only google cloud run envs have a project to grant roles in; the
20074
+ // env's own `deploy` overrides the component's
20075
+ (0, types_1.isOfDeployType)((0, getEnvConfig_1.getEnvConfig)(config, componentName, env).deploy, "google-cloudrun") && (0, exports.getLocalAccessGcpRoles)(config, env).length > 0);
20076
+ exports.getLocalAccessGcpEnvs = getLocalAccessGcpEnvs;
20077
+ /**
20078
+ * name of the secret (stored under the `local` env of the component)
20079
+ * holding the service account key of `env`'s local access
20080
+ */
20081
+ const getLocalAccessGcpSecretName = (env) => `LOCAL_ACCESS_GCP_${env.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CREDENTIALS`;
20082
+ exports.getLocalAccessGcpSecretName = getLocalAccessGcpSecretName;
20083
+ /**
20084
+ * the bindings to add and remove so that `current` matches `desired`.
20085
+ * The local access account is dedicated, so every binding it carries
20086
+ * beyond the declaration is extra.
20087
+ */
20088
+ const diffRoleBindings = (desired, current) => ({
20089
+ missing: desired.filter((role) => !current.includes(role)),
20090
+ extra: current.filter((role) => !desired.includes(role)),
20091
+ });
20092
+ exports.diffRoleBindings = diffRoleBindings;
20093
+ const ENV_VAR_NAME_FORMAT = /^[A-Za-z_][A-Za-z0-9_]*$/;
20094
+ /**
20095
+ * the extra env var names (`credentialsEnvVar`) under which the local
20096
+ * env of a component exports the key file path, mapped to the env whose
20097
+ * key they point at. Throws on a malformed name or on two envs
20098
+ * exporting the same name (e.g. a project default declaring it).
20099
+ */
20100
+ const getLocalAccessGcpCredentialsEnvVars = (config, componentName) => {
20101
+ var _a, _b;
20102
+ const vars = {};
20103
+ for (const env of (0, exports.getLocalAccessGcpEnvs)(config, componentName)) {
20104
+ const name = (_b = (_a = (0, exports.getLocalAccessConfig)(config, env)) === null || _a === void 0 ? void 0 : _a.gcp) === null || _b === void 0 ? void 0 : _b.credentialsEnvVar;
20105
+ if (!name)
20106
+ continue;
20107
+ if (!ENV_VAR_NAME_FORMAT.test(name)) {
20108
+ throw new Error(`localAccess (${env}): credentialsEnvVar '${name}' is not a valid env var name`);
20109
+ }
20110
+ if (vars[name]) {
20111
+ throw new Error(`localAccess (${componentName}): credentialsEnvVar '${name}' is declared by both ${vars[name]} and ${env}; declare it in one env only (environments.<env>.localAccess)`);
20112
+ }
20113
+ vars[name] = env;
20114
+ }
20115
+ return vars;
20116
+ };
20117
+ exports.getLocalAccessGcpCredentialsEnvVars = getLocalAccessGcpCredentialsEnvVars;
19367
20118
  //# sourceMappingURL=index.js.map
19368
20119
 
19369
20120
  /***/ }),