No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

askUserForParam.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.getInquirerDefault = exports.promptCreateSecret = exports.askForParam = exports.ask = exports.checkResponse = exports.SecretLocation = void 0;
  4. const _ = require("lodash");
  5. const clc = require("colorette");
  6. const { marked } = require("marked");
  7. const types_1 = require("./types");
  8. const secretManagerApi = require("../gcp/secretManager");
  9. const secretsUtils = require("./secretsUtils");
  10. const extensionsHelper_1 = require("./extensionsHelper");
  11. const utils_1 = require("./utils");
  12. const logger_1 = require("../logger");
  13. const prompt_1 = require("../prompt");
  14. const utils = require("../utils");
  15. const projectUtils_1 = require("../projectUtils");
  16. var SecretLocation;
  17. (function (SecretLocation) {
  18. SecretLocation[SecretLocation["CLOUD"] = 1] = "CLOUD";
  19. SecretLocation[SecretLocation["LOCAL"] = 2] = "LOCAL";
  20. })(SecretLocation = exports.SecretLocation || (exports.SecretLocation = {}));
  21. var SecretUpdateAction;
  22. (function (SecretUpdateAction) {
  23. SecretUpdateAction[SecretUpdateAction["LEAVE"] = 1] = "LEAVE";
  24. SecretUpdateAction[SecretUpdateAction["SET_NEW"] = 2] = "SET_NEW";
  25. })(SecretUpdateAction || (SecretUpdateAction = {}));
  26. function checkResponse(response, spec) {
  27. var _a;
  28. let valid = true;
  29. let responses;
  30. if (spec.required && (response === "" || response === undefined)) {
  31. utils.logWarning(`Param ${spec.param} is required, but no value was provided.`);
  32. return false;
  33. }
  34. if (spec.type === types_1.ParamType.MULTISELECT) {
  35. responses = response.split(",");
  36. }
  37. else {
  38. responses = [response];
  39. }
  40. if (spec.validationRegex && !!response) {
  41. const re = new RegExp(spec.validationRegex);
  42. for (const resp of responses) {
  43. if ((spec.required || resp !== "") && !re.test(resp)) {
  44. const genericWarn = `${resp} is not a valid value for ${spec.param} since it` +
  45. ` does not meet the requirements of the regex validation: "${spec.validationRegex}"`;
  46. utils.logWarning(spec.validationErrorMessage || genericWarn);
  47. valid = false;
  48. }
  49. }
  50. }
  51. if (spec.type && (spec.type === types_1.ParamType.MULTISELECT || spec.type === types_1.ParamType.SELECT)) {
  52. for (const r of responses) {
  53. const validChoice = (_a = spec.options) === null || _a === void 0 ? void 0 : _a.some((option) => r === option.value);
  54. if (r && !validChoice) {
  55. utils.logWarning(`${r} is not a valid option for ${spec.param}.`);
  56. valid = false;
  57. }
  58. }
  59. }
  60. return valid;
  61. }
  62. exports.checkResponse = checkResponse;
  63. async function ask(args) {
  64. if (_.isEmpty(args.paramSpecs)) {
  65. logger_1.logger.debug("No params were specified for this extension.");
  66. return {};
  67. }
  68. utils.logLabeledBullet(extensionsHelper_1.logPrefix, "answer the questions below to configure your extension:");
  69. const substituted = (0, extensionsHelper_1.substituteParams)(args.paramSpecs, args.firebaseProjectParams);
  70. const result = {};
  71. const promises = substituted.map((paramSpec) => {
  72. return async () => {
  73. result[paramSpec.param] = await askForParam({
  74. projectId: args.projectId,
  75. instanceId: args.instanceId,
  76. paramSpec: paramSpec,
  77. reconfiguring: args.reconfiguring,
  78. });
  79. };
  80. });
  81. await promises.reduce((prev, cur) => prev.then(cur), Promise.resolve());
  82. logger_1.logger.info();
  83. return result;
  84. }
  85. exports.ask = ask;
  86. async function askForParam(args) {
  87. const paramSpec = args.paramSpec;
  88. let valid = false;
  89. let response = "";
  90. let responseForLocal;
  91. let secretLocations = [];
  92. const description = paramSpec.description || "";
  93. const label = paramSpec.label.trim();
  94. logger_1.logger.info(`\n${clc.bold(label)}${clc.bold(paramSpec.required ? "" : " (Optional)")}: ${marked(description).trim()}`);
  95. while (!valid) {
  96. switch (paramSpec.type) {
  97. case types_1.ParamType.SELECT:
  98. response = await (0, prompt_1.promptOnce)({
  99. name: "input",
  100. type: "list",
  101. default: () => {
  102. if (paramSpec.default) {
  103. return getInquirerDefault(_.get(paramSpec, "options", []), paramSpec.default);
  104. }
  105. },
  106. message: "Which option do you want enabled for this parameter? " +
  107. "Select an option with the arrow keys, and use Enter to confirm your choice. " +
  108. "You may only select one option.",
  109. choices: (0, utils_1.convertExtensionOptionToLabeledList)(paramSpec.options),
  110. });
  111. valid = checkResponse(response, paramSpec);
  112. break;
  113. case types_1.ParamType.MULTISELECT:
  114. response = await (0, utils_1.onceWithJoin)({
  115. name: "input",
  116. type: "checkbox",
  117. default: () => {
  118. if (paramSpec.default) {
  119. const defaults = paramSpec.default.split(",");
  120. return defaults.map((def) => {
  121. return getInquirerDefault(_.get(paramSpec, "options", []), def);
  122. });
  123. }
  124. },
  125. message: "Which options do you want enabled for this parameter? " +
  126. "Press Space to select, then Enter to confirm your choices. ",
  127. choices: (0, utils_1.convertExtensionOptionToLabeledList)(paramSpec.options),
  128. });
  129. valid = checkResponse(response, paramSpec);
  130. break;
  131. case types_1.ParamType.SECRET:
  132. do {
  133. secretLocations = await promptSecretLocations(paramSpec);
  134. } while (!isValidSecretLocations(secretLocations, paramSpec));
  135. if (secretLocations.includes(SecretLocation.CLOUD.toString())) {
  136. const projectId = (0, projectUtils_1.needProjectId)({ projectId: args.projectId });
  137. response = args.reconfiguring
  138. ? await promptReconfigureSecret(projectId, args.instanceId, paramSpec)
  139. : await promptCreateSecret(projectId, args.instanceId, paramSpec);
  140. }
  141. if (secretLocations.includes(SecretLocation.LOCAL.toString())) {
  142. responseForLocal = await promptLocalSecret(args.instanceId, paramSpec);
  143. }
  144. valid = true;
  145. break;
  146. default:
  147. response = await (0, prompt_1.promptOnce)({
  148. name: paramSpec.param,
  149. type: "input",
  150. default: paramSpec.default,
  151. message: `Enter a value for ${label}:`,
  152. });
  153. valid = checkResponse(response, paramSpec);
  154. }
  155. }
  156. return Object.assign({ baseValue: response }, (responseForLocal ? { local: responseForLocal } : {}));
  157. }
  158. exports.askForParam = askForParam;
  159. function isValidSecretLocations(secretLocations, paramSpec) {
  160. if (paramSpec.required) {
  161. return !!secretLocations.length;
  162. }
  163. return true;
  164. }
  165. async function promptSecretLocations(paramSpec) {
  166. if (paramSpec.required) {
  167. return await (0, prompt_1.promptOnce)({
  168. name: "input",
  169. type: "checkbox",
  170. message: "Where would you like to store your secrets? You must select at least one value",
  171. choices: [
  172. {
  173. checked: true,
  174. name: "Google Cloud Secret Manager (Used by deployed extensions and emulator)",
  175. value: SecretLocation.CLOUD.toString(),
  176. },
  177. {
  178. checked: false,
  179. name: "Local file (Used by emulator only)",
  180. value: SecretLocation.LOCAL.toString(),
  181. },
  182. ],
  183. });
  184. }
  185. return await (0, prompt_1.promptOnce)({
  186. name: "input",
  187. type: "checkbox",
  188. message: "Where would you like to store your secrets? " +
  189. "If you don't want to set this optional secret, leave both options unselected to skip it",
  190. choices: [
  191. {
  192. checked: false,
  193. name: "Google Cloud Secret Manager (Used by deployed extensions and emulator)",
  194. value: SecretLocation.CLOUD.toString(),
  195. },
  196. {
  197. checked: false,
  198. name: "Local file (Used by emulator only)",
  199. value: SecretLocation.LOCAL.toString(),
  200. },
  201. ],
  202. });
  203. }
  204. async function promptLocalSecret(instanceId, paramSpec) {
  205. let value;
  206. do {
  207. utils.logLabeledBullet(extensionsHelper_1.logPrefix, "Configure a local secret value for Extensions Emulator");
  208. value = await (0, prompt_1.promptOnce)({
  209. name: paramSpec.param,
  210. type: "input",
  211. message: `This secret will be stored in ./extensions/${instanceId}.secret.local.\n` +
  212. `Enter value for "${paramSpec.label.trim()}" to be used by Extensions Emulator:`,
  213. });
  214. } while (!value);
  215. return value;
  216. }
  217. async function promptReconfigureSecret(projectId, instanceId, paramSpec) {
  218. const action = await (0, prompt_1.promptOnce)({
  219. type: "list",
  220. message: `Choose what you would like to do with this secret:`,
  221. choices: [
  222. { name: "Leave unchanged", value: SecretUpdateAction.LEAVE },
  223. { name: "Set new value", value: SecretUpdateAction.SET_NEW },
  224. ],
  225. });
  226. switch (action) {
  227. case SecretUpdateAction.SET_NEW:
  228. let secret;
  229. let secretName;
  230. if (paramSpec.default) {
  231. secret = secretManagerApi.parseSecretResourceName(paramSpec.default);
  232. secretName = secret.name;
  233. }
  234. else {
  235. secretName = await generateSecretName(projectId, instanceId, paramSpec.param);
  236. }
  237. const secretValue = await (0, prompt_1.promptOnce)({
  238. name: paramSpec.param,
  239. type: "password",
  240. message: `This secret will be stored in Cloud Secret Manager as ${secretName}.\nEnter new value for ${paramSpec.label.trim()}:`,
  241. });
  242. if (secretValue === "" && paramSpec.required) {
  243. logger_1.logger.info(`Secret value cannot be empty for required param ${paramSpec.param}`);
  244. return promptReconfigureSecret(projectId, instanceId, paramSpec);
  245. }
  246. else if (secretValue !== "") {
  247. if (checkResponse(secretValue, paramSpec)) {
  248. if (!secret) {
  249. secret = await secretManagerApi.createSecret(projectId, secretName, secretsUtils.getSecretLabels(instanceId));
  250. }
  251. return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue);
  252. }
  253. else {
  254. return promptReconfigureSecret(projectId, instanceId, paramSpec);
  255. }
  256. }
  257. else {
  258. return "";
  259. }
  260. case SecretUpdateAction.LEAVE:
  261. default:
  262. return paramSpec.default || "";
  263. }
  264. }
  265. async function promptCreateSecret(projectId, instanceId, paramSpec, secretName) {
  266. const name = secretName !== null && secretName !== void 0 ? secretName : (await generateSecretName(projectId, instanceId, paramSpec.param));
  267. const secretValue = await (0, prompt_1.promptOnce)({
  268. name: paramSpec.param,
  269. type: "password",
  270. default: paramSpec.default,
  271. message: `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${name} and managed by Firebase Extensions (Firebase Extensions Service Agent will be granted Secret Admin role on this secret).\nEnter a value for ${paramSpec.label.trim()}:`,
  272. });
  273. if (secretValue === "" && paramSpec.required) {
  274. logger_1.logger.info(`Secret value cannot be empty for required param ${paramSpec.param}`);
  275. return promptCreateSecret(projectId, instanceId, paramSpec, name);
  276. }
  277. else if (secretValue !== "") {
  278. if (checkResponse(secretValue, paramSpec)) {
  279. const secret = await secretManagerApi.createSecret(projectId, name, secretsUtils.getSecretLabels(instanceId));
  280. return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue);
  281. }
  282. else {
  283. return promptCreateSecret(projectId, instanceId, paramSpec, name);
  284. }
  285. }
  286. else {
  287. return "";
  288. }
  289. }
  290. exports.promptCreateSecret = promptCreateSecret;
  291. async function generateSecretName(projectId, instanceId, paramName) {
  292. let secretName = `ext-${instanceId}-${paramName}`;
  293. while (await secretManagerApi.secretExists(projectId, secretName)) {
  294. secretName += `-${(0, utils_1.getRandomString)(3)}`;
  295. }
  296. return secretName;
  297. }
  298. async function addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue) {
  299. const version = await secretManagerApi.addVersion(projectId, secret.name, secretValue);
  300. await secretsUtils.grantFirexServiceAgentSecretAdminRole(secret);
  301. return `projects/${version.secret.projectId}/secrets/${version.secret.name}/versions/${version.versionId}`;
  302. }
  303. function getInquirerDefault(options, def) {
  304. const defaultOption = options.find((o) => o.value === def);
  305. return defaultOption ? defaultOption.label || defaultOption.value : "";
  306. }
  307. exports.getInquirerDefault = getInquirerDefault;