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.

checkIam.js 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.ensureServiceAgentRoles = exports.mergeBindings = exports.obtainDefaultComputeServiceAgentBindings = exports.obtainPubSubServiceAgentBindings = exports.getDefaultComputeServiceAgent = exports.checkHttpIam = exports.checkServiceAccountIam = exports.EVENTARC_EVENT_RECEIVER_ROLE = exports.RUN_INVOKER_ROLE = exports.SERVICE_ACCOUNT_TOKEN_CREATOR_ROLE = void 0;
  4. const colorette_1 = require("colorette");
  5. const logger_1 = require("../../logger");
  6. const functionsDeployHelper_1 = require("./functionsDeployHelper");
  7. const error_1 = require("../../error");
  8. const functional_1 = require("../../functional");
  9. const iam = require("../../gcp/iam");
  10. const backend = require("./backend");
  11. const track_1 = require("../../track");
  12. const utils = require("../../utils");
  13. const resourceManager_1 = require("../../gcp/resourceManager");
  14. const services_1 = require("./services");
  15. const PERMISSION = "cloudfunctions.functions.setIamPolicy";
  16. exports.SERVICE_ACCOUNT_TOKEN_CREATOR_ROLE = "roles/iam.serviceAccountTokenCreator";
  17. exports.RUN_INVOKER_ROLE = "roles/run.invoker";
  18. exports.EVENTARC_EVENT_RECEIVER_ROLE = "roles/eventarc.eventReceiver";
  19. async function checkServiceAccountIam(projectId) {
  20. const saEmail = `${projectId}@appspot.gserviceaccount.com`;
  21. let passed = false;
  22. try {
  23. const iamResult = await iam.testResourceIamPermissions("https://iam.googleapis.com", "v1", `projects/${projectId}/serviceAccounts/${saEmail}`, ["iam.serviceAccounts.actAs"]);
  24. passed = iamResult.passed;
  25. }
  26. catch (err) {
  27. logger_1.logger.debug("[functions] service account IAM check errored, deploy may fail:", err);
  28. return;
  29. }
  30. if (!passed) {
  31. 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` +
  32. `To address this error, ask a project Owner to assign your account the "Service Account User" role from this URL:\n\n` +
  33. `https://console.cloud.google.com/iam-admin/iam?project=${projectId}`);
  34. }
  35. }
  36. exports.checkServiceAccountIam = checkServiceAccountIam;
  37. async function checkHttpIam(context, options, payload) {
  38. if (!payload.functions) {
  39. return;
  40. }
  41. const filters = context.filters || (0, functionsDeployHelper_1.getEndpointFilters)(options);
  42. const wantBackends = Object.values(payload.functions).map(({ wantBackend }) => wantBackend);
  43. const httpEndpoints = [...(0, functional_1.flattenArray)(wantBackends.map((b) => backend.allEndpoints(b)))]
  44. .filter(backend.isHttpsTriggered)
  45. .filter((f) => (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(f, filters));
  46. const existing = await backend.existingBackend(context);
  47. const newHttpsEndpoints = httpEndpoints.filter(backend.missingEndpoint(existing));
  48. if (newHttpsEndpoints.length === 0) {
  49. return;
  50. }
  51. logger_1.logger.debug("[functions] found", newHttpsEndpoints.length, "new HTTP functions, testing setIamPolicy permission...");
  52. let passed = true;
  53. try {
  54. const iamResult = await iam.testIamPermissions(context.projectId, [PERMISSION]);
  55. passed = iamResult.passed;
  56. }
  57. catch (e) {
  58. logger_1.logger.debug("[functions] failed http create setIamPolicy permission check. deploy may fail:", e);
  59. return;
  60. }
  61. if (!passed) {
  62. void (0, track_1.track)("Error (User)", "deploy:functions:http_create_missing_iam");
  63. 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- ` +
  64. newHttpsEndpoints.map((func) => func.id).join("\n- ") +
  65. `\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}`);
  66. }
  67. logger_1.logger.debug("[functions] found setIamPolicy permission, proceeding with deploy");
  68. }
  69. exports.checkHttpIam = checkHttpIam;
  70. function getPubsubServiceAgent(projectNumber) {
  71. return `service-${projectNumber}@gcp-sa-pubsub.iam.gserviceaccount.com`;
  72. }
  73. function getDefaultComputeServiceAgent(projectNumber) {
  74. return `${projectNumber}-compute@developer.gserviceaccount.com`;
  75. }
  76. exports.getDefaultComputeServiceAgent = getDefaultComputeServiceAgent;
  77. function reduceEventsToServices(services, endpoint) {
  78. const service = (0, services_1.serviceForEndpoint)(endpoint);
  79. if (service.requiredProjectBindings && !services.find((s) => s.name === service.name)) {
  80. services.push(service);
  81. }
  82. return services;
  83. }
  84. function obtainPubSubServiceAgentBindings(projectNumber) {
  85. const serviceAccountTokenCreatorBinding = {
  86. role: exports.SERVICE_ACCOUNT_TOKEN_CREATOR_ROLE,
  87. members: [`serviceAccount:${getPubsubServiceAgent(projectNumber)}`],
  88. };
  89. return [serviceAccountTokenCreatorBinding];
  90. }
  91. exports.obtainPubSubServiceAgentBindings = obtainPubSubServiceAgentBindings;
  92. function obtainDefaultComputeServiceAgentBindings(projectNumber) {
  93. const defaultComputeServiceAgent = `serviceAccount:${getDefaultComputeServiceAgent(projectNumber)}`;
  94. const runInvokerBinding = {
  95. role: exports.RUN_INVOKER_ROLE,
  96. members: [defaultComputeServiceAgent],
  97. };
  98. const eventarcEventReceiverBinding = {
  99. role: exports.EVENTARC_EVENT_RECEIVER_ROLE,
  100. members: [defaultComputeServiceAgent],
  101. };
  102. return [runInvokerBinding, eventarcEventReceiverBinding];
  103. }
  104. exports.obtainDefaultComputeServiceAgentBindings = obtainDefaultComputeServiceAgentBindings;
  105. function mergeBindings(policy, requiredBindings) {
  106. let updated = false;
  107. for (const requiredBinding of requiredBindings) {
  108. const match = policy.bindings.find((b) => b.role === requiredBinding.role);
  109. if (!match) {
  110. updated = true;
  111. policy.bindings.push(requiredBinding);
  112. continue;
  113. }
  114. for (const requiredMember of requiredBinding.members) {
  115. if (!match.members.find((m) => m === requiredMember)) {
  116. updated = true;
  117. match.members.push(requiredMember);
  118. }
  119. }
  120. }
  121. return updated;
  122. }
  123. exports.mergeBindings = mergeBindings;
  124. function printManualIamConfig(requiredBindings, projectId) {
  125. utils.logLabeledBullet("functions", "Failed to verify the project has the correct IAM bindings for a successful deployment.", "warn");
  126. utils.logLabeledBullet("functions", "You can either re-run `firebase deploy` as a project owner or manually run the following set of `gcloud` commands:", "warn");
  127. for (const binding of requiredBindings) {
  128. for (const member of binding.members) {
  129. utils.logLabeledBullet("functions", `\`gcloud projects add-iam-policy-binding ${projectId} ` +
  130. `--member=${member} ` +
  131. `--role=${binding.role}\``, "warn");
  132. }
  133. }
  134. }
  135. async function ensureServiceAgentRoles(projectId, projectNumber, want, have) {
  136. const wantServices = backend.allEndpoints(want).reduce(reduceEventsToServices, []);
  137. const haveServices = backend.allEndpoints(have).reduce(reduceEventsToServices, []);
  138. const newServices = wantServices.filter((wantS) => !haveServices.find((haveS) => wantS.name === haveS.name));
  139. if (newServices.length === 0) {
  140. return;
  141. }
  142. const requiredBindingsPromises = [];
  143. for (const service of newServices) {
  144. requiredBindingsPromises.push(service.requiredProjectBindings(projectNumber));
  145. }
  146. const nestedRequiredBindings = await Promise.all(requiredBindingsPromises);
  147. const requiredBindings = [...(0, functional_1.flattenArray)(nestedRequiredBindings)];
  148. if (haveServices.length === 0) {
  149. requiredBindings.push(...obtainPubSubServiceAgentBindings(projectNumber));
  150. requiredBindings.push(...obtainDefaultComputeServiceAgentBindings(projectNumber));
  151. }
  152. if (requiredBindings.length === 0) {
  153. return;
  154. }
  155. let policy;
  156. try {
  157. policy = await (0, resourceManager_1.getIamPolicy)(projectNumber);
  158. }
  159. catch (err) {
  160. printManualIamConfig(requiredBindings, projectId);
  161. utils.logLabeledBullet("functions", "Could not verify the necessary IAM configuration for the following newly-integrated services: " +
  162. `${newServices.map((service) => service.api).join(", ")}` +
  163. ". Deployment may fail.", "warn");
  164. return;
  165. }
  166. const hasUpdatedBindings = mergeBindings(policy, requiredBindings);
  167. if (!hasUpdatedBindings) {
  168. return;
  169. }
  170. try {
  171. await (0, resourceManager_1.setIamPolicy)(projectNumber, policy, "bindings");
  172. }
  173. catch (err) {
  174. printManualIamConfig(requiredBindings, projectId);
  175. throw new error_1.FirebaseError("We failed to modify the IAM policy for the project. The functions " +
  176. "deployment requires specific roles to be granted to service agents," +
  177. " otherwise the deployment will fail.", { original: err });
  178. }
  179. }
  180. exports.ensureServiceAgentRoles = ensureServiceAgentRoles;