ZenitechSoftware/visa-js

View on GitHub
lib/acm.js

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
const async = require('async');
const { wrapToPromise } = require('./utils');

class Unauthorized extends Error {
}

class VisaError extends Error {
  constructor(message) {
    super(message);
  }
}

const evaluateRule = (objectName, operationName, rule, subject, object, context) =>
  wrapToPromise(rule, subject, object, context)
    .catch(error => {
      error.message = `visa.js: object '${objectName}' operation '${operationName}': rule failed with error: ${error.message}`;
      throw error;
    })
    .then(authorized => {
      if (!authorized) {
        throw new Unauthorized();
      }
    });

const evaluateObjects = (objectName, operationName, objects, refs) => {
  if (!Array.isArray(objects)) {
    return Promise.reject(new VisaError(`visa.js: object '${objectName}' operation '${operationName}': 'mapRefsToObjects' function should return array of objects`));
  }
  if (objects.length !== refs.length) {
    return Promise.reject(new Unauthorized());
  }
  if (objects.findIndex(object => object === null || object === undefined) !== -1) {
    return Promise.reject(new Unauthorized());
  }
  return Promise.resolve(objects);
};

const evaluateMapRefsToObjects = (objectName, operationName, rule, subject, mapRefsToObjects, args) => {
  const refs = [];
  if (args.ref !== null && args.ref !== undefined) {
    refs.push(args.ref);
  }
  if (args.refs) {
    args.refs.forEach(ref => refs.push(ref));
  }
  if (!refs.length) {
    return Promise.resolve(refs);
  }
  if (!mapRefsToObjects) {
    return Promise.reject(new VisaError(`visa.js: object '${objectName}' operation '${operationName}': object references (${refs}) can not be resolved because 'mapRefsToObjects' method is not provided`));
  }
  return wrapToPromise(mapRefsToObjects, refs)
    .catch(error => {
      error.message = `visa.js: object '${objectName}' operation '${operationName}': object references (${refs}) can not be resolved because of 'mapRefsToObjects' error: ${error.message}`;
      throw error;
    })
    .then(objects => evaluateObjects(objectName, operationName, objects, refs));
};

const evaluateRuleForObjects = (objectName, operationName, rule, subject, objects, context) =>
  new Promise((resolve, reject) => async.eachLimit(
    objects,
    5,
    (object, cb) => evaluateRule(objectName, operationName, rule, subject, object, context).then(cb).catch(cb),
    error => {
      if (error) {
        return reject(error);
      }
      resolve(objects.length === 1 ? objects[0] : objects);
    })
  );

const evaluateRulesForObjects = (objectName, operationName, rule, subject, mapRefsToObjects, args) =>
  evaluateMapRefsToObjects(objectName, operationName, rule, subject, mapRefsToObjects, args)
    .then(objects => {
      if (args.object) {
        objects.push(args.object);
      }
      if (args.objects) {
        args.objects.forEach(object => objects.push(object));
      }
      return evaluateRuleForObjects(objectName, operationName, rule, subject, objects, args.context)
    });

const createEvaluationFunction = (rule, mapRefsToObjects, objectName, operationName) => function (args) {
  const result = args && (args.ref || args.refs || args.object || args.objects)
    ? evaluateRulesForObjects(objectName, operationName, rule, this.subject, mapRefsToObjects, args)
    : evaluateRule(objectName, operationName, rule, this.subject, undefined, args && args.context);
  if (this.isAsk) {
    return result
      .then(() => !this.isNot)
      .catch(error => {
        if (error instanceof Unauthorized) {
          return Boolean(this.isNot);
        }
        throw error;
      });
  }
  if (this.isNot) {
    return result
      .then(() => false)
      .catch(error => {
        if (error instanceof Unauthorized) {
          return true;
        }
        throw error;
      })
      .then(result => {
        if (!result) {
          throw new Unauthorized();
        }
      });
  }
  return result;
};

const createPolicyFunction = (Can) => policy => {
  for (let objectName of Object.keys(policy.objects)) {
    for (let operationName of Object.keys(policy.objects[objectName].operations)) {
      if (!Can.prototype[operationName]) {
        class Operation {
          constructor(subject, isAsk, isNot) {
            this.subject = subject;
            this.isAsk = isAsk;
            this.isNot = isNot;
          }
        }
        Can.prototype[`_${operationName}Class`] = Operation;
        Can.prototype.__defineGetter__(operationName, function () {
          return new Operation(this.subject, this.isAsk, this.isNot);
        });
      }
      Can.prototype[`_${operationName}Class`].prototype[objectName] = createEvaluationFunction(
        policy.objects[objectName].operations[operationName],
        policy.objects[objectName].mapRefsToObjects,
        objectName,
        operationName
      );
    }
  }
};

module.exports.Unauthorized = Unauthorized;

module.exports.build = () => {
  let Can = function (subject, isAsk) {
    this.subject = subject;
    this.isAsk = isAsk;
  }
  Can.prototype.__defineGetter__('not', function () {
    this.isNot = true;
    return this;
  });
  let Check = function (subject, isAsk) {
    this.subject = subject;
    this.isAsk = isAsk;
  }
  Check.prototype.__defineGetter__('can', function () {
    return new Can(this.subject, this.isAsk);
  });
  return {
    policy: createPolicyFunction(Can),
    check: subject => new Check(subject, false),
    ask: subject => new Check(subject, true),
  };
};