@catladder/cli 5.8.1 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +97 -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 +29 -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 +8 -0
  27. package/dist/apps/cli/src/localAccess/declared.js +20 -0
  28. package/dist/apps/cli/src/localAccess/declared.js.map +1 -0
  29. package/dist/apps/cli/src/localAccess/keyFiles.d.ts +26 -0
  30. package/dist/apps/cli/src/localAccess/keyFiles.js +87 -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 +390 -31
  46. package/dist/bundles/cli/index.js +737 -35
  47. package/dist/bundles/skills/catladder-secrets/SKILL.md +24 -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 +46 -0
  52. package/dist/packages/pipeline/src/localAccess/index.js +149 -0
  53. package/dist/packages/pipeline/src/localAccess/index.js.map +1 -0
  54. package/dist/packages/pipeline/src/types/config.d.ts +57 -0
  55. package/dist/packages/pipeline/src/types/config.js.map +1 -1
  56. package/dist/skills/catladder-secrets/SKILL.md +24 -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/checkCloudRun.ts +5 -1
  62. package/src/apps/cli/commands/project/doctor/checkLocalAccess.test.ts +112 -0
  63. package/src/apps/cli/commands/project/doctor/checkLocalAccess.ts +144 -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 +41 -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 +107 -0
  74. package/src/localAccess/declared.ts +19 -0
  75. package/src/localAccess/keyFiles.ts +114 -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,110 @@ 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
+ }
1097
+ catch (e) {
1098
+ return { label, invalid: e.message };
1099
+ }
1100
+ const { fullIdentifier } = (0, localAccess_1.getLocalAccessServiceAccountNames)(context, deployConfig.projectId);
1101
+ const exists = await (0, serviceAccounts_1.accountExists)(fullIdentifier);
1102
+ const member = `serviceAccount:${fullIdentifier}`;
1103
+ const bound = exists
1104
+ ? await inspector.getUnconditionalRolesOfMember(deployConfig.projectId, member)
1105
+ : [];
1106
+ const conditional = exists
1107
+ ? await inspector.getConditionalRolesOfMember(deployConfig.projectId, member)
1108
+ : [];
1109
+ const keyCount = exists
1110
+ ? (await (0, localAccess_1.listUserManagedKeyIds)(fullIdentifier)).length
1111
+ : 0;
1112
+ return {
1113
+ label,
1114
+ declared,
1115
+ fullIdentifier,
1116
+ exists,
1117
+ bound,
1118
+ conditional,
1119
+ keyCount,
1120
+ };
1121
+ });
1122
+ const relevant = results.filter((r) => r && ("invalid" in r || r.declared.length > 0 || r.exists));
1123
+ if (relevant.length === 0)
1124
+ return;
1125
+ report.section("local access (read-only service accounts)");
1126
+ for (const result of relevant) {
1127
+ if (!result)
1128
+ continue;
1129
+ if ("invalid" in result) {
1130
+ report.fail(`${result.label}: ${result.invalid}`);
1131
+ continue;
1132
+ }
1133
+ const { label, declared, fullIdentifier, exists, bound, conditional, keyCount, } = result;
1134
+ if (declared.length > 0 && (0, pipeline_1.getVaultConfig)(config).type === "gitlab") {
1135
+ 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)`);
1136
+ continue;
1137
+ }
1138
+ const heal = `run: catladder project setup`;
1139
+ if (declared.length === 0) {
1140
+ report.warn(`${label}: ${fullIdentifier} exists although no local access is declared`, heal);
1141
+ }
1142
+ else if (!exists) {
1143
+ report.fail(`${label}: ${fullIdentifier} does not exist`, heal);
1144
+ }
1145
+ else {
1146
+ const { missing, extra } = (0, pipeline_1.diffRoleBindings)(declared, bound);
1147
+ if (missing.length > 0) {
1148
+ report.fail(`${label}: local access account is missing roles: ${missing.join(", ")}`, heal);
1149
+ }
1150
+ if (extra.length > 0) {
1151
+ report.fail(`${label}: local access account has undeclared roles: ${extra.join(", ")}`, heal);
1152
+ }
1153
+ if (conditional.length > 0) {
1154
+ report.fail(`${label}: local access account has conditional roles (${conditional.join(", ")}) that setup does not remove`, "remove them by hand");
1155
+ }
1156
+ if (keyCount !== 1) {
1157
+ report.fail(`${label}: local access account has ${keyCount} user-managed keys, expected exactly 1`, heal);
1158
+ }
1159
+ if (missing.length === 0 &&
1160
+ extra.length === 0 &&
1161
+ conditional.length === 0 &&
1162
+ keyCount === 1) {
1163
+ report.ok(`${label}: local access account has exactly the declared roles`);
1164
+ }
1165
+ }
1166
+ }
1167
+ };
1168
+ exports.checkLocalAccess = checkLocalAccess;
1169
+ //# sourceMappingURL=checkLocalAccess.js.map
1170
+
1171
+ /***/ }),
1172
+
1055
1173
  /***/ 62878:
1056
1174
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
1057
1175
 
@@ -1145,7 +1263,7 @@ const doctorProject = async (instance, onlyComponents) => {
1145
1263
  if ((0, pipeline_1.getEnabledPipelineTypes)(config).includes("gitlab")) {
1146
1264
  await (0, checkGitlab_1.checkGitlab)(instance, report, contexts);
1147
1265
  }
1148
- await (0, checkCloudRun_1.checkCloudRun)(report, contexts);
1266
+ await (0, checkCloudRun_1.checkCloudRun)(report, contexts, config);
1149
1267
  await (0, checkGithubImageCasing_1.checkGithubImageCasing)(report, config);
1150
1268
  await (0, checkGithubReleaseWorkflow_1.checkGithubReleaseWorkflow)(report, config);
1151
1269
  await (0, checkGithub_1.checkGithub)(report, config);
@@ -1704,6 +1822,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
1704
1822
  exports.setupContext = void 0;
1705
1823
  const pipeline_1 = __nccwpck_require__(53695);
1706
1824
  const setupCloudRun_1 = __nccwpck_require__(92526);
1825
+ const setupLocalAccess_1 = __nccwpck_require__(3299);
1707
1826
  const setupKubernetes_1 = __nccwpck_require__(51796);
1708
1827
  const logSection_1 = __nccwpck_require__(74120);
1709
1828
  const setupContext = async (instance, context) => {
@@ -1711,6 +1830,7 @@ const setupContext = async (instance, context) => {
1711
1830
  var _a, _b;
1712
1831
  if ((0, pipeline_1.isOfDeployType)((_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config, "google-cloudrun")) {
1713
1832
  await (0, setupCloudRun_1.setupCloudRun)(instance, context);
1833
+ await (0, setupLocalAccess_1.setupLocalAccess)(instance, context);
1714
1834
  }
1715
1835
  const deployConfig = (_b = context.deploy) === null || _b === void 0 ? void 0 : _b.config;
1716
1836
  if ((0, pipeline_1.isOfDeployType)(deployConfig, "kubernetes")) {
@@ -2067,6 +2187,42 @@ exports.setupKubernetes = setupKubernetes;
2067
2187
 
2068
2188
  /***/ }),
2069
2189
 
2190
+ /***/ 3299:
2191
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
2192
+
2193
+ "use strict";
2194
+
2195
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2196
+ exports.setupLocalAccess = void 0;
2197
+ const pipeline_1 = __nccwpck_require__(53695);
2198
+ const getProjectConfig_1 = __nccwpck_require__(70478);
2199
+ const localAccess_1 = __nccwpck_require__(94216);
2200
+ /**
2201
+ * provisions the read-only local access (`localAccess` in catladder.ts)
2202
+ * of a google cloud run component/env: a dedicated service account with
2203
+ * exactly the declared bindings and its key in the secret store. Also
2204
+ * removes what is no longer declared.
2205
+ */
2206
+ const setupLocalAccess = async (instance, context) => {
2207
+ var _a;
2208
+ const config = (_a = context.deploy) === null || _a === void 0 ? void 0 : _a.config;
2209
+ if (!(0, pipeline_1.isOfDeployType)(config, "google-cloudrun"))
2210
+ return;
2211
+ const projectConfig = await (0, getProjectConfig_1.getProjectConfig)();
2212
+ const roles = (0, pipeline_1.getLocalAccessGcpRoles)(projectConfig, context.env);
2213
+ if (roles.length > 0 && (0, pipeline_1.getVaultConfig)(projectConfig).type === "gitlab") {
2214
+ // the gitlab vault stores secrets as CI variables available to every
2215
+ // job on every branch: the key would leak to anyone who can push
2216
+ 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.`);
2217
+ }
2218
+ instance.log("sync local access service account...");
2219
+ await (0, localAccess_1.syncLocalAccessServiceAccount)(instance, context, config.projectId, roles);
2220
+ };
2221
+ exports.setupLocalAccess = setupLocalAccess;
2222
+ //# sourceMappingURL=setupLocalAccess.js.map
2223
+
2224
+ /***/ }),
2225
+
2070
2226
  /***/ 63361:
2071
2227
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
2072
2228
 
@@ -5790,6 +5946,7 @@ exports.uninstallCompletions = uninstallCompletions;
5790
5946
  Object.defineProperty(exports, "__esModule", ({ value: true }));
5791
5947
  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
5948
  const pipeline_1 = __nccwpck_require__(53695);
5949
+ const keyFiles_1 = __nccwpck_require__(70180);
5793
5950
  const gitlab_1 = __nccwpck_require__(17697);
5794
5951
  const projects_1 = __nccwpck_require__(20575);
5795
5952
  var parseChoice_1 = __nccwpck_require__(70769);
@@ -5910,12 +6067,19 @@ const getEnvVarsResolved = async (io, env, componentName) => {
5910
6067
  const envionment = await (0, exports.getEnvironment)(env, componentName);
5911
6068
  // in the pipeline the secrets alreadyy exists and bash will expand them
5912
6069
  // but here we need to manually load them
5913
- return resolveSecrets(io, [
6070
+ const resolved = await resolveSecrets(io, [
5914
6071
  {
5915
6072
  envVars: envionment.envVars,
5916
6073
  secretEnvVarKeys: envionment.secretEnvVarKeys,
5917
6074
  },
5918
6075
  ]);
6076
+ if (env !== "local")
6077
+ return resolved;
6078
+ // paths of the local access key files (written by catenv)
6079
+ return {
6080
+ ...resolved,
6081
+ ...(await (0, keyFiles_1.getLocalAccessKeyFileVars)(await (0, exports.getProjectConfig)(), componentName)),
6082
+ };
5919
6083
  };
5920
6084
  exports.getEnvVarsResolved = getEnvVarsResolved;
5921
6085
  /**
@@ -6230,6 +6394,189 @@ exports.fetchGcloudProjectNumber = fetchGcloudProjectNumber;
6230
6394
 
6231
6395
  /***/ }),
6232
6396
 
6397
+ /***/ 94216:
6398
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
6399
+
6400
+ "use strict";
6401
+
6402
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
6403
+ exports.syncLocalAccessServiceAccount = exports.listUserManagedKeyIds = exports.parseServiceAccountKey = exports.getConditionalRolesOfMemberInBindings = exports.getRolesOfMemberInBindings = exports.getLocalAccessServiceAccountNames = void 0;
6404
+ const pipeline_1 = __nccwpck_require__(53695);
6405
+ const child_process_promise_1 = __nccwpck_require__(11542);
6406
+ const promises_1 = __nccwpck_require__(91943);
6407
+ const os_1 = __nccwpck_require__(70857);
6408
+ const path_1 = __nccwpck_require__(16928);
6409
+ const serviceAccountNames_1 = __nccwpck_require__(67851);
6410
+ const serviceAccounts_1 = __nccwpck_require__(18190);
6411
+ /**
6412
+ * the dedicated read-only service account of a component/env that
6413
+ * `project setup` provisions from the `localAccess` declaration.
6414
+ * Shared with `project doctor` and catenv.
6415
+ */
6416
+ const getLocalAccessServiceAccountNames = (context, projectId) => (0, serviceAccountNames_1.getGcloudServiceAccountNames)(context, {
6417
+ name: pipeline_1.LOCAL_ACCESS_GCP_SA_NAME,
6418
+ projectId,
6419
+ });
6420
+ exports.getLocalAccessServiceAccountNames = getLocalAccessServiceAccountNames;
6421
+ /**
6422
+ * all unconditional roles bound to `member` in the given iam policy
6423
+ * bindings. Conditional bindings are reported separately, see
6424
+ * {@link getConditionalRolesOfMemberInBindings}.
6425
+ */
6426
+ const getRolesOfMemberInBindings = (bindings, member) => bindings
6427
+ .filter((binding) => !binding.condition)
6428
+ .filter((binding) => { var _a; return (_a = binding.members) === null || _a === void 0 ? void 0 : _a.includes(member); })
6429
+ .map((binding) => binding.role)
6430
+ .sort();
6431
+ exports.getRolesOfMemberInBindings = getRolesOfMemberInBindings;
6432
+ /**
6433
+ * roles bound to `member` under an iam condition. These cannot be
6434
+ * removed by role name alone, so they are never touched by `project
6435
+ * setup` — only reported.
6436
+ */
6437
+ const getConditionalRolesOfMemberInBindings = (bindings, member) => bindings
6438
+ .filter((binding) => binding.condition)
6439
+ .filter((binding) => { var _a; return (_a = binding.members) === null || _a === void 0 ? void 0 : _a.includes(member); })
6440
+ .map((binding) => binding.role)
6441
+ .sort();
6442
+ exports.getConditionalRolesOfMemberInBindings = getConditionalRolesOfMemberInBindings;
6443
+ /**
6444
+ * parses the output of `gcloud iam service-accounts keys create` and
6445
+ * makes sure it is a usable service account key
6446
+ */
6447
+ const parseServiceAccountKey = (raw) => {
6448
+ let parsed;
6449
+ try {
6450
+ parsed = JSON.parse(raw);
6451
+ }
6452
+ catch (_a) {
6453
+ throw new Error("gcloud did not return a valid service account key");
6454
+ }
6455
+ if (typeof parsed.private_key_id !== "string" ||
6456
+ typeof parsed.private_key !== "string") {
6457
+ throw new Error("gcloud did not return a valid service account key");
6458
+ }
6459
+ return { key: raw, privateKeyId: parsed.private_key_id };
6460
+ };
6461
+ exports.parseServiceAccountKey = parseServiceAccountKey;
6462
+ const getProjectBindings = async (projectId) => {
6463
+ var _a;
6464
+ return (_a = JSON.parse((await (0, child_process_promise_1.exec)(`gcloud projects get-iam-policy ${projectId} --format=json`))
6465
+ .stdout).bindings) !== null && _a !== void 0 ? _a : [];
6466
+ };
6467
+ const listUserManagedKeyIds = async (fullIdentifier) => {
6468
+ 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);
6469
+ return keys.map((key) => key.name.split("/").pop());
6470
+ };
6471
+ exports.listUserManagedKeyIds = listUserManagedKeyIds;
6472
+ /**
6473
+ * makes the account's bindings equal to `roles`: adds missing, removes
6474
+ * everything else (the account is dedicated, every other binding is
6475
+ * drift). Idempotent — a no-op when nothing differs.
6476
+ */
6477
+ const syncBindings = async (io, projectId, fullIdentifier, roles) => {
6478
+ const member = `serviceAccount:${fullIdentifier}`;
6479
+ const bindings = await getProjectBindings(projectId);
6480
+ const conditional = (0, exports.getConditionalRolesOfMemberInBindings)(bindings, member);
6481
+ if (conditional.length > 0) {
6482
+ io.log(`⚠️ ${fullIdentifier} has conditional bindings (${conditional.join(", ")}) — setup does not touch them, remove them by hand`);
6483
+ }
6484
+ const { missing, extra } = (0, pipeline_1.diffRoleBindings)(roles, (0, exports.getRolesOfMemberInBindings)(bindings, member));
6485
+ for (const role of missing) {
6486
+ io.log(` + ${role}`);
6487
+ await (0, child_process_promise_1.exec)(`gcloud projects add-iam-policy-binding ${projectId} --member=${member} --role=${role} --condition=None`);
6488
+ }
6489
+ for (const role of extra) {
6490
+ io.log(` - ${role}`);
6491
+ await (0, child_process_promise_1.exec)(`gcloud projects remove-iam-policy-binding ${projectId} --member=${member} --role=${role} --condition=None`);
6492
+ }
6493
+ };
6494
+ /**
6495
+ * brings the local access service account of a component/env in line
6496
+ * with the declared `roles`:
6497
+ *
6498
+ * - roles declared: account exists, carries exactly these bindings,
6499
+ * and a key for it is in the secret store (under the `local` env, so
6500
+ * it is never mirrored to ci). The key is only created when the
6501
+ * stored one is missing or no longer valid — re-running is a no-op.
6502
+ * - no roles declared: bindings are removed and the account (with its
6503
+ * keys) deleted if it exists.
6504
+ */
6505
+ const syncLocalAccessServiceAccount = async (io, context, projectId, roles) => {
6506
+ const { fullName, fullIdentifier } = (0, exports.getLocalAccessServiceAccountNames)(context, projectId);
6507
+ const exists = await (0, serviceAccounts_1.accountExists)(fullIdentifier);
6508
+ if (roles.length === 0) {
6509
+ if (exists) {
6510
+ io.log(`local access no longer declared, removing ${fullIdentifier}...`);
6511
+ await syncBindings(io, projectId, fullIdentifier, []);
6512
+ await (0, child_process_promise_1.exec)(`gcloud iam service-accounts delete ${fullIdentifier} --quiet --project="${projectId}"`);
6513
+ }
6514
+ return;
6515
+ }
6516
+ if (!exists) {
6517
+ io.log(`creating local access service account ${fullIdentifier}...`);
6518
+ 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)"`);
6519
+ }
6520
+ await syncBindings(io, projectId, fullIdentifier, roles);
6521
+ const secretName = (0, pipeline_1.getLocalAccessGcpSecretName)(context.env);
6522
+ const vaultManager = await io.getVaultManager();
6523
+ const rawName = (0, pipeline_1.getSecretVarName)("local", context.name, secretName);
6524
+ // straight from the vault, NOT through the local cache: a stale cache
6525
+ // (older than the first setup, or holding an older key) would make us
6526
+ // believe the key is gone, delete the valid one and lock out every
6527
+ // other machine.
6528
+ const stored = (await vaultManager.vault.readAllSecrets(io))[rawName];
6529
+ const validKeyIds = await (0, exports.listUserManagedKeyIds)(fullIdentifier);
6530
+ let storedKeyId;
6531
+ try {
6532
+ storedKeyId = JSON.parse(stored).private_key_id;
6533
+ }
6534
+ catch (_a) {
6535
+ // no (or unparsable) stored key
6536
+ }
6537
+ if (storedKeyId && validKeyIds.includes(storedKeyId)) {
6538
+ // keep the cache in line with the vault
6539
+ await vaultManager.cacheSecrets("local", context.name, {
6540
+ [secretName]: stored,
6541
+ });
6542
+ // at most one key per account: also drop leftovers of a failed run
6543
+ // or keys added by hand
6544
+ await deleteKeys(fullIdentifier, validKeyIds.filter((id) => id !== storedKeyId));
6545
+ return;
6546
+ }
6547
+ io.log("creating local access key...");
6548
+ // create first: a failed create must not leave the vault empty
6549
+ const { key, privateKeyId } = await createServiceAccountKey(fullIdentifier);
6550
+ // straight into the vault, NOT through writeSecretsAndMirror: this
6551
+ // key is for local use only and must never reach ci variables.
6552
+ // A fresh key replaces the old one, a backup would be dead.
6553
+ await vaultManager.writeSecrets("local", context.name, { [secretName]: key }, io, { backup: false });
6554
+ // only now drop the keys that are not the new one
6555
+ await deleteKeys(fullIdentifier, validKeyIds.filter((id) => id !== privateKeyId));
6556
+ };
6557
+ exports.syncLocalAccessServiceAccount = syncLocalAccessServiceAccount;
6558
+ const deleteKeys = async (fullIdentifier, keyIds) => {
6559
+ for (const keyId of keyIds) {
6560
+ await (0, child_process_promise_1.exec)(`gcloud iam service-accounts keys delete ${keyId} --quiet --iam-account=${fullIdentifier}`);
6561
+ }
6562
+ };
6563
+ const createServiceAccountKey = async (fullIdentifier) => {
6564
+ // a private temp file (0700 folder) instead of /dev/stdout | cat:
6565
+ // the pipe hid gcloud's exit code
6566
+ const folder = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), "catladder-local-access-"));
6567
+ try {
6568
+ const file = (0, path_1.join)(folder, "key.json");
6569
+ await (0, child_process_promise_1.exec)(`gcloud iam service-accounts keys create "${file}" --iam-account=${fullIdentifier}`);
6570
+ return (0, exports.parseServiceAccountKey)(await (0, promises_1.readFile)(file, "utf-8"));
6571
+ }
6572
+ finally {
6573
+ await (0, promises_1.rm)(folder, { recursive: true, force: true });
6574
+ }
6575
+ };
6576
+ //# sourceMappingURL=localAccess.js.map
6577
+
6578
+ /***/ }),
6579
+
6233
6580
  /***/ 74241:
6234
6581
  /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
6235
6582
 
@@ -6319,12 +6666,19 @@ const child_process_promise_1 = __nccwpck_require__(11542);
6319
6666
  const secrets_1 = __nccwpck_require__(85200);
6320
6667
  const serviceAccountNames_1 = __nccwpck_require__(67851);
6321
6668
  const accountExists = async (fullIdentifier) => {
6669
+ var _a, _b;
6322
6670
  try {
6323
6671
  await (0, child_process_promise_1.exec)(`gcloud iam service-accounts describe ${fullIdentifier}`);
6324
6672
  return true;
6325
6673
  }
6326
- catch (_a) {
6327
- return false;
6674
+ catch (e) {
6675
+ // only a missing account means "does not exist": an auth or
6676
+ // permission error must not look like one (setup would skip the
6677
+ // deletion, doctor would report it missing)
6678
+ 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")) {
6679
+ return false;
6680
+ }
6681
+ throw e;
6328
6682
  }
6329
6683
  };
6330
6684
  exports.accountExists = accountExists;
@@ -6622,6 +6976,127 @@ exports.startKubePortForward = startKubePortForward;
6622
6976
 
6623
6977
  /***/ }),
6624
6978
 
6979
+ /***/ 83228:
6980
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
6981
+
6982
+ "use strict";
6983
+
6984
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
6985
+ exports.getDeclaredLocalAccessEnvs = void 0;
6986
+ const pipeline_1 = __nccwpck_require__(53695);
6987
+ /**
6988
+ * the envs of a component declaring gcp local access. An invalid
6989
+ * declaration yields none instead of throwing: catenv and the vault
6990
+ * cache must keep working (`project doctor` and `project setup` report
6991
+ * the invalid declaration loudly).
6992
+ */
6993
+ const getDeclaredLocalAccessEnvs = (config, componentName) => {
6994
+ try {
6995
+ return (0, pipeline_1.getLocalAccessGcpEnvs)(config, componentName);
6996
+ }
6997
+ catch (_a) {
6998
+ return [];
6999
+ }
7000
+ };
7001
+ exports.getDeclaredLocalAccessEnvs = getDeclaredLocalAccessEnvs;
7002
+ //# sourceMappingURL=declared.js.map
7003
+
7004
+ /***/ }),
7005
+
7006
+ /***/ 70180:
7007
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
7008
+
7009
+ "use strict";
7010
+
7011
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
7012
+ exports.writeLocalAccessKeyFiles = exports.getLocalAccessKeyFileVars = exports.getLocalAccessKeyFileVarName = exports.getLocalAccessKeyFileName = void 0;
7013
+ const pipeline_1 = __nccwpck_require__(53695);
7014
+ const promises_1 = __nccwpck_require__(91943);
7015
+ const fs_1 = __nccwpck_require__(79896);
7016
+ const path_1 = __nccwpck_require__(16928);
7017
+ const writePrivateFile_1 = __nccwpck_require__(14550);
7018
+ const declared_1 = __nccwpck_require__(83228);
7019
+ const gitProjectInformation_1 = __nccwpck_require__(86884);
7020
+ const sanitize = (s) => s.replace(/[^A-Za-z0-9]+/g, "_");
7021
+ /** file name of a component/env key, relative to the local access folder */
7022
+ const getLocalAccessKeyFileName = (env, componentName) => `gcp-${sanitize(env)}-${sanitize(componentName)}.json`;
7023
+ exports.getLocalAccessKeyFileName = getLocalAccessKeyFileName;
7024
+ /**
7025
+ * env var exposing the key file path. Deliberately NOT
7026
+ * GOOGLE_APPLICATION_CREDENTIALS: the key must only be used where a
7027
+ * command opts in (isolated gcloud config), never by ADC.
7028
+ */
7029
+ const getLocalAccessKeyFileVarName = (env) => `CATLADDER_LOCAL_ACCESS_GCP_${sanitize(env).toUpperCase()}_KEY_FILE`;
7030
+ exports.getLocalAccessKeyFileVarName = getLocalAccessKeyFileVarName;
7031
+ // inside .catladder/, which ignores itself (see vault/cache.ts)
7032
+ const getFolder = async () => (0, path_1.join)(await (0, gitProjectInformation_1.getProjectRootPath)(), ".catladder", "local-access");
7033
+ /**
7034
+ * env vars pointing at the key files that exist for the component
7035
+ * (only paths, no secrets)
7036
+ */
7037
+ const getLocalAccessKeyFileVars = async (config, componentName) => {
7038
+ const folder = await getFolder();
7039
+ return Object.fromEntries((0, declared_1.getDeclaredLocalAccessEnvs)(config, componentName)
7040
+ .map((env) => [
7041
+ (0, exports.getLocalAccessKeyFileVarName)(env),
7042
+ (0, path_1.join)(folder, (0, exports.getLocalAccessKeyFileName)(env, componentName)),
7043
+ ])
7044
+ .filter(([, path]) => (0, fs_1.existsSync)(path)));
7045
+ };
7046
+ exports.getLocalAccessKeyFileVars = getLocalAccessKeyFileVars;
7047
+ /**
7048
+ * writes the stored local access service account keys to gitignored
7049
+ * files (mode 0600) and removes files of declarations that are gone.
7050
+ * A no-op without any declaration (nothing is created). Never throws:
7051
+ * a vault that cannot be reached or an invalid declaration (reported by
7052
+ * `project doctor`) must not break catenv.
7053
+ *
7054
+ * The key is only read through the cache; if setup rotated it on another
7055
+ * machine, refresh with `catenv --vault-mode refresh`.
7056
+ */
7057
+ const writeLocalAccessKeyFiles = async (io, config) => {
7058
+ const wanted = Object.keys(config.components).flatMap((componentName) => (0, declared_1.getDeclaredLocalAccessEnvs)(config, componentName).map((env) => ({
7059
+ env,
7060
+ componentName,
7061
+ secretName: (0, pipeline_1.getSecretVarName)("local", componentName, (0, pipeline_1.getLocalAccessGcpSecretName)(env)),
7062
+ fileName: (0, exports.getLocalAccessKeyFileName)(env, componentName),
7063
+ })));
7064
+ const folder = await getFolder();
7065
+ if (wanted.length === 0 && !(0, fs_1.existsSync)(folder))
7066
+ return;
7067
+ try {
7068
+ const secrets = wanted.length > 0
7069
+ ? await (await io.getVaultManager()).readSecrets(wanted.map((w) => w.secretName), io)
7070
+ : {};
7071
+ await (0, promises_1.mkdir)(folder, { recursive: true, mode: 0o700 });
7072
+ // the parent folder ignores itself
7073
+ await (0, promises_1.writeFile)((0, path_1.join)(folder, "..", ".gitignore"), "*\n");
7074
+ const written = new Set();
7075
+ for (const { secretName, fileName, env, componentName } of wanted) {
7076
+ const key = secrets[secretName];
7077
+ if (!key) {
7078
+ 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)`);
7079
+ continue;
7080
+ }
7081
+ const path = (0, path_1.join)(folder, fileName);
7082
+ await (0, writePrivateFile_1.writePrivateFile)(path, key);
7083
+ written.add(fileName);
7084
+ }
7085
+ for (const file of await (0, promises_1.readdir)(folder)) {
7086
+ if (!written.has(file)) {
7087
+ await (0, promises_1.rm)((0, path_1.join)(folder, file), { force: true });
7088
+ }
7089
+ }
7090
+ }
7091
+ catch (e) {
7092
+ io.log(`⚠️ could not write local access keys: ${e.message}`);
7093
+ }
7094
+ };
7095
+ exports.writeLocalAccessKeyFiles = writeLocalAccessKeyFiles;
7096
+ //# sourceMappingURL=keyFiles.js.map
7097
+
7098
+ /***/ }),
7099
+
6625
7100
  /***/ 55689:
6626
7101
  /***/ ((__unused_webpack_module, exports) => {
6627
7102
 
@@ -8034,6 +8509,38 @@ exports.getShell = getShell;
8034
8509
 
8035
8510
  /***/ }),
8036
8511
 
8512
+ /***/ 14550:
8513
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
8514
+
8515
+ "use strict";
8516
+
8517
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
8518
+ exports.writePrivateFile = void 0;
8519
+ const promises_1 = __nccwpck_require__(91943);
8520
+ /**
8521
+ * writes a file readable by the owner only. Goes through a 0600 temp
8522
+ * file and a rename, so the content is never visible with looser
8523
+ * permissions (writing first and chmod-ing afterwards would leave an
8524
+ * existing 0644 file readable until the chmod).
8525
+ */
8526
+ const writePrivateFile = async (path, content) => {
8527
+ const tmp = `${path}.${process.pid}.tmp`;
8528
+ try {
8529
+ await (0, promises_1.writeFile)(tmp, content, { mode: 0o600 });
8530
+ // the mode is only applied when the file is created
8531
+ await (0, promises_1.chmod)(tmp, 0o600);
8532
+ await (0, promises_1.rename)(tmp, path);
8533
+ }
8534
+ catch (e) {
8535
+ await (0, promises_1.rm)(tmp, { force: true });
8536
+ throw e;
8537
+ }
8538
+ };
8539
+ exports.writePrivateFile = writePrivateFile;
8540
+ //# sourceMappingURL=writePrivateFile.js.map
8541
+
8542
+ /***/ }),
8543
+
8037
8544
  /***/ 42827:
8038
8545
  /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
8039
8546
 
@@ -8140,30 +8647,21 @@ class BitwardenVault {
8140
8647
  : {};
8141
8648
  const notes = (0, yaml_1.stringify)({ ...previous, ...secrets });
8142
8649
  if (existing) {
8143
- await (0, bw_1.runBw)(bwSession, [
8144
- "edit",
8145
- "item",
8146
- existing.id,
8147
- encodePayload({ ...existing, notes }),
8148
- ]);
8650
+ await (0, bw_1.runBw)(bwSession, ["edit", "item", existing.id], encodePayload({ ...existing, notes }));
8149
8651
  }
8150
8652
  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
- ]);
8653
+ await (0, bw_1.runBw)(bwSession, ["create", "item"], encodePayload({
8654
+ type: SECURE_NOTE_TYPE,
8655
+ secureNote: { type: 0 },
8656
+ name,
8657
+ notes,
8658
+ ...(collection.organizationId
8659
+ ? {
8660
+ organizationId: collection.organizationId,
8661
+ collectionIds: [collection.id],
8662
+ }
8663
+ : {}),
8664
+ }));
8167
8665
  }
8168
8666
  }
8169
8667
  }
@@ -8210,6 +8708,7 @@ exports.GitlabVault = GitlabVault;
8210
8708
  Object.defineProperty(exports, "__esModule", ({ value: true }));
8211
8709
  exports.VaultManager = void 0;
8212
8710
  const pipeline_1 = __nccwpck_require__(53695);
8711
+ const declared_1 = __nccwpck_require__(83228);
8213
8712
  const BitwardenVault_1 = __nccwpck_require__(42827);
8214
8713
  const cache_1 = __nccwpck_require__(57011);
8215
8714
  const GitlabVault_1 = __nccwpck_require__(6020);
@@ -8292,6 +8791,15 @@ class VaultManager {
8292
8791
  ])));
8293
8792
  }
8294
8793
  }
8794
+ /**
8795
+ * updates the local cache only (the vault already holds the values),
8796
+ * "local" env only
8797
+ */
8798
+ async cacheSecrets(env, componentName, secrets) {
8799
+ if (env !== CACHED_ENV)
8800
+ return;
8801
+ await (0, cache_1.updateSecretsCache)(this.vault.id, env, componentName, secrets);
8802
+ }
8295
8803
  /**
8296
8804
  * the subset of the vault secrets that may be cached: the declared
8297
8805
  * secrets of the "local" env, per component, as a hierarchy
@@ -8315,6 +8823,14 @@ class VaultManager {
8315
8823
  return Object.keys(local).length > 0 ? { [CACHED_ENV]: local } : {};
8316
8824
  }
8317
8825
  async declaredSecretKeys(componentName) {
8826
+ return [
8827
+ ...(await this.declaredEnvSecretKeys(componentName)),
8828
+ // the local access keys `project setup` provisions are not part
8829
+ // of any env's vars, but belong to the cached local secrets
8830
+ ...(0, declared_1.getDeclaredLocalAccessEnvs)(this.config, componentName).map(pipeline_1.getLocalAccessGcpSecretName),
8831
+ ];
8832
+ }
8833
+ async declaredEnvSecretKeys(componentName) {
8318
8834
  try {
8319
8835
  const { secretEnvVarKeys, jobOnlyVars } = await (0, pipeline_1.getEnvironment)({
8320
8836
  config: this.config,
@@ -8424,14 +8940,40 @@ const getBwSession = async (io, { allowPrompt }) => {
8424
8940
  };
8425
8941
  exports.getBwSession = getBwSession;
8426
8942
  /**
8427
- * runs a bw command with the given session and returns stdout
8943
+ * runs a bw command with the given session and returns stdout.
8944
+ *
8945
+ * `input` is written to stdin: use it for payloads (e.g. the secrets of
8946
+ * `bw create item`), which would otherwise be visible in `ps` and be
8947
+ * echoed by node's "Command failed" error.
8428
8948
  */
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,
8949
+ const runBw = async (session, args, input) => {
8950
+ const fullArgs = [...args, "--raw", "--nointeraction"];
8951
+ const env = { ...process.env, BW_SESSION: session };
8952
+ if (input === undefined) {
8953
+ const { stdout } = await execFile("bw", fullArgs, {
8954
+ env,
8955
+ maxBuffer: 50 * 1024 * 1024,
8956
+ });
8957
+ return stdout;
8958
+ }
8959
+ return new Promise((resolve, reject) => {
8960
+ const child = (0, child_process_1.spawn)("bw", fullArgs, {
8961
+ env,
8962
+ stdio: ["pipe", "pipe", "pipe"],
8963
+ });
8964
+ let stdout = "";
8965
+ let stderr = "";
8966
+ child.stdout.on("data", (data) => (stdout += data.toString("utf-8")));
8967
+ child.stderr.on("data", (data) => (stderr += data.toString("utf-8")));
8968
+ child.on("error", reject);
8969
+ child.on("close", (code) => code === 0
8970
+ ? resolve(stdout)
8971
+ : reject(new Error(`bw ${args[0]} failed: ${stderr.trim()}`)));
8972
+ child.stdin.on("error", () => {
8973
+ // bw exiting early closes the pipe, the close handler reports why
8974
+ });
8975
+ child.stdin.end(input);
8433
8976
  });
8434
- return stdout;
8435
8977
  };
8436
8978
  exports.runBw = runBw;
8437
8979
  const runBwJson = async (session, args) => JSON.parse(await (0, exports.runBw)(session, args));
@@ -8449,6 +8991,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
8449
8991
  exports.updateSecretsCache = exports.writeSecretsCache = exports.readSecretsCache = void 0;
8450
8992
  const promises_1 = __nccwpck_require__(91943);
8451
8993
  const path_1 = __nccwpck_require__(16928);
8994
+ const writePrivateFile_1 = __nccwpck_require__(14550);
8452
8995
  const gitProjectInformation_1 = __nccwpck_require__(86884);
8453
8996
  const getCacheFolder = async () => (0, path_1.join)(await (0, gitProjectInformation_1.getProjectRootPath)(), ".catladder");
8454
8997
  const getCachePath = async () => (0, path_1.join)(await getCacheFolder(), "secrets.json");
@@ -8463,7 +9006,7 @@ const readSecretsCache = async () => {
8463
9006
  exports.readSecretsCache = readSecretsCache;
8464
9007
  const writeSecretsCache = async (vaultId, secrets) => {
8465
9008
  const folder = await getCacheFolder();
8466
- await (0, promises_1.mkdir)(folder, { recursive: true });
9009
+ await (0, promises_1.mkdir)(folder, { recursive: true, mode: 0o700 });
8467
9010
  // the folder ignores itself, so the cache can never be committed
8468
9011
  await (0, promises_1.writeFile)((0, path_1.join)(folder, ".gitignore"), "*\n");
8469
9012
  const cache = {
@@ -8471,7 +9014,9 @@ const writeSecretsCache = async (vaultId, secrets) => {
8471
9014
  updatedAt: new Date().toISOString(),
8472
9015
  secrets,
8473
9016
  };
8474
- await (0, promises_1.writeFile)(await getCachePath(), JSON.stringify(cache, null, 2));
9017
+ const path = await getCachePath();
9018
+ // may hold key material (local access keys): owner only
9019
+ await (0, writePrivateFile_1.writePrivateFile)(path, JSON.stringify(cache, null, 2));
8475
9020
  };
8476
9021
  exports.writeSecretsCache = writeSecretsCache;
8477
9022
  /**
@@ -19364,6 +19909,163 @@ __exportStar(__nccwpck_require__(91804), exports);
19364
19909
  __exportStar(__nccwpck_require__(94143), exports);
19365
19910
  __exportStar(__nccwpck_require__(98011), exports);
19366
19911
  __exportStar(__nccwpck_require__(61459), exports);
19912
+ __exportStar(__nccwpck_require__(54165), exports);
19913
+ //# sourceMappingURL=index.js.map
19914
+
19915
+ /***/ }),
19916
+
19917
+ /***/ 54165:
19918
+ /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
19919
+
19920
+ "use strict";
19921
+
19922
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
19923
+ 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;
19924
+ const configruedEnvs_1 = __nccwpck_require__(38866);
19925
+ const getEnvConfig_1 = __nccwpck_require__(69578);
19926
+ const types_1 = __nccwpck_require__(79689);
19927
+ exports.LOCAL_ACCESS_GCP_PRESETS = {
19928
+ // no run.viewer on purpose, see LocalAccessGcpPreset
19929
+ logs: ["roles/logging.viewer"],
19930
+ };
19931
+ /** name prefix of the service account `project setup` creates (see getGcloudServiceAccountNames) */
19932
+ exports.LOCAL_ACCESS_GCP_SA_NAME = "local-diag";
19933
+ const ROLE_FORMAT = /^(roles\/[A-Za-z0-9_.]+|projects\/[a-z0-9-]+\/roles\/[A-Za-z0-9_.]+|organizations\/\d+\/roles\/[A-Za-z0-9_.]+)$/;
19934
+ /**
19935
+ * roles that are read-only on paper but expose secrets: cloud run
19936
+ * services/jobs carry their secrets as plain env vars, so anything that
19937
+ * may read them reads every deployed secret. Denied even with
19938
+ * `allowAnyRole`.
19939
+ */
19940
+ const SECRET_EXPOSING_ROLES = {
19941
+ "roles/viewer": "roles/viewer can read cloud run env vars (secrets)",
19942
+ "roles/run.viewer": "roles/run.viewer can read cloud run env vars (secrets)",
19943
+ "roles/secretmanager.secretAccessor": "this role reads secret values",
19944
+ "roles/secretmanager.admin": "this role reads secret values",
19945
+ };
19946
+ /**
19947
+ * roles granted without `allowAnyRole`. An explicit list instead of a
19948
+ * name pattern: a role called `*.viewer` / `*.reader` says nothing about
19949
+ * its permissions (custom roles can hold anything, and predefined ones
19950
+ * like storage.objectViewer, cloudfunctions.viewer, compute.viewer or
19951
+ * logging.privateLogViewer expose data or secrets).
19952
+ */
19953
+ const SAFE_ROLES = [
19954
+ "roles/logging.viewer",
19955
+ "roles/monitoring.viewer",
19956
+ "roles/errorreporting.viewer",
19957
+ ];
19958
+ /** `roles/foo.bar.v1` -> `foo.bar` (version suffixes must not defeat the checks) */
19959
+ const getRoleName = (role) => { var _a; return ((_a = role.split("/").pop()) !== null && _a !== void 0 ? _a : role).replace(/(\.v\d+)+$/i, ""); };
19960
+ /**
19961
+ * why a role may not be granted for local access, or undefined if it
19962
+ * is acceptable.
19963
+ *
19964
+ * By default only the roles in {@link SAFE_ROLES} pass; custom roles
19965
+ * and everything else need `allowAnyRole`. `allowAnyRole` lifts the
19966
+ * allow-list, but the obvious write/admin/impersonation roles and the
19967
+ * secret exposing roles stay denied.
19968
+ */
19969
+ const getDeniedLocalAccessRoleReason = (role, options = {}) => {
19970
+ var _a;
19971
+ const secretReason = SECRET_EXPOSING_ROLES[role];
19972
+ if (secretReason)
19973
+ return secretReason;
19974
+ const name = getRoleName(role);
19975
+ if (name === "owner" || name === "editor") {
19976
+ return "owner/editor are full write roles";
19977
+ }
19978
+ const last = (_a = name.split(".").pop()) !== null && _a !== void 0 ? _a : name;
19979
+ if (/admin$/i.test(last)) {
19980
+ return "admin roles can change resources";
19981
+ }
19982
+ if (/(editor|developer|writer|creator|deployer|invoker|builder|publisher|client|user|objectUser)$/i.test(last) &&
19983
+ !/^iam\./.test(name)) {
19984
+ return "this role can change resources";
19985
+ }
19986
+ if (/^iam\./.test(name) &&
19987
+ /(serviceAccountUser|serviceAccountTokenCreator|serviceAccountKeyAdmin|workloadIdentityUser)$/.test(name)) {
19988
+ return "this role allows impersonating service accounts";
19989
+ }
19990
+ if (!options.allowAnyRole && !SAFE_ROLES.includes(role)) {
19991
+ return `only these roles are allowed: ${SAFE_ROLES.join(", ")} (set allowAnyRole: true to grant others at your own risk)`;
19992
+ }
19993
+ return undefined;
19994
+ };
19995
+ exports.getDeniedLocalAccessRoleReason = getDeniedLocalAccessRoleReason;
19996
+ /**
19997
+ * the effective local access declaration of an env: the env's own
19998
+ * declaration replaces the project default entirely
19999
+ */
20000
+ const getLocalAccessConfig = (config, env) => {
20001
+ var _a;
20002
+ if (env === "local")
20003
+ return undefined;
20004
+ const envEntry = (_a = config.environments) === null || _a === void 0 ? void 0 : _a[env];
20005
+ const declared = envEntry && envEntry.localAccess !== undefined
20006
+ ? envEntry.localAccess
20007
+ : config.localAccess;
20008
+ return declared || undefined;
20009
+ };
20010
+ exports.getLocalAccessConfig = getLocalAccessConfig;
20011
+ /**
20012
+ * the gcp roles local access grants in `env`: preset roles plus the
20013
+ * explicit ones, deduplicated and sorted. Throws on an unknown preset,
20014
+ * a malformed role or a denied (write/admin) role.
20015
+ */
20016
+ const getLocalAccessGcpRoles = (config, env) => {
20017
+ var _a, _b, _c;
20018
+ const gcp = (_a = (0, exports.getLocalAccessConfig)(config, env)) === null || _a === void 0 ? void 0 : _a.gcp;
20019
+ if (!gcp)
20020
+ return [];
20021
+ const roles = new Set();
20022
+ for (const preset of (_b = gcp.presets) !== null && _b !== void 0 ? _b : []) {
20023
+ const presetRoles = exports.LOCAL_ACCESS_GCP_PRESETS[preset];
20024
+ if (!presetRoles) {
20025
+ throw new Error(`localAccess (${env}): unknown gcp preset '${preset}', known: ${Object.keys(exports.LOCAL_ACCESS_GCP_PRESETS).join(", ")}`);
20026
+ }
20027
+ presetRoles.forEach((role) => roles.add(role));
20028
+ }
20029
+ for (const role of (_c = gcp.roles) !== null && _c !== void 0 ? _c : []) {
20030
+ if (!ROLE_FORMAT.test(role)) {
20031
+ throw new Error(`localAccess (${env}): '${role}' is not a valid role (expected e.g. roles/logging.viewer)`);
20032
+ }
20033
+ const reason = (0, exports.getDeniedLocalAccessRoleReason)(role, {
20034
+ allowAnyRole: gcp.allowAnyRole,
20035
+ });
20036
+ if (reason) {
20037
+ throw new Error(`localAccess (${env}): role '${role}' is not allowed (${reason})`);
20038
+ }
20039
+ roles.add(role);
20040
+ }
20041
+ return [...roles].sort();
20042
+ };
20043
+ exports.getLocalAccessGcpRoles = getLocalAccessGcpRoles;
20044
+ /**
20045
+ * the envs of a component that declare gcp local access. Empty for
20046
+ * components that do not deploy to google cloud run.
20047
+ */
20048
+ const getLocalAccessGcpEnvs = (config, componentName) => (0, configruedEnvs_1.getAllEnvs)(config, componentName).filter((env) =>
20049
+ // only google cloud run envs have a project to grant roles in; the
20050
+ // env's own `deploy` overrides the component's
20051
+ (0, types_1.isOfDeployType)((0, getEnvConfig_1.getEnvConfig)(config, componentName, env).deploy, "google-cloudrun") && (0, exports.getLocalAccessGcpRoles)(config, env).length > 0);
20052
+ exports.getLocalAccessGcpEnvs = getLocalAccessGcpEnvs;
20053
+ /**
20054
+ * name of the secret (stored under the `local` env of the component)
20055
+ * holding the service account key of `env`'s local access
20056
+ */
20057
+ const getLocalAccessGcpSecretName = (env) => `LOCAL_ACCESS_GCP_${env.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CREDENTIALS`;
20058
+ exports.getLocalAccessGcpSecretName = getLocalAccessGcpSecretName;
20059
+ /**
20060
+ * the bindings to add and remove so that `current` matches `desired`.
20061
+ * The local access account is dedicated, so every binding it carries
20062
+ * beyond the declaration is extra.
20063
+ */
20064
+ const diffRoleBindings = (desired, current) => ({
20065
+ missing: desired.filter((role) => !current.includes(role)),
20066
+ extra: current.filter((role) => !desired.includes(role)),
20067
+ });
20068
+ exports.diffRoleBindings = diffRoleBindings;
19367
20069
  //# sourceMappingURL=index.js.map
19368
20070
 
19369
20071
  /***/ }),