UNPKG

firebase-tools

Version:
162 lines (155 loc) • 7.44 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.REQUIRED_SECURITY_APIS = void 0; exports.defaultServiceAccount = defaultServiceAccount; exports.cloudBuildEnabled = cloudBuildEnabled; exports.secretsAccessDelta = secretsAccessDelta; exports.checkSecretAccess = checkSecretAccess; exports.grantSecretAccess = grantSecretAccess; exports.checkDeclarativeSecurityApisEnabled = checkDeclarativeSecurityApisEnabled; const clc = require("colorette"); const ensureApiEnabled = require("../../ensureApiEnabled"); const error_1 = require("../../error"); const utils_1 = require("../../utils"); const secretManager_1 = require("../../gcp/secretManager"); const projects_1 = require("../../management/projects"); const functional_1 = require("../../functional"); const api_1 = require("../../api"); const backend = require("./backend"); const computeEngine_1 = require("../../gcp/computeEngine"); const logger_1 = require("../../logger"); const FAQ_URL = "https://firebase.google.com/support/faq#functions-runtime"; const metadataCallCache = new Map(); async function defaultServiceAccount(e) { let metadataCall = metadataCallCache.get(e.project); if (!metadataCall) { metadataCall = (0, projects_1.getProject)(e.project); metadataCallCache.set(e.project, metadataCall); } const metadata = await metadataCall; if (e.platform === "gcfv1") { return `${metadata.projectId}@appspot.gserviceaccount.com`; } else if (e.platform === "gcfv2" || e.platform === "run") { return await (0, computeEngine_1.getDefaultServiceAccount)(metadata.projectNumber); } (0, functional_1.assertExhaustive)(e.platform); } function nodeBillingError(projectId) { return new error_1.FirebaseError(`Cloud Functions deployment requires the pay-as-you-go (Blaze) billing plan. To upgrade your project, visit the following URL: https://console.firebase.google.com/project/${projectId}/usage/details For additional information about this requirement, see Firebase FAQs: ${FAQ_URL}`, { exit: 1 }); } function nodePermissionError(projectId) { return new error_1.FirebaseError(`Cloud Functions deployment requires the Cloud Build API to be enabled. The current credentials do not have permission to enable APIs for project ${clc.bold(projectId)}. Please ask a project owner to visit the following URL to enable Cloud Build: https://console.cloud.google.com/apis/library/cloudbuild.googleapis.com?project=${projectId} For additional information about this requirement, see Firebase FAQs: ${FAQ_URL} `); } function isPermissionError(e) { return e.context?.body?.error?.status === "PERMISSION_DENIED"; } async function cloudBuildEnabled(projectId) { try { await ensureApiEnabled.ensure(projectId, (0, api_1.cloudbuildOrigin)(), "functions"); } catch (e) { if ((0, error_1.isBillingError)(e)) { throw nodeBillingError(projectId); } else if (isPermissionError(e)) { throw nodePermissionError(projectId); } throw e; } } async function secretsToServiceAccounts(b) { const secretsToSa = {}; for (const e of backend.allEndpoints(b)) { if (!e.secretEnvironmentVariables || e.secretEnvironmentVariables.length === 0) { continue; } const sa = e.serviceAccount || (await module.exports.defaultServiceAccount(e)); for (const s of e.secretEnvironmentVariables) { const serviceAccounts = secretsToSa[s.secret] || new Set(); serviceAccounts.add(sa); secretsToSa[s.secret] = serviceAccounts; } } return secretsToSa; } async function secretsAccessDelta(args) { const { wantBackend, haveBackend } = args; const wantSecrets = await secretsToServiceAccounts(wantBackend); const haveSecrets = await secretsToServiceAccounts(haveBackend); for (const [secret, serviceAccounts] of Object.entries(haveSecrets)) { for (const serviceAccount of serviceAccounts) { wantSecrets[secret]?.delete(serviceAccount); } if (wantSecrets[secret]?.size === 0) { delete wantSecrets[secret]; } } const delta = {}; for (const [secret, serviceAccounts] of Object.entries(wantSecrets)) { if (serviceAccounts.size > 0) { delta[secret] = Array.from(serviceAccounts); } } return delta; } async function checkSecretAccess(projectId, secretAccessDelta) { for (const [secret, serviceAccounts] of Object.entries(secretAccessDelta)) { (0, utils_1.logLabeledBullet)("functions", `ensuring ${clc.bold(serviceAccounts.join(", "))} access to secret ${clc.bold(secret)}.`); const check = await (0, secretManager_1.checkServiceAgentRole)({ name: secret, projectId }, serviceAccounts, "roles/secretmanager.secretAccessor"); if (check.length) { (0, utils_1.logLabeledBullet)("functions", `On your next deploy, ${clc.bold(serviceAccounts.join(", "))} will be granted access to secret ${clc.bold(secret)}.`); } } } async function grantSecretAccess(args) { const { projectId, secret, serviceAccounts } = args; (0, utils_1.logLabeledBullet)("functions", `ensuring ${clc.bold(serviceAccounts.join(", "))} access to secret ${clc.bold(secret)}.`); await (0, secretManager_1.ensureServiceAgentRole)({ name: secret, projectId }, serviceAccounts, "roles/secretmanager.secretAccessor"); (0, utils_1.logLabeledSuccess)("functions", `ensured ${clc.bold(serviceAccounts.join(", "))} access to ${clc.bold(secret)}.`); } exports.REQUIRED_SECURITY_APIS = [ "iam.googleapis.com", "cloudresourcemanager.googleapis.com", ]; async function checkDeclarativeSecurityApisEnabled(projectId, codebase) { const checks = await Promise.all(exports.REQUIRED_SECURITY_APIS.map(async (api) => { try { return await ensureApiEnabled.check(projectId, api, "functions", true); } catch (err) { const isPermissionDenied = err?.status === 403 || isPermissionError(err); if (isPermissionDenied) { logger_1.logger.debug(`Silencing permission error checking enablement for API ${api}:`, err); return true; } throw err; } })); const disabledApis = exports.REQUIRED_SECURITY_APIS.filter((_, idx) => !checks[idx]); if (disabledApis.length > 0) { const apiBulletList = disabledApis.map((api) => ` - ${clc.bold(api)}`).join("\n"); const enableCmd = clc.bold(`gcloud services enable ${disabledApis.join(" ")} --project ${projectId}`); const consoleLinks = disabledApis .map((api) => ` - ${api}: ${ensureApiEnabled.enableApiURI(projectId, api)}`) .join("\n"); throw new error_1.FirebaseError(`Cannot deploy functions with declarative security in codebase "${codebase}". ` + `The following required Google Cloud API(s) are not enabled on project ${clc.bold(projectId)}:\n` + apiBulletList + `\n\nDeclarative security requires these APIs to provision and configure managed service accounts and IAM roles.\n` + `To enable them, run:\n\n` + ` ${enableCmd}\n\n` + `Or ask a project owner to enable them in the Google Cloud Console:\n` + consoleLinks + `\n`, { exit: 1 }); } }