zemd/node-security-voters

View on GitHub
index.js

Summary

Maintainability
C
1 day
Test Coverage
"use strict";

const voters = [];
const runAsync = require("run-async");
const Rx = require("rxjs/Rx");

const logger = require("logtown").getLogger("security-voters");

const ACCESS = Object.freeze({
  GRANTED: 1,
  ABSTAIN: 0,
  DENIED: -1
});

/**
 * @enum {string}
 */
const STRATEGIES = Object.freeze({
  AFFIRMATIVE: "AFFIRMATIVE", // grant access as soon as there is one voter granting access
  CONSENSUS: "CONSENSUS", // grant access if there are more voters granting access than there are denying
  UNANIMOUS: "UNANIMOUS" // only grant access if none of the voters has denied access
});

/**
 * @param {Function} voterFn
 */
exports.addVoter = voterFn => voters.push(voterFn);

/**
 * @param {(string|undefined)} attr
 * @param {*} subj object preferable type for this param
 * @param {*} user object preferable type for this param
 * @param {('AFFIRMATIVE'|'CONSENSUS'|'UNANIMOUS')} strategy
 * @param {isGrantedCallback} [cb]
 */
exports.isGranted = (attr, subj, user, strategy = STRATEGIES.AFFIRMATIVE, cb) => {
  logger.debug(`Attr: ${attr}, subj: ${subj}, user: ${user}, strategy: ${strategy}`);

  // voters$ is an array of observables
  let voters$ = Rx.Observable.from(
    voters.map(voter => Rx.Observable.defer(
      () => runAsync(voter)(attr, subj, user))
    )
  );

  let result;

  switch (strategy) {
    case STRATEGIES.CONSENSUS:
      result = voters$
        .toArray()
        .mergeMap(votersFn => Rx.Observable.forkJoin(votersFn))
        .map(res => res.reduce((acc, v) => acc + v, 0))
        .map(results => results > 0)
        .do(res => logger.debug(`CONSENSUS RESOLUTION: ${res}`));
      break;
    case STRATEGIES.UNANIMOUS:
      result = voters$
        .mergeMap(v => v) // run observable
        .find(val => val === ACCESS.DENIED)
        .map(res => typeof res === "undefined") // is_granted === true if res === undefined
        .do(res => logger.debug(`UNANIMOUS RESOLUTION: ${res}`));
      break;
    default:
    case STRATEGIES.AFFIRMATIVE:
      result = voters$
        .mergeMap(v => v) // run observable
        .find(val => val === ACCESS.GRANTED)
        .map(res => typeof res !== "undefined") // is_granted === true if res !== undefined
        .do(res => logger.debug(`AFFIRMATIVE RESOLUTION: ${res}`));
      break;
  }

  if (cb) {
    result.subscribe(access => cb(null, access), err => cb(err));
    return;
  }

  return result.toPromise();
};

exports.STRATEGIES = STRATEGIES;
exports.ACCESS = ACCESS;

/**
 * @callback isGrantedCallback
 * @param {?Error} error
 * @param {boolean} isGranted
 */