UNPKG

firebase-tools

Version:
191 lines (190 loc) 9.94 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ensureServiceAgentRoles = exports.ensureGenkitMonitoringRoles = exports.obtainDefaultComputeServiceAgentBindings = exports.obtainPubSubServiceAgentBindings = exports.checkHttpIam = exports.checkServiceAccountIam = exports.GENKIT_MONITORING_ROLES = exports.EVENTARC_EVENT_RECEIVER_ROLE = exports.RUN_INVOKER_ROLE = exports.SERVICE_ACCOUNT_TOKEN_CREATOR_ROLE = void 0; const colorette_1 = require("colorette"); const logger_1 = require("../../logger"); const functionsDeployHelper_1 = require("./functionsDeployHelper"); const error_1 = require("../../error"); const functional_1 = require("../../functional"); const iam = require("../../gcp/iam"); const gce = require("../../gcp/computeEngine"); const backend = require("./backend"); const track_1 = require("../../track"); const utils = require("../../utils"); const resourceManager_1 = require("../../gcp/resourceManager"); const services_1 = require("./services"); const PERMISSION = "cloudfunctions.functions.setIamPolicy"; exports.SERVICE_ACCOUNT_TOKEN_CREATOR_ROLE = "roles/iam.serviceAccountTokenCreator"; exports.RUN_INVOKER_ROLE = "roles/run.invoker"; exports.EVENTARC_EVENT_RECEIVER_ROLE = "roles/eventarc.eventReceiver"; exports.GENKIT_MONITORING_ROLES = [ "roles/monitoring.metricWriter", "roles/cloudtrace.agent", "roles/logging.logWriter", ]; async function checkServiceAccountIam(projectId) { const saEmail = `${projectId}@appspot.gserviceaccount.com`; let passed = false; try { const iamResult = await iam.testResourceIamPermissions("https://iam.googleapis.com", "v1", `projects/${projectId}/serviceAccounts/${saEmail}`, ["iam.serviceAccounts.actAs"]); passed = iamResult.passed; } catch (err) { logger_1.logger.debug("[functions] service account IAM check errored, deploy may fail:", err); return; } if (!passed) { throw new error_1.FirebaseError(`Missing permissions required for functions deploy. You must have permission ${(0, colorette_1.bold)("iam.serviceAccounts.ActAs")} on service account ${(0, colorette_1.bold)(saEmail)}.\n\n` + `To address this error, ask a project Owner to assign your account the "Service Account User" role from this URL:\n\n` + `https://console.cloud.google.com/iam-admin/iam?project=${projectId}`); } } exports.checkServiceAccountIam = checkServiceAccountIam; async function checkHttpIam(context, options, payload) { if (!payload.functions) { return; } const filters = context.filters || (0, functionsDeployHelper_1.getEndpointFilters)(options); const wantBackends = Object.values(payload.functions).map(({ wantBackend }) => wantBackend); const httpEndpoints = [...(0, functional_1.flattenArray)(wantBackends.map((b) => backend.allEndpoints(b)))] .filter(backend.isHttpsTriggered) .filter((f) => (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(f, filters)); const existing = await backend.existingBackend(context); const newHttpsEndpoints = httpEndpoints.filter(backend.missingEndpoint(existing)); if (newHttpsEndpoints.length === 0) { return; } logger_1.logger.debug("[functions] found", newHttpsEndpoints.length, "new HTTP functions, testing setIamPolicy permission..."); let passed = true; try { const iamResult = await iam.testIamPermissions(context.projectId, [PERMISSION]); passed = iamResult.passed; } catch (e) { logger_1.logger.debug("[functions] failed http create setIamPolicy permission check. deploy may fail:", e); return; } if (!passed) { void (0, track_1.trackGA4)("error", { error_type: "Error (User)", details: "deploy:functions:http_create_missing_iam", }); throw new error_1.FirebaseError(`Missing required permission on project ${(0, colorette_1.bold)(context.projectId)} to deploy new HTTPS functions. The permission ${(0, colorette_1.bold)(PERMISSION)} is required to deploy the following functions:\n\n- ` + newHttpsEndpoints.map((func) => func.id).join("\n- ") + `\n\nTo address this error, please ask a project Owner to assign your account the "Cloud Functions Admin" role at the following URL:\n\nhttps://console.cloud.google.com/iam-admin/iam?project=${context.projectId}`); } logger_1.logger.debug("[functions] found setIamPolicy permission, proceeding with deploy"); } exports.checkHttpIam = checkHttpIam; function getPubsubServiceAgent(projectNumber) { return `service-${projectNumber}@gcp-sa-pubsub.iam.gserviceaccount.com`; } function reduceEventsToServices(services, endpoint) { const service = (0, services_1.serviceForEndpoint)(endpoint); if (service.requiredProjectBindings && !services.find((s) => s.name === service.name)) { services.push(service); } return services; } function isGenkitEndpoint(endpoint) { return (backend.isCallableTriggered(endpoint) && endpoint.callableTrigger.genkitAction !== undefined); } function obtainPubSubServiceAgentBindings(projectNumber) { const serviceAccountTokenCreatorBinding = { role: exports.SERVICE_ACCOUNT_TOKEN_CREATOR_ROLE, members: [`serviceAccount:${getPubsubServiceAgent(projectNumber)}`], }; return [serviceAccountTokenCreatorBinding]; } exports.obtainPubSubServiceAgentBindings = obtainPubSubServiceAgentBindings; async function obtainDefaultComputeServiceAgentBindings(projectNumber) { const defaultComputeServiceAgent = `serviceAccount:${await gce.getDefaultServiceAccount(projectNumber)}`; const runInvokerBinding = { role: exports.RUN_INVOKER_ROLE, members: [defaultComputeServiceAgent], }; const eventarcEventReceiverBinding = { role: exports.EVENTARC_EVENT_RECEIVER_ROLE, members: [defaultComputeServiceAgent], }; return [runInvokerBinding, eventarcEventReceiverBinding]; } exports.obtainDefaultComputeServiceAgentBindings = obtainDefaultComputeServiceAgentBindings; async function ensureGenkitMonitoringRoles(projectId, projectNumber, want, have, dryRun) { const wantEndpoints = backend.allEndpoints(want).filter(isGenkitEndpoint); const newEndpoints = wantEndpoints.filter(backend.missingEndpoint(have)); if (newEndpoints.length === 0) { return; } const serviceAccounts = newEndpoints .map((endpoint) => endpoint.serviceAccount || "") .filter((value, index, self) => self.indexOf(value) === index); const defaultServiceAccountIndex = serviceAccounts.indexOf(""); if (defaultServiceAccountIndex) { serviceAccounts[defaultServiceAccountIndex] = await gce.getDefaultServiceAccount(projectNumber); } const members = serviceAccounts.map((sa) => `serviceAccount:${sa}`); const requiredBindings = []; for (const monitoringRole of exports.GENKIT_MONITORING_ROLES) { requiredBindings.push({ role: monitoringRole, members: members, }); } await ensureBindings(projectId, projectNumber, requiredBindings, newEndpoints.map((endpoint) => endpoint.id), dryRun); } exports.ensureGenkitMonitoringRoles = ensureGenkitMonitoringRoles; async function ensureServiceAgentRoles(projectId, projectNumber, want, have, dryRun) { const wantServices = backend.allEndpoints(want).reduce(reduceEventsToServices, []); const haveServices = backend.allEndpoints(have).reduce(reduceEventsToServices, []); const newServices = wantServices.filter((wantS) => !haveServices.find((haveS) => wantS.name === haveS.name)); if (newServices.length === 0) { return; } const requiredBindingsPromises = []; for (const service of newServices) { requiredBindingsPromises.push(service.requiredProjectBindings(projectNumber)); } const nestedRequiredBindings = await Promise.all(requiredBindingsPromises); const requiredBindings = [...(0, functional_1.flattenArray)(nestedRequiredBindings)]; if (haveServices.length === 0) { requiredBindings.push(...obtainPubSubServiceAgentBindings(projectNumber)); requiredBindings.push(...(await obtainDefaultComputeServiceAgentBindings(projectNumber))); } if (requiredBindings.length === 0) { return; } await ensureBindings(projectId, projectNumber, requiredBindings, newServices.map((service) => service.api), dryRun); } exports.ensureServiceAgentRoles = ensureServiceAgentRoles; async function ensureBindings(projectId, projectNumber, requiredBindings, newServicesOrEndpoints, dryRun) { let policy; try { policy = await (0, resourceManager_1.getIamPolicy)(projectNumber); } catch (err) { iam.printManualIamConfig(requiredBindings, projectId, "functions"); utils.logLabeledBullet("functions", "Could not verify the necessary IAM configuration for the following newly-integrated services: " + `${newServicesOrEndpoints.join(", ")}` + ". Deployment may fail.", "warn"); return; } const hasUpdatedBindings = iam.mergeBindings(policy, requiredBindings); if (!hasUpdatedBindings) { return; } try { if (dryRun) { logger_1.logger.info(`On your next deploy, the following required roles will be granted: ${requiredBindings.map((b) => `${b.members.join(", ")}: ${(0, colorette_1.bold)(b.role)}`)}`); } else { await (0, resourceManager_1.setIamPolicy)(projectNumber, policy, "bindings"); } } catch (err) { iam.printManualIamConfig(requiredBindings, projectId, "functions"); throw new error_1.FirebaseError("We failed to modify the IAM policy for the project. The functions " + "deployment requires specific roles to be granted to service agents," + " otherwise the deployment will fail.", { original: err }); } }