transforming cerbos conditions AST to ucast conditions

137 Views Asked by At

We are using Cerbos as an authorization server and one of the features we want to use is the queryPlanner.

My ultimate goal is to be able to create a TypeORM "selectQueryBuilder" from the AST response that I'm getting.

The following AST is an example of a queryPlanner response that we might get from cerbos:

{
  "operator": "and",
  "operands": [
    {
      "operator": "gt",
      "operands": [
        {
          "name": "request.resource.attr.foo"
        },
        {
          "value": 4
        }
      ]
    },
    {
      "operator": "or",
      "operands": [
        {
          "operator": "eq",
          "operands": [
            {
              "name": "request.resource.attr.bar"
            },
            {
              "value": 5
            }
          ]
        },
        {
          "operator": "and",
          "operands": [
            {
              "operator": "eq",
              "operands": [
                {
                  "name": "request.resource.attr.fizz"
                },
                {
                  "value": 6
                }
              ]
            },
            {
              "operator": "in",
              "operands": [
                {
                  "value": "ZZZ"
                },
                {
                  "name": "request.resource.attr.buzz"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

I thought about utilizing ucast library to translate this response to some "CompoundCondition" and then use the @ucast/sql package to create my selectQueryBuilder.

I believe that the condition should look something like this in my case:

import { CompoundCondition, FieldCondition } from '@ucast/core'

const condition = new CompoundCondition('and', [
  new FieldCondition('gt', 'R.attr.foo', 4),
  new CompoundCondition('or', [
    new FieldCondition('eq', 'R.attr.bar', 5),
    new CompoundCondition('and', [
      new FieldCondition('eq', 'R.attr.fizz', 6),
      new FieldCondition('in', 'R.attr.buzz', 'ZZZ')
    ])
  ])
])

Then it should be very easy to create the queryBuilder:

  const conn = await createConnection({
    type: 'mysql',
    database: ':memory:',
    entities: [User]
  });


  const qb = interpret(condition, conn.createQueryBuilder(User, 'u'));
}

I am just having trouble creating the needed function (AST to compoundCondition)...

1

There are 1 best solutions below

0
Umar Farooq Khawaja On

Something like this might help.

import { CompoundCondition, Condition, FieldCondition } from '@ucast/core';

export abstract class Expression {
  protected constructor() {
  }

  abstract renderCondition(): Condition;

  static create(json: any): Expression {
    const {
      operator,
      operands,
      name,
      value
    } = json;

    switch (operator) {
      case 'and':
        return AndExpression.create(operands);

      case 'or':
        return OrExpression.create(operands);

      case 'gt':
        return GreaterThanExpression.create(operands);

      case 'gte':
        return GreaterThanOrEqualToExpression.create(operands);

      case 'lt':
        return LesserThanExpression.create(operands);

      case 'lte':
        return LesserThanOrEqualToExpression.create(operands);

      case 'eq':
        return EqualToExpression.create(operands);

      case 'ne':
        return NotEqualToExpression.create(operands);

      case 'in':
        return InExpression.create(operands);

      default: {
        if (name && !value) {
          return NameExpression.create(name);
        }

        if (!name && value) {
          return ValueExpression.create(value);
        }

        throw new Error(`unsupported expression operator ${operator}`);
      }
    }
  }
}

export abstract class OpExpression extends Expression {
  protected constructor() {
    super();
  }

  abstract renderOperator(): string;
}

export abstract class BinaryExpression extends OpExpression {
  protected constructor(
    protected leftOperand: Expression,
    protected rightOperand: Expression
  ) {
    super();
  }

  override renderCondition(): Condition {
    const isLeftOperandName: boolean = this.leftOperand instanceof NameExpression;
    const isLeftOperandValue: boolean = this.leftOperand instanceof ValueExpression;

    const isRightOperandName: boolean = this.rightOperand instanceof NameExpression;
    const isRightOperandValue: boolean = this.rightOperand instanceof ValueExpression;

    if (isLeftOperandName) {
      const leftExpression: NameExpression = this.leftOperand as NameExpression;

      if (isRightOperandName) {
        const rightExpression: NameExpression = this.rightOperand as NameExpression;

        return new FieldCondition(this.renderOperator(), leftExpression.name, rightExpression.name);
      } else if (isRightOperandValue) {
        const rightExpression: ValueExpression = this.rightOperand as ValueExpression;

        return new FieldCondition(this.renderOperator(), leftExpression.name, rightExpression.value);
      }
    } else if (isLeftOperandValue) {
      const leftExpression: ValueExpression = this.leftOperand as ValueExpression;

      if (isRightOperandName) {
        const rightExpression: NameExpression = this.rightOperand as NameExpression;

        return new FieldCondition(this.renderOperator(), rightExpression.name, leftExpression.value);
      } else if (isRightOperandValue) {
        const rightExpression: ValueExpression = this.rightOperand as ValueExpression;

        return new FieldCondition(this.renderOperator(), rightExpression.value, leftExpression.value);
      }
    }

    return new CompoundCondition(this.renderOperator(), [
      this.leftOperand.renderCondition(),
      this.rightOperand.renderCondition()
    ]);
  }
}

export abstract class UnaryExpression extends OpExpression {
  protected constructor(
    protected operand: Expression
  ) {
    super();
  }

  override renderCondition(): Condition {
    return new CompoundCondition(this.renderOperator(), [
      this.operand.renderCondition()
    ]);
  }
}

export abstract class NaryExpression extends OpExpression {
  protected constructor(
    protected operands: Expression[]
  ) {
    super();
  }

  override renderCondition(): Condition {
    return new CompoundCondition(this.renderOperator(), this.operands.map((operand: Expression) => operand.renderCondition()));
  }
}

export class AndExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'and';
  }

  static create(json: any): AndExpression {
    const [leftOperand, rightOperand, ...otherOperands] = json;

    if (otherOperands.length !== 0) {
      throw new Error('too many operands for a binary expression');
    }

    return new AndExpression(
      Expression.create(leftOperand),
      Expression.create(rightOperand)
    );
  }
}

export class OrExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'or';
  }

  static create(json: any): OrExpression {
    const [leftOperand, rightOperand, ...otherOperands] = json;

    if (otherOperands.length !== 0) {
      throw new Error('too many operands for a binary expression');
    }

    return new OrExpression(
      Expression.create(leftOperand),
      Expression.create(rightOperand)
    );
  }
}

export class GreaterThanExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'gt';
  }

  static create(json: any): GreaterThanExpression {
    const [leftOperand, rightOperand, ...otherOperands] = json;

    if (otherOperands.length !== 0) {
      throw new Error('too many operands for a binary expression');
    }

    return new GreaterThanExpression(
      Expression.create(leftOperand),
      Expression.create(rightOperand)
    );
  }
}

export class GreaterThanOrEqualToExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'gte';
  }

  static create(json: any): GreaterThanExpression {
    const [leftOperand, rightOperand, ...otherOperands] = json;

    if (otherOperands.length !== 0) {
      throw new Error('too many operands for a binary expression');
    }

    return new GreaterThanOrEqualToExpression(
      Expression.create(leftOperand),
      Expression.create(rightOperand)
    );
  }
}

export class LesserThanExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'lt';
  }

  static create(json: any): LesserThanExpression {
    const [leftOperand, rightOperand, ...otherOperands] = json;

    if (otherOperands.length !== 0) {
      throw new Error('too many operands for a binary expression');
    }

    return new LesserThanExpression(
      Expression.create(leftOperand),
      Expression.create(rightOperand)
    );
  }
}

export class LesserThanOrEqualToExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'lte';
  }

  static create(json: any): LesserThanExpression {
    const [leftOperand, rightOperand, ...otherOperands] = json;

    if (otherOperands.length !== 0) {
      throw new Error('too many operands for a binary expression');
    }

    return new LesserThanOrEqualToExpression(
      Expression.create(leftOperand),
      Expression.create(rightOperand)
    );
  }
}

export class EqualToExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'eq';
  }

  static create(json: any): EqualToExpression {
    const [leftOperand, rightOperand, ...otherOperands] = json;

    if (otherOperands.length !== 0) {
      throw new Error('too many operands for a binary expression');
    }

    return new EqualToExpression(
      Expression.create(leftOperand),
      Expression.create(rightOperand)
    );
  }
}

export class NotEqualToExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'ne';
  }

  static create(json: any): NotEqualToExpression {
    const [leftOperand, rightOperand, ...otherOperands] = json;

    if (otherOperands.length !== 0) {
      throw new Error('too many operands for a binary expression');
    }

    return new NotEqualToExpression(
      Expression.create(leftOperand),
      Expression.create(rightOperand)
    );
  }
}

export class InExpression extends BinaryExpression {
  protected constructor(
    leftOperand: Expression,
    rightOperand: Expression
  ) {
    super(leftOperand, rightOperand);
  }

  override renderOperator(): string {
    return 'in';
  }

  static create(json: any): InExpression {
    const [operand, ...operands] = json;

    return new InExpression(
      Expression.create(operand),
      ValueExpression.create(
        operands
          .map((operand: any) => ValueExpression.create(operand))
          .map((valueExpression: ValueExpression) => valueExpression.value)
      )
    );
  }
}

export class NameExpression extends Expression {
  protected constructor(public name: string) {
    super();
  }

  override renderCondition(): Condition {
    throw new Error('Method not implemented.');
  }

  static create(name: string): NameExpression {
    return new NameExpression(name);
  }
}

export class ValueExpression extends Expression {
  protected constructor(public value: any) {
    super();
  }

  override renderCondition(): Condition {
    throw new Error('Method not implemented.');
  }

  static create(value: string): ValueExpression {
    return new ValueExpression(value);
  }
}