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.

secrets.js 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.updateEndpointSecret = exports.pruneAndDestroySecrets = exports.pruneSecrets = exports.inUse = exports.getSecretVersions = exports.of = exports.ensureSecret = exports.ensureValidKey = exports.labels = exports.isFirebaseManaged = void 0;
  4. const utils = require("../utils");
  5. const poller = require("../operation-poller");
  6. const gcf = require("../gcp/cloudfunctions");
  7. const secretManager_1 = require("../gcp/secretManager");
  8. const error_1 = require("../error");
  9. const utils_1 = require("../utils");
  10. const prompt_1 = require("../prompt");
  11. const env_1 = require("./env");
  12. const logger_1 = require("../logger");
  13. const api_1 = require("../api");
  14. const functional_1 = require("../functional");
  15. const FIREBASE_MANGED = "firebase-managed";
  16. function isFirebaseManaged(secret) {
  17. return Object.keys(secret.labels || []).includes(FIREBASE_MANGED);
  18. }
  19. exports.isFirebaseManaged = isFirebaseManaged;
  20. function labels() {
  21. return { [FIREBASE_MANGED]: "true" };
  22. }
  23. exports.labels = labels;
  24. function toUpperSnakeCase(key) {
  25. return key
  26. .replace(/[.-]/g, "_")
  27. .replace(/([a-z])([A-Z])/g, "$1_$2")
  28. .toUpperCase();
  29. }
  30. async function ensureValidKey(key, options) {
  31. const transformedKey = toUpperSnakeCase(key);
  32. if (transformedKey !== key) {
  33. if (options.force) {
  34. throw new error_1.FirebaseError("Secret key must be in UPPER_SNAKE_CASE.");
  35. }
  36. (0, utils_1.logWarning)(`By convention, secret key must be in UPPER_SNAKE_CASE.`);
  37. const confirm = await (0, prompt_1.promptOnce)({
  38. name: "updateKey",
  39. type: "confirm",
  40. default: true,
  41. message: `Would you like to use ${transformedKey} as key instead?`,
  42. }, options);
  43. if (!confirm) {
  44. throw new error_1.FirebaseError("Secret key must be in UPPER_SNAKE_CASE.");
  45. }
  46. }
  47. try {
  48. (0, env_1.validateKey)(transformedKey);
  49. }
  50. catch (err) {
  51. throw new error_1.FirebaseError(`Invalid secret key ${transformedKey}`, { children: [err] });
  52. }
  53. return transformedKey;
  54. }
  55. exports.ensureValidKey = ensureValidKey;
  56. async function ensureSecret(projectId, name, options) {
  57. try {
  58. const secret = await (0, secretManager_1.getSecret)(projectId, name);
  59. if (!isFirebaseManaged(secret)) {
  60. if (!options.force) {
  61. (0, utils_1.logWarning)("Your secret is not managed by Firebase. " +
  62. "Firebase managed secrets are automatically pruned to reduce your monthly cost for using Secret Manager. ");
  63. const confirm = await (0, prompt_1.promptOnce)({
  64. name: "updateLabels",
  65. type: "confirm",
  66. default: true,
  67. message: `Would you like to have your secret ${secret.name} managed by Firebase?`,
  68. }, options);
  69. if (confirm) {
  70. return (0, secretManager_1.patchSecret)(projectId, secret.name, Object.assign(Object.assign({}, secret.labels), labels()));
  71. }
  72. }
  73. }
  74. return secret;
  75. }
  76. catch (err) {
  77. if (err.status !== 404) {
  78. throw err;
  79. }
  80. }
  81. return await (0, secretManager_1.createSecret)(projectId, name, labels());
  82. }
  83. exports.ensureSecret = ensureSecret;
  84. function of(endpoints) {
  85. return endpoints.reduce((envs, endpoint) => [...envs, ...(endpoint.secretEnvironmentVariables || [])], []);
  86. }
  87. exports.of = of;
  88. function getSecretVersions(endpoint) {
  89. return (endpoint.secretEnvironmentVariables || []).reduce((memo, { secret, version }) => {
  90. memo[secret] = version || "";
  91. return memo;
  92. }, {});
  93. }
  94. exports.getSecretVersions = getSecretVersions;
  95. function inUse(projectInfo, secret, endpoint) {
  96. const { projectId, projectNumber } = projectInfo;
  97. for (const sev of of([endpoint])) {
  98. if ((sev.projectId === projectId || sev.projectId === projectNumber) &&
  99. sev.secret === secret.name) {
  100. return true;
  101. }
  102. }
  103. return false;
  104. }
  105. exports.inUse = inUse;
  106. async function pruneSecrets(projectInfo, endpoints) {
  107. const { projectId, projectNumber } = projectInfo;
  108. const pruneKey = (name, version) => `${name}@${version}`;
  109. const prunedSecrets = new Set();
  110. const haveSecrets = await (0, secretManager_1.listSecrets)(projectId, `labels.${FIREBASE_MANGED}=true`);
  111. for (const secret of haveSecrets) {
  112. const versions = await (0, secretManager_1.listSecretVersions)(projectId, secret.name, `NOT state: DESTROYED`);
  113. for (const version of versions) {
  114. prunedSecrets.add(pruneKey(secret.name, version.versionId));
  115. }
  116. }
  117. const secrets = [];
  118. for (const secret of of(endpoints)) {
  119. if (!secret.version) {
  120. throw new error_1.FirebaseError(`Secret ${secret.secret} version is unexpectedly empty.`);
  121. }
  122. if (secret.projectId === projectId || secret.projectId === projectNumber) {
  123. if (secret.version) {
  124. secrets.push(Object.assign(Object.assign({}, secret), { version: secret.version }));
  125. }
  126. }
  127. }
  128. for (const sev of secrets) {
  129. let name = sev.secret;
  130. if (name.includes("/")) {
  131. const secret = (0, secretManager_1.parseSecretResourceName)(name);
  132. name = secret.name;
  133. }
  134. let version = sev.version;
  135. if (version === "latest") {
  136. const resolved = await (0, secretManager_1.getSecretVersion)(projectId, name, version);
  137. version = resolved.versionId;
  138. }
  139. prunedSecrets.delete(pruneKey(name, version));
  140. }
  141. return Array.from(prunedSecrets)
  142. .map((key) => key.split("@"))
  143. .map(([secret, version]) => ({ projectId, version, secret, key: secret }));
  144. }
  145. exports.pruneSecrets = pruneSecrets;
  146. async function pruneAndDestroySecrets(projectInfo, endpoints) {
  147. const { projectId, projectNumber } = projectInfo;
  148. logger_1.logger.debug("Pruning secrets to find unused secret versions...");
  149. const unusedSecrets = await module.exports.pruneSecrets({ projectId, projectNumber }, endpoints);
  150. if (unusedSecrets.length === 0) {
  151. return { destroyed: [], erred: [] };
  152. }
  153. const destroyed = [];
  154. const erred = [];
  155. const msg = unusedSecrets.map((s) => `${s.secret}@${s.version}`);
  156. logger_1.logger.debug(`Found unused secret versions: ${msg}. Destroying them...`);
  157. const destroyResults = await utils.allSettled(unusedSecrets.map(async (sev) => {
  158. await (0, secretManager_1.destroySecretVersion)(sev.projectId, sev.secret, sev.version);
  159. return sev;
  160. }));
  161. for (const result of destroyResults) {
  162. if (result.status === "fulfilled") {
  163. destroyed.push(result.value);
  164. }
  165. else {
  166. erred.push(result.reason);
  167. }
  168. }
  169. return { destroyed, erred };
  170. }
  171. exports.pruneAndDestroySecrets = pruneAndDestroySecrets;
  172. async function updateEndpointSecret(projectInfo, secretVersion, endpoint) {
  173. const { projectId, projectNumber } = projectInfo;
  174. if (!inUse(projectInfo, secretVersion.secret, endpoint)) {
  175. return endpoint;
  176. }
  177. const updatedSevs = [];
  178. for (const sev of of([endpoint])) {
  179. const updatedSev = Object.assign({}, sev);
  180. if ((updatedSev.projectId === projectId || updatedSev.projectId === projectNumber) &&
  181. updatedSev.secret === secretVersion.secret.name) {
  182. updatedSev.version = secretVersion.versionId;
  183. }
  184. updatedSevs.push(updatedSev);
  185. }
  186. if (endpoint.platform === "gcfv1") {
  187. const fn = gcf.functionFromEndpoint(endpoint, "");
  188. const op = await gcf.updateFunction({
  189. name: fn.name,
  190. runtime: fn.runtime,
  191. entryPoint: fn.entryPoint,
  192. secretEnvironmentVariables: updatedSevs,
  193. });
  194. const gcfV1PollerOptions = {
  195. apiOrigin: api_1.functionsOrigin,
  196. apiVersion: gcf.API_VERSION,
  197. masterTimeout: 25 * 60 * 1000,
  198. maxBackoff: 10000,
  199. pollerName: `update-${endpoint.region}-${endpoint.id}`,
  200. operationResourceName: op.name,
  201. };
  202. const cfn = await poller.pollOperation(gcfV1PollerOptions);
  203. return gcf.endpointFromFunction(cfn);
  204. }
  205. else if (endpoint.platform === "gcfv2") {
  206. throw new error_1.FirebaseError(`Unsupported platform ${endpoint.platform}`);
  207. }
  208. else {
  209. (0, functional_1.assertExhaustive)(endpoint.platform);
  210. }
  211. }
  212. exports.updateEndpointSecret = updateEndpointSecret;