説明なし
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

rulesDeploy.js 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.RulesDeploy = exports.RulesetServiceType = void 0;
  4. const _ = require("lodash");
  5. const colorette_1 = require("colorette");
  6. const fs = require("fs-extra");
  7. const gcp = require("./gcp");
  8. const logger_1 = require("./logger");
  9. const error_1 = require("./error");
  10. const utils = require("./utils");
  11. const prompt_1 = require("./prompt");
  12. const getProjectNumber_1 = require("./getProjectNumber");
  13. const resourceManager_1 = require("./gcp/resourceManager");
  14. const QUOTA_EXCEEDED_STATUS_CODE = 429;
  15. const RULESET_COUNT_LIMIT = 1000;
  16. const RULESETS_TO_GC = 10;
  17. const CROSS_SERVICE_FUNCTIONS = /firestore\.(get|exists)/;
  18. const CROSS_SERVICE_RULES_ROLE = "roles/firebaserules.firestoreServiceAgent";
  19. var RulesetServiceType;
  20. (function (RulesetServiceType) {
  21. RulesetServiceType["CLOUD_FIRESTORE"] = "cloud.firestore";
  22. RulesetServiceType["FIREBASE_STORAGE"] = "firebase.storage";
  23. })(RulesetServiceType = exports.RulesetServiceType || (exports.RulesetServiceType = {}));
  24. const RulesetType = {
  25. [RulesetServiceType.CLOUD_FIRESTORE]: "firestore",
  26. [RulesetServiceType.FIREBASE_STORAGE]: "storage",
  27. };
  28. class RulesDeploy {
  29. constructor(options, type) {
  30. this.options = options;
  31. this.type = type;
  32. this.project = options.project;
  33. this.rulesFiles = {};
  34. this.rulesetNames = {};
  35. }
  36. addFile(path) {
  37. const fullPath = this.options.config.path(path);
  38. let src;
  39. try {
  40. src = fs.readFileSync(fullPath, "utf8");
  41. }
  42. catch (e) {
  43. logger_1.logger.debug("[rules read error]", e.stack);
  44. throw new error_1.FirebaseError(`Error reading rules file ${(0, colorette_1.bold)(path)}`);
  45. }
  46. this.rulesFiles[path] = [{ name: path, content: src }];
  47. }
  48. async compile() {
  49. await Promise.all(Object.keys(this.rulesFiles).map((filename) => {
  50. return this.compileRuleset(filename, this.rulesFiles[filename]);
  51. }));
  52. }
  53. async getCurrentRules(service) {
  54. const latestName = await gcp.rules.getLatestRulesetName(this.options.project, service);
  55. let latestContent = null;
  56. if (latestName) {
  57. latestContent = await gcp.rules.getRulesetContent(latestName);
  58. }
  59. return { latestName, latestContent };
  60. }
  61. async checkStorageRulesIamPermissions(rulesContent) {
  62. if ((rulesContent === null || rulesContent === void 0 ? void 0 : rulesContent.match(CROSS_SERVICE_FUNCTIONS)) === null) {
  63. return;
  64. }
  65. if (this.options.nonInteractive) {
  66. return;
  67. }
  68. const projectNumber = await (0, getProjectNumber_1.getProjectNumber)(this.options);
  69. const saEmail = `service-${projectNumber}@gcp-sa-firebasestorage.iam.gserviceaccount.com`;
  70. try {
  71. if (await (0, resourceManager_1.serviceAccountHasRoles)(projectNumber, saEmail, [CROSS_SERVICE_RULES_ROLE], true)) {
  72. return;
  73. }
  74. const addRole = await (0, prompt_1.promptOnce)({
  75. type: "confirm",
  76. name: "rulesRole",
  77. message: `Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?`,
  78. default: true,
  79. }, this.options);
  80. if (addRole) {
  81. await (0, resourceManager_1.addServiceAccountToRoles)(projectNumber, saEmail, [CROSS_SERVICE_RULES_ROLE], true);
  82. utils.logLabeledBullet(RulesetType[this.type], "updated service account for cross-service rules...");
  83. }
  84. }
  85. catch (e) {
  86. logger_1.logger.warn("[rules] Error checking or updating Cloud Storage for Firebase service account permissions.");
  87. logger_1.logger.warn("[rules] Cross-service Storage rules may not function properly", e.message);
  88. }
  89. }
  90. async createRulesets(service) {
  91. var _a;
  92. const createdRulesetNames = [];
  93. const { latestName: latestRulesetName, latestContent: latestRulesetContent } = await this.getCurrentRules(service);
  94. const newRulesetsByFilename = new Map();
  95. for (const [filename, files] of Object.entries(this.rulesFiles)) {
  96. if (latestRulesetName && _.isEqual(files, latestRulesetContent)) {
  97. utils.logLabeledBullet(RulesetType[this.type], `latest version of ${(0, colorette_1.bold)(filename)} already up to date, skipping upload...`);
  98. this.rulesetNames[filename] = latestRulesetName;
  99. continue;
  100. }
  101. if (service === RulesetServiceType.FIREBASE_STORAGE) {
  102. await this.checkStorageRulesIamPermissions((_a = files[0]) === null || _a === void 0 ? void 0 : _a.content);
  103. }
  104. utils.logLabeledBullet(RulesetType[this.type], `uploading rules ${(0, colorette_1.bold)(filename)}...`);
  105. newRulesetsByFilename.set(filename, gcp.rules.createRuleset(this.options.project, files));
  106. }
  107. try {
  108. await Promise.all(newRulesetsByFilename.values());
  109. for (const [filename, rulesetName] of newRulesetsByFilename) {
  110. this.rulesetNames[filename] = await rulesetName;
  111. createdRulesetNames.push(await rulesetName);
  112. }
  113. }
  114. catch (err) {
  115. if (err.status !== QUOTA_EXCEEDED_STATUS_CODE) {
  116. throw err;
  117. }
  118. utils.logLabeledBullet(RulesetType[this.type], "quota exceeded error while uploading rules");
  119. const history = await gcp.rules.listAllRulesets(this.options.project);
  120. if (history.length > RULESET_COUNT_LIMIT) {
  121. const confirm = await (0, prompt_1.promptOnce)({
  122. type: "confirm",
  123. name: "force",
  124. message: `You have ${history.length} rules, do you want to delete the oldest ${RULESETS_TO_GC} to free up space?`,
  125. default: false,
  126. }, this.options);
  127. if (confirm) {
  128. const releases = await gcp.rules.listAllReleases(this.options.project);
  129. const unreleased = history.filter((ruleset) => {
  130. return !releases.find((release) => release.rulesetName === ruleset.name);
  131. });
  132. const entriesToDelete = unreleased.reverse().slice(0, RULESETS_TO_GC);
  133. for (const entry of entriesToDelete) {
  134. await gcp.rules.deleteRuleset(this.options.project, gcp.rules.getRulesetId(entry));
  135. logger_1.logger.debug(`[rules] Deleted ${entry.name}`);
  136. }
  137. utils.logLabeledWarning(RulesetType[this.type], "retrying rules upload");
  138. return this.createRulesets(service);
  139. }
  140. }
  141. }
  142. return createdRulesetNames;
  143. }
  144. async release(filename, resourceName, subResourceName) {
  145. if (resourceName === RulesetServiceType.FIREBASE_STORAGE && !subResourceName) {
  146. throw new error_1.FirebaseError(`Cannot release resource type "${resourceName}"`);
  147. }
  148. await gcp.rules.updateOrCreateRelease(this.options.project, this.rulesetNames[filename], resourceName === RulesetServiceType.FIREBASE_STORAGE
  149. ? `${resourceName}/${subResourceName}`
  150. : resourceName);
  151. utils.logLabeledSuccess(RulesetType[this.type], `released rules ${(0, colorette_1.bold)(filename)} to ${(0, colorette_1.bold)(resourceName)}`);
  152. }
  153. async compileRuleset(filename, files) {
  154. utils.logLabeledBullet(this.type, `checking ${(0, colorette_1.bold)(filename)} for compilation errors...`);
  155. const response = await gcp.rules.testRuleset(this.options.project, files);
  156. if (_.get(response, "body.issues", []).length) {
  157. const warnings = [];
  158. const errors = [];
  159. response.body.issues.forEach((issue) => {
  160. const issueMessage = `[${issue.severity.substring(0, 1)}] ${issue.sourcePosition.line}:${issue.sourcePosition.column} - ${issue.description}`;
  161. if (issue.severity === "ERROR") {
  162. errors.push(issueMessage);
  163. }
  164. else {
  165. warnings.push(issueMessage);
  166. }
  167. });
  168. if (warnings.length > 0) {
  169. warnings.forEach((warning) => {
  170. utils.logWarning(warning);
  171. });
  172. }
  173. if (errors.length > 0) {
  174. const add = errors.length === 1 ? "" : "s";
  175. const message = `Compilation error${add} in ${(0, colorette_1.bold)(filename)}:\n${errors.join("\n")}`;
  176. throw new error_1.FirebaseError(message, { exit: 1 });
  177. }
  178. }
  179. utils.logLabeledSuccess(this.type, `rules file ${(0, colorette_1.bold)(filename)} compiled successfully`);
  180. }
  181. }
  182. exports.RulesDeploy = RulesDeploy;