security-rules-api-client-internal.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /*! firebase-admin v12.1.1 */
  2. "use strict";
  3. /*!
  4. * Copyright 2019 Google Inc.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.SecurityRulesApiClient = void 0;
  20. const api_request_1 = require("../utils/api-request");
  21. const error_1 = require("../utils/error");
  22. const security_rules_internal_1 = require("./security-rules-internal");
  23. const utils = require("../utils/index");
  24. const validator = require("../utils/validator");
  25. const RULES_V1_API = 'https://firebaserules.googleapis.com/v1';
  26. const FIREBASE_VERSION_HEADER = {
  27. 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`,
  28. };
  29. /**
  30. * Class that facilitates sending requests to the Firebase security rules backend API.
  31. *
  32. * @private
  33. */
  34. class SecurityRulesApiClient {
  35. constructor(app) {
  36. this.app = app;
  37. if (!validator.isNonNullObject(app) || !('options' in app)) {
  38. throw new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', 'First argument passed to admin.securityRules() must be a valid Firebase app '
  39. + 'instance.');
  40. }
  41. this.httpClient = new api_request_1.AuthorizedHttpClient(app);
  42. }
  43. getRuleset(name) {
  44. return Promise.resolve()
  45. .then(() => {
  46. return this.getRulesetName(name);
  47. })
  48. .then((rulesetName) => {
  49. return this.getResource(rulesetName);
  50. });
  51. }
  52. createRuleset(ruleset) {
  53. if (!validator.isNonNullObject(ruleset) ||
  54. !validator.isNonNullObject(ruleset.source) ||
  55. !validator.isNonEmptyArray(ruleset.source.files)) {
  56. const err = new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', 'Invalid rules content.');
  57. return Promise.reject(err);
  58. }
  59. for (const rf of ruleset.source.files) {
  60. if (!validator.isNonNullObject(rf) ||
  61. !validator.isNonEmptyString(rf.name) ||
  62. !validator.isNonEmptyString(rf.content)) {
  63. const err = new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', `Invalid rules file argument: ${JSON.stringify(rf)}`);
  64. return Promise.reject(err);
  65. }
  66. }
  67. return this.getUrl()
  68. .then((url) => {
  69. const request = {
  70. method: 'POST',
  71. url: `${url}/rulesets`,
  72. data: ruleset,
  73. };
  74. return this.sendRequest(request);
  75. });
  76. }
  77. deleteRuleset(name) {
  78. return this.getUrl()
  79. .then((url) => {
  80. const rulesetName = this.getRulesetName(name);
  81. const request = {
  82. method: 'DELETE',
  83. url: `${url}/${rulesetName}`,
  84. };
  85. return this.sendRequest(request);
  86. });
  87. }
  88. listRulesets(pageSize = 100, pageToken) {
  89. if (!validator.isNumber(pageSize)) {
  90. const err = new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', 'Invalid page size.');
  91. return Promise.reject(err);
  92. }
  93. if (pageSize < 1 || pageSize > 100) {
  94. const err = new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', 'Page size must be between 1 and 100.');
  95. return Promise.reject(err);
  96. }
  97. if (typeof pageToken !== 'undefined' && !validator.isNonEmptyString(pageToken)) {
  98. const err = new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', 'Next page token must be a non-empty string.');
  99. return Promise.reject(err);
  100. }
  101. const data = {
  102. pageSize,
  103. pageToken,
  104. };
  105. if (!pageToken) {
  106. delete data.pageToken;
  107. }
  108. return this.getUrl()
  109. .then((url) => {
  110. const request = {
  111. method: 'GET',
  112. url: `${url}/rulesets`,
  113. data,
  114. };
  115. return this.sendRequest(request);
  116. });
  117. }
  118. getRelease(name) {
  119. return this.getResource(`releases/${name}`);
  120. }
  121. updateOrCreateRelease(name, rulesetName) {
  122. return this.updateRelease(name, rulesetName).catch((error) => {
  123. // if ruleset update failed with a NOT_FOUND error, attempt to create instead.
  124. if (error.code === `security-rules/${ERROR_CODE_MAPPING.NOT_FOUND}`) {
  125. return this.createRelease(name, rulesetName);
  126. }
  127. throw error;
  128. });
  129. }
  130. updateRelease(name, rulesetName) {
  131. return this.getUrl()
  132. .then((url) => {
  133. return this.getReleaseDescription(name, rulesetName)
  134. .then((release) => {
  135. const request = {
  136. method: 'PATCH',
  137. url: `${url}/releases/${name}`,
  138. data: { release },
  139. };
  140. return this.sendRequest(request);
  141. });
  142. });
  143. }
  144. createRelease(name, rulesetName) {
  145. return this.getUrl()
  146. .then((url) => {
  147. return this.getReleaseDescription(name, rulesetName)
  148. .then((release) => {
  149. const request = {
  150. method: 'POST',
  151. url: `${url}/releases`,
  152. data: release,
  153. };
  154. return this.sendRequest(request);
  155. });
  156. });
  157. }
  158. getUrl() {
  159. return this.getProjectIdPrefix()
  160. .then((projectIdPrefix) => {
  161. return `${RULES_V1_API}/${projectIdPrefix}`;
  162. });
  163. }
  164. getProjectIdPrefix() {
  165. if (this.projectIdPrefix) {
  166. return Promise.resolve(this.projectIdPrefix);
  167. }
  168. return utils.findProjectId(this.app)
  169. .then((projectId) => {
  170. if (!validator.isNonEmptyString(projectId)) {
  171. throw new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', 'Failed to determine project ID. Initialize the SDK with service account credentials, or '
  172. + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT '
  173. + 'environment variable.');
  174. }
  175. this.projectIdPrefix = `projects/${projectId}`;
  176. return this.projectIdPrefix;
  177. });
  178. }
  179. /**
  180. * Gets the specified resource from the rules API. Resource names must be the short names without project
  181. * ID prefix (e.g. `rulesets/ruleset-name`).
  182. *
  183. * @param {string} name Full qualified name of the resource to get.
  184. * @returns {Promise<T>} A promise that fulfills with the resource.
  185. */
  186. getResource(name) {
  187. return this.getUrl()
  188. .then((url) => {
  189. const request = {
  190. method: 'GET',
  191. url: `${url}/${name}`,
  192. };
  193. return this.sendRequest(request);
  194. });
  195. }
  196. getReleaseDescription(name, rulesetName) {
  197. return this.getProjectIdPrefix()
  198. .then((projectIdPrefix) => {
  199. return {
  200. name: `${projectIdPrefix}/releases/${name}`,
  201. rulesetName: `${projectIdPrefix}/${this.getRulesetName(rulesetName)}`,
  202. };
  203. });
  204. }
  205. getRulesetName(name) {
  206. if (!validator.isNonEmptyString(name)) {
  207. throw new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', 'Ruleset name must be a non-empty string.');
  208. }
  209. if (name.indexOf('/') !== -1) {
  210. throw new security_rules_internal_1.FirebaseSecurityRulesError('invalid-argument', 'Ruleset name must not contain any "/" characters.');
  211. }
  212. return `rulesets/${name}`;
  213. }
  214. sendRequest(request) {
  215. request.headers = FIREBASE_VERSION_HEADER;
  216. return this.httpClient.send(request)
  217. .then((resp) => {
  218. return resp.data;
  219. })
  220. .catch((err) => {
  221. throw this.toFirebaseError(err);
  222. });
  223. }
  224. toFirebaseError(err) {
  225. if (err instanceof error_1.PrefixedFirebaseError) {
  226. return err;
  227. }
  228. const response = err.response;
  229. if (!response.isJson()) {
  230. return new security_rules_internal_1.FirebaseSecurityRulesError('unknown-error', `Unexpected response with status: ${response.status} and body: ${response.text}`);
  231. }
  232. const error = response.data.error || {};
  233. let code = 'unknown-error';
  234. if (error.status && error.status in ERROR_CODE_MAPPING) {
  235. code = ERROR_CODE_MAPPING[error.status];
  236. }
  237. const message = error.message || `Unknown server error: ${response.text}`;
  238. return new security_rules_internal_1.FirebaseSecurityRulesError(code, message);
  239. }
  240. }
  241. exports.SecurityRulesApiClient = SecurityRulesApiClient;
  242. const ERROR_CODE_MAPPING = {
  243. INVALID_ARGUMENT: 'invalid-argument',
  244. NOT_FOUND: 'not-found',
  245. RESOURCE_EXHAUSTED: 'resource-exhausted',
  246. UNAUTHENTICATED: 'authentication-error',
  247. UNKNOWN: 'unknown-error',
  248. };