firebase-tools
Version:
Command-Line Interface for Firebase
364 lines (363 loc) • 13.9 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageRulesRuntime = exports.StorageRulesIssues = exports.StorageRulesetInstance = void 0;
const cross_spawn_1 = require("cross-spawn");
const error_1 = require("../../../error");
const AsyncLock = require("async-lock");
const types_1 = require("./types");
const jwt = require("jsonwebtoken");
const emulatorLogger_1 = require("../../emulatorLogger");
const types_2 = require("../../types");
const metadata_1 = require("../metadata");
const utils = require("../../../utils");
const constants_1 = require("../../constants");
const download_1 = require("../../download");
const fs = require("fs-extra");
const downloadableEmulators_1 = require("../../downloadableEmulators");
const registry_1 = require("../../registry");
const lock = new AsyncLock();
const synchonizationKey = "key";
class StorageRulesetInstance {
constructor(runtime, rulesVersion, rulesetName) {
this.runtime = runtime;
this.rulesVersion = rulesVersion;
this.rulesetName = rulesetName;
}
async verify(opts, runtimeVariableOverrides = {}) {
if (opts.method === types_1.RulesetOperationMethod.LIST && this.rulesVersion < 2) {
const issues = new StorageRulesIssues();
issues.warnings.push("Permission denied. List operations are only allowed for rules_version='2'.");
return {
permitted: false,
issues,
};
}
return this.runtime.verifyWithRuleset(this.rulesetName, opts, runtimeVariableOverrides);
}
unload() {
throw new Error("NOT_IMPLEMENTED");
}
}
exports.StorageRulesetInstance = StorageRulesetInstance;
class StorageRulesIssues {
constructor(errors = [], warnings = []) {
this.errors = errors;
this.warnings = warnings;
}
static fromResponse(resp) {
return new StorageRulesIssues(resp.errors || [], resp.warnings || []);
}
get all() {
return [...this.errors, ...this.warnings];
}
exist() {
return !!(this.errors.length || this.warnings.length);
}
extend(other) {
this.errors.push(...other.errors);
this.warnings.push(...other.warnings);
}
}
exports.StorageRulesIssues = StorageRulesIssues;
class StorageRulesRuntime {
constructor() {
this._rulesetCount = 0;
this._requestCount = 0;
this._requests = {};
this._alive = false;
}
get alive() {
return this._alive;
}
async start(autoDownload = true) {
var _a, _b;
if (this.alive) {
return;
}
const downloadDetails = (0, downloadableEmulators_1.getDownloadDetails)(types_2.Emulators.STORAGE);
const hasEmulator = fs.existsSync(downloadDetails.downloadPath);
if (!hasEmulator) {
if (autoDownload) {
if (process.env.CI) {
utils.logWarning(`It appears you are running in a CI environment. You can avoid downloading the ${constants_1.Constants.description(types_2.Emulators.STORAGE)} repeatedly by caching the ${downloadDetails.opts.cacheDir} directory.`);
}
await (0, download_1.downloadEmulator)(types_2.Emulators.STORAGE);
}
else {
utils.logWarning("Setup required, please run: firebase setup:emulators:storage");
throw new error_1.FirebaseError("emulator not found");
}
}
this._alive = true;
const command = (0, downloadableEmulators_1._getCommand)(types_2.Emulators.STORAGE, {});
this._childprocess = (0, cross_spawn_1.spawn)(command.binary, command.args, {
stdio: ["pipe", "pipe", "pipe"],
});
this._childprocess.on("exit", () => {
var _a;
this._alive = false;
(_a = this._childprocess) === null || _a === void 0 ? void 0 : _a.removeAllListeners();
this._childprocess = undefined;
});
const startPromise = new Promise((resolve) => {
this._requests[-1] = {
handler: resolve,
request: {
action: "",
id: -1,
},
};
});
this._childprocess.on("error", (err) => {
void (0, downloadableEmulators_1.handleEmulatorProcessError)(types_2.Emulators.STORAGE, err);
});
(_a = this._childprocess.stderr) === null || _a === void 0 ? void 0 : _a.on("data", (buf) => {
const error = buf.toString();
if (error.includes("jarfile")) {
emulatorLogger_1.EmulatorLogger.forEmulator(types_2.Emulators.STORAGE).log("ERROR", error);
throw new error_1.FirebaseError("There was an issue starting the rules emulator, please run 'firebase setup:emulators:storage` again");
}
else {
emulatorLogger_1.EmulatorLogger.forEmulator(types_2.Emulators.STORAGE).log("WARN", `Unexpected rules runtime error: ${buf.toString()}`);
}
});
(_b = this._childprocess.stdout) === null || _b === void 0 ? void 0 : _b.on("data", (buf) => {
var _a;
const serializedRuntimeActionResponse = buf.toString("utf-8").trim();
if (serializedRuntimeActionResponse !== "") {
let rap;
try {
rap = JSON.parse(serializedRuntimeActionResponse);
}
catch (err) {
emulatorLogger_1.EmulatorLogger.forEmulator(types_2.Emulators.STORAGE).log("INFO", serializedRuntimeActionResponse);
return;
}
const id = (_a = rap.id) !== null && _a !== void 0 ? _a : rap.server_request_id;
if (id === undefined) {
console.log(`Received no ID from server response ${serializedRuntimeActionResponse}`);
return;
}
const request = this._requests[id];
if (rap.status !== "ok" && !("action" in rap)) {
console.warn(`[RULES] ${rap.status}: ${rap.message}`);
rap.errors.forEach(console.warn.bind(console));
return;
}
if (request) {
request.handler(rap);
}
else {
console.log(`No handler for event ${serializedRuntimeActionResponse}`);
}
}
});
return startPromise;
}
stop() {
emulatorLogger_1.EmulatorLogger.forEmulator(types_2.Emulators.STORAGE).log("DEBUG", "Stopping rules runtime.");
return new Promise((resolve) => {
var _a;
if (this.alive) {
this._childprocess.on("exit", () => {
resolve();
});
(_a = this._childprocess) === null || _a === void 0 ? void 0 : _a.kill("SIGINT");
}
else {
resolve();
}
});
}
async _sendRequest(rab, overrideId) {
if (!this._childprocess) {
throw new error_1.FirebaseError("Failed to send Cloud Storage rules request due to rules runtime not available.");
}
const runtimeActionRequest = Object.assign(Object.assign({}, rab), { id: overrideId !== null && overrideId !== void 0 ? overrideId : this._requestCount++ });
if (overrideId !== undefined) {
delete this._requests[overrideId];
}
else if (this._requests[runtimeActionRequest.id]) {
throw new error_1.FirebaseError("Attempted to send Cloud Storage rules request with stale id");
}
return new Promise((resolve) => {
this._requests[runtimeActionRequest.id] = {
request: runtimeActionRequest,
handler: resolve,
};
const serializedRequest = JSON.stringify(runtimeActionRequest);
lock.acquire(synchonizationKey, (done) => {
var _a, _b;
(_b = (_a = this._childprocess) === null || _a === void 0 ? void 0 : _a.stdin) === null || _b === void 0 ? void 0 : _b.write(serializedRequest + "\n");
setTimeout(() => {
done();
}, 15);
});
});
}
async loadRuleset(source) {
const runtimeActionRequest = {
action: "load_ruleset",
context: {
rulesetName: (this._rulesetCount++).toString(),
source,
},
};
const response = (await this._sendRequest(runtimeActionRequest));
if (response.errors.length) {
return {
issues: StorageRulesIssues.fromResponse(response),
};
}
else {
return {
issues: StorageRulesIssues.fromResponse(response),
ruleset: new StorageRulesetInstance(this, response.result.rulesVersion, runtimeActionRequest.context.rulesetName),
};
}
}
async verifyWithRuleset(rulesetName, opts, runtimeVariableOverrides = {}) {
if (!opts.path.startsWith("/")) {
opts.path = `/${opts.path}`;
}
if (opts.path.endsWith("/")) {
opts.path = opts.path.slice(0, -1);
}
const runtimeVariables = Object.assign({ resource: toExpressionValue(opts.file.before || null), request: createRequestExpressionValue(opts) }, runtimeVariableOverrides);
const runtimeActionRequest = {
action: "verify",
context: {
rulesetName: rulesetName,
service: "firebase.storage",
path: opts.path,
method: opts.method,
delimiter: opts.delimiter,
variables: runtimeVariables,
},
};
return this._completeVerifyWithRuleset(opts.projectId, runtimeActionRequest);
}
async _completeVerifyWithRuleset(projectId, runtimeActionRequest, overrideId) {
const response = (await this._sendRequest(runtimeActionRequest, overrideId));
if ("context" in response) {
const dataResponse = await fetchFirestoreDocument(projectId, response);
return this._completeVerifyWithRuleset(projectId, dataResponse, response.server_request_id);
}
if (!response.errors)
response.errors = [];
if (!response.warnings)
response.warnings = [];
if (response.errors.length) {
return {
issues: StorageRulesIssues.fromResponse(response),
};
}
else {
return {
issues: StorageRulesIssues.fromResponse(response),
permitted: response.result.permit,
};
}
}
}
exports.StorageRulesRuntime = StorageRulesRuntime;
function toExpressionValue(obj) {
if (typeof obj === "string") {
return { string_value: obj };
}
if (typeof obj === "boolean") {
return { bool_value: obj };
}
if (typeof obj === "number") {
if (Math.floor(obj) === obj) {
return { int_value: obj };
}
else {
return { float_value: obj };
}
}
if (obj instanceof Date) {
return {
timestamp_value: (0, metadata_1.toSerializedDate)(obj),
};
}
if (Array.isArray(obj)) {
return {
list_value: {
values: obj.map(toExpressionValue),
},
};
}
if (obj instanceof Set) {
return {
set_value: {
values: [...obj].map(toExpressionValue),
},
};
}
if (obj == null) {
return {
null_value: null,
};
}
if (typeof obj === "object") {
const fields = {};
Object.keys(obj).forEach((key) => {
fields[key] = toExpressionValue(obj[key]);
});
return {
map_value: {
fields,
},
};
}
throw new error_1.FirebaseError(`Cannot convert "${obj}" of type ${typeof obj} for Firebase Storage rules runtime`);
}
async function fetchFirestoreDocument(projectId, request) {
const pathname = `projects/${projectId}${request.context.path}`;
const client = registry_1.EmulatorRegistry.client(types_2.Emulators.FIRESTORE, { apiVersion: "v1", auth: true });
try {
const doc = await client.get(pathname);
const { name, fields } = doc.body;
const result = { name, fields };
return { result, status: types_1.DataLoadStatus.OK, warnings: [], errors: [] };
}
catch (e) {
return { status: types_1.DataLoadStatus.NOT_FOUND, warnings: [], errors: [] };
}
}
function createAuthExpressionValue(opts) {
if (!opts.token) {
return toExpressionValue(null);
}
else {
const tokenPayload = jwt.decode(opts.token, { json: true });
const jsonValue = {
uid: tokenPayload.user_id,
token: tokenPayload,
};
return toExpressionValue(jsonValue);
}
}
function createRequestExpressionValue(opts) {
const fields = {
path: {
path_value: {
segments: opts.path
.split("/")
.filter((s) => s)
.map((simple) => ({
simple,
})),
},
},
time: toExpressionValue(new Date()),
resource: toExpressionValue(opts.file.after ? opts.file.after : null),
auth: opts.token ? createAuthExpressionValue(opts) : { null_value: null },
};
return {
map_value: {
fields,
},
};
}
;