Switching to new security rules
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
349
src/rules/parser.ts
Normal file
349
src/rules/parser.ts
Normal file
@ -0,0 +1,349 @@
|
||||
import { Token } from "./tokenise";
|
||||
|
||||
export interface Node {
|
||||
type: string;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
export interface PathStatement extends Node {
|
||||
type: "path";
|
||||
segments: (string | { type: "variable"; name: string })[];
|
||||
}
|
||||
|
||||
export interface ValueStatement extends Node {
|
||||
type: "value";
|
||||
isNull: boolean;
|
||||
isTrue: boolean;
|
||||
isFalse: boolean;
|
||||
isNumber: boolean;
|
||||
isString: boolean;
|
||||
isVariable: boolean;
|
||||
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export type Operators = "&&" | "||" | "==" | "<=" | ">=" | "!=" | ">" | "<";
|
||||
|
||||
export interface Expression extends Node {
|
||||
type: "expression";
|
||||
|
||||
left: ValueStatement | Expression;
|
||||
operator: Operators;
|
||||
right: ValueStatement | Expression;
|
||||
}
|
||||
|
||||
export type Operations = "read" | "write" | "list"; // | "update" | "create" | "delete" | "list";
|
||||
|
||||
export interface AllowStatement extends Node {
|
||||
type: "permission";
|
||||
operations: Operations[];
|
||||
condition: Expression | ValueStatement;
|
||||
}
|
||||
|
||||
export interface MatchStatement extends Node {
|
||||
type: "match";
|
||||
path: PathStatement;
|
||||
matches: MatchStatement[];
|
||||
rules: AllowStatement[];
|
||||
}
|
||||
|
||||
export interface ServiceStatement extends Node {
|
||||
type: "service";
|
||||
name: string;
|
||||
matches: MatchStatement[];
|
||||
}
|
||||
|
||||
export class ParserError extends Error {
|
||||
token: Token;
|
||||
constructor(message: string, token: Token) {
|
||||
super(message);
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
export default function parse(tokens: Token[]) {
|
||||
const tokenIterator = tokens[Symbol.iterator]();
|
||||
let currentToken: Token = tokenIterator.next().value;
|
||||
let nextToken: Token = tokenIterator.next().value;
|
||||
|
||||
const eatToken = (value?: string) => {
|
||||
if (value && value !== currentToken.value) {
|
||||
throw new ParserError(
|
||||
`Unexpected token value, expected '${value}', received '${currentToken.value}'`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
let idx = currentToken.startIdx;
|
||||
currentToken = nextToken;
|
||||
nextToken = tokenIterator.next().value;
|
||||
return idx;
|
||||
};
|
||||
|
||||
const eatText = (): [string, number] => {
|
||||
checkTypes("text");
|
||||
let val = currentToken.value;
|
||||
let idx = currentToken.startIdx;
|
||||
eatToken();
|
||||
return [val, idx];
|
||||
};
|
||||
const eatNumber = (): number => {
|
||||
checkTypes("number");
|
||||
let val = Number(currentToken.value);
|
||||
if (Number.isNaN(val)) {
|
||||
throw new ParserError(
|
||||
`Value cannot be parsed as number! ${currentToken.value}`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
eatToken();
|
||||
return val;
|
||||
};
|
||||
|
||||
const checkTypes = (...types: string[]) => {
|
||||
if (types.indexOf(currentToken.type) < 0) {
|
||||
throw new ParserError(
|
||||
`Unexpected token value, expected ${types.join(" | ")}, received '${
|
||||
currentToken.value
|
||||
}'`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const parsePathStatement = (): PathStatement => {
|
||||
const segments: (string | { name: string; type: "variable" })[] = [];
|
||||
const idx = currentToken.startIdx;
|
||||
let next = currentToken.type === "slash";
|
||||
while (next) {
|
||||
eatToken("/");
|
||||
if (currentToken.type === "curly_open" && nextToken.type === "text") {
|
||||
eatToken("{");
|
||||
const [name] = eatText();
|
||||
segments.push({
|
||||
type: "variable",
|
||||
name,
|
||||
});
|
||||
eatToken("}");
|
||||
} else if (currentToken.type === "text") {
|
||||
const [name] = eatText();
|
||||
segments.push(name);
|
||||
}
|
||||
next = currentToken.type === "slash";
|
||||
}
|
||||
|
||||
return {
|
||||
type: "path",
|
||||
idx,
|
||||
segments,
|
||||
};
|
||||
};
|
||||
|
||||
const parseValue = (): ValueStatement => {
|
||||
const idx = currentToken.startIdx;
|
||||
|
||||
let isTrue = false;
|
||||
let isFalse = false;
|
||||
let isNull = false;
|
||||
let isVariable = false;
|
||||
let isNumber = false;
|
||||
let isString = false;
|
||||
let value: any = undefined;
|
||||
if (currentToken.type === "keyword") {
|
||||
if (currentToken.value === "true") isTrue = true;
|
||||
else if (currentToken.value === "false") isFalse = true;
|
||||
else if (currentToken.value === "null") isNull = true;
|
||||
else {
|
||||
throw new ParserError(
|
||||
`Invalid keyword at this position ${currentToken.value}`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
eatToken();
|
||||
} else if (currentToken.type === "string") {
|
||||
isString = true;
|
||||
value = currentToken.value.slice(1, currentToken.value.length - 1);
|
||||
eatToken();
|
||||
} else if (currentToken.type === "number") {
|
||||
isNumber = true;
|
||||
value = eatNumber();
|
||||
} else if (currentToken.type === "text") {
|
||||
isVariable = true;
|
||||
[value] = eatText();
|
||||
} else {
|
||||
throw new ParserError(
|
||||
`Expected value got ${currentToken.type}`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "value",
|
||||
isFalse,
|
||||
isNull,
|
||||
isNumber,
|
||||
isString,
|
||||
isTrue,
|
||||
isVariable,
|
||||
value,
|
||||
idx,
|
||||
};
|
||||
};
|
||||
|
||||
const parseCondition = (): Expression | ValueStatement => {
|
||||
// let running = true;
|
||||
let res: Expression | ValueStatement;
|
||||
let left: Expression | ValueStatement | undefined;
|
||||
|
||||
// while (running) {
|
||||
const idx = currentToken.startIdx;
|
||||
|
||||
if (!left) {
|
||||
if (currentToken.type === "bracket_open") {
|
||||
eatToken("(");
|
||||
left = parseCondition();
|
||||
eatToken(")");
|
||||
} else {
|
||||
left = parseValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToken.type === "comparison_operator") {
|
||||
const operator = currentToken.value;
|
||||
|
||||
eatToken();
|
||||
|
||||
let right: Expression | ValueStatement;
|
||||
|
||||
let ct = currentToken; //Quick hack because of TypeScript
|
||||
if (ct.type === "bracket_open") {
|
||||
eatToken("(");
|
||||
right = parseCondition();
|
||||
eatToken(")");
|
||||
} else {
|
||||
right = parseValue();
|
||||
}
|
||||
|
||||
res = {
|
||||
type: "expression",
|
||||
left,
|
||||
right,
|
||||
operator: operator as Operators,
|
||||
idx,
|
||||
};
|
||||
} else if (currentToken.type === "logic_operator") {
|
||||
const operator = currentToken.value;
|
||||
|
||||
eatToken();
|
||||
|
||||
const right = parseCondition();
|
||||
|
||||
res = {
|
||||
type: "expression",
|
||||
left,
|
||||
operator: operator as Operators,
|
||||
right,
|
||||
idx,
|
||||
};
|
||||
} else {
|
||||
res = left;
|
||||
}
|
||||
|
||||
// let ct = currentToken;
|
||||
// if (
|
||||
// ct.type === "comparison_operator" ||
|
||||
// ct.type === "logic_operator"
|
||||
// ) {
|
||||
// left = res;
|
||||
// } else {
|
||||
// running = false;
|
||||
// }
|
||||
// }
|
||||
return res;
|
||||
};
|
||||
|
||||
const parsePermissionStatement = (): AllowStatement => {
|
||||
const idx = eatToken("allow");
|
||||
|
||||
const operations: Operations[] = [];
|
||||
let next = currentToken.type !== "colon";
|
||||
while (next) {
|
||||
const [operation] = eatText();
|
||||
operations.push(operation as Operations);
|
||||
if (currentToken.type === "comma") {
|
||||
next = true;
|
||||
eatToken(",");
|
||||
} else {
|
||||
next = false;
|
||||
}
|
||||
}
|
||||
|
||||
eatToken(":");
|
||||
|
||||
eatToken("if");
|
||||
|
||||
const condition = parseCondition();
|
||||
|
||||
eatToken(";");
|
||||
|
||||
return {
|
||||
type: "permission",
|
||||
idx,
|
||||
operations,
|
||||
condition,
|
||||
};
|
||||
};
|
||||
|
||||
const parseMatchStatement = (): MatchStatement => {
|
||||
const idx = eatToken("match");
|
||||
const path = parsePathStatement();
|
||||
|
||||
eatToken("{");
|
||||
const matches: MatchStatement[] = [];
|
||||
const permissions: AllowStatement[] = [];
|
||||
while (currentToken.type !== "curly_close") {
|
||||
if (currentToken.value === "match") {
|
||||
matches.push(parseMatchStatement());
|
||||
} else if (currentToken.value === "allow") {
|
||||
permissions.push(parsePermissionStatement());
|
||||
} else {
|
||||
throw new ParserError(
|
||||
`Unexpected token value, expected 'match' or 'allow', received '${currentToken.value}'`,
|
||||
currentToken
|
||||
);
|
||||
}
|
||||
}
|
||||
eatToken("}");
|
||||
|
||||
return {
|
||||
type: "match",
|
||||
path,
|
||||
idx,
|
||||
matches,
|
||||
rules: permissions,
|
||||
};
|
||||
};
|
||||
|
||||
const parseServiceStatement = (): ServiceStatement => {
|
||||
const idx = eatToken("service");
|
||||
let [name] = eatText();
|
||||
eatToken("{");
|
||||
const matches: MatchStatement[] = [];
|
||||
while (currentToken.value === "match") {
|
||||
matches.push(parseMatchStatement());
|
||||
}
|
||||
eatToken("}");
|
||||
|
||||
return {
|
||||
type: "service",
|
||||
name: name,
|
||||
idx,
|
||||
matches,
|
||||
};
|
||||
};
|
||||
|
||||
const nodes: ServiceStatement[] = [];
|
||||
while (currentToken) {
|
||||
nodes.push(parseServiceStatement());
|
||||
}
|
||||
return nodes;
|
||||
}
|
Reference in New Issue
Block a user