///
///
///
///
require('source-map-support').install();
import fs = require("fs");
import tv4 = require('tv4');
import expression = require('../src/expression');
import schema = require('../src/schema');
import path = require('path');
import Json = require('source-processor');
import error = require('source-processor');
export var debug = false;
/**
* synchronously loads a file resource, converting from YAML to JSON
* @param filepath location on filesystem
*/
export function load_yaml(filepath: string): Json.JValue{
var yaml_text = fs.readFileSync(filepath, {encoding: 'utf8'}).toString();
return Json.parse_yaml(yaml_text);
}
export function load_json(filepath: string): Json.JValue{
var json_text = fs.readFileSync(filepath, {encoding: 'utf8'}).toString();
return Json.parse(json_text);
}
/**
* synchronously loads a file resource, converting from multi document YAML to a callback with a JSON param
*/
export function load_yaml_collection(filepath: string, cb:(json: Json.JValue) => void):void{
var yaml_text = fs.readFileSync(filepath, {encoding: 'utf8'}).toString();
Json.parse_yaml_collection(yaml_text, cb);
}
export var root:string = path.dirname(fs.realpathSync(__filename)) + "/../";
export var rules_schema: Json.JValue = load_yaml(root + "schema/security_rules.yaml");
export function validate_rules(rules: Json.JValue): boolean{
tv4.addSchema("http://firebase.com/schema/types/object#", this.load_yaml(root + "schema/types/object.yaml").toJSON());
tv4.addSchema("http://firebase.com/schema/types/string#", this.load_yaml(root + "schema/types/string.yaml").toJSON());
tv4.addSchema("http://firebase.com/schema/types/boolean#",this.load_yaml(root + "schema/types/boolean.yaml").toJSON());
tv4.addSchema("http://firebase.com/schema/types/number#", this.load_yaml(root + "schema/types/number.yaml").toJSON());
tv4.addSchema("http://firebase.com/schema/types/any#", this.load_yaml(root + "schema/types/any.yaml").toJSON());
tv4.addSchema("http://firebase.com/schema#", this.load_yaml(root + "schema/schema.yaml").toJSON());
tv4.addSchema("http://json-schema.org/draft-04/schema#", fs.readFileSync(root + "schema/jsonschema", {encoding: 'utf8'}).toString());
var valid: boolean = tv4.validate(rules.toJSON(), this.rules_schema.toJSON(), true, true);
if (!valid){
throw error.validation(
rules,
this.rules_schema,
"blaze file",
"blaze schema",
tv4.error)
.source(rules.lookup(tv4.error.dataPath.split("/")))
.on(new Error())
}
if (tv4.getMissingUris().length != 0){
throw error.missingURI(tv4.getMissingUris()).on(new Error());
}
return valid;
}
export class SchemaRoot{
json: Json.JValue;
root: schema.SchemaNode;
constructor(json: Json.JValue){
this.json = json;
}
static parse(json: Json.JValue): SchemaRoot {
if (json == null) {
json = new Json.JObject()
}
return new SchemaRoot(json);
}
}
export class AccessEntry{
location: string[]; //components of address, e.g. users/$userid is mapped to ['users', '$userid']
read: expression.Expression = expression.Expression.FALSE;
write: expression.Expression = expression.Expression.FALSE;
static parse(json: Json.JValue): AccessEntry{
//console.log("AccessEntry.parse:", json);
var accessEntry = new AccessEntry();
accessEntry.location =
json.getOrThrow("location", "all access entries require a location to be defined")
.asString().value.split("/");
//deal with issue of "/*" being split to "['', '*']" by removing first element
while (accessEntry.location[0] === ''){
accessEntry.location.shift();
}
if (accessEntry.location[accessEntry.location.length-1] == "") {
accessEntry.location.pop()
}
if (json.has("read")){
accessEntry.read = expression.Expression.parseUser(json.getOrThrow("read", "").coerceString());
}
if (json.has("write")){
accessEntry.write = expression.Expression.parseUser(json.getOrThrow("write", "").coerceString());
}
//console.log("accessEntry.location", accessEntry.location);
return accessEntry
}
match(location: string[]): boolean{
//console.log("checking if ", location, " matches ", this.location);
//candidate location must be at least as specific as this location
if(this.location.length > location.length) return false;
for(var idx in this.location){
if (!this.matchSegment(this.location[idx], location[idx])) return false
}
//console.log("access entry is applicable");
return true;
}
// todo hot method
isChildOf(location: string[]): boolean{
if (this.location.length <= location.length) return false;
for(var idx in location){
if (!this.matchSegment(this.location[idx], location[idx])) return false
}
return true;
}
matchSegment(rule_segment: string, path_segment): boolean{
if (rule_segment.indexOf("~$") == 0) rule_segment = rule_segment.substr(1);
if (path_segment.indexOf("~$") == 0) path_segment = path_segment.substr(1);
if (rule_segment.indexOf("$") == 0 && path_segment.indexOf("$") == 0) return true;
return rule_segment == path_segment
}
/**
*localize the read expression for the given path
*/
getReadFor(symbols: expression.Symbols, path: string[]): expression.Expression {
var base_read = expression.Expression.parse(this.read.expandFunctions(symbols));
for (var i = 0; i < path.length - this.location.length; i++) {
var rewrite = base_read.rewriteForChild();
base_read = expression.Expression.parse(rewrite)
}
return base_read
}
/**
*localize the read expression for the given path
* we have to expand functions first, so that the insides of functions are also localised
*/
getWriteFor(symbols: expression.Symbols, path: string[]): expression.Expression {
var base_write = expression.Expression.parse(this.write.expandFunctions(symbols));
for (var i = 0; i < path.length - this.location.length; i++) {
base_write = expression.Expression.parse(base_write.rewriteForChild())
}
return base_write
}
}
export class Access {
[index: number]: AccessEntry;
static parse(json: Json.JValue):Access{
//console.log("Access.parse:", json);
var access = new Access();
if (json == null) return access;
json.asArray().forEachIndexed(function(entry: Json.JValue, id: number){
var accessEntry = AccessEntry.parse(entry);
access[id] = (accessEntry);
});
return access
}
// todo hot method
static getChildren(self: Access, path: string[]): AccessEntry[] {
var children: AccessEntry[] = [];
for (var i = 0; self[i] != undefined; i++) {
var rule: AccessEntry = self[i];
if (rule.isChildOf(path)) children.push(rule)
}
return children;
}
}
export class Rules{
functions: expression.Functions;
schema: SchemaRoot;
access: Access;
code: string = null; //generated rules, or null
static parse(json: Json.JObject):Rules{
var rules = new Rules();
rules.functions = expression.Functions.parse(json.getOrNull("functions"));
rules.schema = SchemaRoot.parse(json.getOrNull("schema"));
rules.access = Access.parse(json.getOrWarn("access", "no access list defined, this Firebase will be inactive"));
return rules
}
/**
* adds dummy schema
*/
inflateSchema():void {
this.inflateSchemaRecursive(this.schema.json.asObject(), [], {})
}
inflateSchemaRecursive(json: Json.JObject, path: string[], memoization: {[path: string]: boolean}): void {
if (memoization[path.join("/")]) return; //we processed this before
else {
memoization[path.join("/")] = true; // indicate this has been done
}
// console.log("inflateSchemaRecursive", path);
var children = Access.getChildren(this.access, path);
children.map(function(child: AccessEntry) {
var childSegment = child.location[path.length];
var schemaChild = null;
if (childSegment.indexOf("~$") == 0 || childSegment.indexOf("$") == 0) {
//the child is a wildchild
var wildKey = schema.getWildchild(json);
if (wildKey == null){
schemaChild = new Json.JObject();
console.error("WARNING: " + child.location.join("/") + " in access rule but not in a schema");
json.put(new Json.JString(childSegment, -1,-1), schemaChild);
} else {
schemaChild = json.getOrThrow(wildKey.getString(), "error");
}
} else {
//the child is a fixed child, should be declared in properties
//lazy create properties if necessary
var properties = json.getOrNull("properties");
if (properties == null) {
properties = new Json.JObject();
json.put(new Json.JString("properties", -1,-1), properties)
}
//lazy add child if necessary
schemaChild = properties.asObject().getOrNull(childSegment);
if (schemaChild == null){
console.error("WARNING: " + child.location.join("/") + " in access rule but not in a schema");
schemaChild = new Json.JObject();
properties.asObject().put(new Json.JString(childSegment, -1,-1), schemaChild);
}
}
this.inflateSchemaRecursive(schemaChild, child.location.slice(0, path.length + 1), memoization);
}, this)
}
}